Split useWiredTools into state + actions via useBetween singleton

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.
This commit is contained in:
simoleo89
2026-05-11 22:00:31 +02:00
parent a4c9dd87db
commit e1f5df6b1c
5 changed files with 738 additions and 614 deletions
+3
View File
@@ -1 +1,4 @@
export * from './useWiredTools';
export * from './useWiredToolsActions';
export * from './useWiredToolsState';
export * from './useWiredToolsStore';
+14 -614
View File
@@ -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<IWiredAccountPreferences>(DEFAULT_ACCOUNT_PREFERENCES);
const [ roomSettings, setRoomSettings ] = useState<IWiredRoomSettings>(DEFAULT_ROOM_SETTINGS);
const [ userVariableDefinitions, setUserVariableDefinitions ] = useState<IWiredUserVariableDefinition[]>([]);
const [ userVariableAssignments, setUserVariableAssignments ] = useState<Record<number, IWiredUserVariableAssignment[]>>({});
const [ furniVariableDefinitions, setFurniVariableDefinitions ] = useState<IWiredFurniVariableDefinition[]>([]);
const [ furniVariableAssignments, setFurniVariableAssignments ] = useState<Record<number, IWiredFurniVariableAssignment[]>>({});
const [ roomVariableDefinitions, setRoomVariableDefinitions ] = useState<IWiredRoomVariableDefinition[]>([]);
const [ roomVariableAssignments, setRoomVariableAssignments ] = useState<IWiredRoomVariableAssignment[]>([]);
const [ contextVariableDefinitions, setContextVariableDefinitions ] = useState<IWiredContextVariableDefinition[]>([]);
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<IWiredAccountPreferences>;
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>(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>(WiredUserVariablesDataEvent, event =>
{
const parser = event.getParser();
if(roomSession?.roomId && parser.roomId && (parser.roomId !== roomSession.roomId)) return;
const nextAssignments: Record<number, IWiredUserVariableAssignment[]> = {};
const nextFurniAssignments: Record<number, IWiredFurniVariableAssignment[]> = {};
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<IWiredAccountPreferences>) =>
{
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);
@@ -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
};
};
@@ -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
};
};
+630
View File
@@ -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<IWiredAccountPreferences>(DEFAULT_ACCOUNT_PREFERENCES);
const [ roomSettings, setRoomSettings ] = useState<IWiredRoomSettings>(DEFAULT_ROOM_SETTINGS);
const [ userVariableDefinitions, setUserVariableDefinitions ] = useState<IWiredUserVariableDefinition[]>([]);
const [ userVariableAssignments, setUserVariableAssignments ] = useState<Record<number, IWiredUserVariableAssignment[]>>({});
const [ furniVariableDefinitions, setFurniVariableDefinitions ] = useState<IWiredFurniVariableDefinition[]>([]);
const [ furniVariableAssignments, setFurniVariableAssignments ] = useState<Record<number, IWiredFurniVariableAssignment[]>>({});
const [ roomVariableDefinitions, setRoomVariableDefinitions ] = useState<IWiredRoomVariableDefinition[]>([]);
const [ roomVariableAssignments, setRoomVariableAssignments ] = useState<IWiredRoomVariableAssignment[]>([]);
const [ contextVariableDefinitions, setContextVariableDefinitions ] = useState<IWiredContextVariableDefinition[]>([]);
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<IWiredAccountPreferences>;
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>(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>(WiredUserVariablesDataEvent, event =>
{
const parser = event.getParser();
if(roomSession?.roomId && parser.roomId && (parser.roomId !== roomSession.roomId)) return;
const nextAssignments: Record<number, IWiredUserVariableAssignment[]> = {};
const nextFurniAssignments: Record<number, IWiredFurniVariableAssignment[]> = {};
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<IWiredAccountPreferences>) =>
{
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
};
};