From 69cfebee3d580ae2042b8a481f4a639d085f9002 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Mon, 9 Mar 2026 12:30:29 +0800 Subject: [PATCH] moved friend cursor data aggregation from frontend to backend --- src-tauri/src/lib.rs | 10 +- src-tauri/src/services/app_events.rs | 9 +- src-tauri/src/services/friend_cursor.rs | 147 ++++++++++++++++++++++++ src-tauri/src/services/mod.rs | 1 + src-tauri/src/services/ws/friend.rs | 28 +++-- src-tauri/src/state/ui.rs | 6 +- src/events/friend-cursor.ts | 85 +++----------- src/lib/bindings.ts | 11 +- 8 files changed, 201 insertions(+), 96 deletions(-) create mode 100644 src-tauri/src/services/friend_cursor.rs diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ce7a759..f72f268 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -25,10 +25,10 @@ use tauri_specta::{Builder as SpectaBuilder, ErrorHandlingMode, collect_commands use crate::services::app_events::{ AppDataRefreshed, CreateDoll, CursorMoved, EditDoll, FriendActiveDollChanged, - FriendCursorPositionUpdated, FriendDisconnected, FriendRequestAccepted, - FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged, - InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged, - SetInteractionOverlay, Unfriended, UserStatusChanged, + FriendCursorPositionsUpdated, FriendDisconnected, + FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, + FriendUserStatusChanged, InteractionDeliveryFailed, InteractionReceived, + SceneInteractiveChanged, SetInteractionOverlay, Unfriended, UserStatusChanged, }; static APP_HANDLE: std::sync::OnceLock> = std::sync::OnceLock::new(); @@ -109,7 +109,7 @@ pub fn run() { EditDoll, CreateDoll, UserStatusChanged, - FriendCursorPositionUpdated, + FriendCursorPositionsUpdated, FriendDisconnected, FriendActiveDollChanged, FriendUserStatusChanged, diff --git a/src-tauri/src/services/app_events.rs b/src-tauri/src/services/app_events.rs index 2f6ccbb..6903636 100644 --- a/src-tauri/src/services/app_events.rs +++ b/src-tauri/src/services/app_events.rs @@ -12,7 +12,10 @@ use crate::{ }, interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto}, }, - services::{cursor::CursorPositions, ws::OutgoingFriendCursorPayload}, + services::{ + cursor::CursorPositions, friend_cursor::FriendCursorPositionsDto, + ws::OutgoingFriendCursorPayload, + }, }; #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] @@ -47,6 +50,10 @@ pub struct UserStatusChanged(pub UserStatusPayload); #[tauri_specta(event_name = "friend-cursor-position")] pub struct FriendCursorPositionUpdated(pub OutgoingFriendCursorPayload); +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "friend-cursor-positions")] +pub struct FriendCursorPositionsUpdated(pub FriendCursorPositionsDto); + #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] #[tauri_specta(event_name = "friend-disconnected")] pub struct FriendDisconnected(pub FriendDisconnectedPayload); diff --git a/src-tauri/src/services/friend_cursor.rs b/src-tauri/src/services/friend_cursor.rs new file mode 100644 index 0000000..e7d5305 --- /dev/null +++ b/src-tauri/src/services/friend_cursor.rs @@ -0,0 +1,147 @@ +use std::{ + collections::HashMap, + sync::{Arc, LazyLock, RwLock}, +}; + +use serde::{Deserialize, Serialize}; +use specta::Type; +use tauri_specta::Event as _; + +use crate::{ + get_app_handle, lock_r, + services::{app_events::FriendCursorPositionsUpdated, cursor::CursorPositions}, + state::FDOLL, +}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, Type)] +#[serde(transparent)] +pub struct FriendCursorPositionsDto(pub HashMap); + +#[derive(Default)] +struct FriendCursorProjection { + active_dolls: HashMap, + positions: HashMap, +} + +static FRIEND_CURSOR_PROJECTION: LazyLock>> = + LazyLock::new(|| Arc::new(RwLock::new(FriendCursorProjection::default()))); + +pub fn sync_from_app_data() { + let friends = { + let guard = lock_r!(FDOLL); + guard.user_data.friends.clone().unwrap_or_default() + }; + + let mut projection = FRIEND_CURSOR_PROJECTION + .write() + .expect("friend cursor projection lock poisoned"); + + projection.active_dolls = friends + .into_iter() + .filter_map(|friendship| { + friendship.friend.map(|friend| { + let has_active_doll = friend.active_doll.is_some(); + (friend.id, has_active_doll) + }) + }) + .collect(); + + let active_dolls = projection.active_dolls.clone(); + projection + .positions + .retain(|user_id, _| active_dolls.get(user_id) == Some(&true)); + + emit_snapshot(&projection.positions); +} + +pub fn clear() { + let mut projection = FRIEND_CURSOR_PROJECTION + .write() + .expect("friend cursor projection lock poisoned"); + + projection.active_dolls.clear(); + projection.positions.clear(); + + emit_snapshot(&projection.positions); +} + +pub fn update_position(user_id: String, position: CursorPositions) { + let mut projection = FRIEND_CURSOR_PROJECTION + .write() + .expect("friend cursor projection lock poisoned"); + + if !has_active_doll(&mut projection, &user_id) { + if projection.positions.remove(&user_id).is_some() { + emit_snapshot(&projection.positions); + } + return; + } + + projection.positions.insert(user_id, position); + emit_snapshot(&projection.positions); +} + +pub fn remove_friend(user_id: &str) { + let mut projection = FRIEND_CURSOR_PROJECTION + .write() + .expect("friend cursor projection lock poisoned"); + + let removed_active_doll = projection.active_dolls.remove(user_id).is_some(); + let removed_position = projection.positions.remove(user_id).is_some(); + + if removed_active_doll || removed_position { + emit_snapshot(&projection.positions); + } +} + +pub fn set_active_doll(user_id: &str, has_active_doll: bool) { + let mut projection = FRIEND_CURSOR_PROJECTION + .write() + .expect("friend cursor projection lock poisoned"); + + projection + .active_dolls + .insert(user_id.to_string(), has_active_doll); + + if !has_active_doll { + if projection.positions.remove(user_id).is_some() { + emit_snapshot(&projection.positions); + } + } +} + +fn has_active_doll(projection: &mut FriendCursorProjection, user_id: &str) -> bool { + if let Some(has_active_doll) = projection.active_dolls.get(user_id) { + return *has_active_doll; + } + + let has_active_doll = { + let guard = lock_r!(FDOLL); + guard + .user_data + .friends + .as_ref() + .and_then(|friends| { + friends.iter().find_map(|friendship| { + let friend = friendship.friend.as_ref()?; + (friend.id == user_id).then_some(friend) + }) + }) + .and_then(|friend| friend.active_doll.as_ref()) + .is_some() + }; + + projection + .active_dolls + .insert(user_id.to_string(), has_active_doll); + + has_active_doll +} + +fn emit_snapshot(positions: &HashMap) { + let payload = FriendCursorPositionsDto(positions.clone()); + + if let Err(err) = FriendCursorPositionsUpdated(payload).emit(get_app_handle()) { + tracing::warn!("Failed to emit friend cursor positions update: {}", err); + } +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index c694080..7e6b30c 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -8,6 +8,7 @@ pub mod auth; pub mod client_config_manager; pub mod cursor; pub mod doll_editor; +pub mod friend_cursor; pub mod health_manager; pub mod health_monitor; pub mod interaction; diff --git a/src-tauri/src/services/ws/friend.rs b/src-tauri/src/services/ws/friend.rs index 03693db..f865c23 100644 --- a/src-tauri/src/services/ws/friend.rs +++ b/src-tauri/src/services/ws/friend.rs @@ -7,18 +7,16 @@ use crate::models::event_payloads::{ UnfriendedPayload, }; use crate::services::app_events::{ - FriendActiveDollChanged, FriendCursorPositionUpdated, FriendDisconnected, - FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged, - Unfriended, + FriendActiveDollChanged, FriendDisconnected, FriendRequestAccepted, FriendRequestDenied, + FriendRequestReceived, FriendUserStatusChanged, Unfriended, +}; +use crate::services::{ + cursor::{normalized_to_absolute, CursorPositions}, + friend_cursor, }; -use crate::services::cursor::{normalized_to_absolute, CursorPositions}; use crate::state::AppDataRefreshScope; -use super::{ - emitter, refresh, - types::{IncomingFriendCursorPayload, OutgoingFriendCursorPayload}, - utils, -}; +use super::{emitter, refresh, types::IncomingFriendCursorPayload, utils}; /// Handler for friend-request-received event pub fn on_friend_request_received(payload: Payload, _socket: RawClient) { @@ -64,15 +62,13 @@ pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) { let mapped_pos = &friend_data.position; let raw_pos = normalized_to_absolute(mapped_pos); - let outgoing_payload = OutgoingFriendCursorPayload { - user_id: friend_data.user_id, - position: CursorPositions { + friend_cursor::update_position( + friend_data.user_id, + CursorPositions { raw: raw_pos, mapped: mapped_pos.clone(), }, - }; - - emitter::emit_to_frontend_typed(&FriendCursorPositionUpdated(outgoing_payload)); + ); } } @@ -81,6 +77,7 @@ pub fn on_friend_disconnected(payload: Payload, _socket: RawClient) { if let Ok(data) = utils::extract_and_parse::(payload, "friend-disconnected") { + friend_cursor::remove_friend(&data.user_id); emitter::emit_to_frontend_typed(&FriendDisconnected(data)); } } @@ -114,6 +111,7 @@ pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) { payload, "friend-active-doll-changed", ) { + friend_cursor::set_active_doll(&data.friend_id, data.doll.is_some()); emitter::emit_to_frontend_typed(&FriendActiveDollChanged(data)); refresh::refresh_app_data(AppDataRefreshScope::Friends); } diff --git a/src-tauri/src/state/ui.rs b/src-tauri/src/state/ui.rs index bda3834..264c42a 100644 --- a/src-tauri/src/state/ui.rs +++ b/src-tauri/src/state/ui.rs @@ -1,7 +1,7 @@ use crate::{ get_app_handle, lock_r, lock_w, remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote}, - services::app_events::AppDataRefreshed, + services::{app_events::AppDataRefreshed, friend_cursor}, state::FDOLL, }; use std::{collections::HashSet, sync::LazyLock}; @@ -161,6 +161,8 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { Ok(friends) => { let mut guard = lock_w!(crate::state::FDOLL); guard.user_data.friends = Some(friends); + drop(guard); + friend_cursor::sync_from_app_data(); } Err(e) => { warn!("Failed to fetch friends list: {}", e); @@ -260,4 +262,6 @@ pub fn clear_app_data() { guard.user_data.dolls = None; guard.user_data.user = None; guard.user_data.friends = None; + drop(guard); + friend_cursor::clear(); } diff --git a/src/events/friend-cursor.ts b/src/events/friend-cursor.ts index 731d14c..f98009c 100644 --- a/src/events/friend-cursor.ts +++ b/src/events/friend-cursor.ts @@ -1,81 +1,32 @@ import { writable } from "svelte/store"; -import { - events, - type CursorPositions, - type DollDto, - type OutgoingFriendCursorPayload, -} from "$lib/bindings"; -import { createEventSource, removeFromStore } from "./listener-utils"; - -type FriendCursorData = { - position: CursorPositions; - lastUpdated: number; -}; +import { events, type CursorPositions } from "$lib/bindings"; +import { createEventSource } from "./listener-utils"; export const friendsCursorPositions = writable>( {}, ); -export const friendsActiveDolls = writable>({}); + +// Here for now. Will extract into shared +// util when there's more similar cases. +function toCursorPositionsRecord( + payload: Partial>, +): Record { + return Object.fromEntries( + Object.entries(payload).filter( + (entry): entry is [string, CursorPositions] => { + return entry[1] !== undefined; + }, + ), + ); +} export const { start: startFriendCursorTracking, stop: stopFriendCursorTracking, } = createEventSource(async (addEventListener) => { - let friendCursorState: Record = {}; addEventListener( - await events.friendCursorPositionUpdated.listen((event) => { - const data: OutgoingFriendCursorPayload = event.payload; - - friendCursorState[data.userId] = { - position: data.position, - lastUpdated: Date.now(), - }; - - friendsCursorPositions.update((current) => { - return { - ...current, - [data.userId]: data.position, - }; - }); - }), - ); - - addEventListener( - await events.friendDisconnected.listen((event) => { - const data = event.payload; - - if (friendCursorState[data.userId]) { - delete friendCursorState[data.userId]; - } - - friendsCursorPositions.update((current) => - removeFromStore(current, data.userId), - ); - }), - ); - - addEventListener( - await events.friendActiveDollChanged.listen((event) => { - const payload = event.payload; - - 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, - }; - }); - } + await events.friendCursorPositionsUpdated.listen((event) => { + friendsCursorPositions.set(toCursorPositionsRecord(event.payload)); }), ); }); diff --git a/src/lib/bindings.ts b/src/lib/bindings.ts index e5f00cf..c300d90 100644 --- a/src/lib/bindings.ts +++ b/src/lib/bindings.ts @@ -130,7 +130,7 @@ createDoll: CreateDoll, cursorMoved: CursorMoved, editDoll: EditDoll, friendActiveDollChanged: FriendActiveDollChanged, -friendCursorPositionUpdated: FriendCursorPositionUpdated, +friendCursorPositionsUpdated: FriendCursorPositionsUpdated, friendDisconnected: FriendDisconnected, friendRequestAccepted: FriendRequestAccepted, friendRequestDenied: FriendRequestDenied, @@ -148,7 +148,7 @@ createDoll: "create-doll", cursorMoved: "cursor-moved", editDoll: "edit-doll", friendActiveDollChanged: "friend-active-doll-changed", -friendCursorPositionUpdated: "friend-cursor-position-updated", +friendCursorPositionsUpdated: "friend-cursor-positions-updated", friendDisconnected: "friend-disconnected", friendRequestAccepted: "friend-request-accepted", friendRequestDenied: "friend-request-denied", @@ -182,7 +182,8 @@ export type DollDto = { id: string; name: string; configuration: DollConfigurati export type EditDoll = string export type FriendActiveDollChanged = FriendActiveDollChangedPayload export type FriendActiveDollChangedPayload = { friendId: string; doll: DollDto | null } -export type FriendCursorPositionUpdated = OutgoingFriendCursorPayload +export type FriendCursorPositionsDto = Partial<{ [key in string]: CursorPositions }> +export type FriendCursorPositionsUpdated = FriendCursorPositionsDto export type FriendDisconnected = FriendDisconnectedPayload export type FriendDisconnectedPayload = { userId: string } export type FriendRequestAccepted = FriendRequestAcceptedPayload @@ -200,10 +201,6 @@ export type InteractionDeliveryFailedDto = { recipientUserId: string; reason: st export type InteractionPayloadDto = { senderUserId: string; senderName: string; content: string; type: string; timestamp: string } export type InteractionReceived = InteractionPayloadDto export type ModuleMetadata = { id: string; name: string; version: string; description: string | null } -/** - * Outgoing friend cursor position to frontend - */ -export type OutgoingFriendCursorPayload = { userId: string; position: CursorPositions } export type PresenceStatus = { title: string | null; subtitle: string | null; graphicsB64: string | null } export type SceneData = { display: DisplayData; grid_size: number } export type SceneInteractiveChanged = boolean