From 5d8717dedb8c94e8e2c9b604add4a8e611406360 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 11 May 2026 16:31:52 +0000 Subject: [PATCH] Split WiredCreatorToolsView: extract types/constants/helpers into 3 sibling files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The single-file WiredCreatorToolsView.tsx was 4493 lines, which is one of the main reasons the React Compiler reports "Compilation Skipped: Existing memoization could not be preserved" on this module. Split is conservative — only the pure leading sections move out, the component itself is untouched (state, effects, JSX all stay in place). New files (sibling to the view): - WiredCreatorTools.types.ts (~233 lines): every interface and type alias declared at the top of the original file. - WiredCreatorTools.constants.ts (~225 lines): TABS, MONITOR_LOG_ORDER, poll constants, MONITOR_ERROR_INFO, INSPECTION_ELEMENTS, VARIABLES_ELEMENTS, EDITABLE_*, VARIABLE_DEFINITIONS, WIRED_FREEZE_EFFECT_IDS, TEAM_COLOR_NAMES, WEEKDAY/MONTH/DIRECTION names. The createVariableDefinition factory is kept as a local helper in this file (only used to build VARIABLE_DEFINITIONS). - WiredCreatorTools.helpers.ts (~147 lines): createEmptyMonitorSnapshot, getHotelTimeFormatter (with its module-private cache map), getHotelDateTimeParts, formatMonitorLatestOccurrence, formatMonitorHistoryOccurrence, formatVariableTimestamp, formatMonitorSource, normalizeMonitorReason. All pure (or cache-stable), no closure on component state. WiredCreatorToolsView.tsx changes: - 4493 -> 3901 lines (-592, ~13% reduction). - The four inspection-icon asset imports (furni/global/user/context) move to the constants file alongside the only consumers (INSPECTION_ELEMENTS / VARIABLES_ELEMENTS). - AvatarInfoFurni was only referenced by the extracted InspectionFurniSelection interface and is removed from the main file's api import. - New import block at the top pulls back the symbols actually used by the component body. Verification: - yarn eslint on the three new files: 0 errors / 0 warnings. - yarn eslint on WiredCreatorToolsView.tsx: 26 errors before split, 26 errors after split (identical pre-existing set; nothing new introduced). - yarn tsc --noEmit on the four files: clean (only the project-wide pre-existing TS2307 about @nitrots/nitro-renderer not being installed locally remains, same as before). This unblocks future per-tab splits (Monitor / Inspection / Variables JSX panels are still inline in the view and represent the next ~1600 lines that could move out, but require introducing a shared state context first since the current setState chain is intertwined). https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q --- .../WiredCreatorTools.constants.ts | 225 +++++++ .../wired-tools/WiredCreatorTools.helpers.ts | 147 +++++ .../wired-tools/WiredCreatorTools.types.ts | 233 +++++++ .../wired-tools/WiredCreatorToolsView.tsx | 601 +----------------- 4 files changed, 609 insertions(+), 597 deletions(-) create mode 100644 src/components/wired-tools/WiredCreatorTools.constants.ts create mode 100644 src/components/wired-tools/WiredCreatorTools.helpers.ts create mode 100644 src/components/wired-tools/WiredCreatorTools.types.ts diff --git a/src/components/wired-tools/WiredCreatorTools.constants.ts b/src/components/wired-tools/WiredCreatorTools.constants.ts new file mode 100644 index 0000000..056b552 --- /dev/null +++ b/src/components/wired-tools/WiredCreatorTools.constants.ts @@ -0,0 +1,225 @@ +import contextInspectionIcon from '../../assets/images/wiredtools/context.png'; +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 { InspectionElementButton, VariableDefinition, VariablesElementButton, VariablesElementType, WiredToolsTab } from './WiredCreatorTools.types'; + +export const TABS: Array<{ key: WiredToolsTab; label: string; }> = [ + { key: 'monitor', label: 'Monitor' }, + { key: 'variables', label: 'Variables' }, + { key: 'inspection', label: 'Inspection' }, + { key: 'chests', label: 'Chests' }, + { key: 'settings', label: 'Settings' } +]; + +export const MONITOR_LOG_ORDER: string[] = [ + 'EXECUTION_CAP', + 'DELAYED_EVENTS_CAP', + 'EXECUTOR_OVERLOAD', + 'MARKED_AS_HEAVY', + 'KILLED', + 'RECURSION_TIMEOUT' +]; + +export const WIRED_MONITOR_ACTION_FETCH = 0; +export const WIRED_MONITOR_ACTION_CLEAR_LOGS = 1; +export const WIRED_MONITOR_POLL_MS = 50; +export const WIRED_VARIABLES_POLL_MS = 50; +export const WIRED_INSPECTION_REFRESH_MS = 50; +export const WIRED_CLOCK_REFRESH_MS = 50; + +export 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.' + ] + } +}; + +export const INSPECTION_ELEMENTS: InspectionElementButton[] = [ + { key: 'furni', label: 'Furni', icon: furniInspectionIcon }, + { key: 'user', label: 'User', icon: userInspectionIcon }, + { key: 'global', label: 'Global', icon: globalInspectionIcon } +]; + +export 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 } +]; + +export const EDITABLE_FURNI_VARIABLES: string[] = [ '@position_x', '@position_y', '@rotation', '@altitude', '@state', '@wallitem_offset' ]; +export 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') + }); + +export 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') + ] +}; + +export const WIRED_FREEZE_EFFECT_IDS: Set = new Set([ 218, 12, 11, 53, 163 ]); + +export const TEAM_COLOR_NAMES: Record = { + 1: 'red', + 2: 'green', + 3: 'blue', + 4: 'yellow' +}; + +export const WEEKDAY_NAMES: string[] = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ]; +export const MONTH_NAMES: string[] = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; +export const DIRECTION_NAMES: string[] = [ 'North', 'North-East', 'East', 'South-East', 'South', 'South-West', 'West', 'North-West' ]; diff --git a/src/components/wired-tools/WiredCreatorTools.helpers.ts b/src/components/wired-tools/WiredCreatorTools.helpers.ts new file mode 100644 index 0000000..7633324 --- /dev/null +++ b/src/components/wired-tools/WiredCreatorTools.helpers.ts @@ -0,0 +1,147 @@ +import { HotelDateTimeParts, MonitorSnapshot } from './WiredCreatorTools.types'; + +const HOTEL_TIME_FORMATTERS: Map = new Map(); + +export 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: [] + }); + +export const getHotelTimeFormatter = (timeZone: string): Intl.DateTimeFormat => +{ + const formatterTimeZone = (timeZone || 'UTC'); + const existingFormatter = HOTEL_TIME_FORMATTERS.get(formatterTimeZone); + + if(existingFormatter) return existingFormatter; + + let formatter: Intl.DateTimeFormat = null; + + try + { + formatter = new Intl.DateTimeFormat('en-GB', { + timeZone: formatterTimeZone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hourCycle: 'h23' + }); + } + catch + { + formatter = new Intl.DateTimeFormat('en-GB', { + timeZone: 'UTC', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hourCycle: 'h23' + }); + } + + HOTEL_TIME_FORMATTERS.set(formatterTimeZone, formatter); + + return formatter; +}; + +export const getHotelDateTimeParts = (epochMs: number, timeZone: string): HotelDateTimeParts => +{ + const normalizedEpochMs = Number.isFinite(epochMs) ? epochMs : Date.now(); + const date = new Date(normalizedEpochMs); + const formatter = getHotelTimeFormatter(timeZone); + const formattedParts = formatter.formatToParts(date); + const partsMap = new Map(); + + for(const part of formattedParts) + { + if(part.type === 'literal') continue; + + partsMap.set(part.type, part.value); + } + + return { + year: Number(partsMap.get('year') ?? date.getUTCFullYear()), + month: Number(partsMap.get('month') ?? (date.getUTCMonth() + 1)), + day: Number(partsMap.get('day') ?? date.getUTCDate()), + hour: Number(partsMap.get('hour') ?? date.getUTCHours()), + minute: Number(partsMap.get('minute') ?? date.getUTCMinutes()), + second: Number(partsMap.get('second') ?? date.getUTCSeconds()), + millisecond: (((normalizedEpochMs % 1000) + 1000) % 1000) + }; +}; + +export 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`; +}; + +export const formatMonitorHistoryOccurrence = (occurredAtSeconds: number): string => +{ + if(occurredAtSeconds <= 0) return '/'; + + return new Date(occurredAtSeconds * 1000).toLocaleString('en-GB'); +}; + +export const formatVariableTimestamp = (timestamp: number): string => +{ + if(!timestamp || (timestamp <= 0)) return '/'; + + return new Date(timestamp * 1000).toLocaleString('en-GB'); +}; + +export 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; +}; + +export const normalizeMonitorReason = (reason: string): string => +{ + const normalizedReason = (reason || '').trim(); + + return normalizedReason || 'No detailed reason was recorded for this entry.'; +}; diff --git a/src/components/wired-tools/WiredCreatorTools.types.ts b/src/components/wired-tools/WiredCreatorTools.types.ts new file mode 100644 index 0000000..c2f631b --- /dev/null +++ b/src/components/wired-tools/WiredCreatorTools.types.ts @@ -0,0 +1,233 @@ +import { AvatarInfoFurni } from '../../api'; + +export type WiredToolsTab = 'monitor' | 'variables' | 'inspection' | 'chests' | 'settings'; +export type InspectionElementType = 'furni' | 'user' | 'global'; +export type VariablesElementType = InspectionElementType | 'context'; + +export interface InspectionElementButton +{ + key: InspectionElementType; + label: string; + icon: string; +} + +export interface VariablesElementButton +{ + key: VariablesElementType; + label: string; + icon: string; + disabled?: boolean; +} + +export interface InspectionFurniSelection +{ + objectId: number; + category: number; + info: AvatarInfoFurni; +} + +export interface InspectionFurniLiveState +{ + positionX: number; + positionY: number; + altitude: number; + rotation: number; + state: number; +} + +export interface InspectionUserSelection +{ + kind: 'user' | 'bot' | 'rentable_bot' | 'pet'; + roomIndex: number; + name: string; + figure: string; + gender: string; + userId: number; + level: number; + achievementScore: number; + isHC: boolean; + hasRights: boolean; + isOwner: boolean; + favouriteGroupId: number; + roomEntryMethod: string; + roomEntryTeleportId: number; + posture?: string; +} + +export interface InspectionUserLiveState +{ + positionX: number; + positionY: number; + altitude: number; + direction: number; +} + +export interface MonitorStat +{ + label: string; + value: string; +} + +export interface MonitorLog +{ + type: string; + category: string; + amount: string; + latest: string; + latestReason: string; + latestSourceId: number; + latestSourceLabel: string; +} + +export 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; + }>; +} + +export interface MonitorLogDetails +{ + amount?: string; + latest?: string; + occurredAt?: string; + reason: string; + severity: string; + sourceId: number; + sourceLabel: string; + type: string; +} + +export interface InspectionVariable +{ + key: string; + value: string; + editable?: boolean; + valueClassName?: string; +} + +export 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; +} + +export interface VariableTextValue +{ + value: string; + text: string; +} + +export interface VariableManageEntry +{ + categoryLabel: string; + createdAt: number; + entityId: number; + entityName: string; + manageLabel: string; + updatedAt: number; + value: number | null; +} + +export interface VariableHighlightTarget +{ + category: number; + hasValue: boolean; + objectId: number; + value: number | null; +} + +export interface VariableHighlightOverlay extends VariableHighlightTarget +{ + key: string; + x: number; + y: number; +} + +export interface ManagedHolderVariableEntry +{ + availability: string; + createdAt: number; + hasValue: boolean; + name: string; + isReadOnly?: boolean; + updatedAt: number; + value: number | null; + variableItemId: number; +} + +export interface InspectionUserTeamData +{ + colorId: number; + typeId: number; + score: number; +} + +export interface TeamEffectData +{ + colorId: number; + typeId: number; +} + +export interface ParsedWallLocation +{ + width: number; + height: number; + localX: number; + localY: number; + direction: string; +} + +export interface HotelDateTimeParts +{ + year: number; + month: number; + day: number; + hour: number; + minute: number; + second: number; + millisecond: number; +} diff --git a/src/components/wired-tools/WiredCreatorToolsView.tsx b/src/components/wired-tools/WiredCreatorToolsView.tsx index 86c57ff..798d3ad 100644 --- a/src/components/wired-tools/WiredCreatorToolsView.tsx +++ b/src/components/wired-tools/WiredCreatorToolsView.tsx @@ -1,609 +1,16 @@ import { AddLinkEventTracker, AvatarExpressionEnum, FigureUpdateEvent, FurnitureFloorUpdateEvent, FurnitureMultiStateComposer, FurnitureWallMultiStateComposer, FurnitureWallUpdateComposer, FurnitureWallUpdateEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, GetStage, GetTicker, 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, useRef, 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, GetRoomObjectBounds, GetRoomObjectScreenLocation, LocalizeText, NotificationAlertType, SendMessageComposer, WiredSelectionVisualizer } from '../../api'; +import { AvatarInfoUtilities, GetRoomObjectBounds, GetRoomObjectScreenLocation, LocalizeText, NotificationAlertType, SendMessageComposer, WiredSelectionVisualizer } from '../../api'; import { Button, DraggableWindowPosition, LayoutAvatarImageView, LayoutPetImageView, LayoutRoomObjectImageView, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../common'; import { useInventoryTrade, useMessageEvent, useNotification, useObjectSelectedEvent, useRoom, useWiredTools } from '../../hooks'; +import { DIRECTION_NAMES, EDITABLE_FURNI_VARIABLES, EDITABLE_USER_VARIABLES, INSPECTION_ELEMENTS, MONITOR_ERROR_INFO, MONITOR_LOG_ORDER, MONTH_NAMES, TABS, TEAM_COLOR_NAMES, VARIABLES_ELEMENTS, VARIABLE_DEFINITIONS, WEEKDAY_NAMES, WIRED_CLOCK_REFRESH_MS, WIRED_FREEZE_EFFECT_IDS, WIRED_INSPECTION_REFRESH_MS, WIRED_MONITOR_ACTION_CLEAR_LOGS, WIRED_MONITOR_ACTION_FETCH, WIRED_MONITOR_POLL_MS, WIRED_VARIABLES_POLL_MS } from './WiredCreatorTools.constants'; +import { createEmptyMonitorSnapshot, formatMonitorHistoryOccurrence, formatMonitorLatestOccurrence, formatMonitorSource, formatVariableTimestamp, getHotelDateTimeParts, getHotelTimeFormatter, normalizeMonitorReason } from './WiredCreatorTools.helpers'; +import { HotelDateTimeParts, InspectionElementButton, InspectionElementType, InspectionFurniLiveState, InspectionFurniSelection, InspectionUserLiveState, InspectionUserSelection, InspectionUserTeamData, InspectionVariable, ManagedHolderVariableEntry, MonitorLog, MonitorLogDetails, MonitorSnapshot, MonitorStat, ParsedWallLocation, TeamEffectData, VariableDefinition, VariableHighlightOverlay, VariableHighlightTarget, VariableManageEntry, VariableTextValue, VariablesElementButton, VariablesElementType, WiredToolsTab } from './WiredCreatorTools.types'; import { WiredToolsSettingsTabView } from './WiredToolsSettingsTabView'; -type WiredToolsTab = 'monitor' | 'variables' | 'inspection' | 'chests' | 'settings'; -type InspectionElementType = 'furni' | 'user' | 'global'; -type VariablesElementType = InspectionElementType | 'context'; - -interface InspectionElementButton -{ - key: InspectionElementType; - label: string; - icon: string; -} - -interface VariablesElementButton -{ - key: VariablesElementType; - label: string; - icon: string; - disabled?: boolean; -} - -interface InspectionFurniSelection -{ - objectId: number; - category: number; - info: AvatarInfoFurni; -} - -interface InspectionFurniLiveState -{ - positionX: number; - positionY: number; - altitude: number; - rotation: number; - state: number; -} - -interface InspectionUserSelection -{ - kind: 'user' | 'bot' | 'rentable_bot' | 'pet'; - roomIndex: number; - name: string; - figure: string; - gender: string; - userId: number; - level: number; - achievementScore: number; - isHC: boolean; - hasRights: boolean; - isOwner: boolean; - favouriteGroupId: number; - roomEntryMethod: string; - roomEntryTeleportId: number; - posture?: string; -} - -interface InspectionUserLiveState -{ - positionX: number; - positionY: number; - altitude: number; - direction: number; -} - -interface MonitorStat -{ - label: string; - value: string; -} - -interface MonitorLog -{ - type: string; - 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 -{ - key: string; - value: string; - editable?: boolean; - 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 VariableHighlightTarget -{ - category: number; - hasValue: boolean; - objectId: number; - value: number | null; -} - -interface VariableHighlightOverlay extends VariableHighlightTarget -{ - key: string; - x: number; - y: number; -} - -interface ManagedHolderVariableEntry -{ - availability: string; - createdAt: number; - hasValue: boolean; - name: string; - isReadOnly?: boolean; - updatedAt: number; - value: number | null; - variableItemId: number; -} - -interface InspectionUserTeamData -{ - colorId: number; - typeId: number; - score: number; -} - -interface TeamEffectData -{ - colorId: number; - typeId: number; -} - -interface ParsedWallLocation -{ - width: number; - height: number; - localX: number; - localY: number; - direction: string; -} - -interface HotelDateTimeParts -{ - year: number; - month: number; - day: number; - hour: number; - minute: number; - second: number; - millisecond: number; -} - -const TABS: Array<{ key: WiredToolsTab; label: string; }> = [ - { key: 'monitor', label: 'Monitor' }, - { key: 'variables', label: 'Variables' }, - { key: 'inspection', label: 'Inspection' }, - { key: 'chests', label: 'Chests' }, - { key: 'settings', label: 'Settings' } -]; - -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 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', - 2: 'green', - 3: 'blue', - 4: 'yellow' -}; -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'); - const existingFormatter = HOTEL_TIME_FORMATTERS.get(formatterTimeZone); - - if(existingFormatter) return existingFormatter; - - let formatter: Intl.DateTimeFormat = null; - - try - { - formatter = new Intl.DateTimeFormat('en-GB', { - timeZone: formatterTimeZone, - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hourCycle: 'h23' - }); - } - catch - { - formatter = new Intl.DateTimeFormat('en-GB', { - timeZone: 'UTC', - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hourCycle: 'h23' - }); - } - - HOTEL_TIME_FORMATTERS.set(formatterTimeZone, formatter); - - return formatter; -}; - -const getHotelDateTimeParts = (epochMs: number, timeZone: string): HotelDateTimeParts => -{ - const normalizedEpochMs = Number.isFinite(epochMs) ? epochMs : Date.now(); - const date = new Date(normalizedEpochMs); - const formatter = getHotelTimeFormatter(timeZone); - const formattedParts = formatter.formatToParts(date); - const partsMap = new Map(); - - for(const part of formattedParts) - { - if(part.type === 'literal') continue; - - partsMap.set(part.type, part.value); - } - - return { - year: Number(partsMap.get('year') ?? date.getUTCFullYear()), - month: Number(partsMap.get('month') ?? (date.getUTCMonth() + 1)), - day: Number(partsMap.get('day') ?? date.getUTCDate()), - hour: Number(partsMap.get('hour') ?? date.getUTCHours()), - minute: Number(partsMap.get('minute') ?? date.getUTCMinutes()), - second: Number(partsMap.get('second') ?? date.getUTCSeconds()), - millisecond: (((normalizedEpochMs % 1000) + 1000) % 1000) - }; -}; - -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);