diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8a3ba4f..191ca40 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,7 +2,7 @@ use crate::{ commands::app_state::get_modules, services::{ doll_editor::open_doll_editor_window, - scene::{set_pet_menu_state, set_scene_interactive}, + scene::{get_scene_interactive, set_pet_menu_state, set_scene_interactive}, }, }; use commands::app::{quit_app, restart_app, retry_connection}; @@ -83,6 +83,7 @@ pub fn run() { save_client_config, open_client_config_manager, open_doll_editor_window, + get_scene_interactive, set_scene_interactive, set_pet_menu_state, login, diff --git a/src-tauri/src/services/scene.rs b/src-tauri/src/services/scene.rs index 3275b61..0060ef5 100644 --- a/src-tauri/src/services/scene.rs +++ b/src-tauri/src/services/scene.rs @@ -82,6 +82,11 @@ pub fn set_scene_interactive(interactive: bool, should_click: bool) { update_scene_interactive(interactive, should_click); } +#[tauri::command] +pub fn get_scene_interactive() -> Result { + Ok(scene_interactive_state().load(Ordering::SeqCst)) +} + #[tauri::command] pub fn set_pet_menu_state(id: String, open: bool) { let menus_arc = get_open_pet_menus(); diff --git a/src/events/app-data.ts b/src/events/app-data.ts index 3d6010d..3b2be92 100644 --- a/src/events/app-data.ts +++ b/src/events/app-data.ts @@ -9,14 +9,17 @@ export const appData = writable(null); const subscription = createListenerSubscription(); -export async function initAppDataListener() { +/** + * Starts listening for app data refresh events. + * Initializes app data from the backend. + */ +export async function startAppData() { try { if (subscription.isListening()) return; appData.set(await invoke("get_app_data")); const unlisten = await listen( AppEvents.AppDataRefreshed, (event) => { - console.log("app-data-refreshed", event.payload); appData.set(event.payload); }, ); @@ -28,8 +31,8 @@ export async function initAppDataListener() { } } -export function stopAppDataListener() { +export function stopAppData() { subscription.stop(); } -setupHmrCleanup(stopAppDataListener); +setupHmrCleanup(stopAppData); diff --git a/src/events/cursor.ts b/src/events/cursor.ts index 13bf23c..7dfcb35 100644 --- a/src/events/cursor.ts +++ b/src/events/cursor.ts @@ -2,155 +2,32 @@ import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { writable } from "svelte/store"; import type { CursorPositions } from "../types/bindings/CursorPositions"; -import type { CursorPosition } from "../types/bindings/CursorPosition"; -import type { DollDto } from "../types/bindings/DollDto"; import { AppEvents } from "../types/bindings/AppEventsConstants"; -import { - createMultiListenerSubscription, - parseEventPayload, - removeFromStore, - setupHmrCleanup, -} from "./listener-utils"; +import { createListenerSubscription, setupHmrCleanup } from "./listener-utils"; export const cursorPositionOnScreen = writable({ raw: { x: 0, y: 0 }, mapped: { x: 0, y: 0 }, }); -export type FriendCursorPosition = { - userId: string; - position: CursorPositions; -}; - -// Map of userId -> { position: CursorPositions, lastUpdated: number } -// We store the timestamp to detect stale cursors -type FriendCursorData = { - position: CursorPositions; - lastUpdated: number; -}; - -// 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 const friendsCursorPositions = writable>( - {}, -); -export const friendsActiveDolls = writable>({}); - -const subscription = createMultiListenerSubscription(); - -// Internal state to track timestamps -let friendCursorState: Record = {}; +const subscription = createListenerSubscription(); /** - * Initialize cursor tracking for this window. - * Can be called from multiple windows - only the first call starts tracking on the Rust side, - * but all windows can independently listen to the broadcast events. + * Starts tracking the local cursor position. + * Initializes cursor position from the backend and listens for updates. */ -export async function initCursorTracking() { +export async function startCursorTracking() { if (subscription.isListening()) return; try { - // Listen to cursor position events (each window subscribes independently) - const unlistenCursor = await listen( + cursorPositionOnScreen.set(await invoke("get_cursor_position")); + const unlisten = await listen( AppEvents.CursorPosition, (event) => { cursorPositionOnScreen.set(event.payload); }, ); - subscription.addUnlisten(unlistenCursor); - - // Listen to friend cursor position events - const unlistenFriendCursor = await listen( - AppEvents.FriendCursorPosition, - (event) => { - // We now receive a clean object from Rust - const data = event.payload; - - // Update internal state with timestamp - friendCursorState[data.userId] = { - position: data.position, - lastUpdated: Date.now(), - }; - - friendsCursorPositions.update((current) => { - return { - ...current, - [data.userId]: data.position, - }; - }); - }, - ); - subscription.addUnlisten(unlistenFriendCursor); - - // Listen to friend disconnected events - const unlistenFriendDisconnected = await listen< - [{ userId: string }] | { userId: string } | string - >(AppEvents.FriendDisconnected, (event) => { - const payload = parseEventPayload< - [{ userId: string }] | { userId: string } - >(event.payload, AppEvents.FriendDisconnected); - if (!payload) return; - - const data = Array.isArray(payload) ? payload[0] : payload; - - // Remove from internal state - if (friendCursorState[data.userId]) { - delete friendCursorState[data.userId]; - } - - // Update svelte store - friendsCursorPositions.update((current) => - removeFromStore(current, data.userId), - ); - }); - subscription.addUnlisten(unlistenFriendDisconnected); - - // Listen to friend active doll changed events - const unlistenFriendActiveDollChanged = await listen< - | string - | { - friendId: string; - doll: DollDto | null; - } - >(AppEvents.FriendActiveDollChanged, (event) => { - const data = parseEventPayload<{ - friendId: string; - doll: DollDto | null; - }>(event.payload, AppEvents.FriendActiveDollChanged); - if (!data) return; - - // Cast to expected type after parsing - const payload = data as { friendId: string; doll: DollDto | null }; - - if (!payload.doll) { - // If doll is null, it means the friend deactivated their doll. - - // 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) => - removeFromStore(current, payload.friendId), - ); - } else { - // Update or add the new doll configuration - friendsActiveDolls.update((current) => { - return { - ...current, - [payload.friendId]: payload.doll!, - }; - }); - } - }); - subscription.addUnlisten(unlistenFriendActiveDollChanged); - + subscription.setUnlisten(unlisten); subscription.setListening(true); } catch (err) { console.error("Failed to initialize cursor tracking:", err); @@ -158,10 +35,6 @@ export async function initCursorTracking() { } } -/** - * Stop listening to cursor events in this window. - * Note: This doesn't stop the Rust-side tracking, just stops this window from receiving events. - */ export function stopCursorTracking() { subscription.stop(); } diff --git a/src/events/friend-cursor.ts b/src/events/friend-cursor.ts new file mode 100644 index 0000000..b47eb3a --- /dev/null +++ b/src/events/friend-cursor.ts @@ -0,0 +1,129 @@ +import { listen } from "@tauri-apps/api/event"; +import { writable } from "svelte/store"; +import type { CursorPositions } from "../types/bindings/CursorPositions"; +import type { DollDto } from "../types/bindings/DollDto"; +import { AppEvents } from "../types/bindings/AppEventsConstants"; +import { + createMultiListenerSubscription, + parseEventPayload, + removeFromStore, + setupHmrCleanup, +} from "./listener-utils"; + +export type FriendCursorPosition = { + userId: string; + position: CursorPositions; +}; + +type FriendCursorData = { + position: CursorPositions; + lastUpdated: number; +}; + +export const friendsCursorPositions = writable>( + {}, +); +export const friendsActiveDolls = writable>({}); + +const subscription = createMultiListenerSubscription(); + +let friendCursorState: Record = {}; + +/** + * Starts listening for friend cursor position and active doll changes. + * Also handles friend disconnection events. + */ +export async function startFriendCursorTracking() { + if (subscription.isListening()) return; + + try { + // TODO: Add initial sync for existing friends' cursors and dolls if needed + + const unlistenFriendCursor = await listen( + AppEvents.FriendCursorPosition, + (event) => { + const data = event.payload; + + friendCursorState[data.userId] = { + position: data.position, + lastUpdated: Date.now(), + }; + + friendsCursorPositions.update((current) => { + return { + ...current, + [data.userId]: data.position, + }; + }); + }, + ); + subscription.addUnlisten(unlistenFriendCursor); + + const unlistenFriendDisconnected = await listen< + [{ userId: string }] | { userId: string } | string + >(AppEvents.FriendDisconnected, (event) => { + const payload = parseEventPayload< + [{ userId: string }] | { userId: string } + >(event.payload, AppEvents.FriendDisconnected); + if (!payload) return; + + const data = Array.isArray(payload) ? payload[0] : payload; + + if (friendCursorState[data.userId]) { + delete friendCursorState[data.userId]; + } + + friendsCursorPositions.update((current) => + removeFromStore(current, data.userId), + ); + }); + subscription.addUnlisten(unlistenFriendDisconnected); + + const unlistenFriendActiveDollChanged = await listen< + | string + | { + friendId: string; + doll: DollDto | null; + } + >(AppEvents.FriendActiveDollChanged, (event) => { + const data = parseEventPayload<{ + friendId: string; + doll: DollDto | null; + }>(event.payload, AppEvents.FriendActiveDollChanged); + if (!data) return; + + const payload = data as { friendId: string; doll: DollDto | null }; + + if (!payload.doll) { + friendsActiveDolls.update((current) => { + const next = { ...current }; + next[payload.friendId] = null; + return next; + }); + + friendsCursorPositions.update((current) => + removeFromStore(current, payload.friendId), + ); + } else { + friendsActiveDolls.update((current) => { + return { + ...current, + [payload.friendId]: payload.doll!, + }; + }); + } + }); + subscription.addUnlisten(unlistenFriendActiveDollChanged); + + subscription.setListening(true); + } catch (err) { + console.error("Failed to initialize friend cursor tracking:", err); + throw err; + } +} + +export function stopFriendCursorTracking() { + subscription.stop(); +} + +setupHmrCleanup(stopFriendCursorTracking); diff --git a/src/events/interaction.ts b/src/events/interaction.ts index 860046f..21904bc 100644 --- a/src/events/interaction.ts +++ b/src/events/interaction.ts @@ -1,5 +1,5 @@ import { listen } from "@tauri-apps/api/event"; -import { addInteraction } from "$lib/stores/interaction-store"; +import { writable } from "svelte/store"; import type { InteractionPayloadDto } from "../types/bindings/InteractionPayloadDto"; import type { InteractionDeliveryFailedDto } from "../types/bindings/InteractionDeliveryFailedDto"; import { AppEvents } from "../types/bindings/AppEventsConstants"; @@ -8,35 +8,62 @@ import { setupHmrCleanup, } from "./listener-utils"; -const subscription = createMultiListenerSubscription(); +export const receivedInteractions = writable>( + new Map(), +); -export async function initInteractionListeners() { - if (subscription.isListening()) return; - - const unlistenReceived = await listen( - AppEvents.InteractionReceived, - (event) => { - addInteraction(event.payload); - }, - ); - subscription.addUnlisten(unlistenReceived); - - const unlistenFailed = await listen( - AppEvents.InteractionDeliveryFailed, - (event) => { - console.error("Interaction delivery failed:", event.payload); - // You might want to show a toast or alert here - alert( - `Failed to send message to user ${event.payload.recipientUserId}: ${event.payload.reason}`, - ); - }, - ); - subscription.addUnlisten(unlistenFailed); - subscription.setListening(true); +export function addInteraction(interaction: InteractionPayloadDto) { + receivedInteractions.update((map) => { + const newMap = new Map(map); + newMap.set(interaction.senderUserId, interaction); + return newMap; + }); } -export function stopInteractionListeners() { +export function clearInteraction(userId: string) { + receivedInteractions.update((map) => { + const newMap = new Map(map); + newMap.delete(userId); + return newMap; + }); +} + +const subscription = createMultiListenerSubscription(); + +/** + * Starts listening for interaction events (received and delivery failed). + */ +export async function startInteraction() { + if (subscription.isListening()) return; + + try { + const unlistenReceived = await listen( + AppEvents.InteractionReceived, + (event) => { + addInteraction(event.payload); + }, + ); + subscription.addUnlisten(unlistenReceived); + + const unlistenFailed = await listen( + AppEvents.InteractionDeliveryFailed, + (event) => { + console.error("Interaction delivery failed:", event.payload); + alert( + `Failed to send message to user ${event.payload.recipientUserId}: ${event.payload.reason}`, + ); + }, + ); + subscription.addUnlisten(unlistenFailed); + subscription.setListening(true); + } catch (err) { + console.error("Failed to initialize interaction listeners:", err); + throw err; + } +} + +export function stopInteraction() { subscription.stop(); } -setupHmrCleanup(stopInteractionListeners); +setupHmrCleanup(stopInteraction); diff --git a/src/events/scene-interactive.ts b/src/events/scene-interactive.ts index 7fc1850..aaa5a15 100644 --- a/src/events/scene-interactive.ts +++ b/src/events/scene-interactive.ts @@ -1,4 +1,5 @@ import { listen } from "@tauri-apps/api/event"; +import { invoke } from "@tauri-apps/api/core"; import { writable } from "svelte/store"; import { AppEvents } from "../types/bindings/AppEventsConstants"; import { createListenerSubscription, setupHmrCleanup } from "./listener-utils"; @@ -7,12 +8,15 @@ export const sceneInteractive = writable(false); const subscription = createListenerSubscription(); -export async function initSceneInteractiveListener() { +/** + * Starts listening for scene interactive state changes. + * Initializes the scene interactive state from the backend. + */ +export async function startSceneInteractive() { if (subscription.isListening()) return; try { - // ensure initial default matches backend default - sceneInteractive.set(false); + sceneInteractive.set(await invoke("get_scene_interactive")); const unlisten = await listen( AppEvents.SceneInteractive, (event) => { @@ -27,8 +31,8 @@ export async function initSceneInteractiveListener() { } } -export function stopSceneInteractiveListener() { +export function stopSceneInteractive() { subscription.stop(); } -setupHmrCleanup(stopSceneInteractiveListener); +setupHmrCleanup(stopSceneInteractive); diff --git a/src/events/user-status.ts b/src/events/user-status.ts index 14239f9..2604e6f 100644 --- a/src/events/user-status.ts +++ b/src/events/user-status.ts @@ -9,17 +9,20 @@ import { setupHmrCleanup, } from "./listener-utils"; -export type UserStatus = { +export type PresenceState = { presenceStatus: PresenceStatus; state: "idle" | "resting"; }; -export const friendsUserStatuses = writable>({}); -export const currentUserStatus = writable(null); +export const friendsPresenceStates = writable>({}); +export const currentPresenceState = writable(null); const subscription = createMultiListenerSubscription(); -export async function initUserStatusListeners() { +/** + * Starts listening for user status changes and friend status updates. + */ +export async function startUserStatus() { if (subscription.isListening()) return; try { @@ -28,7 +31,7 @@ export async function initUserStatusListeners() { (event) => { const payload = parseEventPayload<{ userId?: string; - status?: UserStatus; + status?: PresenceState; }>(event.payload, AppEvents.FriendUserStatus); if (!payload) return; @@ -48,7 +51,7 @@ export async function initUserStatusListeners() { if (status.state !== "idle" && status.state !== "resting") return; - friendsUserStatuses.update((current) => ({ + friendsPresenceStates.update((current) => ({ ...current, [userId]: { presenceStatus: status.presenceStatus, @@ -59,10 +62,10 @@ export async function initUserStatusListeners() { ); subscription.addUnlisten(unlistenStatus); - const unlistenUserStatusChanged = await listen( + const unlistenUserStatusChanged = await listen( AppEvents.UserStatusChanged, (event) => { - currentUserStatus.set(event.payload); + currentPresenceState.set(event.payload); }, ); subscription.addUnlisten(unlistenUserStatusChanged); @@ -79,7 +82,7 @@ export async function initUserStatusListeners() { const userId = data?.userId as string | undefined; if (!userId) return; - friendsUserStatuses.update((current) => removeFromStore(current, userId)); + friendsPresenceStates.update((current) => removeFromStore(current, userId)); }); subscription.addUnlisten(unlistenFriendDisconnected); @@ -90,8 +93,8 @@ export async function initUserStatusListeners() { } } -export function stopUserStatusListeners() { +export function stopUserStatus() { subscription.stop(); } -setupHmrCleanup(stopUserStatusListeners); +setupHmrCleanup(stopUserStatus); diff --git a/src/lib/stores/interaction-store.ts b/src/lib/stores/interaction-store.ts deleted file mode 100644 index 9029ad7..0000000 --- a/src/lib/stores/interaction-store.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { writable } from "svelte/store"; -import type { InteractionPayloadDto } from "../../types/bindings/InteractionPayloadDto"; - -// Map senderUserId -> InteractionPayloadDto -export const receivedInteractions = writable>(new Map()); - -export function addInteraction(interaction: InteractionPayloadDto) { - receivedInteractions.update((map) => { - // For now, we only store the latest message per user. - // In the future, we could store an array if we want a history. - const newMap = new Map(map); - newMap.set(interaction.senderUserId, interaction); - return newMap; - }); -} - -export function clearInteraction(userId: string) { - receivedInteractions.update((map) => { - const newMap = new Map(map); - newMap.delete(userId); - return newMap; - }); -} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index daf4b17..de48d52 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,24 +1,29 @@ diff --git a/src/routes/scene/+page.svelte b/src/routes/scene/+page.svelte index 68a3408..a232332 100644 --- a/src/routes/scene/+page.svelte +++ b/src/routes/scene/+page.svelte @@ -1,13 +1,11 @@