CreateRoomSession(room.roomId) }>
{ room.roomName }
{ room.description && { room.description } }
diff --git a/src/components/wired-tools/WiredCreatorToolsView.tsx b/src/components/wired-tools/WiredCreatorToolsView.tsx
index 93280b8..7d1f539 100644
--- a/src/components/wired-tools/WiredCreatorToolsView.tsx
+++ b/src/components/wired-tools/WiredCreatorToolsView.tsx
@@ -1,16 +1,20 @@
-import { AddLinkEventTracker, AvatarExpressionEnum, FigureUpdateEvent, FurnitureFloorUpdateEvent, FurnitureMultiStateComposer, FurnitureWallMultiStateComposer, FurnitureWallUpdateComposer, FurnitureWallUpdateEvent, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, PetMoveComposer, RemoveLinkEventTracker, RoomControllerLevel, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomUnitDanceEvent, RoomUnitEffectEvent, RoomUnitExpressionEvent, RoomUnitHandItemEvent, RoomUnitInfoEvent, RoomUnitLookComposer, RoomUnitStatusEvent, RoomUnitWalkComposer, UpdateFurniturePositionComposer, Vector3d } from '@nitrots/nitro-renderer';
-import { FC, KeyboardEvent, useEffect, useMemo, useState } from 'react';
+import { AddLinkEventTracker, AvatarExpressionEnum, FigureUpdateEvent, FurnitureFloorUpdateEvent, FurnitureMultiStateComposer, FurnitureWallMultiStateComposer, FurnitureWallUpdateComposer, FurnitureWallUpdateEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker, RoomControllerLevel, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomUnitDanceEvent, RoomUnitEffectEvent, RoomUnitExpressionEvent, RoomUnitHandItemEvent, RoomUnitInfoEvent, RoomUnitStatusEvent, UpdateFurniturePositionComposer, Vector3d, WiredUserInspectMoveComposer } from '@nitrots/nitro-renderer';
+import { WiredMonitorDataEvent, WiredMonitorRequestComposer } from '@nitrots/nitro-renderer';
+import { FC, KeyboardEvent, useCallback, useEffect, useMemo, useState } from 'react';
import furniInspectionIcon from '../../assets/images/wiredtools/furni.png';
import globalInspectionIcon from '../../assets/images/wiredtools/global.png';
import userInspectionIcon from '../../assets/images/wiredtools/user.png';
+import contextInspectionIcon from '../../assets/images/wiredtools/context.png';
import wiredGlobalPlaceholderImage from '../../assets/images/wiredtools/wired_global_placeholder.png';
import wiredMonitorImage from '../../assets/images/wiredtools/wired_monitor.png';
-import { AvatarInfoFurni, AvatarInfoUtilities, LocalizeText, SendMessageComposer } from '../../api';
+import { AvatarInfoFurni, AvatarInfoUtilities, LocalizeText, NotificationAlertType, SendMessageComposer } from '../../api';
import { Button, DraggableWindowPosition, LayoutAvatarImageView, LayoutPetImageView, LayoutRoomObjectImageView, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../common';
-import { useInventoryTrade, useMessageEvent, useObjectSelectedEvent, useRoom } from '../../hooks';
+import { useInventoryTrade, useMessageEvent, useNotification, useObjectSelectedEvent, useRoom, useWiredTools } from '../../hooks';
+import { WiredToolsSettingsTabView } from './WiredToolsSettingsTabView';
type WiredToolsTab = 'monitor' | 'variables' | 'inspection' | 'chests' | 'settings';
type InspectionElementType = 'furni' | 'user' | 'global';
+type VariablesElementType = InspectionElementType | 'context';
interface InspectionElementButton
{
@@ -19,6 +23,14 @@ interface InspectionElementButton
icon: string;
}
+interface VariablesElementButton
+{
+ key: VariablesElementType;
+ label: string;
+ icon: string;
+ disabled?: boolean;
+}
+
interface InspectionFurniSelection
{
objectId: number;
@@ -74,6 +86,59 @@ interface MonitorLog
category: string;
amount: string;
latest: string;
+ latestReason: string;
+ latestSourceId: number;
+ latestSourceLabel: string;
+}
+
+interface MonitorSnapshot
+{
+ usageCurrentWindow: number;
+ usageLimitPerWindow: number;
+ isHeavy: boolean;
+ delayedEventsPending: number;
+ delayedEventsLimit: number;
+ averageExecutionMs: number;
+ peakExecutionMs: number;
+ recursionDepthCurrent: number;
+ recursionDepthLimit: number;
+ killedRemainingSeconds: number;
+ usageWindowMs: number;
+ overloadAverageThresholdMs: number;
+ overloadPeakThresholdMs: number;
+ heavyUsageThresholdPercent: number;
+ heavyConsecutiveWindowsThreshold: number;
+ overloadConsecutiveWindowsThreshold: number;
+ heavyDelayedThresholdPercent: number;
+ logs: Array<{
+ amount: number;
+ latestOccurrenceSeconds: number;
+ latestReason: string;
+ latestSourceId: number;
+ latestSourceLabel: string;
+ severity: string;
+ type: string;
+ }>;
+ history: Array<{
+ occurredAtSeconds: number;
+ reason: string;
+ sourceId: number;
+ sourceLabel: string;
+ severity: string;
+ type: string;
+ }>;
+}
+
+interface MonitorLogDetails
+{
+ amount?: string;
+ latest?: string;
+ occurredAt?: string;
+ reason: string;
+ severity: string;
+ sourceId: number;
+ sourceLabel: string;
+ type: string;
}
interface InspectionVariable
@@ -84,6 +149,53 @@ interface InspectionVariable
valueClassName?: string;
}
+interface VariableDefinition
+{
+ key: string;
+ itemId?: number;
+ target: 'Furni' | 'User' | 'Global' | 'Context';
+ type: string;
+ hasValue: boolean;
+ isReadOnly?: boolean;
+ availability: string;
+ canWriteTo: boolean;
+ canCreateDelete: boolean;
+ canIntercept: boolean;
+ hasCreationTime: boolean;
+ hasUpdateTime: boolean;
+ isTextConnected: boolean;
+ isAlwaysAvailable?: boolean;
+}
+
+interface VariableTextValue
+{
+ value: string;
+ text: string;
+}
+
+interface VariableManageEntry
+{
+ categoryLabel: string;
+ createdAt: number;
+ entityId: number;
+ entityName: string;
+ manageLabel: string;
+ updatedAt: number;
+ value: number | null;
+}
+
+interface ManagedHolderVariableEntry
+{
+ availability: string;
+ createdAt: number;
+ hasValue: boolean;
+ name: string;
+ isReadOnly?: boolean;
+ updatedAt: number;
+ value: number | null;
+ variableItemId: number;
+}
+
interface InspectionUserTeamData
{
colorId: number;
@@ -125,33 +237,202 @@ const TABS: Array<{ key: WiredToolsTab; label: string; }> = [
{ key: 'settings', label: 'Settings' }
];
-const MONITOR_LOGS: MonitorLog[] = [
- { type: 'EXECUTION_CAP', category: 'ERROR', amount: '0', latest: '/' },
- { type: 'DELAYED_EVENTS_CAP', category: 'ERROR', amount: '0', latest: '/' },
- { type: 'EXECUTOR_OVERLOAD', category: 'ERROR', amount: '0', latest: '/' },
- { type: 'MARKED_AS_HEAVY', category: 'WARNING', amount: '0', latest: '/' },
- { type: 'KILLED', category: 'ERROR', amount: '0', latest: '/' },
- { type: 'RECURSION_TIMEOUT', category: 'ERROR', amount: '0', latest: '/' }
+const MONITOR_LOG_ORDER: string[] = [
+ 'EXECUTION_CAP',
+ 'DELAYED_EVENTS_CAP',
+ 'EXECUTOR_OVERLOAD',
+ 'MARKED_AS_HEAVY',
+ 'KILLED',
+ 'RECURSION_TIMEOUT'
];
+const WIRED_MONITOR_ACTION_FETCH = 0;
+const WIRED_MONITOR_ACTION_CLEAR_LOGS = 1;
+const WIRED_MONITOR_POLL_MS = 50;
+const WIRED_VARIABLES_POLL_MS = 50;
+const WIRED_INSPECTION_REFRESH_MS = 50;
+const WIRED_CLOCK_REFRESH_MS = 50;
+
+const MONITOR_ERROR_INFO: Record = {
+ EXECUTION_CAP: {
+ title: 'EXECUTION_CAP',
+ severity: 'ERROR',
+ description: [
+ 'This error occurs when the maximum Wired usage limit is about to be exceeded by a Wired execution.',
+ 'When this happens, the current execution is cancelled so the room never goes over the configured usage budget.',
+ 'If this happens too often, it usually means the setup is too complex for the amount of triggers firing in a short time.'
+ ]
+ },
+ DELAYED_EVENTS_CAP: {
+ title: 'DELAYED_EVENTS_CAP',
+ severity: 'ERROR',
+ description: [
+ 'Delayed Wired events happen when effects are scheduled to run later.',
+ 'There is a limit to how many delayed events can be pending at the same time. Once the limit is reached, new delayed executions are refused.',
+ 'If this appears often, the setup is likely relying too heavily on delayed effects and should be simplified.'
+ ]
+ },
+ EXECUTOR_OVERLOAD: {
+ title: 'EXECUTOR_OVERLOAD',
+ severity: 'ERROR',
+ description: [
+ 'This error occurs when the Wired engine is receiving a lot of instructions and the room cannot keep up with the execution time.',
+ 'This can be a sign of server pressure or of a setup that is too expensive to evaluate repeatedly.',
+ 'If the room is also marked as heavy, it is a good sign that the setup should be reduced or optimized.'
+ ]
+ },
+ MARKED_AS_HEAVY: {
+ title: 'MARKED_AS_HEAVY',
+ severity: 'WARNING',
+ description: [
+ 'The room is being considered heavy because its Wired usage stays high across multiple monitor windows.',
+ 'This is not a fatal error by itself, but it means the room is consuming a significant portion of the execution budget.',
+ 'If the room is not intentionally complex, it is worth reviewing the setup before it starts triggering harder limits.'
+ ]
+ },
+ KILLED: {
+ title: 'KILLED',
+ severity: 'ERROR',
+ description: [
+ 'This happens when the room is temporarily halted by the protection layer because the Wired flow looks abusive or unstable.',
+ 'While the room is killed, Wired execution is paused for a cooldown period.',
+ 'This is usually caused by loops, event spam, or repeated limit violations.'
+ ]
+ },
+ RECURSION_TIMEOUT: {
+ title: 'RECURSION_TIMEOUT',
+ severity: 'ERROR',
+ description: [
+ 'Recursive Wired events happen when signals keep re-triggering other stacks in the same room.',
+ 'When the recursion depth limit is reached, execution is stopped to prevent runaway loops.',
+ 'In most cases this means two or more stacks are indirectly calling each other too many times.'
+ ]
+ }
+};
+
const INSPECTION_ELEMENTS: InspectionElementButton[] = [
{ key: 'furni', label: 'Furni', icon: furniInspectionIcon },
{ key: 'user', label: 'User', icon: userInspectionIcon },
{ key: 'global', label: 'Global', icon: globalInspectionIcon }
];
-const EDITABLE_FURNI_VARIABLES: string[] = [ '@position.x', '@position.y', '@rotation', '@altitude', '@state', '@wallitem_offset' ];
-const EDITABLE_USER_VARIABLES: string[] = [ '@position.x', '@position.y', '@direction' ];
-const USER_DIRECTION_VECTORS: Array<{ x: number; y: number; }> = [
- { x: 0, y: -1 },
- { x: 1, y: -1 },
- { x: 1, y: 0 },
- { x: 1, y: 1 },
- { x: 0, y: 1 },
- { x: -1, y: 1 },
- { x: -1, y: 0 },
- { x: -1, y: -1 }
+const VARIABLES_ELEMENTS: VariablesElementButton[] = [
+ { key: 'furni', label: 'Furni', icon: furniInspectionIcon },
+ { key: 'user', label: 'User', icon: userInspectionIcon },
+ { key: 'global', label: 'Global', icon: globalInspectionIcon },
+ { key: 'context', label: 'Context', icon: contextInspectionIcon }
];
+
+const EDITABLE_FURNI_VARIABLES: string[] = [ '@position_x', '@position_y', '@rotation', '@altitude', '@state', '@wallitem_offset' ];
+const EDITABLE_USER_VARIABLES: string[] = [ '@position_x', '@position_y', '@direction' ];
+const createVariableDefinition = (key: string, target: 'Furni' | 'User' | 'Global' | 'Context', availability: string = 'Always', canWriteTo = false): VariableDefinition =>
+({
+ key,
+ target,
+ type: 'Internal',
+ hasValue: true,
+ availability,
+ canWriteTo,
+ canCreateDelete: false,
+ canIntercept: false,
+ hasCreationTime: false,
+ hasUpdateTime: false,
+ isTextConnected: false,
+ isAlwaysAvailable: (availability === 'Always')
+});
+const VARIABLE_DEFINITIONS: Record = {
+ furni: [
+ createVariableDefinition('~teleport.target_id', 'Furni', 'Conditional'),
+ createVariableDefinition('@id', 'Furni'),
+ createVariableDefinition('@class_id', 'Furni'),
+ createVariableDefinition('@height', 'Furni'),
+ createVariableDefinition('@state', 'Furni', 'Always', true),
+ createVariableDefinition('@position_x', 'Furni', 'Always', true),
+ createVariableDefinition('@position_y', 'Furni', 'Always', true),
+ createVariableDefinition('@rotation', 'Furni', 'Always', true),
+ createVariableDefinition('@altitude', 'Furni', 'Always', true),
+ createVariableDefinition('@is_invisible', 'Furni', 'Conditional'),
+ createVariableDefinition('@wallitem_offset', 'Furni', 'Conditional', true),
+ createVariableDefinition('@type', 'Furni'),
+ createVariableDefinition('@can_sit_on', 'Furni', 'Conditional'),
+ createVariableDefinition('@can_lay_on', 'Furni', 'Conditional'),
+ createVariableDefinition('@can_stand_on', 'Furni', 'Conditional'),
+ createVariableDefinition('@is_stackable', 'Furni', 'Conditional'),
+ createVariableDefinition('@dimensions.x', 'Furni'),
+ createVariableDefinition('@dimensions.y', 'Furni'),
+ createVariableDefinition('@owner_id', 'Furni')
+ ],
+ user: [
+ createVariableDefinition('@index', 'User'),
+ createVariableDefinition('@type', 'User'),
+ createVariableDefinition('@gender', 'User'),
+ createVariableDefinition('@level', 'User'),
+ createVariableDefinition('@achievement_score', 'User'),
+ createVariableDefinition('@is_hc', 'User', 'Conditional'),
+ createVariableDefinition('@has_rights', 'User', 'Conditional'),
+ createVariableDefinition('@is_owner', 'User', 'Conditional'),
+ createVariableDefinition('@is_group_admin', 'User', 'Conditional'),
+ createVariableDefinition('@is_muted', 'User', 'Conditional'),
+ createVariableDefinition('@is_trading', 'User', 'Conditional'),
+ createVariableDefinition('@is_frozen', 'User', 'Conditional'),
+ createVariableDefinition('@effect_id', 'User', 'Conditional'),
+ createVariableDefinition('@team_score', 'User', 'Conditional'),
+ createVariableDefinition('@team_color', 'User', 'Conditional'),
+ createVariableDefinition('@team_type', 'User', 'Conditional'),
+ createVariableDefinition('@sign', 'User', 'Conditional'),
+ createVariableDefinition('@dance', 'User', 'Conditional'),
+ createVariableDefinition('@is_idle', 'User', 'Conditional'),
+ createVariableDefinition('@handitem_id', 'User', 'Conditional'),
+ createVariableDefinition('@position_x', 'User', 'Always', true),
+ createVariableDefinition('@position_y', 'User', 'Always', true),
+ createVariableDefinition('@direction', 'User', 'Always', true),
+ createVariableDefinition('@altitude', 'User'),
+ createVariableDefinition('@favourite_group_id', 'User', 'Conditional'),
+ createVariableDefinition('@room_entry.method', 'User', 'Conditional'),
+ createVariableDefinition('@room_entry.teleport_id', 'User', 'Conditional'),
+ createVariableDefinition('@user_id', 'User', 'Conditional'),
+ createVariableDefinition('@bot_id', 'User', 'Conditional'),
+ createVariableDefinition('@pet_id', 'User', 'Conditional'),
+ createVariableDefinition('@pet_owner_id', 'User', 'Conditional')
+ ],
+ global: [
+ createVariableDefinition('@furni_count', 'Global'),
+ createVariableDefinition('@user_count', 'Global'),
+ createVariableDefinition('@wired_timer', 'Global'),
+ createVariableDefinition('@team_red_score', 'Global'),
+ createVariableDefinition('@team_green_score', 'Global'),
+ createVariableDefinition('@team_blue_score', 'Global'),
+ createVariableDefinition('@team_yellow_score', 'Global'),
+ createVariableDefinition('@team_red_size', 'Global'),
+ createVariableDefinition('@team_green_size', 'Global'),
+ createVariableDefinition('@team_blue_size', 'Global'),
+ createVariableDefinition('@team_yellow_size', 'Global'),
+ createVariableDefinition('@room_id', 'Global'),
+ createVariableDefinition('@group_id', 'Global'),
+ createVariableDefinition('@timezone_server', 'Global'),
+ createVariableDefinition('@timezone_client', 'Global'),
+ createVariableDefinition('@current_time', 'Global'),
+ createVariableDefinition('@current_time.millisecond_of_second', 'Global'),
+ createVariableDefinition('@current_time.seconds_of_minute', 'Global'),
+ createVariableDefinition('@current_time.minute_of_hour', 'Global'),
+ createVariableDefinition('@current_time.hour_of_day', 'Global'),
+ createVariableDefinition('@current_time.day_of_week', 'Global'),
+ createVariableDefinition('@current_time.day_of_month', 'Global'),
+ createVariableDefinition('@current_time.day_of_year', 'Global'),
+ createVariableDefinition('@current_time.week_of_year', 'Global'),
+ createVariableDefinition('@current_time.month_of_year', 'Global'),
+ createVariableDefinition('@current_time.year', 'Global')
+ ],
+ context: [
+ createVariableDefinition('@selector_furni_count', 'Context', 'Conditional'),
+ createVariableDefinition('@selector_user_count', 'Context', 'Conditional'),
+ createVariableDefinition('@signal_furni_count', 'Context', 'Conditional'),
+ createVariableDefinition('@signal_user_count', 'Context', 'Conditional'),
+ createVariableDefinition('@antenna_id', 'Context', 'Conditional'),
+ createVariableDefinition('@chat_type', 'Context', 'Conditional'),
+ createVariableDefinition('@chat_style', 'Context', 'Conditional')
+ ]
+};
const WIRED_FREEZE_EFFECT_IDS: Set = new Set([ 218, 12, 11, 53, 163 ]);
const TEAM_COLOR_NAMES: Record = {
1: 'red',
@@ -161,8 +442,32 @@ const TEAM_COLOR_NAMES: Record = {
};
const WEEKDAY_NAMES: string[] = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ];
const MONTH_NAMES: string[] = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];
+const DIRECTION_NAMES: string[] = [ 'North', 'North-East', 'East', 'South-East', 'South', 'South-West', 'West', 'North-West' ];
const HOTEL_TIME_FORMATTERS: Map = new Map();
+const createEmptyMonitorSnapshot = (): MonitorSnapshot =>
+({
+ usageCurrentWindow: 0,
+ usageLimitPerWindow: 0,
+ isHeavy: false,
+ delayedEventsPending: 0,
+ delayedEventsLimit: 0,
+ averageExecutionMs: 0,
+ peakExecutionMs: 0,
+ recursionDepthCurrent: 0,
+ recursionDepthLimit: 0,
+ killedRemainingSeconds: 0,
+ usageWindowMs: 0,
+ overloadAverageThresholdMs: 0,
+ overloadPeakThresholdMs: 0,
+ heavyUsageThresholdPercent: 0,
+ heavyConsecutiveWindowsThreshold: 0,
+ overloadConsecutiveWindowsThreshold: 0,
+ heavyDelayedThresholdPercent: 0,
+ logs: [],
+ history: []
+});
+
const getHotelTimeFormatter = (timeZone: string): Intl.DateTimeFormat =>
{
const formatterTimeZone = (timeZone || 'UTC');
@@ -230,11 +535,66 @@ const getHotelDateTimeParts = (epochMs: number, timeZone: string): HotelDateTime
};
};
+const formatMonitorLatestOccurrence = (latestOccurrenceSeconds: number, nowMs: number): string =>
+{
+ if(latestOccurrenceSeconds <= 0) return '/';
+
+ const diffMs = Math.max(0, (nowMs - (latestOccurrenceSeconds * 1000)));
+ const diffSeconds = Math.floor(diffMs / 1000);
+
+ if(diffSeconds < 5) return 'Just now';
+ if(diffSeconds < 60) return `${ diffSeconds }s ago`;
+
+ const diffMinutes = Math.floor(diffSeconds / 60);
+
+ if(diffMinutes < 60) return `${ diffMinutes }m ago`;
+
+ const diffHours = Math.floor(diffMinutes / 60);
+
+ if(diffHours < 24) return `${ diffHours }h ago`;
+
+ const diffDays = Math.floor(diffHours / 24);
+
+ return `${ diffDays }d ago`;
+};
+
+const formatMonitorHistoryOccurrence = (occurredAtSeconds: number): string =>
+{
+ if(occurredAtSeconds <= 0) return '/';
+
+ return new Date(occurredAtSeconds * 1000).toLocaleString('en-GB');
+};
+
+const formatVariableTimestamp = (timestamp: number): string =>
+{
+ if(!timestamp || (timestamp <= 0)) return '/';
+
+ return new Date(timestamp * 1000).toLocaleString('en-GB');
+};
+
+const formatMonitorSource = (sourceLabel: string, sourceId: number): string =>
+{
+ const normalizedLabel = (sourceLabel || '').trim();
+
+ if(!normalizedLabel && !(sourceId > 0)) return 'Room monitor';
+ if(sourceId > 0) return `${ normalizedLabel || 'wired' } (#${ sourceId })`;
+
+ return normalizedLabel;
+};
+
+const normalizeMonitorReason = (reason: string): string =>
+{
+ const normalizedReason = (reason || '').trim();
+
+ return normalizedReason || 'No detailed reason was recorded for this entry.';
+};
+
export const WiredCreatorToolsView: FC<{}> = () =>
{
const [ isVisible, setIsVisible ] = useState(false);
- const [ activeTab, setActiveTab ] = useState('inspection');
+ const [ activeTab, setActiveTab ] = useState('monitor');
const [ inspectionType, setInspectionType ] = useState('furni');
+ const [ variablesType, setVariablesType ] = useState('furni');
const [ keepSelected, setKeepSelected ] = useState(false);
const [ selectedFurni, setSelectedFurni ] = useState(null);
const [ selectedFurniLiveState, setSelectedFurniLiveState ] = useState(null);
@@ -243,12 +603,47 @@ export const WiredCreatorToolsView: FC<{}> = () =>
const [ selectedUserActionVersion, setSelectedUserActionVersion ] = useState(0);
const [ globalClock, setGlobalClock ] = useState(Date.now());
const [ roomEnteredAt, setRoomEnteredAt ] = useState(Date.now());
+ const [ monitorSnapshot, setMonitorSnapshot ] = useState(() => createEmptyMonitorSnapshot());
+ const [ selectedMonitorErrorType, setSelectedMonitorErrorType ] = useState(null);
+ const [ selectedMonitorLogDetails, setSelectedMonitorLogDetails ] = useState(null);
+ const [ isMonitorHistoryOpen, setIsMonitorHistoryOpen ] = useState(false);
+ const [ isMonitorInfoOpen, setIsMonitorInfoOpen ] = useState(false);
+ const [ monitorHistorySeverityFilter, setMonitorHistorySeverityFilter ] = useState<'ALL' | 'ERROR' | 'WARNING'>('ALL');
+ const [ monitorHistoryTypeFilter, setMonitorHistoryTypeFilter ] = useState('ALL');
const [ editingVariable, setEditingVariable ] = useState(null);
const [ editingValue, setEditingValue ] = useState('');
+ const [ selectedInspectionVariableKeys, setSelectedInspectionVariableKeys ] = useState>({
+ furni: '',
+ user: '',
+ global: ''
+ });
+ const [ isInspectionGiveOpen, setIsInspectionGiveOpen ] = useState(false);
+ const [ inspectionGiveVariableItemId, setInspectionGiveVariableItemId ] = useState(0);
+ const [ inspectionGiveValue, setInspectionGiveValue ] = useState('0');
+ const [ isVariableManageOpen, setIsVariableManageOpen ] = useState(false);
+ const [ variableManageTypeFilter, setVariableManageTypeFilter ] = useState('ALL');
+ const [ variableManageSort, setVariableManageSort ] = useState('highest_value');
+ const [ variableManagePage, setVariableManagePage ] = useState(1);
+ const [ selectedManagedVariableEntry, setSelectedManagedVariableEntry ] = useState(null);
+ const [ selectedManagedHolderVariableId, setSelectedManagedHolderVariableId ] = useState(0);
+ const [ editingManagedHolderVariableId, setEditingManagedHolderVariableId ] = useState(0);
+ const [ editingManagedHolderValue, setEditingManagedHolderValue ] = useState('');
+ const [ isManagedGiveOpen, setIsManagedGiveOpen ] = useState(false);
+ const [ managedGiveVariableItemId, setManagedGiveVariableItemId ] = useState(0);
+ const [ managedGiveValue, setManagedGiveValue ] = useState('0');
+ const shouldPauseVariableSnapshotRefresh = (!!editingVariable || !!editingManagedHolderVariableId || isInspectionGiveOpen || isManagedGiveOpen);
+ const [ selectedVariableKeys, setSelectedVariableKeys ] = useState>({
+ furni: VARIABLE_DEFINITIONS.furni[0].key,
+ user: VARIABLE_DEFINITIONS.user[0].key,
+ global: VARIABLE_DEFINITIONS.global[0].key,
+ context: VARIABLE_DEFINITIONS.context[0].key
+ });
const { roomSession = null } = useRoom();
const { ownUser: tradeOwnUser = null, otherUser: tradeOtherUser = null, isTrading = false } = useInventoryTrade();
+ const { roomSettings, userVariableDefinitions, userVariableAssignments, furniVariableDefinitions, furniVariableAssignments, roomVariableDefinitions, roomVariableAssignments, contextVariableDefinitions, requestUserVariables, assignUserVariable, removeUserVariable, updateUserVariableValue, assignFurniVariable, removeFurniVariable, updateFurniVariableValue, updateRoomVariableValue } = useWiredTools();
+ const { simpleAlert = null } = useNotification();
- const getFurniLiveState = (objectId: number, category: number): InspectionFurniLiveState =>
+ const getFurniLiveState = useCallback((objectId: number, category: number): InspectionFurniLiveState =>
{
if(!roomSession) return null;
@@ -266,7 +661,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
rotation: ((((rawRotation % 8) + 8) % 8)),
state: Number(roomObject.getState(0) ?? 0)
};
- };
+ }, [ roomSession ]);
const parseWallLocation = (wallLocation: string): ParsedWallLocation =>
{
@@ -333,6 +728,61 @@ export const WiredCreatorToolsView: FC<{}> = () =>
return `Effect ${ value }`;
};
+ const getRoomEntryMethodNumericValue = (value: string): number =>
+ {
+ switch((value || '').trim().toLowerCase())
+ {
+ case 'door': return 1;
+ case 'teleport': return 2;
+ case 'unknown':
+ case '': return 0;
+ default: return 3;
+ }
+ };
+
+ const getTimeZoneOffsetMinutes = (epochMs: number, timeZone: string): number =>
+ {
+ try
+ {
+ const formatter = new Intl.DateTimeFormat('en-US', {
+ timeZone,
+ timeZoneName: 'shortOffset',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ const timeZonePart = formatter.formatToParts(new Date(epochMs)).find(part => (part.type === 'timeZoneName'))?.value ?? 'GMT';
+ const match = timeZonePart.match(/^GMT(?:(\+|-)(\d{1,2})(?::?(\d{2}))?)?$/i);
+
+ if(!match?.[1]) return 0;
+
+ const sign = (match[1] === '-') ? -1 : 1;
+ const hours = parseInt(match[2] ?? '0', 10);
+ const minutes = parseInt(match[3] ?? '0', 10);
+
+ return (sign * ((hours * 60) + minutes));
+ }
+ catch
+ {
+ return 0;
+ }
+ };
+
+ const getVariableAvailabilityLabel = (value: number, targetType: 'user' | 'furni' | 'global' | 'context'): string =>
+ {
+ if(targetType === 'context') return 'Current wired execution';
+
+ if(value === 11)
+ {
+ const localizedValue = LocalizeText('wiredfurni.params.variables.availability.11');
+
+ return ((localizedValue && (localizedValue !== 'wiredfurni.params.variables.availability.11')) ? localizedValue : 'Permanent, shared between rooms');
+ }
+
+ if(value === 10) return 'Permanent';
+
+ return ((targetType === 'furni') || (targetType === 'global')) ? 'While the room is active' : 'While the user is in the room';
+ };
+
const getTeamColorDisplayName = (value: number): string =>
{
if(value <= 0) return '';
@@ -466,7 +916,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
return Math.ceil((((utcDate.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
};
- const getUserLiveState = (roomIndex: number): InspectionUserLiveState =>
+ const getUserLiveState = useCallback((roomIndex: number): InspectionUserLiveState =>
{
if(!roomSession) return null;
@@ -483,9 +933,9 @@ export const WiredCreatorToolsView: FC<{}> = () =>
altitude: Math.round(Number(location?.z ?? 0) * 100),
direction: ((((rawDirection % 8) + 8) % 8))
};
- };
+ }, [ roomSession ]);
- const refreshSelectedUser = (roomIndex: number = selectedUser?.roomIndex) =>
+ const refreshSelectedUser = useCallback((roomIndex: number = selectedUser?.roomIndex) =>
{
if((roomIndex === null) || (roomIndex === undefined) || !roomSession) return;
@@ -595,9 +1045,9 @@ export const WiredCreatorToolsView: FC<{}> = () =>
}
setSelectedUserLiveState(getUserLiveState(roomIndex));
- };
+ }, [ getUserLiveState, roomSession, selectedUser?.roomIndex ]);
- const refreshSelectedFurni = (objectId: number = selectedFurni?.objectId, category: number = selectedFurni?.category) =>
+ const refreshSelectedFurni = useCallback((objectId: number = selectedFurni?.objectId, category: number = selectedFurni?.category) =>
{
if(!objectId && (objectId !== 0)) return;
@@ -612,7 +1062,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
});
setSelectedFurniLiveState(getFurniLiveState(objectId, category));
- };
+ }, [ getFurniLiveState, selectedFurni?.category, selectedFurni?.objectId ]);
useObjectSelectedEvent(event =>
{
@@ -630,6 +1080,33 @@ export const WiredCreatorToolsView: FC<{}> = () =>
refreshSelectedUser(event.id);
});
+ useMessageEvent(WiredMonitorDataEvent, event =>
+ {
+ const parser = event.getParser();
+
+ setMonitorSnapshot({
+ usageCurrentWindow: Number(parser.usageCurrentWindow ?? 0),
+ usageLimitPerWindow: Number(parser.usageLimitPerWindow ?? 0),
+ isHeavy: !!parser.isHeavy,
+ delayedEventsPending: Number(parser.delayedEventsPending ?? 0),
+ delayedEventsLimit: Number(parser.delayedEventsLimit ?? 0),
+ averageExecutionMs: Number(parser.averageExecutionMs ?? 0),
+ peakExecutionMs: Number(parser.peakExecutionMs ?? 0),
+ recursionDepthCurrent: Number(parser.recursionDepthCurrent ?? 0),
+ recursionDepthLimit: Number(parser.recursionDepthLimit ?? 0),
+ killedRemainingSeconds: Number(parser.killedRemainingSeconds ?? 0),
+ usageWindowMs: Number(parser.usageWindowMs ?? 0),
+ overloadAverageThresholdMs: Number(parser.overloadAverageThresholdMs ?? 0),
+ overloadPeakThresholdMs: Number(parser.overloadPeakThresholdMs ?? 0),
+ heavyUsageThresholdPercent: Number(parser.heavyUsageThresholdPercent ?? 0),
+ heavyConsecutiveWindowsThreshold: Number(parser.heavyConsecutiveWindowsThreshold ?? 0),
+ overloadConsecutiveWindowsThreshold: Number(parser.overloadConsecutiveWindowsThreshold ?? 0),
+ heavyDelayedThresholdPercent: Number(parser.heavyDelayedThresholdPercent ?? 0),
+ logs: [ ...(parser.logs ?? []) ],
+ history: [ ...(parser.history ?? []) ]
+ });
+ });
+
useMessageEvent(FurnitureFloorUpdateEvent, event =>
{
if(!selectedFurni) return;
@@ -742,20 +1219,20 @@ export const WiredCreatorToolsView: FC<{}> = () =>
lastMutedValue = currentMutedValue;
setSelectedUserActionVersion(previousValue => (previousValue + 1));
- }, 250);
+ }, WIRED_INSPECTION_REFRESH_MS);
return () => window.clearInterval(interval);
}, [ isVisible, inspectionType, selectedUser, roomSession ]);
useEffect(() =>
{
- const shouldTick = isVisible && ((activeTab === 'monitor') || ((activeTab === 'inspection') && (inspectionType === 'global')));
+ const shouldTick = isVisible;
if(!shouldTick) return;
setGlobalClock(Date.now());
- const interval = window.setInterval(() => setGlobalClock(Date.now()), 100);
+ const interval = window.setInterval(() => setGlobalClock(Date.now()), WIRED_CLOCK_REFRESH_MS);
return () => window.clearInterval(interval);
}, [ isVisible, activeTab, inspectionType, roomSession?.roomId ]);
@@ -767,6 +1244,107 @@ export const WiredCreatorToolsView: FC<{}> = () =>
setRoomEnteredAt(Date.now());
}, [ roomSession?.roomId ]);
+ useEffect(() =>
+ {
+ setMonitorSnapshot(createEmptyMonitorSnapshot());
+ setSelectedMonitorErrorType(null);
+ setSelectedMonitorLogDetails(null);
+ setIsMonitorHistoryOpen(false);
+ setIsMonitorInfoOpen(false);
+ setMonitorHistorySeverityFilter('ALL');
+ setMonitorHistoryTypeFilter('ALL');
+ }, [ roomSession?.roomId ]);
+
+ useEffect(() =>
+ {
+ if(activeTab === 'monitor') return;
+
+ setSelectedMonitorErrorType(null);
+ setSelectedMonitorLogDetails(null);
+ setIsMonitorHistoryOpen(false);
+ setIsMonitorInfoOpen(false);
+ }, [ activeTab ]);
+
+ useEffect(() =>
+ {
+ if(selectedMonitorErrorType) return;
+
+ setSelectedMonitorLogDetails(null);
+ }, [ selectedMonitorErrorType ]);
+
+ useEffect(() =>
+ {
+ if(!isVisible || (activeTab !== 'monitor') || !roomSession?.roomId) return;
+
+ const requestSnapshot = () => SendMessageComposer(new WiredMonitorRequestComposer(WIRED_MONITOR_ACTION_FETCH));
+
+ requestSnapshot();
+
+ const interval = window.setInterval(requestSnapshot, WIRED_MONITOR_POLL_MS);
+
+ return () => window.clearInterval(interval);
+ }, [ isVisible, activeTab, roomSession?.roomId ]);
+
+ useEffect(() =>
+ {
+ if(!isVisible || !roomSession?.roomId || !roomSettings.canInspect || shouldPauseVariableSnapshotRefresh) return;
+
+ requestUserVariables();
+
+ const interval = window.setInterval(requestUserVariables, WIRED_VARIABLES_POLL_MS);
+
+ return () => window.clearInterval(interval);
+ }, [ isVisible, roomSession?.roomId, roomSettings.canInspect, requestUserVariables, shouldPauseVariableSnapshotRefresh ]);
+
+ useEffect(() =>
+ {
+ if(!isVisible || (activeTab !== 'inspection')) return;
+
+ const refreshInspectionState = () =>
+ {
+ if((inspectionType === 'furni') && selectedFurni)
+ {
+ const nextLiveState = getFurniLiveState(selectedFurni.objectId, selectedFurni.category);
+
+ setSelectedFurniLiveState(previousValue =>
+ {
+ if((previousValue?.positionX === nextLiveState?.positionX)
+ && (previousValue?.positionY === nextLiveState?.positionY)
+ && (previousValue?.altitude === nextLiveState?.altitude)
+ && (previousValue?.rotation === nextLiveState?.rotation)
+ && (previousValue?.state === nextLiveState?.state)) return previousValue;
+
+ return nextLiveState;
+ });
+
+ return;
+ }
+
+ if((inspectionType === 'user') && selectedUser)
+ {
+ const nextLiveState = getUserLiveState(selectedUser.roomIndex);
+
+ setSelectedUserLiveState(previousValue =>
+ {
+ if((previousValue?.positionX === nextLiveState?.positionX)
+ && (previousValue?.positionY === nextLiveState?.positionY)
+ && (previousValue?.altitude === nextLiveState?.altitude)
+ && (previousValue?.direction === nextLiveState?.direction)) return previousValue;
+
+ return nextLiveState;
+ });
+
+ setSelectedUserActionVersion(previousValue => (previousValue + 1));
+ }
+ };
+
+ refreshInspectionState();
+
+ const interval = window.setInterval(refreshInspectionState, WIRED_INSPECTION_REFRESH_MS);
+
+ return () => window.clearInterval(interval);
+ }, [ isVisible, activeTab, inspectionType, selectedFurni, selectedUser, getFurniLiveState, getUserLiveState ]);
+
useEffect(() =>
{
const linkTracker: ILinkEventTracker = {
@@ -779,13 +1357,57 @@ export const WiredCreatorToolsView: FC<{}> = () =>
switch(parts[1])
{
case 'show':
+ setActiveTab('monitor');
setIsVisible(true);
return;
case 'hide':
setIsVisible(false);
return;
case 'toggle':
- setIsVisible(prevValue => !prevValue);
+ setIsVisible(prevValue =>
+ {
+ const nextValue = !prevValue;
+
+ if(nextValue) setActiveTab('monitor');
+
+ return nextValue;
+ });
+ return;
+ case 'invalid':
+ if(simpleAlert) simpleAlert(LocalizeText('wiredmenu.invalid_room.desc'), NotificationAlertType.ALERT, null, null, LocalizeText('generic.alert.title'));
+ return;
+ case 'inspection':
+ if(parts.length > 3)
+ {
+ switch(parts[2])
+ {
+ case 'furni': {
+ const objectId = parseInt(parts[3], 10);
+ const category = parseInt(parts[4] ?? '-1', 10);
+
+ if(Number.isInteger(objectId) && Number.isInteger(category))
+ {
+ setInspectionType('furni');
+ refreshSelectedFurni(objectId, category);
+ setActiveTab('inspection');
+ setIsVisible(true);
+ }
+ return;
+ }
+ case 'user': {
+ const roomIndex = parseInt(parts[3], 10);
+
+ if(Number.isInteger(roomIndex))
+ {
+ setInspectionType('user');
+ refreshSelectedUser(roomIndex);
+ setActiveTab('inspection');
+ setIsVisible(true);
+ }
+ return;
+ }
+ }
+ }
return;
case 'tab':
if(parts.length > 2)
@@ -804,7 +1426,14 @@ export const WiredCreatorToolsView: FC<{}> = () =>
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
- }, []);
+ }, [ refreshSelectedFurni, refreshSelectedUser, simpleAlert ]);
+
+ useEffect(() =>
+ {
+ if(!isVisible || !roomSession?.roomId || !roomSettings.isLoaded || roomSettings.canInspect) return;
+
+ setIsVisible(false);
+ }, [ isVisible, roomSession?.roomId, roomSettings.isLoaded, roomSettings.canInspect ]);
const selectedRoomObject = ((roomSession && selectedFurni)
? GetRoomEngine().getRoomObject(roomSession.roomId, selectedFurni.objectId, selectedFurni.category)
@@ -814,6 +1443,52 @@ export const WiredCreatorToolsView: FC<{}> = () =>
: null);
const currentTabLabel = useMemo(() => TABS.find(tab => tab.key === activeTab)?.label ?? 'Monitor', [ activeTab ]);
+ const selectedMonitorErrorInfo = useMemo(() =>
+ {
+ if(!selectedMonitorErrorType) return null;
+
+ return MONITOR_ERROR_INFO[selectedMonitorErrorType] ?? null;
+ }, [ selectedMonitorErrorType ]);
+ const monitorRoomStats = useMemo(() =>
+ {
+ if(!roomSession)
+ {
+ return {
+ roomFurniCount: 0,
+ roomItemLimit: 0,
+ wallFurniCount: 0,
+ permanentFurniVariables: 0
+ };
+ }
+
+ const roomId = roomSession.roomId;
+ const floorObjects = GetRoomEngine().getRoomObjects(roomId, RoomObjectCategory.FLOOR);
+ const wallObjects = GetRoomEngine().getRoomObjects(roomId, RoomObjectCategory.WALL);
+
+ return {
+ roomFurniCount: (floorObjects.length + wallObjects.length),
+ roomItemLimit: Number(roomSession.roomItemLimit ?? 0),
+ wallFurniCount: wallObjects.length,
+ permanentFurniVariables: [ ...floorObjects, ...wallObjects ].reduce((total, roomObject) =>
+ {
+ if(!roomObject) return total;
+
+ const customVariables = roomObject.model.getValue(RoomObjectVariable.FURNITURE_CUSTOM_VARIABLES);
+
+ return (total + (customVariables?.length ?? 0));
+ }, 0)
+ };
+ }, [ roomSession, globalClock ]);
+ const selectedMonitorDetailSource = useMemo(() =>
+ {
+ if(!selectedMonitorLogDetails) return '';
+
+ return formatMonitorSource(selectedMonitorLogDetails.sourceLabel, selectedMonitorLogDetails.sourceId);
+ }, [ selectedMonitorLogDetails ]);
+ const monitorHistoryTypeOptions = useMemo(() =>
+ {
+ return [ 'ALL', ...Array.from(new Set([ ...MONITOR_LOG_ORDER, ...monitorSnapshot.history.map(entry => entry.type) ])) ];
+ }, [ monitorSnapshot.history ]);
const previewPlaceholder = useMemo(() =>
{
switch(inspectionType)
@@ -831,30 +1506,124 @@ export const WiredCreatorToolsView: FC<{}> = () =>
if(!roomSession)
{
return [
- { label: 'Wired usage', value: '0/10000' },
+ { label: 'Wired usage', value: '0/0' },
{ label: 'Is heavy', value: 'No' },
{ label: 'Room furni', value: '0/0' },
{ label: 'Wall furni', value: '0/0' },
+ { label: 'Delayed events', value: '0/0' },
+ { label: 'Average execution', value: '0ms' },
+ { label: 'Peak execution', value: '0ms' },
+ { label: 'Recursion', value: '0/0' },
+ { label: 'Killed remaining', value: '0s' },
{ label: 'Permanent furni vars', value: '0/60' }
];
}
- const roomId = roomSession.roomId;
- const floorObjects = GetRoomEngine().getRoomObjects(roomId, RoomObjectCategory.FLOOR);
- const wallObjects = GetRoomEngine().getRoomObjects(roomId, RoomObjectCategory.WALL);
- const roomFurniCount = (floorObjects.length + wallObjects.length);
- const roomItemLimit = Number(roomSession.roomItemLimit ?? 0);
- const roomFurniValue = (roomItemLimit > 0) ? `${ roomFurniCount }/${ roomItemLimit }` : String(roomFurniCount);
- const wallFurniValue = (roomItemLimit > 0) ? `${ wallObjects.length }/${ roomItemLimit }` : String(wallObjects.length);
+ const roomFurniValue = (monitorRoomStats.roomItemLimit > 0) ? `${ monitorRoomStats.roomFurniCount }/${ monitorRoomStats.roomItemLimit }` : String(monitorRoomStats.roomFurniCount);
+ const wallFurniValue = (monitorRoomStats.roomItemLimit > 0) ? `${ monitorRoomStats.wallFurniCount }/${ monitorRoomStats.roomItemLimit }` : String(monitorRoomStats.wallFurniCount);
+ const usageValue = `${ monitorSnapshot.usageCurrentWindow }/${ Math.max(0, monitorSnapshot.usageLimitPerWindow) }`;
+ const delayedValue = `${ monitorSnapshot.delayedEventsPending }/${ Math.max(0, monitorSnapshot.delayedEventsLimit) }`;
return [
- { label: 'Wired usage', value: '0/10000' },
- { label: 'Is heavy', value: 'No' },
+ { label: 'Wired usage', value: usageValue },
+ { label: 'Is heavy', value: monitorSnapshot.isHeavy ? 'Yes' : 'No' },
{ label: 'Room furni', value: roomFurniValue },
{ label: 'Wall furni', value: wallFurniValue },
- { label: 'Permanent furni vars', value: '0/60' }
+ { label: 'Delayed events', value: delayedValue },
+ { label: 'Average execution', value: `${ monitorSnapshot.averageExecutionMs }ms` },
+ { label: 'Peak execution', value: `${ monitorSnapshot.peakExecutionMs }ms` },
+ { label: 'Recursion', value: `${ monitorSnapshot.recursionDepthCurrent }/${ Math.max(0, monitorSnapshot.recursionDepthLimit) }` },
+ { label: 'Killed remaining', value: `${ Math.max(0, monitorSnapshot.killedRemainingSeconds) }s` },
+ { label: 'Permanent furni vars', value: `${ monitorRoomStats.permanentFurniVariables }/60` }
];
- }, [ roomSession, globalClock ]);
+ }, [ roomSession, monitorRoomStats, monitorSnapshot ]);
+ const monitorLogs = useMemo(() =>
+ {
+ return MONITOR_LOG_ORDER.map(type =>
+ {
+ const log = monitorSnapshot.logs.find(entry => entry.type === type);
+ const fallbackInfo = MONITOR_ERROR_INFO[type];
+ const amount = Number(log?.amount ?? 0);
+
+ return {
+ type,
+ category: String(log?.severity ?? fallbackInfo?.severity ?? 'ERROR'),
+ amount: String(amount),
+ latest: (amount > 0) ? formatMonitorLatestOccurrence(Number(log?.latestOccurrenceSeconds ?? 0), globalClock) : '/',
+ latestReason: normalizeMonitorReason(log?.latestReason),
+ latestSourceLabel: String(log?.latestSourceLabel ?? ''),
+ latestSourceId: Number(log?.latestSourceId ?? 0)
+ };
+ });
+ }, [ monitorSnapshot.logs, globalClock ]);
+ const monitorHistoryRows = useMemo(() =>
+ {
+ return monitorSnapshot.history.map((entry, index) => ({
+ id: `${ entry.type }-${ entry.occurredAtSeconds }-${ index }`,
+ type: entry.type,
+ category: entry.severity,
+ occurredAt: formatMonitorHistoryOccurrence(Number(entry.occurredAtSeconds ?? 0)),
+ occurredAtSeconds: Number(entry.occurredAtSeconds ?? 0),
+ reason: normalizeMonitorReason(entry.reason),
+ sourceLabel: String(entry.sourceLabel ?? ''),
+ sourceId: Number(entry.sourceId ?? 0)
+ }));
+ }, [ monitorSnapshot.history ]);
+ const filteredMonitorHistoryRows = useMemo(() =>
+ {
+ return monitorHistoryRows.filter(row =>
+ {
+ if((monitorHistorySeverityFilter !== 'ALL') && (row.category !== monitorHistorySeverityFilter)) return false;
+ if((monitorHistoryTypeFilter !== 'ALL') && (row.type !== monitorHistoryTypeFilter)) return false;
+
+ return true;
+ });
+ }, [ monitorHistoryRows, monitorHistorySeverityFilter, monitorHistoryTypeFilter ]);
+ const monitorInfoSections = useMemo(() =>
+ {
+ return [
+ {
+ title: 'Wired usage',
+ lines: [
+ `Current value: ${ monitorSnapshot.usageCurrentWindow }/${ Math.max(0, monitorSnapshot.usageLimitPerWindow) }`,
+ `This is the room execution budget used inside a ${ Math.max(0, monitorSnapshot.usageWindowMs) }ms server window.`,
+ 'Each triggered stack consumes cost based on conditions, selectors, effects, delayed effects, and recursion depth.'
+ ]
+ },
+ {
+ title: 'Heavy / overload',
+ lines: [
+ `Heavy warning starts when usage stays above ${ Math.max(0, monitorSnapshot.heavyUsageThresholdPercent) }% for ${ Math.max(0, monitorSnapshot.heavyConsecutiveWindowsThreshold) } consecutive window(s).`,
+ `Delayed pressure also contributes when the queue stays above ${ Math.max(0, monitorSnapshot.heavyDelayedThresholdPercent) }% of its limit.`,
+ `Overload is tracked from execution time and currently trips after ${ Math.max(0, monitorSnapshot.overloadConsecutiveWindowsThreshold) } consecutive overloaded window(s).`
+ ]
+ },
+ {
+ title: 'Execution times',
+ lines: [
+ `Average execution: ${ monitorSnapshot.averageExecutionMs }ms (threshold ${ Math.max(0, monitorSnapshot.overloadAverageThresholdMs) }ms)`,
+ `Peak execution: ${ monitorSnapshot.peakExecutionMs }ms (threshold ${ Math.max(0, monitorSnapshot.overloadPeakThresholdMs) }ms)`,
+ 'These values reset with each server usage window.'
+ ]
+ },
+ {
+ title: 'Other numbers',
+ lines: [
+ `Delayed events: ${ monitorSnapshot.delayedEventsPending }/${ Math.max(0, monitorSnapshot.delayedEventsLimit) } pending scheduled execution(s).`,
+ `Recursion: ${ monitorSnapshot.recursionDepthCurrent }/${ Math.max(0, monitorSnapshot.recursionDepthLimit) } nested wired call(s).`,
+ `Killed remaining: ${ Math.max(0, monitorSnapshot.killedRemainingSeconds) } second(s) of room cooldown when protection has halted execution.`
+ ]
+ },
+ {
+ title: 'Room counters',
+ lines: [
+ `Room furni: ${ monitorRoomStats.roomFurniCount }/${ Math.max(0, monitorRoomStats.roomItemLimit) || 0 }`,
+ `Wall furni: ${ monitorRoomStats.wallFurniCount }/${ Math.max(0, monitorRoomStats.roomItemLimit) || 0 }`,
+ `Permanent furni vars: ${ monitorRoomStats.permanentFurniVariables }/60 renderer-side custom variable entries currently attached to room items.`
+ ]
+ }
+ ];
+ }, [ monitorSnapshot, monitorRoomStats ]);
const selectedFurnitureData = useMemo(() =>
{
if(!selectedRoomObject || !selectedFurni) return null;
@@ -884,9 +1653,45 @@ export const WiredCreatorToolsView: FC<{}> = () =>
return `${ parsedWallLocation.localX },${ parsedWallLocation.localY }`;
}, [ parsedWallLocation ]);
+ const canEditInspection = roomSettings.canModify;
+ const roomVariableAssignmentMap = useMemo(() =>
+ {
+ return new Map(roomVariableAssignments.map(assignment => [ assignment.variableItemId, assignment ]));
+ }, [ roomVariableAssignments ]);
+ const roomCustomVariableDefinitions = useMemo(() =>
+ {
+ return [ ...roomVariableDefinitions ]
+ .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }) || (left.itemId - right.itemId));
+ }, [ roomVariableDefinitions ]);
+ const roomCustomVariableDefinitionMap = useMemo(() =>
+ {
+ return new Map(roomCustomVariableDefinitions.map(definition => [ definition.name, definition ]));
+ }, [ roomCustomVariableDefinitions ]);
+ const selectedFurniAssignments = useMemo(() =>
+ {
+ if(!selectedFurni) return [];
+
+ return furniVariableAssignments[selectedFurni.objectId] ?? [];
+ }, [ selectedFurni, furniVariableAssignments ]);
+ const selectedFurniAssignmentMap = useMemo(() =>
+ {
+ return new Map(selectedFurniAssignments.map(assignment => [ assignment.variableItemId, assignment ]));
+ }, [ selectedFurniAssignments ]);
+ const selectedFurniCustomVariableDefinitions = useMemo(() =>
+ {
+ if(!selectedFurniAssignments.length) return [];
+
+ return furniVariableDefinitions
+ .filter(definition => selectedFurniAssignmentMap.has(definition.itemId))
+ .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }) || (left.itemId - right.itemId));
+ }, [ selectedFurniAssignments, selectedFurniAssignmentMap, furniVariableDefinitions ]);
+ const selectedFurniCustomVariableDefinitionMap = useMemo(() =>
+ {
+ return new Map(selectedFurniCustomVariableDefinitions.map(definition => [ definition.name, definition ]));
+ }, [ selectedFurniCustomVariableDefinitions ]);
const furniVariables = useMemo(() =>
{
- if((inspectionType !== 'furni') || !selectedFurni || !selectedRoomObject) return [];
+ if(!selectedFurni || !selectedRoomObject) return [];
const classId = selectedRoomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID);
const tileSizeZ = Number(selectedFurnitureData?.tileSizeZ ?? 0);
@@ -899,20 +1704,33 @@ export const WiredCreatorToolsView: FC<{}> = () =>
if(selectedFurni.info?.allowWalk) dynamicFlags.push({ key: '@can_stand_on', value: '' });
if(selectedFurni.info?.allowStack) dynamicFlags.push({ key: '@is_stackable', value: '' });
+ const customVariables: InspectionVariable[] = selectedFurniCustomVariableDefinitions.map(definition =>
+ {
+ const assignment = selectedFurniAssignmentMap.get(definition.itemId);
+
+ return {
+ key: definition.name,
+ value: (definition.hasValue ? String(assignment?.value ?? 0) : ''),
+ editable: (canEditInspection && definition.hasValue)
+ };
+ });
+
const variables: InspectionVariable[] = [
+ ...customVariables,
...((Number(selectedFurni.info?.teleportTargetId ?? 0) > 0)
? [ { key: '~teleport.target_id', value: String(selectedFurni.info.teleportTargetId) } ]
: []),
{ key: '@id', value: String(selectedFurni.objectId) },
{ key: '@class_id', value: String(classId) },
{ key: '@height', value: String(Math.round(tileSizeZ * 100)) },
- { key: '@state', value: String(liveState?.state ?? 0), editable: true },
- { key: '@position.x', value: String(liveState?.positionX ?? 0), editable: true },
- { key: '@position.y', value: String(liveState?.positionY ?? 0), editable: true },
- { key: '@rotation', value: String(liveState?.rotation ?? 0), editable: true },
- { key: '@altitude', value: String(liveState?.altitude ?? 0), editable: true },
- ...(wallItemOffset ? [ { key: '@wallitem_offset', value: wallItemOffset, editable: true } ] : []),
- { key: '@type', value: `${ (selectedFurni.category === RoomObjectCategory.WALL) ? 'wall' : 'floor' }${ selectedFurnitureData?.availableForBuildersClub ? ' (BC)' : '' }` },
+ { key: '@state', value: String(liveState?.state ?? 0), editable: canEditInspection },
+ { key: '@position_x', value: String(liveState?.positionX ?? 0), editable: canEditInspection },
+ { key: '@position_y', value: String(liveState?.positionY ?? 0), editable: canEditInspection },
+ { key: '@rotation', value: String(liveState?.rotation ?? 0), editable: canEditInspection },
+ { key: '@altitude', value: String(Math.round((liveState?.altitude ?? 0) * 100)), editable: canEditInspection },
+ { key: '@is_invisible', value: '0' },
+ ...(wallItemOffset ? [ { key: '@wallitem_offset', value: wallItemOffset, editable: canEditInspection } ] : []),
+ { key: '@type', value: `${ selectedFurnitureData?.availableForBuildersClub ? 1 : 0 }${ selectedFurnitureData?.availableForBuildersClub ? ' (BC)' : ' (Normal)' }` },
...dynamicFlags,
{ key: '@dimensions.x', value: String(selectedFurni.info?.tileSizeX ?? 0) },
{ key: '@dimensions.y', value: String(selectedFurni.info?.tileSizeY ?? 0) },
@@ -920,18 +1738,36 @@ export const WiredCreatorToolsView: FC<{}> = () =>
];
return variables;
- }, [ inspectionType, selectedFurni, selectedFurniLiveState, selectedRoomObject, selectedFurnitureData, wallItemOffset ]);
+ }, [ selectedFurni, selectedFurniLiveState, selectedRoomObject, selectedFurnitureData, wallItemOffset, canEditInspection, selectedFurniCustomVariableDefinitions, selectedFurniAssignmentMap ]);
const canEditSelectedUser = useMemo(() =>
{
- if(!selectedUser || !roomSession) return false;
+ return !!selectedUser && !!roomSession && roomSettings.canModify;
+ }, [ selectedUser, roomSession, roomSettings.canModify ]);
+ const selectedUserAssignments = useMemo(() =>
+ {
+ if(!selectedUser) return [];
- if(selectedUser.kind === 'pet') return true;
+ return userVariableAssignments[selectedUser.userId] ?? [];
+ }, [ selectedUser, userVariableAssignments ]);
+ const selectedUserAssignmentMap = useMemo(() =>
+ {
+ return new Map(selectedUserAssignments.map(assignment => [ assignment.variableItemId, assignment ]));
+ }, [ selectedUserAssignments ]);
+ const selectedUserCustomVariableDefinitions = useMemo(() =>
+ {
+ if(!selectedUserAssignments.length) return [];
- return ((selectedUser.kind === 'user') && (selectedUser.roomIndex === roomSession.ownRoomIndex));
- }, [ selectedUser, roomSession ]);
+ return userVariableDefinitions
+ .filter(definition => selectedUserAssignmentMap.has(definition.itemId))
+ .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }) || (left.itemId - right.itemId));
+ }, [ selectedUserAssignments.length, selectedUserAssignmentMap, userVariableDefinitions ]);
+ const selectedUserCustomVariableDefinitionMap = useMemo(() =>
+ {
+ return new Map(selectedUserCustomVariableDefinitions.map(definition => [ definition.name, definition ]));
+ }, [ selectedUserCustomVariableDefinitions ]);
const userVariables = useMemo(() =>
{
- if((inspectionType !== 'user') || !selectedUser) return [];
+ if(!selectedUser) return [];
const liveState = selectedUserLiveState ?? getUserLiveState(selectedUser.roomIndex);
const currentControllerLevel = ((selectedUser.kind === 'user')
@@ -955,6 +1791,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
const handItemValue = Number(selectedUserRoomObject?.model.getValue(RoomObjectVariable.FIGURE_CARRY_OBJECT) ?? 0);
const expressionValue = Number(selectedUserRoomObject?.model.getValue(RoomObjectVariable.FIGURE_EXPRESSION) ?? 0);
const effectValue = Number(selectedUserRoomObject?.model.getValue(RoomObjectVariable.FIGURE_EFFECT) ?? 0);
+ const petOwnerId = ((selectedUser.kind === 'pet') ? Number(roomSession?.userDataManager.getPetData(selectedUser.userId)?.ownerId ?? 0) : 0);
const identityKey = ((selectedUser.kind === 'pet')
? '@pet_id'
: ((selectedUser.kind === 'bot') || (selectedUser.kind === 'rentable_bot'))
@@ -968,45 +1805,60 @@ export const WiredCreatorToolsView: FC<{}> = () =>
if(hasSelectedUserRights) dynamicUserFlags.push({ key: '@has_rights', value: '' });
if(isSelectedUserOwner) dynamicUserFlags.push({ key: '@is_owner', value: '' });
if(isSelectedUserGroupAdmin) dynamicUserFlags.push({ key: '@is_group_admin', value: '' });
- if(isSelectedUserMuted) dynamicUserFlags.push({ key: '@is_mute', value: '' });
+ if(isSelectedUserMuted) dynamicUserFlags.push({ key: '@is_muted', value: '' });
if(isSelectedUserTrading) dynamicUserFlags.push({ key: '@is_trading', value: '' });
if(WIRED_FREEZE_EFFECT_IDS.has(effectValue)) dynamicUserFlags.push({ key: '@is_frozen', value: '' });
- if(effectValue > 0) dynamicUserActions.push({ key: '@effect', value: `${ effectValue } (${ getEffectDisplayName(effectValue) })` });
+ if(effectValue > 0) dynamicUserActions.push({ key: '@effect_id', value: `${ effectValue } (${ getEffectDisplayName(effectValue) })` });
if(teamData) dynamicUserActions.push({ key: '@team_score', value: String(teamData.score) });
if(teamData) dynamicUserActions.push({ key: '@team_color', value: `${ teamData.colorId } (${ getTeamColorDisplayName(teamData.colorId) })` });
if(teamData) dynamicUserActions.push({ key: '@team_type', value: `${ teamData.typeId } (${ getTeamTypeDisplayName(teamData.typeId) })` });
if(signValue >= 0) dynamicUserActions.push({ key: '@sign', value: `${ signValue } (${ getSignDisplayName(signValue) })` });
if(danceValue > 0) dynamicUserActions.push({ key: '@dance', value: `${ danceValue } (${ getDanceDisplayName(danceValue) })` });
if(expressionValue === AvatarExpressionEnum.IDLE.ordinal) dynamicUserActions.push({ key: '@is_idle', value: '' });
- if(handItemValue > 0) dynamicUserActions.push({ key: '@handitems', value: `${ handItemValue } (${ getHandItemDisplayName(handItemValue) })` });
+ if(handItemValue > 0) dynamicUserActions.push({ key: '@handitem_id', value: `${ handItemValue } (${ getHandItemDisplayName(handItemValue) })` });
+
+ const customVariables: InspectionVariable[] = selectedUserCustomVariableDefinitions.map(definition =>
+ {
+ const assignment = selectedUserAssignmentMap.get(definition.itemId);
+
+ return {
+ key: definition.name,
+ value: (definition.hasValue ? String(assignment?.value ?? 0) : ''),
+ editable: (canEditSelectedUser && definition.hasValue)
+ };
+ });
return [
+ ...customVariables,
{ key: '@index', value: String(selectedUser.roomIndex) },
- { key: '@type', value: selectedUser.kind },
- { key: '@gender', value: (selectedUser.gender || 'U') },
+ { key: '@type', value: `${ (selectedUser.kind === 'user') ? 1 : ((selectedUser.kind === 'pet') ? 2 : 4) } (${ selectedUser.kind })` },
+ { key: '@gender', value: `${ (selectedUser.gender === 'F') ? 1 : ((selectedUser.gender === 'M') ? 0 : -1) } (${ selectedUser.gender || 'U' })` },
{ key: '@level', value: String(currentControllerLevel) },
{ key: '@achievement_score', value: String(selectedUser.achievementScore ?? 0) },
...dynamicUserFlags,
...dynamicUserActions,
- { key: '@position.x', value: String(liveState?.positionX ?? 0), editable: canEditSelectedUser },
- { key: '@position.y', value: String(liveState?.positionY ?? 0), editable: canEditSelectedUser },
+ { key: '@position_x', value: String(liveState?.positionX ?? 0), editable: canEditSelectedUser },
+ { key: '@position_y', value: String(liveState?.positionY ?? 0), editable: canEditSelectedUser },
{ key: '@direction', value: String(liveState?.direction ?? 0), editable: canEditSelectedUser },
- { key: '@altitude', value: String(liveState?.altitude ?? 0) },
+ { key: '@altitude', value: String(Math.round((liveState?.altitude ?? 0) * 100)) },
...((Number(selectedUser.favouriteGroupId ?? 0) > 0)
? [ { key: '@favourite_group_id', value: String(selectedUser.favouriteGroupId) } ]
: []),
...((selectedUser.roomEntryMethod && (selectedUser.roomEntryMethod !== 'unknown'))
- ? [ { key: '@room_entry', value: selectedUser.roomEntryMethod } ]
+ ? [ { key: '@room_entry.method', value: `${ getRoomEntryMethodNumericValue(selectedUser.roomEntryMethod) } (${ selectedUser.roomEntryMethod })` } ]
: []),
...(((selectedUser.roomEntryMethod === 'teleport') && (Number(selectedUser.roomEntryTeleportId ?? 0) > 0))
? [ { key: '@room_entry.teleport_id', value: String(selectedUser.roomEntryTeleportId) } ]
: []),
- { key: identityKey, value: String(selectedUser.userId ?? 0) }
+ { key: identityKey, value: String(selectedUser.userId ?? 0) },
+ ...((petOwnerId > 0)
+ ? [ { key: '@pet_owner_id', value: String(petOwnerId) } ]
+ : [])
];
- }, [ inspectionType, selectedUser, selectedUserLiveState, canEditSelectedUser, selectedUserRoomObject, selectedUserActionVersion, roomSession, isTrading, tradeOwnUser, tradeOtherUser ]);
+ }, [ selectedUser, selectedUserLiveState, canEditSelectedUser, selectedUserRoomObject, selectedUserActionVersion, roomSession, isTrading, tradeOwnUser, tradeOtherUser, selectedUserCustomVariableDefinitions, selectedUserAssignmentMap ]);
const globalVariables = useMemo(() =>
{
- if((inspectionType !== 'global') || !roomSession) return [];
+ if(!roomSession) return [];
const roomId = roomSession.roomId;
const unitObjects = GetRoomEngine().getRoomObjects(roomId, RoomObjectCategory.UNIT);
@@ -1048,26 +1900,39 @@ export const WiredCreatorToolsView: FC<{}> = () =>
: globalClock;
const hotelNow = getHotelDateTimeParts(hotelCurrentTimeMs, hotelTimeZone);
const clientTimeZone = (Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC');
+ const hotelTimeZoneOffsetMinutes = getTimeZoneOffsetMinutes(hotelCurrentTimeMs, hotelTimeZone);
+ const clientTimeZoneOffsetMinutes = getTimeZoneOffsetMinutes(hotelCurrentTimeMs, clientTimeZone);
const weekdayIndex = getMondayBasedWeekday(hotelNow);
const monthIndex = hotelNow.month;
+ const customVariables: InspectionVariable[] = roomCustomVariableDefinitions.map(definition =>
+ {
+ const assignment = roomVariableAssignmentMap.get(definition.itemId);
+
+ return {
+ key: definition.name,
+ value: String(assignment?.value ?? 0),
+ editable: canEditInspection
+ };
+ });
return [
+ ...customVariables,
{ key: '@furni_count', value: String(floorObjects.length + wallObjects.length) },
{ key: '@user_count', value: String(userCount) },
- { key: '@wired_timer', value: String(Math.max(0, Math.floor((globalClock - roomEnteredAt) / 1000))) },
- { key: '@teams.red.score', value: String(getRoomTeamScore(1)) },
- { key: '@teams.green.score', value: String(getRoomTeamScore(2)) },
- { key: '@teams.blue.score', value: String(getRoomTeamScore(3)) },
- { key: '@teams.yellow.score', value: String(getRoomTeamScore(4)) },
- { key: '@teams.red.size', value: String(teamSizes[1]) },
- { key: '@teams.green.size', value: String(teamSizes[2]) },
- { key: '@teams.blue.size', value: String(teamSizes[3]) },
- { key: '@teams.yellow.size', value: String(teamSizes[4]) },
+ { key: '@wired_timer', value: String(Math.max(0, Math.floor((globalClock - roomEnteredAt) / 500))) },
+ { key: '@team_red_score', value: String(getRoomTeamScore(1)) },
+ { key: '@team_green_score', value: String(getRoomTeamScore(2)) },
+ { key: '@team_blue_score', value: String(getRoomTeamScore(3)) },
+ { key: '@team_yellow_score', value: String(getRoomTeamScore(4)) },
+ { key: '@team_red_size', value: String(teamSizes[1]) },
+ { key: '@team_green_size', value: String(teamSizes[2]) },
+ { key: '@team_blue_size', value: String(teamSizes[3]) },
+ { key: '@team_yellow_size', value: String(teamSizes[4]) },
{ key: '@room_id', value: String(roomId) },
{ key: '@group_id', value: String(Number(roomSession.groupId ?? 0)) },
- { key: '@timezone_server', value: hotelTimeZone },
- { key: '@timezone_client', value: clientTimeZone },
- { key: '@current_time', value: 'Hidden', valueClassName: 'text-[#d97b78]' },
+ { key: '@timezone_server', value: `${ hotelTimeZoneOffsetMinutes } (${ hotelTimeZone })` },
+ { key: '@timezone_client', value: `${ clientTimeZoneOffsetMinutes } (${ clientTimeZone })` },
+ { key: '@current_time', value: String(Math.floor(hotelCurrentTimeMs / 1000)) },
{ key: '@current_time.millisecond_of_second', value: String(hotelNow.millisecond) },
{ key: '@current_time.seconds_of_minute', value: String(hotelNow.second) },
{ key: '@current_time.minute_of_hour', value: String(hotelNow.minute) },
@@ -1079,19 +1944,921 @@ export const WiredCreatorToolsView: FC<{}> = () =>
{ key: '@current_time.month_of_year', value: `${ monthIndex } (${ MONTH_NAMES[monthIndex - 1] })` },
{ key: '@current_time.year', value: String(hotelNow.year) }
];
- }, [ inspectionType, roomSession, globalClock, roomEnteredAt ]);
+ }, [ roomSession, globalClock, roomEnteredAt, roomCustomVariableDefinitions, roomVariableAssignmentMap, canEditInspection ]);
const displayedVariables = ((inspectionType === 'user')
? userVariables
: ((inspectionType === 'global')
? globalVariables
: furniVariables));
+ const selectedInspectionVariableKey = (selectedInspectionVariableKeys[inspectionType] ?? '');
+ const currentVariableMaps = useMemo(() => ({
+ furni: new Map(furniVariables.map(variable => [ variable.key, variable ])),
+ user: new Map(userVariables.map(variable => [ variable.key, variable ])),
+ global: new Map(globalVariables.map(variable => [ variable.key, variable ]))
+ }), [ furniVariables, userVariables, globalVariables ]);
+ const selectedInspectionCustomDefinition = useMemo(() =>
+ {
+ if(!selectedInspectionVariableKey) return null;
+
+ switch(inspectionType)
+ {
+ case 'user':
+ return (selectedUserCustomVariableDefinitionMap.get(selectedInspectionVariableKey) ?? null);
+ case 'global':
+ return (roomCustomVariableDefinitionMap.get(selectedInspectionVariableKey) ?? null);
+ default:
+ return (selectedFurniCustomVariableDefinitionMap.get(selectedInspectionVariableKey) ?? null);
+ }
+ }, [ inspectionType, selectedInspectionVariableKey, selectedUserCustomVariableDefinitionMap, roomCustomVariableDefinitionMap, selectedFurniCustomVariableDefinitionMap ]);
+ const availableInspectionDefinitions = useMemo(() =>
+ {
+ if(inspectionType === 'global') return [];
+ if((inspectionType === 'user') && !selectedUser) return [];
+ if((inspectionType === 'furni') && !selectedFurni) return [];
+
+ const definitions = ((inspectionType === 'user') ? userVariableDefinitions : furniVariableDefinitions);
+ const assignments = ((inspectionType === 'user') ? selectedUserAssignments : selectedFurniAssignments);
+ const assignedIds = new Set(assignments.map(assignment => assignment.variableItemId));
+
+ return definitions
+ .filter(definition => !assignedIds.has(definition.itemId) && !definition.isReadOnly)
+ .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }) || (left.itemId - right.itemId));
+ }, [ inspectionType, selectedUser, selectedFurni, userVariableDefinitions, furniVariableDefinitions, selectedUserAssignments, selectedFurniAssignments ]);
+ const selectedInspectionGiveDefinition = useMemo(() =>
+ {
+ if(!availableInspectionDefinitions.length) return null;
+
+ return (availableInspectionDefinitions.find(definition => (definition.itemId === inspectionGiveVariableItemId)) ?? availableInspectionDefinitions[0]);
+ }, [ availableInspectionDefinitions, inspectionGiveVariableItemId ]);
+ const canManageInspectionVariableAssignments = useMemo(() =>
+ {
+ if(!roomSettings.canModify) return false;
+
+ if(inspectionType === 'user') return !!selectedUser;
+ if(inspectionType === 'furni') return !!selectedFurni;
+
+ return false;
+ }, [ roomSettings.canModify, inspectionType, selectedUser, selectedFurni ]);
+ const canRemoveInspectionVariable = (canManageInspectionVariableAssignments && !!selectedInspectionCustomDefinition && !selectedInspectionCustomDefinition.isReadOnly);
+ const canGiveInspectionVariable = (canManageInspectionVariableAssignments && !!availableInspectionDefinitions.length);
+ const variableDefinitionsByType = useMemo>(() =>
+ {
+ const customUserDefinitions: VariableDefinition[] = userVariableDefinitions
+ .map(definition => ({
+ key: definition.name,
+ itemId: definition.itemId,
+ target: 'User' as const,
+ type: 'Custom',
+ hasValue: definition.hasValue,
+ isReadOnly: !!definition.isReadOnly,
+ availability: getVariableAvailabilityLabel(definition.availability, 'user'),
+ canWriteTo: (definition.hasValue && !definition.isReadOnly),
+ canCreateDelete: false,
+ canIntercept: false,
+ hasCreationTime: true,
+ hasUpdateTime: true,
+ isTextConnected: definition.isTextConnected,
+ isAlwaysAvailable: false
+ }))
+ .sort((left, right) => left.key.localeCompare(right.key, undefined, { sensitivity: 'base' }));
+ const customFurniDefinitions: VariableDefinition[] = furniVariableDefinitions
+ .map(definition => ({
+ key: definition.name,
+ itemId: definition.itemId,
+ target: 'Furni' as const,
+ type: 'Custom',
+ hasValue: definition.hasValue,
+ isReadOnly: !!definition.isReadOnly,
+ availability: getVariableAvailabilityLabel(definition.availability, 'furni'),
+ canWriteTo: (definition.hasValue && !definition.isReadOnly),
+ canCreateDelete: false,
+ canIntercept: false,
+ hasCreationTime: true,
+ hasUpdateTime: true,
+ isTextConnected: definition.isTextConnected,
+ isAlwaysAvailable: false
+ }))
+ .sort((left, right) => left.key.localeCompare(right.key, undefined, { sensitivity: 'base' }));
+ const customRoomDefinitions: VariableDefinition[] = roomVariableDefinitions
+ .map(definition => ({
+ key: definition.name,
+ itemId: definition.itemId,
+ target: 'Global' as const,
+ type: 'Custom',
+ hasValue: definition.hasValue,
+ isReadOnly: !!definition.isReadOnly,
+ availability: getVariableAvailabilityLabel(definition.availability, 'global'),
+ canWriteTo: !definition.isReadOnly,
+ canCreateDelete: false,
+ canIntercept: false,
+ hasCreationTime: false,
+ hasUpdateTime: true,
+ isTextConnected: definition.isTextConnected,
+ isAlwaysAvailable: false
+ }))
+ .sort((left, right) => left.key.localeCompare(right.key, undefined, { sensitivity: 'base' }));
+ const customContextDefinitions: VariableDefinition[] = contextVariableDefinitions
+ .map(definition => ({
+ key: definition.name,
+ itemId: definition.itemId,
+ target: 'Context' as const,
+ type: 'Custom',
+ hasValue: definition.hasValue,
+ isReadOnly: !!definition.isReadOnly,
+ availability: getVariableAvailabilityLabel(definition.availability, 'context'),
+ canWriteTo: (definition.hasValue && !definition.isReadOnly),
+ canCreateDelete: true,
+ canIntercept: !!definition.hasValue,
+ hasCreationTime: true,
+ hasUpdateTime: true,
+ isTextConnected: definition.isTextConnected,
+ isAlwaysAvailable: false
+ }))
+ .sort((left, right) => left.key.localeCompare(right.key, undefined, { sensitivity: 'base' }));
+
+ return {
+ furni: [ ...customFurniDefinitions, ...VARIABLE_DEFINITIONS.furni ],
+ user: [ ...customUserDefinitions, ...VARIABLE_DEFINITIONS.user ],
+ global: [ ...customRoomDefinitions, ...VARIABLE_DEFINITIONS.global ],
+ context: [ ...customContextDefinitions, ...VARIABLE_DEFINITIONS.context ]
+ };
+ }, [ contextVariableDefinitions, userVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions ]);
+ const variablePickerDefinitions = variableDefinitionsByType[variablesType];
+
+ useEffect(() =>
+ {
+ setSelectedVariableKeys(prevValue =>
+ {
+ let didChange = false;
+ const nextValue = { ...prevValue };
+
+ for(const type of [ 'furni', 'user', 'global', 'context' ] as VariablesElementType[])
+ {
+ const definitions = variableDefinitionsByType[type];
+
+ if(!definitions.length)
+ {
+ if(nextValue[type] !== '')
+ {
+ nextValue[type] = '';
+ didChange = true;
+ }
+
+ continue;
+ }
+
+ if(!definitions.some(definition => (definition.key === nextValue[type])))
+ {
+ nextValue[type] = definitions[0].key;
+ didChange = true;
+ }
+ }
+
+ return (didChange ? nextValue : prevValue);
+ });
+ }, [ variableDefinitionsByType ]);
+ useEffect(() =>
+ {
+ setVariableManageTypeFilter('ALL');
+ setVariableManageSort('highest_value');
+ setVariableManagePage(1);
+ setSelectedManagedVariableEntry(null);
+ }, [ variablesType, selectedVariableKeys[variablesType] ]);
+ useEffect(() =>
+ {
+ if(isVariableManageOpen) return;
+
+ setSelectedManagedVariableEntry(null);
+ }, [ isVariableManageOpen ]);
+
+ const selectedVariableDefinition = useMemo(() =>
+ {
+ const selectedKey = selectedVariableKeys[variablesType];
+
+ return (variablePickerDefinitions.find(variable => (variable.key === selectedKey)) ?? variablePickerDefinitions[0] ?? null);
+ }, [ variablesType, selectedVariableKeys, variablePickerDefinitions ]);
+ const selectedVariableHasInspectionValue = useMemo(() =>
+ {
+ if(!selectedVariableDefinition) return false;
+
+ if(variablesType === 'context') return !!selectedVariableDefinition.hasValue;
+
+ return currentVariableMaps[variablesType].has(selectedVariableDefinition.key);
+ }, [ selectedVariableDefinition, currentVariableMaps, variablesType ]);
+ const selectedVariableProperties = useMemo(() =>
+ {
+ if(!selectedVariableDefinition) return [];
+
+ return [
+ { key: 'Name', value: selectedVariableDefinition.key },
+ { key: 'Type', value: selectedVariableDefinition.type },
+ { key: 'Target', value: selectedVariableDefinition.target },
+ { key: 'Availability', value: selectedVariableDefinition.availability },
+ { key: 'Has value', value: (selectedVariableHasInspectionValue ? 'Yes' : 'No') },
+ { key: 'Can write to', value: ((selectedVariableDefinition.canWriteTo && roomSettings.canModify) ? 'Yes' : 'No') },
+ { key: 'Can create/delete', value: (selectedVariableDefinition.canCreateDelete ? 'Yes' : 'No') },
+ { key: 'Can intercept', value: (selectedVariableDefinition.canIntercept ? 'Yes' : 'No') },
+ { key: 'Is always available', value: (selectedVariableDefinition.isAlwaysAvailable ? 'Yes' : 'No') },
+ { key: 'Has creation time', value: (selectedVariableDefinition.hasCreationTime ? 'Yes' : 'No') },
+ { key: 'Has update time', value: (selectedVariableDefinition.hasUpdateTime ? 'Yes' : 'No') },
+ { key: 'Is text connected', value: (selectedVariableDefinition.isTextConnected ? 'Yes' : 'No') }
+ ];
+ }, [ selectedVariableDefinition, selectedVariableHasInspectionValue, roomSettings.canModify ]);
+ const selectedVariableTextValues = useMemo((): VariableTextValue[] =>
+ {
+ if(!selectedVariableDefinition) return [];
+
+ const localization = GetLocalizationManager();
+ const variableKey = selectedVariableDefinition.key;
+ const textValues: VariableTextValue[] = [];
+ const pushIfLocalized = (value: string, localizationKey: string, fallback: string = null) =>
+ {
+ if(localization?.hasValue(localizationKey))
+ {
+ textValues.push({ value, text: localization.getValue(localizationKey) });
+ return;
+ }
+
+ if(fallback) textValues.push({ value, text: fallback });
+ };
+
+ switch(variableKey)
+ {
+ case '@gender':
+ return [
+ { value: '-1', text: 'Unknown' },
+ { value: '0', text: 'M' },
+ { value: '1', text: 'F' }
+ ];
+ case '@type':
+ if(variablesType === 'furni')
+ {
+ return [
+ { value: '1', text: 'floor' },
+ { value: '2', text: 'wall' }
+ ];
+ }
+
+ if(variablesType === 'user')
+ {
+ return [
+ { value: 'user', text: 'User' },
+ { value: 'bot', text: 'Bot' },
+ { value: 'rentable_bot', text: 'Rentable bot' },
+ { value: 'pet', text: 'Pet' }
+ ];
+ }
+
+ return [];
+ case '@direction':
+ return DIRECTION_NAMES.map((name, index) => ({ value: index.toString(), text: name }));
+ case '@room_entry.method':
+ return [
+ { value: 'door', text: 'Door' },
+ { value: 'teleport', text: 'Teleport' }
+ ];
+ case '@team_color':
+ return [ 1, 2, 3, 4 ].map(value => ({
+ value: value.toString(),
+ text: getTeamColorDisplayName(value)
+ }));
+ case '@team_type':
+ return [ 0, 1, 2 ].map(value => ({
+ value: value.toString(),
+ text: getTeamTypeDisplayName(value)
+ }));
+ case '@sign':
+ for(let value = 0; value <= 17; value++)
+ {
+ textValues.push({
+ value: value.toString(),
+ text: getSignDisplayName(value)
+ });
+ }
+
+ return textValues;
+ case '@dance':
+ pushIfLocalized('0', 'widget.memenu.dance.stop', 'Stop');
+
+ for(let value = 1; value <= 4; value++)
+ {
+ textValues.push({
+ value: value.toString(),
+ text: getDanceDisplayName(value)
+ });
+ }
+
+ return textValues;
+ case '@handitem_id':
+ for(let value = 1; value <= 5000; value++)
+ {
+ const localizationKey = `handitem${ value }`;
+
+ if(!localization?.hasValue(localizationKey)) continue;
+
+ textValues.push({
+ value: value.toString(),
+ text: localization.getValue(localizationKey)
+ });
+ }
+
+ return textValues;
+ case '@effect_id':
+ for(let value = 1; value <= 500; value++)
+ {
+ const localizationKey = `fx_${ value }`;
+
+ if(!localization?.hasValue(localizationKey)) continue;
+
+ textValues.push({
+ value: value.toString(),
+ text: localization.getValue(localizationKey)
+ });
+ }
+
+ return textValues;
+ case '@current_time.day_of_week':
+ return WEEKDAY_NAMES.map((name, index) => ({
+ value: (index + 1).toString(),
+ text: name
+ }));
+ case '@current_time.month_of_year':
+ return MONTH_NAMES.map((name, index) => ({
+ value: (index + 1).toString(),
+ text: name
+ }));
+ case '@chat_type':
+ return [
+ { value: '0', text: 'Talk' },
+ { value: '1', text: 'Shout' },
+ { value: '2', text: 'Whisper' }
+ ];
+ default:
+ return [];
+ }
+ }, [ selectedVariableDefinition, variablesType ]);
+ const variableManageEntries = useMemo((): VariableManageEntry[] =>
+ {
+ if(!selectedVariableDefinition?.itemId || (selectedVariableDefinition.type !== 'Custom') || !roomSession) return [];
+ if(variablesType === 'context') return [];
+
+ if(variablesType === 'user')
+ {
+ const entries: VariableManageEntry[] = [];
+
+ for(const [ userIdString, assignments ] of Object.entries(userVariableAssignments))
+ {
+ const userId = Number(userIdString);
+ const assignment = assignments.find(entry => (entry.variableItemId === selectedVariableDefinition.itemId));
+
+ if(!assignment) continue;
+
+ const userData = roomSession.userDataManager.getUserData(userId)
+ ?? roomSession.userDataManager.getBotData(userId)
+ ?? roomSession.userDataManager.getRentableBotData(userId)
+ ?? roomSession.userDataManager.getPetData(userId);
+
+ let categoryLabel = 'Unknown';
+ let entityName = `#${ userId }`;
+
+ if(userData)
+ {
+ entityName = userData.name || entityName;
+
+ switch(userData.type)
+ {
+ case RoomObjectType.USER:
+ categoryLabel = 'Habbo';
+ break;
+ case RoomObjectType.BOT:
+ categoryLabel = 'Bot';
+ break;
+ case RoomObjectType.RENTABLE_BOT:
+ categoryLabel = 'Rentable bot';
+ break;
+ case RoomObjectType.PET:
+ categoryLabel = 'Pet';
+ break;
+ }
+ }
+
+ entries.push({
+ entityId: userId,
+ entityName,
+ categoryLabel,
+ createdAt: Number(assignment.createdAt ?? 0),
+ updatedAt: Number(assignment.updatedAt ?? 0),
+ value: assignment.value,
+ manageLabel: 'Manage'
+ });
+ }
+
+ return entries;
+ }
+
+ if(variablesType === 'furni')
+ {
+ const entries: VariableManageEntry[] = [];
+
+ for(const [ furniIdString, assignments ] of Object.entries(furniVariableAssignments))
+ {
+ const furniId = Number(furniIdString);
+ const assignment = assignments.find(entry => (entry.variableItemId === selectedVariableDefinition.itemId));
+
+ if(!assignment) continue;
+
+ const floorObject = GetRoomEngine().getRoomObject(roomSession.roomId, furniId, RoomObjectCategory.FLOOR);
+ const wallObject = floorObject ? null : GetRoomEngine().getRoomObject(roomSession.roomId, furniId, RoomObjectCategory.WALL);
+ const roomObject = floorObject ?? wallObject;
+ const category = floorObject ? RoomObjectCategory.FLOOR : (wallObject ? RoomObjectCategory.WALL : -1);
+ const typeId = roomObject?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID);
+ const furnitureData = ((category === RoomObjectCategory.WALL) ? GetSessionDataManager().getWallItemData(typeId) : GetSessionDataManager().getFloorItemData(typeId));
+
+ entries.push({
+ entityId: furniId,
+ entityName: furnitureData?.name || furnitureData?.className || `#${ furniId }`,
+ categoryLabel: (category === RoomObjectCategory.WALL) ? 'Wall furni' : 'Floor furni',
+ createdAt: Number(assignment.createdAt ?? 0),
+ updatedAt: Number(assignment.updatedAt ?? 0),
+ value: assignment.value,
+ manageLabel: 'Manage'
+ });
+ }
+
+ return entries;
+ }
+
+ const assignment = roomVariableAssignmentMap.get(selectedVariableDefinition.itemId);
+
+ return [ {
+ entityId: roomSession.roomId,
+ entityName: `Room #${ roomSession.roomId }`,
+ categoryLabel: 'Global',
+ createdAt: 0,
+ updatedAt: Number(assignment?.updatedAt ?? 0),
+ value: assignment?.value ?? 0,
+ manageLabel: 'Manage'
+ } ];
+ }, [ selectedVariableDefinition, variablesType, roomSession, userVariableAssignments, furniVariableAssignments, roomVariableAssignmentMap ]);
+ const variableManageTypeOptions = useMemo(() =>
+ {
+ switch(variablesType)
+ {
+ case 'user':
+ return [ 'ALL', 'Habbo', 'Bot', 'Rentable bot', 'Pet' ];
+ case 'furni':
+ return [ 'ALL', 'Floor furni', 'Wall furni' ];
+ case 'context':
+ return [ 'ALL', 'Execution' ];
+ default:
+ return [ 'ALL', 'Global' ];
+ }
+ }, [ variablesType ]);
+ const filteredVariableManageEntries = useMemo(() =>
+ {
+ const filtered = variableManageEntries.filter(entry =>
+ {
+ if((variableManageTypeFilter !== 'ALL') && (entry.categoryLabel !== variableManageTypeFilter)) return false;
+
+ return true;
+ });
+
+ filtered.sort((left, right) =>
+ {
+ switch(variableManageSort)
+ {
+ case 'lowest_value':
+ return Number(left.value ?? 0) - Number(right.value ?? 0);
+ case 'newest_update':
+ return Number(right.updatedAt ?? 0) - Number(left.updatedAt ?? 0);
+ case 'oldest_update':
+ return Number(left.updatedAt ?? 0) - Number(right.updatedAt ?? 0);
+ case 'newest_creation':
+ return Number(right.createdAt ?? 0) - Number(left.createdAt ?? 0);
+ case 'oldest_creation':
+ return Number(left.createdAt ?? 0) - Number(right.createdAt ?? 0);
+ case 'name':
+ return left.entityName.localeCompare(right.entityName, undefined, { sensitivity: 'base' });
+ default:
+ return Number(right.value ?? 0) - Number(left.value ?? 0);
+ }
+ });
+
+ return filtered;
+ }, [ variableManageEntries, variableManageTypeFilter, variableManageSort ]);
+ const userVariableDefinitionsById = useMemo(() => new Map(userVariableDefinitions.map(definition => [ definition.itemId, definition ])), [ userVariableDefinitions ]);
+ const furniVariableDefinitionsById = useMemo(() => new Map(furniVariableDefinitions.map(definition => [ definition.itemId, definition ])), [ furniVariableDefinitions ]);
+ const managedHolderVariableEntries = useMemo((): ManagedHolderVariableEntry[] =>
+ {
+ if(!selectedManagedVariableEntry) return [];
+
+ switch(variablesType)
+ {
+ case 'user':
+ return [ ...(userVariableAssignments[selectedManagedVariableEntry.entityId] || []) ]
+ .map((assignment): ManagedHolderVariableEntry | null =>
+ {
+ const definition = userVariableDefinitionsById.get(assignment.variableItemId);
+
+ if(!definition) return null;
+
+ return {
+ variableItemId: assignment.variableItemId,
+ name: definition.name,
+ hasValue: definition.hasValue,
+ isReadOnly: !!definition.isReadOnly,
+ value: assignment.value,
+ availability: getVariableAvailabilityLabel(definition.availability, 'user'),
+ createdAt: Number(assignment.createdAt ?? 0),
+ updatedAt: Number(assignment.updatedAt ?? 0)
+ };
+ })
+ .filter((entry): entry is ManagedHolderVariableEntry => !!entry)
+ .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }));
+ case 'furni':
+ return [ ...(furniVariableAssignments[selectedManagedVariableEntry.entityId] || []) ]
+ .map((assignment): ManagedHolderVariableEntry | null =>
+ {
+ const definition = furniVariableDefinitionsById.get(assignment.variableItemId);
+
+ if(!definition) return null;
+
+ return {
+ variableItemId: assignment.variableItemId,
+ name: definition.name,
+ hasValue: definition.hasValue,
+ isReadOnly: !!definition.isReadOnly,
+ value: assignment.value,
+ availability: getVariableAvailabilityLabel(definition.availability, 'furni'),
+ createdAt: Number(assignment.createdAt ?? 0),
+ updatedAt: Number(assignment.updatedAt ?? 0)
+ };
+ })
+ .filter((entry): entry is ManagedHolderVariableEntry => !!entry)
+ .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }));
+ case 'context':
+ return [];
+ default:
+ return [ ...roomVariableDefinitions ]
+ .map(definition =>
+ {
+ const assignment = roomVariableAssignmentMap.get(definition.itemId);
+
+ return {
+ variableItemId: definition.itemId,
+ name: definition.name,
+ hasValue: definition.hasValue,
+ isReadOnly: !!definition.isReadOnly,
+ value: assignment?.value ?? 0,
+ availability: getVariableAvailabilityLabel(definition.availability, 'global'),
+ createdAt: 0,
+ updatedAt: Number(assignment?.updatedAt ?? 0)
+ };
+ })
+ .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }));
+ }
+ }, [ selectedManagedVariableEntry, variablesType, userVariableAssignments, userVariableDefinitionsById, furniVariableAssignments, furniVariableDefinitionsById, roomVariableDefinitions, roomVariableAssignmentMap ]);
+ const selectedManagedHolderVariableEntry = useMemo(() =>
+ {
+ if(!managedHolderVariableEntries.length) return null;
+
+ return (managedHolderVariableEntries.find(entry => (entry.variableItemId === selectedManagedHolderVariableId)) ?? managedHolderVariableEntries[0]);
+ }, [ managedHolderVariableEntries, selectedManagedHolderVariableId ]);
+ const availableManagedHolderDefinitions = useMemo(() =>
+ {
+ if(!selectedManagedVariableEntry || (variablesType === 'global') || (variablesType === 'context')) return [];
+
+ const assignedIds = new Set(managedHolderVariableEntries.map(entry => entry.variableItemId));
+ const definitions = ((variablesType === 'user') ? userVariableDefinitions : furniVariableDefinitions);
+
+ return definitions
+ .filter(definition => !assignedIds.has(definition.itemId) && !definition.isReadOnly)
+ .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }));
+ }, [ selectedManagedVariableEntry, variablesType, managedHolderVariableEntries, userVariableDefinitions, furniVariableDefinitions ]);
+ const selectedManagedGiveDefinition = useMemo(() =>
+ {
+ if(!availableManagedHolderDefinitions.length) return null;
+
+ return (availableManagedHolderDefinitions.find(definition => (definition.itemId === managedGiveVariableItemId)) ?? availableManagedHolderDefinitions[0]);
+ }, [ availableManagedHolderDefinitions, managedGiveVariableItemId ]);
+ const variableManageTypeLabel = useMemo(() =>
+ {
+ switch(variablesType)
+ {
+ case 'furni':
+ return 'Furni type';
+ case 'global':
+ return 'Scope';
+ case 'context':
+ return 'Execution scope';
+ default:
+ return 'User type';
+ }
+ }, [ variablesType ]);
+ const variableManageCategoryHeader = useMemo(() =>
+ {
+ switch(variablesType)
+ {
+ case 'furni':
+ return 'Furni type';
+ case 'global':
+ return 'Scope';
+ case 'context':
+ return 'Execution scope';
+ default:
+ return 'User type';
+ }
+ }, [ variablesType ]);
+ const managedHolderPanelTitle = useMemo(() =>
+ {
+ switch(variablesType)
+ {
+ case 'furni':
+ return 'Manage Furni Variables';
+ case 'global':
+ return 'Manage Global Variables';
+ case 'context':
+ return 'Manage Context Variables';
+ default:
+ return 'Manage User Variables';
+ }
+ }, [ variablesType ]);
+ const managedHolderInfoLines = useMemo(() =>
+ {
+ if(!selectedManagedVariableEntry || !roomSession) return [];
+
+ if(variablesType === 'user')
+ {
+ const userData = roomSession.userDataManager.getUserData(selectedManagedVariableEntry.entityId)
+ ?? roomSession.userDataManager.getBotData(selectedManagedVariableEntry.entityId)
+ ?? roomSession.userDataManager.getRentableBotData(selectedManagedVariableEntry.entityId)
+ ?? roomSession.userDataManager.getPetData(selectedManagedVariableEntry.entityId);
+
+ return [
+ `${ variableManageCategoryHeader }: ${ selectedManagedVariableEntry.categoryLabel }`,
+ `Name: ${ selectedManagedVariableEntry.entityName }`,
+ `User id: ${ selectedManagedVariableEntry.entityId }`,
+ ...(userData?.type === RoomObjectType.PET ? [ `Pet level: ${ Number(userData.petLevel ?? 0) }` ] : [])
+ ];
+ }
+
+ if(variablesType === 'furni')
+ {
+ return [
+ `${ variableManageCategoryHeader }: ${ selectedManagedVariableEntry.categoryLabel }`,
+ `Name: ${ selectedManagedVariableEntry.entityName }`,
+ `Furni id: ${ selectedManagedVariableEntry.entityId }`
+ ];
+ }
+
+ if(variablesType === 'context')
+ {
+ return [
+ `${ variableManageCategoryHeader }: Current execution`,
+ `Name: ${ selectedManagedVariableEntry.entityName }`,
+ `Context id: ${ selectedManagedVariableEntry.entityId }`
+ ];
+ }
+
+ return [
+ `${ variableManageCategoryHeader }: Room`,
+ `Name: Room #${ roomSession.roomId }`,
+ `Room id: ${ roomSession.roomId }`
+ ];
+ }, [ selectedManagedVariableEntry, roomSession, variablesType, variableManageCategoryHeader ]);
+ const managedHolderWarningText = useMemo(() =>
+ {
+ switch(variablesType)
+ {
+ case 'furni':
+ return 'Use this tool only for furni that are currently loaded in the room. Reload the panel after major room changes.';
+ case 'global':
+ return 'Global variables are room-scoped. Only the latest update time is tracked for these values.';
+ case 'context':
+ return 'Context variables only live inside the current wired execution. They are shared through the signal chain until a branch overrides them.';
+ default:
+ return 'Do not use this tool for users who are currently in the room/using the changed variable in another room.';
+ }
+ }, [ variablesType ]);
+ const managedHolderUserData = useMemo(() =>
+ {
+ if((variablesType !== 'user') || !selectedManagedVariableEntry || !roomSession) return null;
+
+ return roomSession.userDataManager.getUserData(selectedManagedVariableEntry.entityId)
+ ?? roomSession.userDataManager.getBotData(selectedManagedVariableEntry.entityId)
+ ?? roomSession.userDataManager.getRentableBotData(selectedManagedVariableEntry.entityId)
+ ?? roomSession.userDataManager.getPetData(selectedManagedVariableEntry.entityId);
+ }, [ variablesType, selectedManagedVariableEntry, roomSession ]);
+ const managedHolderFurniCategory = useMemo(() =>
+ {
+ if((variablesType !== 'furni') || !selectedManagedVariableEntry) return RoomObjectCategory.FLOOR;
+
+ return (selectedManagedVariableEntry.categoryLabel === 'Wall furni') ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR;
+ }, [ variablesType, selectedManagedVariableEntry ]);
+ const canManageHolderVariables = roomSettings.canModify && !!selectedManagedVariableEntry;
+ const canRemoveManagedHolderVariable = canManageHolderVariables && (variablesType !== 'global') && (variablesType !== 'context') && !!selectedManagedHolderVariableEntry && !selectedManagedHolderVariableEntry.isReadOnly;
+ const canGiveManagedHolderVariable = canManageHolderVariables && (variablesType !== 'global') && (variablesType !== 'context') && !!availableManagedHolderDefinitions.length;
+ useEffect(() =>
+ {
+ if(!selectedManagedVariableEntry)
+ {
+ setSelectedManagedHolderVariableId(0);
+ setEditingManagedHolderVariableId(0);
+ setEditingManagedHolderValue('');
+ setIsManagedGiveOpen(false);
+
+ return;
+ }
+
+ setEditingManagedHolderVariableId(0);
+ setEditingManagedHolderValue('');
+ setIsManagedGiveOpen(false);
+ }, [ selectedManagedVariableEntry ]);
+ useEffect(() =>
+ {
+ if(!managedHolderVariableEntries.length)
+ {
+ setSelectedManagedHolderVariableId(0);
+ return;
+ }
+
+ if(managedHolderVariableEntries.some(entry => (entry.variableItemId === selectedManagedHolderVariableId))) return;
+
+ const preferredItemId = Number(selectedVariableDefinition?.itemId ?? 0);
+ const preferredEntry = managedHolderVariableEntries.find(entry => (entry.variableItemId === preferredItemId));
+
+ setSelectedManagedHolderVariableId(preferredEntry?.variableItemId ?? managedHolderVariableEntries[0].variableItemId);
+ }, [ managedHolderVariableEntries, selectedManagedHolderVariableId, selectedVariableDefinition?.itemId ]);
+ useEffect(() =>
+ {
+ if(!availableManagedHolderDefinitions.length)
+ {
+ setManagedGiveVariableItemId(0);
+ return;
+ }
+
+ if(availableManagedHolderDefinitions.some(definition => (definition.itemId === managedGiveVariableItemId))) return;
+
+ setManagedGiveVariableItemId(availableManagedHolderDefinitions[0].itemId);
+ }, [ availableManagedHolderDefinitions, managedGiveVariableItemId ]);
+ useEffect(() =>
+ {
+ if(!selectedManagedHolderVariableEntry || !editingManagedHolderVariableId) return;
+ if(selectedManagedHolderVariableEntry.variableItemId !== editingManagedHolderVariableId) return;
+
+ setEditingManagedHolderValue(String(selectedManagedHolderVariableEntry.value ?? 0));
+ }, [ selectedManagedHolderVariableEntry, editingManagedHolderVariableId ]);
+ const variableManageDescription = useMemo(() =>
+ {
+ switch(variablesType)
+ {
+ case 'furni':
+ return 'This tool lists every furni in the room that currently holds the selected variable.';
+ case 'global':
+ return 'This tool shows the current room-level state for the selected global variable.';
+ case 'context':
+ return 'Context variables live only inside a wired execution. Use the Variables tab to inspect their definitions, text mappings and execution-only behavior.';
+ default:
+ return 'This tool lists every user in the room that currently holds the selected variable.';
+ }
+ }, [ variablesType ]);
+ const variableManageSortOptions = useMemo(() =>
+ {
+ const options = [
+ { value: 'highest_value', label: 'Highest value' },
+ { value: 'lowest_value', label: 'Lowest value' },
+ { value: 'newest_update', label: 'Most recently updated' },
+ { value: 'oldest_update', label: 'Least recently updated' },
+ { value: 'name', label: 'Name' }
+ ];
+
+ if(selectedVariableDefinition?.hasCreationTime)
+ {
+ options.splice(4, 0,
+ { value: 'newest_creation', label: 'Newest creation' },
+ { value: 'oldest_creation', label: 'Oldest creation' });
+ }
+
+ return options;
+ }, [ selectedVariableDefinition?.hasCreationTime ]);
+ const variableManagePageSize = 12;
+ const variableManagePageCount = Math.max(1, Math.ceil(filteredVariableManageEntries.length / variableManagePageSize));
+ const clampedVariableManagePage = Math.min(variableManagePage, variableManagePageCount);
+ useEffect(() =>
+ {
+ if(variableManagePage <= variableManagePageCount) return;
+
+ setVariableManagePage(variableManagePageCount);
+ }, [ variableManagePage, variableManagePageCount ]);
+ const pagedVariableManageEntries = useMemo(() =>
+ {
+ const startIndex = ((clampedVariableManagePage - 1) * variableManagePageSize);
+
+ return filteredVariableManageEntries.slice(startIndex, (startIndex + variableManagePageSize));
+ }, [ clampedVariableManagePage, filteredVariableManageEntries ]);
+ const variableManageNoValueLabel = (selectedVariableDefinition?.hasValue ? '/' : 'Not supported');
+ const variableManageCanOpen = !!selectedVariableDefinition && (selectedVariableDefinition.type === 'Custom') && (variablesType !== 'context');
+ const commitManagedHolderValueEdit = useCallback(() =>
+ {
+ if(!selectedManagedVariableEntry || !selectedManagedHolderVariableEntry || !roomSettings.canModify || selectedManagedHolderVariableEntry.isReadOnly) return;
+ if(!selectedManagedHolderVariableEntry.hasValue) return;
+ if(variablesType === 'context') return;
+
+ const parsedValue = Number(editingManagedHolderValue.trim());
+ const nextValue = (Number.isFinite(parsedValue) ? Math.trunc(parsedValue) : 0);
+
+ switch(variablesType)
+ {
+ case 'user':
+ updateUserVariableValue(selectedManagedVariableEntry.entityId, selectedManagedHolderVariableEntry.variableItemId, nextValue);
+ break;
+ case 'furni':
+ updateFurniVariableValue(selectedManagedVariableEntry.entityId, selectedManagedHolderVariableEntry.variableItemId, nextValue);
+ break;
+ default:
+ updateRoomVariableValue(selectedManagedHolderVariableEntry.variableItemId, nextValue);
+ break;
+ }
+
+ setEditingManagedHolderVariableId(0);
+ }, [ selectedManagedVariableEntry, selectedManagedHolderVariableEntry, roomSettings.canModify, editingManagedHolderValue, variablesType, updateUserVariableValue, updateFurniVariableValue, updateRoomVariableValue ]);
+ const giveManagedHolderVariable = useCallback(() =>
+ {
+ if(!selectedManagedVariableEntry || !selectedManagedGiveDefinition || !roomSettings.canModify) return;
+ if((variablesType === 'global') || (variablesType === 'context')) return;
+
+ const parsedValue = Number(managedGiveValue.trim());
+ const nextValue = (Number.isFinite(parsedValue) ? Math.trunc(parsedValue) : 0);
+
+ if(variablesType === 'user') assignUserVariable(selectedManagedVariableEntry.entityId, selectedManagedGiveDefinition.itemId, nextValue);
+ else assignFurniVariable(selectedManagedVariableEntry.entityId, selectedManagedGiveDefinition.itemId, nextValue);
+
+ setSelectedManagedHolderVariableId(selectedManagedGiveDefinition.itemId);
+ setManagedGiveValue('0');
+ setIsManagedGiveOpen(false);
+ }, [ selectedManagedVariableEntry, selectedManagedGiveDefinition, roomSettings.canModify, variablesType, managedGiveValue, assignUserVariable, assignFurniVariable ]);
+ const removeManagedHolderVariable = useCallback(() =>
+ {
+ if(!selectedManagedVariableEntry || !selectedManagedHolderVariableEntry || !roomSettings.canModify || selectedManagedHolderVariableEntry.isReadOnly) return;
+ if((variablesType === 'global') || (variablesType === 'context')) return;
+
+ if(variablesType === 'user') removeUserVariable(selectedManagedVariableEntry.entityId, selectedManagedHolderVariableEntry.variableItemId);
+ else removeFurniVariable(selectedManagedVariableEntry.entityId, selectedManagedHolderVariableEntry.variableItemId);
+
+ setEditingManagedHolderVariableId(0);
+ }, [ selectedManagedVariableEntry, selectedManagedHolderVariableEntry, roomSettings.canModify, variablesType, removeUserVariable, removeFurniVariable ]);
+
+ const clearMonitorLogs = () =>
+ {
+ setSelectedMonitorErrorType(null);
+ setSelectedMonitorLogDetails(null);
+ setIsMonitorHistoryOpen(false);
+ setIsMonitorInfoOpen(false);
+ SendMessageComposer(new WiredMonitorRequestComposer(WIRED_MONITOR_ACTION_CLEAR_LOGS));
+ };
+
+ const openMonitorLogDetails = (type: string, details: Partial) =>
+ {
+ setSelectedMonitorErrorType(type);
+ setSelectedMonitorLogDetails({
+ type,
+ severity: String(details.severity ?? MONITOR_ERROR_INFO[type]?.severity ?? 'ERROR'),
+ reason: normalizeMonitorReason(details.reason),
+ sourceLabel: String(details.sourceLabel ?? ''),
+ sourceId: Number(details.sourceId ?? 0),
+ amount: details.amount ? String(details.amount) : undefined,
+ latest: details.latest ? String(details.latest) : undefined,
+ occurredAt: details.occurredAt ? String(details.occurredAt) : undefined
+ });
+ };
const beginVariableEdit = (variable: InspectionVariable) =>
{
if(!variable.editable) return;
- if((inspectionType === 'furni') && !EDITABLE_FURNI_VARIABLES.includes(variable.key)) return;
- if((inspectionType === 'user') && !EDITABLE_USER_VARIABLES.includes(variable.key)) return;
+ if((inspectionType === 'furni'))
+ {
+ const isEditableBuiltIn = EDITABLE_FURNI_VARIABLES.includes(variable.key);
+ const customDefinition = selectedFurniCustomVariableDefinitionMap.get(variable.key);
+ const isEditableCustom = !!customDefinition?.hasValue && !customDefinition?.isReadOnly;
+
+ if(!isEditableBuiltIn && !isEditableCustom) return;
+ }
+ if((inspectionType === 'user'))
+ {
+ const isEditableBuiltIn = EDITABLE_USER_VARIABLES.includes(variable.key);
+ const customDefinition = selectedUserCustomVariableDefinitionMap.get(variable.key);
+ const isEditableCustom = !!customDefinition?.hasValue && !customDefinition?.isReadOnly;
+
+ if(!isEditableBuiltIn && !isEditableCustom) return;
+ }
+ if(inspectionType === 'global')
+ {
+ const customDefinition = roomCustomVariableDefinitionMap.get(variable.key);
+ const isEditableCustom = !!customDefinition && !customDefinition.isReadOnly;
+
+ if(!isEditableCustom) return;
+ }
setEditingVariable(variable.key);
setEditingValue(variable.value);
@@ -1099,8 +2866,72 @@ export const WiredCreatorToolsView: FC<{}> = () =>
const commitVariableEdit = () =>
{
- if((inspectionType === 'user') && selectedUser && roomSession)
+ if(inspectionType === 'global')
{
+ const customDefinition = roomCustomVariableDefinitionMap.get(editingVariable);
+
+ if(!customDefinition || customDefinition.isReadOnly)
+ {
+ cancelVariableEdit();
+ return;
+ }
+
+ const parsed = parseInt(editingValue.trim(), 10);
+
+ if(Number.isNaN(parsed))
+ {
+ cancelVariableEdit();
+ return;
+ }
+
+ const currentValue = roomVariableAssignmentMap.get(customDefinition.itemId)?.value ?? 0;
+
+ if(currentValue === parsed)
+ {
+ cancelVariableEdit();
+ return;
+ }
+
+ updateRoomVariableValue(customDefinition.itemId, parsed);
+ setEditingVariable(null);
+ setEditingValue('');
+ return;
+ }
+
+ if((inspectionType === 'user') && selectedUser)
+ {
+ const customDefinition = selectedUserCustomVariableDefinitionMap.get(editingVariable);
+
+ if(customDefinition?.hasValue && !customDefinition.isReadOnly)
+ {
+ const parsed = parseInt(editingValue.trim(), 10);
+
+ if(Number.isNaN(parsed))
+ {
+ cancelVariableEdit();
+ return;
+ }
+
+ const assignment = selectedUserAssignmentMap.get(customDefinition.itemId);
+
+ if(!assignment || (assignment.value === parsed))
+ {
+ cancelVariableEdit();
+ return;
+ }
+
+ updateUserVariableValue(selectedUser.userId, customDefinition.itemId, parsed);
+ setEditingVariable(null);
+ setEditingValue('');
+ return;
+ }
+
+ if(!roomSession)
+ {
+ cancelVariableEdit();
+ return;
+ }
+
const currentLiveState = (selectedUserLiveState ?? getUserLiveState(selectedUser.roomIndex));
if(!currentLiveState)
@@ -1116,7 +2947,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
switch(editingVariable)
{
- case '@position.x': {
+ case '@position_x': {
const parsed = parseInt(editingValue.trim(), 10);
if(Number.isNaN(parsed))
@@ -1128,7 +2959,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
nextX = parsed;
break;
}
- case '@position.y': {
+ case '@position_y': {
const parsed = parseInt(editingValue.trim(), 10);
if(Number.isNaN(parsed))
@@ -1169,28 +3000,24 @@ export const WiredCreatorToolsView: FC<{}> = () =>
return;
}
- if(selectedUser.kind === 'pet')
- {
- SendMessageComposer(new PetMoveComposer(selectedUser.userId, nextX, nextY, nextDirection));
- }
- else if((selectedUser.kind === 'user') && (selectedUser.roomIndex === roomSession.ownRoomIndex))
- {
- if(editingVariable === '@direction')
- {
- const directionVector = USER_DIRECTION_VECTORS[nextDirection] ?? USER_DIRECTION_VECTORS[0];
+ const selectedRoomObject = GetRoomEngine().getRoomObject(roomSession.roomId, selectedUser.roomIndex, RoomObjectCategory.UNIT);
+ const currentLocation = (selectedRoomObject?.getLocation() ?? new Vector3d(currentLiveState.positionX, currentLiveState.positionY, (currentLiveState.altitude / 100)));
+ const nextLocation = new Vector3d(nextX, nextY, currentLocation.z);
+ const nextDirectionVector = new Vector3d((nextDirection * 45));
- SendMessageComposer(new RoomUnitLookComposer((currentLiveState.positionX + directionVector.x), (currentLiveState.positionY + directionVector.y)));
- }
- else
- {
- SendMessageComposer(new RoomUnitWalkComposer(nextX, nextY));
- }
- }
- else
- {
- cancelVariableEdit();
- return;
- }
+ GetRoomEngine().updateRoomObjectUserLocation(
+ roomSession.roomId,
+ selectedUser.roomIndex,
+ currentLocation,
+ nextLocation,
+ false,
+ 0,
+ nextDirectionVector,
+ (nextDirection * 45),
+ false,
+ true);
+
+ SendMessageComposer(new WiredUserInspectMoveComposer(selectedUser.roomIndex, nextX, nextY, nextDirection));
setSelectedUserLiveState({
...currentLiveState,
@@ -1204,6 +3031,32 @@ export const WiredCreatorToolsView: FC<{}> = () =>
return;
}
+ const customFurniDefinition = selectedFurniCustomVariableDefinitionMap.get(editingVariable);
+
+ if(customFurniDefinition?.hasValue && !customFurniDefinition.isReadOnly)
+ {
+ const parsed = parseInt(editingValue.trim(), 10);
+
+ if(Number.isNaN(parsed))
+ {
+ cancelVariableEdit();
+ return;
+ }
+
+ const assignment = selectedFurniAssignmentMap.get(customFurniDefinition.itemId);
+
+ if(!assignment || (assignment.value === parsed))
+ {
+ cancelVariableEdit();
+ return;
+ }
+
+ updateFurniVariableValue(selectedFurni.objectId, customFurniDefinition.itemId, parsed);
+ setEditingVariable(null);
+ setEditingValue('');
+ return;
+ }
+
if(!editingVariable || !selectedFurni || !selectedRoomObject || !roomSession) return;
const currentLiveState = (selectedFurniLiveState ?? getFurniLiveState(selectedFurni.objectId, selectedFurni.category));
@@ -1225,7 +3078,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
switch(editingVariable)
{
- case '@position.x': {
+ case '@position_x': {
const parsed = parseInt(editingValue.trim(), 10);
if(Number.isNaN(parsed))
@@ -1237,7 +3090,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
nextX = parsed;
break;
}
- case '@position.y': {
+ case '@position_y': {
const parsed = parseInt(editingValue.trim(), 10);
if(Number.isNaN(parsed))
@@ -1465,22 +3318,82 @@ export const WiredCreatorToolsView: FC<{}> = () =>
setEditingVariable(null);
setEditingValue('');
};
+ const giveInspectionVariable = useCallback(() =>
+ {
+ if(!canManageInspectionVariableAssignments || !selectedInspectionGiveDefinition) return;
+
+ const parsedValue = Number(inspectionGiveValue.trim());
+ const nextValue = Number.isFinite(parsedValue) ? parsedValue : 0;
+
+ if((inspectionType === 'user') && selectedUser)
+ {
+ assignUserVariable(selectedUser.userId, selectedInspectionGiveDefinition.itemId, nextValue);
+ setSelectedInspectionVariableKeys(prev => ({ ...prev, user: selectedInspectionGiveDefinition.name }));
+ }
+ else if((inspectionType === 'furni') && selectedFurni)
+ {
+ assignFurniVariable(selectedFurni.objectId, selectedInspectionGiveDefinition.itemId, nextValue);
+ setSelectedInspectionVariableKeys(prev => ({ ...prev, furni: selectedInspectionGiveDefinition.name }));
+ }
+
+ setInspectionGiveValue('0');
+ setIsInspectionGiveOpen(false);
+ }, [ canManageInspectionVariableAssignments, selectedInspectionGiveDefinition, inspectionGiveValue, inspectionType, selectedUser, assignUserVariable, selectedFurni, assignFurniVariable ]);
+ const removeInspectionVariable = useCallback(() =>
+ {
+ if(!canManageInspectionVariableAssignments || !selectedInspectionCustomDefinition || selectedInspectionCustomDefinition.isReadOnly) return;
+
+ cancelVariableEdit();
+
+ if((inspectionType === 'user') && selectedUser)
+ {
+ removeUserVariable(selectedUser.userId, selectedInspectionCustomDefinition.itemId);
+ setSelectedInspectionVariableKeys(prev => ({ ...prev, user: '' }));
+ }
+ else if((inspectionType === 'furni') && selectedFurni)
+ {
+ removeFurniVariable(selectedFurni.objectId, selectedInspectionCustomDefinition.itemId);
+ setSelectedInspectionVariableKeys(prev => ({ ...prev, furni: '' }));
+ }
+
+ setIsInspectionGiveOpen(false);
+ }, [ canManageInspectionVariableAssignments, selectedInspectionCustomDefinition, inspectionType, selectedUser, removeUserVariable, selectedFurni, removeFurniVariable ]);
const onVariableInputKeyDown = (event: KeyboardEvent) =>
{
event.stopPropagation();
- if(event.nativeEvent.stopImmediatePropagation) event.nativeEvent.stopImmediatePropagation();
-
switch(event.key)
{
case 'Enter':
+ case 'NumpadEnter':
event.preventDefault();
commitVariableEdit();
+ window.requestAnimationFrame(() => event.currentTarget.blur());
return;
case 'Escape':
event.preventDefault();
cancelVariableEdit();
+ window.requestAnimationFrame(() => event.currentTarget.blur());
+ return;
+ }
+ };
+ const onManagedHolderValueInputKeyDown = (event: KeyboardEvent) =>
+ {
+ event.stopPropagation();
+
+ switch(event.key)
+ {
+ case 'Enter':
+ case 'NumpadEnter':
+ event.preventDefault();
+ commitManagedHolderValueEdit();
+ window.requestAnimationFrame(() => event.currentTarget.blur());
+ return;
+ case 'Escape':
+ event.preventDefault();
+ setEditingManagedHolderVariableId(0);
+ window.requestAnimationFrame(() => event.currentTarget.blur());
return;
}
};
@@ -1490,6 +3403,39 @@ export const WiredCreatorToolsView: FC<{}> = () =>
setEditingVariable(null);
setEditingValue('');
}, [ selectedFurni?.objectId, selectedUser?.roomIndex, inspectionType ]);
+ useEffect(() =>
+ {
+ if(!displayedVariables.length)
+ {
+ if(selectedInspectionVariableKey)
+ {
+ setSelectedInspectionVariableKeys(prev => ({ ...prev, [inspectionType]: '' }));
+ }
+
+ return;
+ }
+
+ if(displayedVariables.some(variable => (variable.key === selectedInspectionVariableKey))) return;
+
+ setSelectedInspectionVariableKeys(prev => ({ ...prev, [inspectionType]: displayedVariables[0].key }));
+ }, [ displayedVariables, inspectionType, selectedInspectionVariableKey ]);
+ useEffect(() =>
+ {
+ if(!availableInspectionDefinitions.length)
+ {
+ setInspectionGiveVariableItemId(0);
+ return;
+ }
+
+ if(availableInspectionDefinitions.some(definition => (definition.itemId === inspectionGiveVariableItemId))) return;
+
+ setInspectionGiveVariableItemId(availableInspectionDefinitions[0].itemId);
+ }, [ availableInspectionDefinitions, inspectionGiveVariableItemId ]);
+ useEffect(() =>
+ {
+ setIsInspectionGiveOpen(false);
+ setInspectionGiveValue('0');
+ }, [ inspectionType, selectedUser?.userId, selectedFurni?.objectId ]);
useEffect(() =>
{
@@ -1518,6 +3464,7 @@ export const WiredCreatorToolsView: FC<{}> = () =>
if(!isVisible) return null;
return (
+ <>