From e1f5df6b1cece7f60795c7a739ae511762d4b6fc Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 11 May 2026 22:00:31 +0200 Subject: [PATCH] Split useWiredTools into state + actions via useBetween singleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useWiredTools backs 20 consumers with a 618-line wide state + actions surface; split it along the read/write seam so it's clear at the import site whether a view is rendering Wired data or mutating it. Because the actions need access to setters (setUserVariableAssignments, setFurniVariableAssignments, ...), this isn't the same pure-action shape as doorbell/friend-request. Used the useBetween singleton indirection instead: - useWiredToolsStore (internal) — the entire previous useWiredToolsState body, untouched. State + listeners + effects + actions in one closure. - useWiredToolsState (public, read-only) — useBetween(useWiredToolsStore) filtered to the 12 state fields (accountPreferences, roomSettings, showInspect/Toolbar booleans, variable definitions+assignments, areUserVariablesLoaded). - useWiredToolsActions (public, imperative) — same singleton filtered to the 13 actions (updateAccountPreferences, saveRoomSettings, requestUserVariables, assignXxx/removeXxx/updateXxx variable helpers, openMonitor / openInspectionForFurni / openInspectionForUser). - useWiredTools (deprecated shim) — composes both, preserves the full historical shape so the 20 existing consumers keep working. useBetween ensures all four entry points hit the same instance, so the state + dispatch loop stays a single source of truth. This is also the shape that a future migration to a Zustand slice would inherit cleanly — each public hook becomes a slice subscription. --- src/hooks/wired-tools/index.ts | 3 + src/hooks/wired-tools/useWiredTools.ts | 628 +---------------- src/hooks/wired-tools/useWiredToolsActions.ts | 47 ++ src/hooks/wired-tools/useWiredToolsState.ts | 44 ++ src/hooks/wired-tools/useWiredToolsStore.ts | 630 ++++++++++++++++++ 5 files changed, 738 insertions(+), 614 deletions(-) create mode 100644 src/hooks/wired-tools/useWiredToolsActions.ts create mode 100644 src/hooks/wired-tools/useWiredToolsState.ts create mode 100644 src/hooks/wired-tools/useWiredToolsStore.ts diff --git a/src/hooks/wired-tools/index.ts b/src/hooks/wired-tools/index.ts index 31c7739..c5c55d4 100644 --- a/src/hooks/wired-tools/index.ts +++ b/src/hooks/wired-tools/index.ts @@ -1 +1,4 @@ export * from './useWiredTools'; +export * from './useWiredToolsActions'; +export * from './useWiredToolsState'; +export * from './useWiredToolsStore'; diff --git a/src/hooks/wired-tools/useWiredTools.ts b/src/hooks/wired-tools/useWiredTools.ts index bac47ef..88805f4 100644 --- a/src/hooks/wired-tools/useWiredTools.ts +++ b/src/hooks/wired-tools/useWiredTools.ts @@ -1,618 +1,18 @@ -import { CreateLinkEvent, GetSessionDataManager, WiredRoomSettingsDataEvent, WiredRoomSettingsRequestComposer, WiredRoomSettingsSaveComposer, WiredUserVariableManageComposer, WiredUserVariableUpdateComposer, WiredUserVariablesDataEvent, WiredUserVariablesRequestComposer } from '@nitrots/nitro-renderer'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { useBetween } from 'use-between'; -import { LocalizeText, NotificationAlertType, SendMessageComposer } from '../../api'; -import { useMessageEvent } from '../events'; -import { useNotification } from '../notification'; -import { useRoom } from '../rooms'; +import { useWiredToolsActions } from './useWiredToolsActions'; +import { useWiredToolsState } from './useWiredToolsState'; -export interface IWiredAccountPreferences +export type { IWiredAccountPreferences, IWiredContextVariableDefinition, IWiredFurniVariableAssignment, IWiredFurniVariableDefinition, IWiredRoomSettings, IWiredRoomVariableAssignment, IWiredRoomVariableDefinition, IWiredUserVariableAssignment, IWiredUserVariableDefinition } from './useWiredToolsStore'; + +/** + * @deprecated Prefer `useWiredToolsState` (read-only) and + * `useWiredToolsActions` (imperative) directly. This shim composes + * both into the historical `useWiredTools()` shape so the ~20 + * existing consumers keep working unchanged. + */ +export const useWiredTools = () => { - showInspectButton: boolean; - showSystemNotifications: boolean; - showToolbarButton: boolean; -} + const state = useWiredToolsState(); + const actions = useWiredToolsActions(); -export interface IWiredRoomSettings -{ - canInspect: boolean; - canManageSettings: boolean; - canModify: boolean; - inspectMask: number; - isLoaded: boolean; - modifyMask: number; - roomId: number; -} - -export interface IWiredUserVariableDefinition -{ - availability: number; - hasValue: boolean; - isReadOnly?: boolean; - isTextConnected: boolean; - itemId: number; - name: string; -} - -export interface IWiredUserVariableAssignment -{ - createdAt: number; - hasValue: boolean; - updatedAt: number; - value: number | null; - variableItemId: number; -} - -export interface IWiredFurniVariableDefinition -{ - availability: number; - hasValue: boolean; - isReadOnly?: boolean; - isTextConnected: boolean; - itemId: number; - name: string; -} - -export interface IWiredFurniVariableAssignment -{ - createdAt: number; - hasValue: boolean; - updatedAt: number; - value: number | null; - variableItemId: number; -} - -export interface IWiredRoomVariableDefinition -{ - availability: number; - hasValue: boolean; - isReadOnly?: boolean; - isTextConnected: boolean; - itemId: number; - name: string; -} - -export interface IWiredRoomVariableAssignment -{ - createdAt: number; - hasValue: boolean; - updatedAt: number; - value: number | null; - variableItemId: number; -} - -export interface IWiredContextVariableDefinition -{ - availability: number; - hasValue: boolean; - isReadOnly?: boolean; - isTextConnected: boolean; - itemId: number; - name: string; -} - -const WIRED_VARIABLE_TARGET_USER = 0; -const WIRED_VARIABLE_TARGET_FURNI = 1; -const WIRED_VARIABLE_TARGET_ROOM = 3; -const WIRED_VARIABLE_MANAGE_ACTION_ASSIGN = 0; -const WIRED_VARIABLE_MANAGE_ACTION_REMOVE = 1; - -const WIRED_TOOLS_STORAGE_PREFIX = 'nitro.wired.tools.preferences'; -const getCurrentUnixTime = () => Math.floor(Date.now() / 1000); -const DEFAULT_ACCOUNT_PREFERENCES: IWiredAccountPreferences = { - showToolbarButton: false, - showInspectButton: false, - showSystemNotifications: false + return { ...state, ...actions }; }; - -const DEFAULT_ROOM_SETTINGS: IWiredRoomSettings = { - roomId: 0, - inspectMask: 0, - modifyMask: 0, - canInspect: false, - canModify: false, - canManageSettings: false, - isLoaded: false -}; - -const useWiredToolsState = () => -{ - const { roomSession = null } = useRoom(); - const { simpleAlert = null } = useNotification(); - const [ accountPreferences, setAccountPreferences ] = useState(DEFAULT_ACCOUNT_PREFERENCES); - const [ roomSettings, setRoomSettings ] = useState(DEFAULT_ROOM_SETTINGS); - const [ userVariableDefinitions, setUserVariableDefinitions ] = useState([]); - const [ userVariableAssignments, setUserVariableAssignments ] = useState>({}); - const [ furniVariableDefinitions, setFurniVariableDefinitions ] = useState([]); - const [ furniVariableAssignments, setFurniVariableAssignments ] = useState>({}); - const [ roomVariableDefinitions, setRoomVariableDefinitions ] = useState([]); - const [ roomVariableAssignments, setRoomVariableAssignments ] = useState([]); - const [ contextVariableDefinitions, setContextVariableDefinitions ] = useState([]); - const [ areUserVariablesLoaded, setAreUserVariablesLoaded ] = useState(false); - - const storageKey = useMemo(() => - { - const userId = GetSessionDataManager().userId; - - return `${ WIRED_TOOLS_STORAGE_PREFIX }.${ userId || 'guest' }`; - }, []); - - useEffect(() => - { - try - { - const rawValue = window.localStorage.getItem(storageKey); - - if(!rawValue) - { - setAccountPreferences(DEFAULT_ACCOUNT_PREFERENCES); - return; - } - - const parsedValue = JSON.parse(rawValue) as Partial; - - setAccountPreferences({ - ...DEFAULT_ACCOUNT_PREFERENCES, - ...(parsedValue || {}) - }); - } - catch - { - setAccountPreferences(DEFAULT_ACCOUNT_PREFERENCES); - } - }, [ storageKey ]); - - useEffect(() => - { - try - { - window.localStorage.setItem(storageKey, JSON.stringify(accountPreferences)); - } - catch - { - } - }, [ accountPreferences, storageKey ]); - - useEffect(() => - { - if(!roomSession?.roomId) - { - setRoomSettings(DEFAULT_ROOM_SETTINGS); - setUserVariableDefinitions([]); - setUserVariableAssignments({}); - setFurniVariableDefinitions([]); - setFurniVariableAssignments({}); - setRoomVariableDefinitions([]); - setRoomVariableAssignments([]); - setContextVariableDefinitions([]); - setAreUserVariablesLoaded(false); - return; - } - - setRoomSettings(prevValue => ({ - ...DEFAULT_ROOM_SETTINGS, - roomId: roomSession.roomId, - canInspect: prevValue.roomId === roomSession.roomId ? prevValue.canInspect : false, - canModify: prevValue.roomId === roomSession.roomId ? prevValue.canModify : false, - canManageSettings: prevValue.roomId === roomSession.roomId ? prevValue.canManageSettings : false - })); - - SendMessageComposer(new WiredRoomSettingsRequestComposer()); - }, [ roomSession?.roomId ]); - - useEffect(() => - { - if(!roomSession?.roomId || !roomSettings.canInspect) - { - setUserVariableDefinitions([]); - setUserVariableAssignments({}); - setFurniVariableDefinitions([]); - setFurniVariableAssignments({}); - setRoomVariableDefinitions([]); - setRoomVariableAssignments([]); - setContextVariableDefinitions([]); - setAreUserVariablesLoaded(false); - return; - } - - SendMessageComposer(new WiredUserVariablesRequestComposer()); - }, [ roomSession?.roomId, roomSettings.canInspect ]); - - useMessageEvent(WiredRoomSettingsDataEvent, event => - { - const parser = event.getParser(); - - if(roomSession?.roomId && parser.roomId && (parser.roomId !== roomSession.roomId)) return; - - setRoomSettings({ - roomId: parser.roomId, - inspectMask: parser.inspectMask, - modifyMask: parser.modifyMask, - canInspect: parser.canInspect, - canModify: parser.canModify, - canManageSettings: parser.canManageSettings, - isLoaded: true - }); - }); - - useMessageEvent(WiredUserVariablesDataEvent, event => - { - const parser = event.getParser(); - - if(roomSession?.roomId && parser.roomId && (parser.roomId !== roomSession.roomId)) return; - - const nextAssignments: Record = {}; - const nextFurniAssignments: Record = {}; - - for(const userEntry of (parser.users || [])) - { - nextAssignments[userEntry.userId] = [ ...(userEntry.assignments || []) ]; - } - - for(const furniEntry of (parser.furnis || [])) - { - nextFurniAssignments[furniEntry.furniId] = [ ...(furniEntry.assignments || []) ]; - } - - setUserVariableDefinitions([ ...(parser.definitions || []) ]); - setUserVariableAssignments(nextAssignments); - setFurniVariableDefinitions([ ...(parser.furniDefinitions || []) ]); - setFurniVariableAssignments(nextFurniAssignments); - setRoomVariableDefinitions([ ...(parser.roomDefinitions || []) ]); - setRoomVariableAssignments([ ...(parser.roomAssignments || []) ]); - setContextVariableDefinitions([ ...(parser.contextDefinitions || []) ]); - setAreUserVariablesLoaded(true); - }); - - const updateAccountPreferences = useCallback((partialPreferences: Partial) => - { - setAccountPreferences(prevValue => ({ - ...prevValue, - ...partialPreferences - })); - }, []); - - const saveRoomSettings = useCallback((inspectMask: number, modifyMask: number) => - { - if(!roomSettings.canManageSettings) return; - - setRoomSettings(prevValue => ({ - ...prevValue, - inspectMask, - modifyMask - })); - - SendMessageComposer(new WiredRoomSettingsSaveComposer(inspectMask, modifyMask)); - }, [ roomSettings.canManageSettings ]); - - const requestUserVariables = useCallback(() => - { - if(!roomSettings.canInspect) return; - - SendMessageComposer(new WiredUserVariablesRequestComposer()); - }, [ roomSettings.canInspect ]); - - const updateUserVariableValue = useCallback((userId: number, variableItemId: number, value: number) => - { - if(!roomSettings.canModify) return; - - setUserVariableAssignments(prevValue => - { - const existingAssignments = prevValue[userId]; - - if(!existingAssignments?.length) return prevValue; - - let didChange = false; - const nextAssignments = existingAssignments.map(assignment => - { - if(assignment.variableItemId !== variableItemId) return assignment; - - didChange = true; - - return { - ...assignment, - hasValue: true, - value, - updatedAt: getCurrentUnixTime() - }; - }); - - if(!didChange) return prevValue; - - return { - ...prevValue, - [userId]: nextAssignments - }; - }); - - SendMessageComposer(new WiredUserVariableUpdateComposer(WIRED_VARIABLE_TARGET_USER, userId, variableItemId, value)); - }, [ roomSettings.canModify ]); - - const updateFurniVariableValue = useCallback((furniId: number, variableItemId: number, value: number) => - { - if(!roomSettings.canModify) return; - - setFurniVariableAssignments(prevValue => - { - const existingAssignments = prevValue[furniId]; - - if(!existingAssignments?.length) return prevValue; - - let didChange = false; - const nextAssignments = existingAssignments.map(assignment => - { - if(assignment.variableItemId !== variableItemId) return assignment; - - didChange = true; - - return { - ...assignment, - hasValue: true, - value, - updatedAt: getCurrentUnixTime() - }; - }); - - if(!didChange) return prevValue; - - return { - ...prevValue, - [furniId]: nextAssignments - }; - }); - - SendMessageComposer(new WiredUserVariableUpdateComposer(WIRED_VARIABLE_TARGET_FURNI, furniId, variableItemId, value)); - }, [ roomSettings.canModify ]); - - const updateRoomVariableValue = useCallback((variableItemId: number, value: number) => - { - if(!roomSettings.canModify) return; - - setRoomVariableAssignments(prevValue => - { - const now = getCurrentUnixTime(); - let didChange = false; - const nextAssignments = prevValue.map(assignment => - { - if(assignment.variableItemId !== variableItemId) return assignment; - - didChange = true; - - return { - ...assignment, - hasValue: true, - value, - updatedAt: now - }; - }); - - if(didChange) return nextAssignments; - - return [ - ...prevValue, - { - variableItemId, - hasValue: true, - value, - createdAt: 0, - updatedAt: now - } - ]; - }); - - SendMessageComposer(new WiredUserVariableUpdateComposer(WIRED_VARIABLE_TARGET_ROOM, roomSettings.roomId, variableItemId, value)); - }, [ roomSettings.canModify, roomSettings.roomId ]); - const assignUserVariable = useCallback((userId: number, variableItemId: number, value: number) => - { - if(!roomSettings.canModify) return; - - const definition = userVariableDefinitions.find(entry => (entry.itemId === variableItemId)); - - if(!definition) return; - - const now = getCurrentUnixTime(); - const normalizedValue = (definition.hasValue ? value : null); - - setUserVariableAssignments(prevValue => - { - const existingAssignments = [ ...(prevValue[userId] || []) ]; - const existingIndex = existingAssignments.findIndex(assignment => (assignment.variableItemId === variableItemId)); - - if(existingIndex >= 0) - { - const existingAssignment = existingAssignments[existingIndex]; - - existingAssignments[existingIndex] = { - ...existingAssignment, - hasValue: definition.hasValue, - value: normalizedValue, - updatedAt: now - }; - } - else - { - existingAssignments.push({ - variableItemId, - hasValue: definition.hasValue, - value: normalizedValue, - createdAt: now, - updatedAt: now - }); - } - - return { - ...prevValue, - [userId]: existingAssignments - }; - }); - - SendMessageComposer(new WiredUserVariableManageComposer(WIRED_VARIABLE_MANAGE_ACTION_ASSIGN, WIRED_VARIABLE_TARGET_USER, userId, variableItemId, Number(normalizedValue ?? 0))); - }, [ roomSettings.canModify, userVariableDefinitions ]); - const removeUserVariable = useCallback((userId: number, variableItemId: number) => - { - if(!roomSettings.canModify) return; - - setUserVariableAssignments(prevValue => - { - const existingAssignments = prevValue[userId]; - - if(!existingAssignments?.length) return prevValue; - - const nextAssignments = existingAssignments.filter(assignment => (assignment.variableItemId !== variableItemId)); - - if(nextAssignments.length === existingAssignments.length) return prevValue; - - const nextValue = { ...prevValue }; - - if(nextAssignments.length) nextValue[userId] = nextAssignments; - else delete nextValue[userId]; - - return nextValue; - }); - - SendMessageComposer(new WiredUserVariableManageComposer(WIRED_VARIABLE_MANAGE_ACTION_REMOVE, WIRED_VARIABLE_TARGET_USER, userId, variableItemId, 0)); - }, [ roomSettings.canModify ]); - const assignFurniVariable = useCallback((furniId: number, variableItemId: number, value: number) => - { - if(!roomSettings.canModify) return; - - const definition = furniVariableDefinitions.find(entry => (entry.itemId === variableItemId)); - - if(!definition) return; - - const now = getCurrentUnixTime(); - const normalizedValue = (definition.hasValue ? value : null); - - setFurniVariableAssignments(prevValue => - { - const existingAssignments = [ ...(prevValue[furniId] || []) ]; - const existingIndex = existingAssignments.findIndex(assignment => (assignment.variableItemId === variableItemId)); - - if(existingIndex >= 0) - { - const existingAssignment = existingAssignments[existingIndex]; - - existingAssignments[existingIndex] = { - ...existingAssignment, - hasValue: definition.hasValue, - value: normalizedValue, - updatedAt: now - }; - } - else - { - existingAssignments.push({ - variableItemId, - hasValue: definition.hasValue, - value: normalizedValue, - createdAt: now, - updatedAt: now - }); - } - - return { - ...prevValue, - [furniId]: existingAssignments - }; - }); - - SendMessageComposer(new WiredUserVariableManageComposer(WIRED_VARIABLE_MANAGE_ACTION_ASSIGN, WIRED_VARIABLE_TARGET_FURNI, furniId, variableItemId, Number(normalizedValue ?? 0))); - }, [ furniVariableDefinitions, roomSettings.canModify ]); - const removeFurniVariable = useCallback((furniId: number, variableItemId: number) => - { - if(!roomSettings.canModify) return; - - setFurniVariableAssignments(prevValue => - { - const existingAssignments = prevValue[furniId]; - - if(!existingAssignments?.length) return prevValue; - - const nextAssignments = existingAssignments.filter(assignment => (assignment.variableItemId !== variableItemId)); - - if(nextAssignments.length === existingAssignments.length) return prevValue; - - const nextValue = { ...prevValue }; - - if(nextAssignments.length) nextValue[furniId] = nextAssignments; - else delete nextValue[furniId]; - - return nextValue; - }); - - SendMessageComposer(new WiredUserVariableManageComposer(WIRED_VARIABLE_MANAGE_ACTION_REMOVE, WIRED_VARIABLE_TARGET_FURNI, furniId, variableItemId, 0)); - }, [ roomSettings.canModify ]); - - const showInvalidRoomAlert = useCallback(() => - { - if(!simpleAlert) return; - - simpleAlert(LocalizeText('wiredmenu.invalid_room.desc'), NotificationAlertType.ALERT, null, null, LocalizeText('generic.alert.title')); - }, [ simpleAlert ]); - - const openMonitor = useCallback(() => - { - if(!roomSettings.canInspect) - { - showInvalidRoomAlert(); - return; - } - - CreateLinkEvent('wired-tools/show'); - }, [ roomSettings.canInspect, showInvalidRoomAlert ]); - - const openInspectionForFurni = useCallback((objectId: number, category: number) => - { - if(!roomSettings.canInspect) - { - showInvalidRoomAlert(); - return; - } - - CreateLinkEvent(`wired-tools/inspection/furni/${ objectId }/${ category }`); - }, [ roomSettings.canInspect, showInvalidRoomAlert ]); - - const openInspectionForUser = useCallback((roomIndex: number) => - { - if(!roomSettings.canInspect) - { - showInvalidRoomAlert(); - return; - } - - CreateLinkEvent(`wired-tools/inspection/user/${ roomIndex }`); - }, [ roomSettings.canInspect, showInvalidRoomAlert ]); - - const showToolbarButton = !!roomSession?.roomId && roomSettings.canInspect && accountPreferences.showToolbarButton; - const showInspectButton = !!roomSession?.roomId && roomSettings.canInspect && accountPreferences.showInspectButton; - - return { - accountPreferences, - roomSettings, - showInspectButton, - showToolbarButton, - userVariableDefinitions, - userVariableAssignments, - furniVariableDefinitions, - furniVariableAssignments, - roomVariableDefinitions, - roomVariableAssignments, - contextVariableDefinitions, - areUserVariablesLoaded, - updateAccountPreferences, - saveRoomSettings, - requestUserVariables, - assignUserVariable, - removeUserVariable, - updateUserVariableValue, - assignFurniVariable, - removeFurniVariable, - updateFurniVariableValue, - updateRoomVariableValue, - openMonitor, - openInspectionForFurni, - openInspectionForUser - }; -}; - -export const useWiredTools = () => useBetween(useWiredToolsState); diff --git a/src/hooks/wired-tools/useWiredToolsActions.ts b/src/hooks/wired-tools/useWiredToolsActions.ts new file mode 100644 index 0000000..6549977 --- /dev/null +++ b/src/hooks/wired-tools/useWiredToolsActions.ts @@ -0,0 +1,47 @@ +import { useBetween } from 'use-between'; +import { useWiredToolsStore } from './useWiredToolsStore'; + +/** + * Imperative slice of the Wired tools store: state-mutating actions + * (assignXxx / removeXxx / updateXxx) plus the link-event openers + * (openMonitor / openInspectionForFurni / openInspectionForUser) and + * the persistence helpers (saveRoomSettings, updateAccountPreferences, + * requestUserVariables). + * + * Stays separate from useWiredToolsState so components that only need + * to trigger Wired actions don't have to pull in the full state shape. + */ +export const useWiredToolsActions = () => +{ + const { + updateAccountPreferences, + saveRoomSettings, + requestUserVariables, + assignUserVariable, + removeUserVariable, + updateUserVariableValue, + assignFurniVariable, + removeFurniVariable, + updateFurniVariableValue, + updateRoomVariableValue, + openMonitor, + openInspectionForFurni, + openInspectionForUser + } = useBetween(useWiredToolsStore); + + return { + updateAccountPreferences, + saveRoomSettings, + requestUserVariables, + assignUserVariable, + removeUserVariable, + updateUserVariableValue, + assignFurniVariable, + removeFurniVariable, + updateFurniVariableValue, + updateRoomVariableValue, + openMonitor, + openInspectionForFurni, + openInspectionForUser + }; +}; diff --git a/src/hooks/wired-tools/useWiredToolsState.ts b/src/hooks/wired-tools/useWiredToolsState.ts new file mode 100644 index 0000000..1d48cd2 --- /dev/null +++ b/src/hooks/wired-tools/useWiredToolsState.ts @@ -0,0 +1,44 @@ +import { useBetween } from 'use-between'; +import { useWiredToolsStore } from './useWiredToolsStore'; + +/** + * Read-only slice of the Wired tools store: account preferences, + * room-settings flags, variable definitions / assignments, plus the + * two derived 'should show X button' booleans. + * + * Components that only need to render Wired state subscribe through + * this hook so it's easy to grep for read-only consumers vs. the + * imperative ones (which use useWiredToolsActions). + */ +export const useWiredToolsState = () => +{ + const { + accountPreferences, + roomSettings, + showInspectButton, + showToolbarButton, + userVariableDefinitions, + userVariableAssignments, + furniVariableDefinitions, + furniVariableAssignments, + roomVariableDefinitions, + roomVariableAssignments, + contextVariableDefinitions, + areUserVariablesLoaded + } = useBetween(useWiredToolsStore); + + return { + accountPreferences, + roomSettings, + showInspectButton, + showToolbarButton, + userVariableDefinitions, + userVariableAssignments, + furniVariableDefinitions, + furniVariableAssignments, + roomVariableDefinitions, + roomVariableAssignments, + contextVariableDefinitions, + areUserVariablesLoaded + }; +}; diff --git a/src/hooks/wired-tools/useWiredToolsStore.ts b/src/hooks/wired-tools/useWiredToolsStore.ts new file mode 100644 index 0000000..e24ff13 --- /dev/null +++ b/src/hooks/wired-tools/useWiredToolsStore.ts @@ -0,0 +1,630 @@ +import { CreateLinkEvent, GetSessionDataManager, WiredRoomSettingsDataEvent, WiredRoomSettingsRequestComposer, WiredRoomSettingsSaveComposer, WiredUserVariableManageComposer, WiredUserVariableUpdateComposer, WiredUserVariablesDataEvent, WiredUserVariablesRequestComposer } from '@nitrots/nitro-renderer'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, NotificationAlertType, SendMessageComposer } from '../../api'; +import { useMessageEvent } from '../events'; +import { useNotification } from '../notification'; +import { useRoom } from '../rooms'; + +export interface IWiredAccountPreferences +{ + showInspectButton: boolean; + showSystemNotifications: boolean; + showToolbarButton: boolean; +} + +export interface IWiredRoomSettings +{ + canInspect: boolean; + canManageSettings: boolean; + canModify: boolean; + inspectMask: number; + isLoaded: boolean; + modifyMask: number; + roomId: number; +} + +export interface IWiredUserVariableDefinition +{ + availability: number; + hasValue: boolean; + isReadOnly?: boolean; + isTextConnected: boolean; + itemId: number; + name: string; +} + +export interface IWiredUserVariableAssignment +{ + createdAt: number; + hasValue: boolean; + updatedAt: number; + value: number | null; + variableItemId: number; +} + +export interface IWiredFurniVariableDefinition +{ + availability: number; + hasValue: boolean; + isReadOnly?: boolean; + isTextConnected: boolean; + itemId: number; + name: string; +} + +export interface IWiredFurniVariableAssignment +{ + createdAt: number; + hasValue: boolean; + updatedAt: number; + value: number | null; + variableItemId: number; +} + +export interface IWiredRoomVariableDefinition +{ + availability: number; + hasValue: boolean; + isReadOnly?: boolean; + isTextConnected: boolean; + itemId: number; + name: string; +} + +export interface IWiredRoomVariableAssignment +{ + createdAt: number; + hasValue: boolean; + updatedAt: number; + value: number | null; + variableItemId: number; +} + +export interface IWiredContextVariableDefinition +{ + availability: number; + hasValue: boolean; + isReadOnly?: boolean; + isTextConnected: boolean; + itemId: number; + name: string; +} + +const WIRED_VARIABLE_TARGET_USER = 0; +const WIRED_VARIABLE_TARGET_FURNI = 1; +const WIRED_VARIABLE_TARGET_ROOM = 3; +const WIRED_VARIABLE_MANAGE_ACTION_ASSIGN = 0; +const WIRED_VARIABLE_MANAGE_ACTION_REMOVE = 1; + +const WIRED_TOOLS_STORAGE_PREFIX = 'nitro.wired.tools.preferences'; +const getCurrentUnixTime = () => Math.floor(Date.now() / 1000); +const DEFAULT_ACCOUNT_PREFERENCES: IWiredAccountPreferences = { + showToolbarButton: false, + showInspectButton: false, + showSystemNotifications: false +}; + +const DEFAULT_ROOM_SETTINGS: IWiredRoomSettings = { + roomId: 0, + inspectMask: 0, + modifyMask: 0, + canInspect: false, + canModify: false, + canManageSettings: false, + isLoaded: false +}; + +/** + * Internal singleton state-+-actions hook for the Wired tools subsystem. + * Public consumers should NOT call this directly — go through + * useWiredToolsState (read-only slice) or useWiredToolsActions + * (imperative slice). useWiredTools is the legacy shim that composes + * both into the original shape. + * + * Wrapped in useBetween at the public-hook layer so every consumer in + * the tree sees the same instance (matches the previous + * useBetween(useWiredToolsState) wiring). + */ +export const useWiredToolsStore = () => +{ + const { roomSession = null } = useRoom(); + const { simpleAlert = null } = useNotification(); + const [ accountPreferences, setAccountPreferences ] = useState(DEFAULT_ACCOUNT_PREFERENCES); + const [ roomSettings, setRoomSettings ] = useState(DEFAULT_ROOM_SETTINGS); + const [ userVariableDefinitions, setUserVariableDefinitions ] = useState([]); + const [ userVariableAssignments, setUserVariableAssignments ] = useState>({}); + const [ furniVariableDefinitions, setFurniVariableDefinitions ] = useState([]); + const [ furniVariableAssignments, setFurniVariableAssignments ] = useState>({}); + const [ roomVariableDefinitions, setRoomVariableDefinitions ] = useState([]); + const [ roomVariableAssignments, setRoomVariableAssignments ] = useState([]); + const [ contextVariableDefinitions, setContextVariableDefinitions ] = useState([]); + const [ areUserVariablesLoaded, setAreUserVariablesLoaded ] = useState(false); + + const storageKey = useMemo(() => + { + const userId = GetSessionDataManager().userId; + + return `${ WIRED_TOOLS_STORAGE_PREFIX }.${ userId || 'guest' }`; + }, []); + + useEffect(() => + { + try + { + const rawValue = window.localStorage.getItem(storageKey); + + if(!rawValue) + { + setAccountPreferences(DEFAULT_ACCOUNT_PREFERENCES); + return; + } + + const parsedValue = JSON.parse(rawValue) as Partial; + + setAccountPreferences({ + ...DEFAULT_ACCOUNT_PREFERENCES, + ...(parsedValue || {}) + }); + } + catch + { + setAccountPreferences(DEFAULT_ACCOUNT_PREFERENCES); + } + }, [ storageKey ]); + + useEffect(() => + { + try + { + window.localStorage.setItem(storageKey, JSON.stringify(accountPreferences)); + } + catch + { + } + }, [ accountPreferences, storageKey ]); + + useEffect(() => + { + if(!roomSession?.roomId) + { + setRoomSettings(DEFAULT_ROOM_SETTINGS); + setUserVariableDefinitions([]); + setUserVariableAssignments({}); + setFurniVariableDefinitions([]); + setFurniVariableAssignments({}); + setRoomVariableDefinitions([]); + setRoomVariableAssignments([]); + setContextVariableDefinitions([]); + setAreUserVariablesLoaded(false); + return; + } + + setRoomSettings(prevValue => ({ + ...DEFAULT_ROOM_SETTINGS, + roomId: roomSession.roomId, + canInspect: prevValue.roomId === roomSession.roomId ? prevValue.canInspect : false, + canModify: prevValue.roomId === roomSession.roomId ? prevValue.canModify : false, + canManageSettings: prevValue.roomId === roomSession.roomId ? prevValue.canManageSettings : false + })); + + SendMessageComposer(new WiredRoomSettingsRequestComposer()); + }, [ roomSession?.roomId ]); + + useEffect(() => + { + if(!roomSession?.roomId || !roomSettings.canInspect) + { + setUserVariableDefinitions([]); + setUserVariableAssignments({}); + setFurniVariableDefinitions([]); + setFurniVariableAssignments({}); + setRoomVariableDefinitions([]); + setRoomVariableAssignments([]); + setContextVariableDefinitions([]); + setAreUserVariablesLoaded(false); + return; + } + + SendMessageComposer(new WiredUserVariablesRequestComposer()); + }, [ roomSession?.roomId, roomSettings.canInspect ]); + + useMessageEvent(WiredRoomSettingsDataEvent, event => + { + const parser = event.getParser(); + + if(roomSession?.roomId && parser.roomId && (parser.roomId !== roomSession.roomId)) return; + + setRoomSettings({ + roomId: parser.roomId, + inspectMask: parser.inspectMask, + modifyMask: parser.modifyMask, + canInspect: parser.canInspect, + canModify: parser.canModify, + canManageSettings: parser.canManageSettings, + isLoaded: true + }); + }); + + useMessageEvent(WiredUserVariablesDataEvent, event => + { + const parser = event.getParser(); + + if(roomSession?.roomId && parser.roomId && (parser.roomId !== roomSession.roomId)) return; + + const nextAssignments: Record = {}; + const nextFurniAssignments: Record = {}; + + for(const userEntry of (parser.users || [])) + { + nextAssignments[userEntry.userId] = [ ...(userEntry.assignments || []) ]; + } + + for(const furniEntry of (parser.furnis || [])) + { + nextFurniAssignments[furniEntry.furniId] = [ ...(furniEntry.assignments || []) ]; + } + + setUserVariableDefinitions([ ...(parser.definitions || []) ]); + setUserVariableAssignments(nextAssignments); + setFurniVariableDefinitions([ ...(parser.furniDefinitions || []) ]); + setFurniVariableAssignments(nextFurniAssignments); + setRoomVariableDefinitions([ ...(parser.roomDefinitions || []) ]); + setRoomVariableAssignments([ ...(parser.roomAssignments || []) ]); + setContextVariableDefinitions([ ...(parser.contextDefinitions || []) ]); + setAreUserVariablesLoaded(true); + }); + + const updateAccountPreferences = useCallback((partialPreferences: Partial) => + { + setAccountPreferences(prevValue => ({ + ...prevValue, + ...partialPreferences + })); + }, []); + + const saveRoomSettings = useCallback((inspectMask: number, modifyMask: number) => + { + if(!roomSettings.canManageSettings) return; + + setRoomSettings(prevValue => ({ + ...prevValue, + inspectMask, + modifyMask + })); + + SendMessageComposer(new WiredRoomSettingsSaveComposer(inspectMask, modifyMask)); + }, [ roomSettings.canManageSettings ]); + + const requestUserVariables = useCallback(() => + { + if(!roomSettings.canInspect) return; + + SendMessageComposer(new WiredUserVariablesRequestComposer()); + }, [ roomSettings.canInspect ]); + + const updateUserVariableValue = useCallback((userId: number, variableItemId: number, value: number) => + { + if(!roomSettings.canModify) return; + + setUserVariableAssignments(prevValue => + { + const existingAssignments = prevValue[userId]; + + if(!existingAssignments?.length) return prevValue; + + let didChange = false; + const nextAssignments = existingAssignments.map(assignment => + { + if(assignment.variableItemId !== variableItemId) return assignment; + + didChange = true; + + return { + ...assignment, + hasValue: true, + value, + updatedAt: getCurrentUnixTime() + }; + }); + + if(!didChange) return prevValue; + + return { + ...prevValue, + [userId]: nextAssignments + }; + }); + + SendMessageComposer(new WiredUserVariableUpdateComposer(WIRED_VARIABLE_TARGET_USER, userId, variableItemId, value)); + }, [ roomSettings.canModify ]); + + const updateFurniVariableValue = useCallback((furniId: number, variableItemId: number, value: number) => + { + if(!roomSettings.canModify) return; + + setFurniVariableAssignments(prevValue => + { + const existingAssignments = prevValue[furniId]; + + if(!existingAssignments?.length) return prevValue; + + let didChange = false; + const nextAssignments = existingAssignments.map(assignment => + { + if(assignment.variableItemId !== variableItemId) return assignment; + + didChange = true; + + return { + ...assignment, + hasValue: true, + value, + updatedAt: getCurrentUnixTime() + }; + }); + + if(!didChange) return prevValue; + + return { + ...prevValue, + [furniId]: nextAssignments + }; + }); + + SendMessageComposer(new WiredUserVariableUpdateComposer(WIRED_VARIABLE_TARGET_FURNI, furniId, variableItemId, value)); + }, [ roomSettings.canModify ]); + + const updateRoomVariableValue = useCallback((variableItemId: number, value: number) => + { + if(!roomSettings.canModify) return; + + setRoomVariableAssignments(prevValue => + { + const now = getCurrentUnixTime(); + let didChange = false; + const nextAssignments = prevValue.map(assignment => + { + if(assignment.variableItemId !== variableItemId) return assignment; + + didChange = true; + + return { + ...assignment, + hasValue: true, + value, + updatedAt: now + }; + }); + + if(didChange) return nextAssignments; + + return [ + ...prevValue, + { + variableItemId, + hasValue: true, + value, + createdAt: 0, + updatedAt: now + } + ]; + }); + + SendMessageComposer(new WiredUserVariableUpdateComposer(WIRED_VARIABLE_TARGET_ROOM, roomSettings.roomId, variableItemId, value)); + }, [ roomSettings.canModify, roomSettings.roomId ]); + + const assignUserVariable = useCallback((userId: number, variableItemId: number, value: number) => + { + if(!roomSettings.canModify) return; + + const definition = userVariableDefinitions.find(entry => (entry.itemId === variableItemId)); + + if(!definition) return; + + const now = getCurrentUnixTime(); + const normalizedValue = (definition.hasValue ? value : null); + + setUserVariableAssignments(prevValue => + { + const existingAssignments = [ ...(prevValue[userId] || []) ]; + const existingIndex = existingAssignments.findIndex(assignment => (assignment.variableItemId === variableItemId)); + + if(existingIndex >= 0) + { + const existingAssignment = existingAssignments[existingIndex]; + + existingAssignments[existingIndex] = { + ...existingAssignment, + hasValue: definition.hasValue, + value: normalizedValue, + updatedAt: now + }; + } + else + { + existingAssignments.push({ + variableItemId, + hasValue: definition.hasValue, + value: normalizedValue, + createdAt: now, + updatedAt: now + }); + } + + return { + ...prevValue, + [userId]: existingAssignments + }; + }); + + SendMessageComposer(new WiredUserVariableManageComposer(WIRED_VARIABLE_MANAGE_ACTION_ASSIGN, WIRED_VARIABLE_TARGET_USER, userId, variableItemId, Number(normalizedValue ?? 0))); + }, [ roomSettings.canModify, userVariableDefinitions ]); + + const removeUserVariable = useCallback((userId: number, variableItemId: number) => + { + if(!roomSettings.canModify) return; + + setUserVariableAssignments(prevValue => + { + const existingAssignments = prevValue[userId]; + + if(!existingAssignments?.length) return prevValue; + + const nextAssignments = existingAssignments.filter(assignment => (assignment.variableItemId !== variableItemId)); + + if(nextAssignments.length === existingAssignments.length) return prevValue; + + const nextValue = { ...prevValue }; + + if(nextAssignments.length) nextValue[userId] = nextAssignments; + else delete nextValue[userId]; + + return nextValue; + }); + + SendMessageComposer(new WiredUserVariableManageComposer(WIRED_VARIABLE_MANAGE_ACTION_REMOVE, WIRED_VARIABLE_TARGET_USER, userId, variableItemId, 0)); + }, [ roomSettings.canModify ]); + + const assignFurniVariable = useCallback((furniId: number, variableItemId: number, value: number) => + { + if(!roomSettings.canModify) return; + + const definition = furniVariableDefinitions.find(entry => (entry.itemId === variableItemId)); + + if(!definition) return; + + const now = getCurrentUnixTime(); + const normalizedValue = (definition.hasValue ? value : null); + + setFurniVariableAssignments(prevValue => + { + const existingAssignments = [ ...(prevValue[furniId] || []) ]; + const existingIndex = existingAssignments.findIndex(assignment => (assignment.variableItemId === variableItemId)); + + if(existingIndex >= 0) + { + const existingAssignment = existingAssignments[existingIndex]; + + existingAssignments[existingIndex] = { + ...existingAssignment, + hasValue: definition.hasValue, + value: normalizedValue, + updatedAt: now + }; + } + else + { + existingAssignments.push({ + variableItemId, + hasValue: definition.hasValue, + value: normalizedValue, + createdAt: now, + updatedAt: now + }); + } + + return { + ...prevValue, + [furniId]: existingAssignments + }; + }); + + SendMessageComposer(new WiredUserVariableManageComposer(WIRED_VARIABLE_MANAGE_ACTION_ASSIGN, WIRED_VARIABLE_TARGET_FURNI, furniId, variableItemId, Number(normalizedValue ?? 0))); + }, [ furniVariableDefinitions, roomSettings.canModify ]); + + const removeFurniVariable = useCallback((furniId: number, variableItemId: number) => + { + if(!roomSettings.canModify) return; + + setFurniVariableAssignments(prevValue => + { + const existingAssignments = prevValue[furniId]; + + if(!existingAssignments?.length) return prevValue; + + const nextAssignments = existingAssignments.filter(assignment => (assignment.variableItemId !== variableItemId)); + + if(nextAssignments.length === existingAssignments.length) return prevValue; + + const nextValue = { ...prevValue }; + + if(nextAssignments.length) nextValue[furniId] = nextAssignments; + else delete nextValue[furniId]; + + return nextValue; + }); + + SendMessageComposer(new WiredUserVariableManageComposer(WIRED_VARIABLE_MANAGE_ACTION_REMOVE, WIRED_VARIABLE_TARGET_FURNI, furniId, variableItemId, 0)); + }, [ roomSettings.canModify ]); + + const showInvalidRoomAlert = useCallback(() => + { + if(!simpleAlert) return; + + simpleAlert(LocalizeText('wiredmenu.invalid_room.desc'), NotificationAlertType.ALERT, null, null, LocalizeText('generic.alert.title')); + }, [ simpleAlert ]); + + const openMonitor = useCallback(() => + { + if(!roomSettings.canInspect) + { + showInvalidRoomAlert(); + return; + } + + CreateLinkEvent('wired-tools/show'); + }, [ roomSettings.canInspect, showInvalidRoomAlert ]); + + const openInspectionForFurni = useCallback((objectId: number, category: number) => + { + if(!roomSettings.canInspect) + { + showInvalidRoomAlert(); + return; + } + + CreateLinkEvent(`wired-tools/inspection/furni/${ objectId }/${ category }`); + }, [ roomSettings.canInspect, showInvalidRoomAlert ]); + + const openInspectionForUser = useCallback((roomIndex: number) => + { + if(!roomSettings.canInspect) + { + showInvalidRoomAlert(); + return; + } + + CreateLinkEvent(`wired-tools/inspection/user/${ roomIndex }`); + }, [ roomSettings.canInspect, showInvalidRoomAlert ]); + + const showToolbarButton = !!roomSession?.roomId && roomSettings.canInspect && accountPreferences.showToolbarButton; + const showInspectButton = !!roomSession?.roomId && roomSettings.canInspect && accountPreferences.showInspectButton; + + return { + accountPreferences, + roomSettings, + showInspectButton, + showToolbarButton, + userVariableDefinitions, + userVariableAssignments, + furniVariableDefinitions, + furniVariableAssignments, + roomVariableDefinitions, + roomVariableAssignments, + contextVariableDefinitions, + areUserVariablesLoaded, + updateAccountPreferences, + saveRoomSettings, + requestUserVariables, + assignUserVariable, + removeUserVariable, + updateUserVariableValue, + assignFurniVariable, + removeFurniVariable, + updateFurniVariableValue, + updateRoomVariableValue, + openMonitor, + openInspectionForFurni, + openInspectionForUser + }; +};