}
{ (activeTab === 'inspection') &&
@@ -1642,8 +3717,12 @@ export const WiredCreatorToolsView: FC<{}> = () =>
{ displayedVariables.map((variable, index) => (
beginVariableEdit(variable) }>
+ className={ `${ (selectedInspectionVariableKey === variable.key) ? 'bg-[#d7dfea]' : ((index % 2 === 0) ? 'bg-white' : 'bg-[#f3f3f3]') } ${ variable.editable ? 'cursor-pointer hover:bg-[#e8eefc]' : 'cursor-pointer' }` }
+ onClick={ () =>
+ {
+ setSelectedInspectionVariableKeys(prev => ({ ...prev, [inspectionType]: variable.key }));
+ beginVariableEdit(variable);
+ } }>
{ variable.key }
{ (editingVariable === variable.key) &&
@@ -1654,23 +3733,18 @@ export const WiredCreatorToolsView: FC<{}> = () =>
type="text"
value={ editingValue }
onClick={ event => event.stopPropagation() }
- onBlur={ commitVariableEdit }
+ onBlur={ cancelVariableEdit }
onChange={ event => setEditingValue(event.target.value) }
- onKeyDownCapture={ event =>
- {
- event.stopPropagation();
-
- if(event.nativeEvent.stopImmediatePropagation) event.nativeEvent.stopImmediatePropagation();
- } }
- onKeyDown={ onVariableInputKeyDown } /> }
+ onKeyDownCapture={ onVariableInputKeyDown } /> }
{ (editingVariable !== variable.key) && !variable.editable && { variable.value } }
- { (editingVariable !== variable.key) && variable.editable &&
+ { (editingVariable !== variable.key) && variable.editable &&
{
event.stopPropagation();
+ setSelectedInspectionVariableKeys(prev => ({ ...prev, [inspectionType]: variable.key }));
beginVariableEdit(variable);
} }>
{ variable.value }
@@ -1683,14 +3757,153 @@ export const WiredCreatorToolsView: FC<{}> = () =>
}
-
-
Remove variable
-
Give variable
+
+ { isInspectionGiveOpen &&
+
+ Variable:
+ setInspectionGiveVariableItemId(Number(event.target.value)) }>
+ { !availableInspectionDefinitions.length && No variables available }
+ { availableInspectionDefinitions.map(definition => (
+
+ { definition.name }
+
+ )) }
+
+ Value:
+ setInspectionGiveValue(event.target.value) } />
+ giveInspectionVariable() }>Create
+
}
+
removeInspectionVariable() }>Remove variable
+
setIsInspectionGiveOpen(value => !value) }>
+ Give variable
+
}
+ { (activeTab === 'variables') &&
+
+
+
+
Variable type:
+
+ { VARIABLES_ELEMENTS.map(element => (
+
!element.disabled && setVariablesType(element.key) }
+ title={ element.label }>
+
+
+ )) }
+
+
+
+
Variable picker:
+
+
+
+
+ { variablePickerDefinitions.map((variable, index) => (
+ setSelectedVariableKeys(prev => ({ ...prev, [variablesType]: variable.key })) }>
+ { variable.key }
+
+ )) }
+
+
+
+
+
+
+ Highlight
+
+ {
+ requestUserVariables();
+ setVariableManagePage(1);
+ setSelectedManagedVariableEntry(null);
+ setIsVariableManageOpen(true);
+ } }>
+ Manage
+
+
+
+
+ { (variablesType === 'context') &&
+
+ Context variables live only during the current wired execution. This tab shows their definitions, text mappings and execution-scoped capabilities, but not live values from a running stack.
+
}
+
+
Properties:
+
+
+ Property
+ Value
+
+
+
+
+ { selectedVariableProperties.map((property, index) => (
+
+ { property.key }
+ { property.value }
+
+ )) }
+
+
+
+
+
+
+
Text values:
+
+
+ Value
+ Text
+
+ { !selectedVariableTextValues.length &&
+
+ Nothing to display
+
}
+ { !!selectedVariableTextValues.length &&
+
+
+
+ { selectedVariableTextValues.map((entry, index) => (
+
+ { entry.value }
+ { entry.text }
+
+ )) }
+
+
+
}
+
+
+
+
}
+ { (activeTab === 'settings') && }
{ (activeTab !== 'monitor') &&
(activeTab !== 'inspection') &&
+ (activeTab !== 'variables') &&
+ (activeTab !== 'settings') &&
{ currentTabLabel }
@@ -1701,5 +3914,387 @@ export const WiredCreatorToolsView: FC<{}> = () =>
}
+ { isMonitorHistoryOpen &&
+
+ setIsMonitorHistoryOpen(false) } />
+
+
+ Severity:
+ { [ 'ALL', 'WARNING', 'ERROR' ].map(severity => (
+ setMonitorHistorySeverityFilter(severity as 'ALL' | 'ERROR' | 'WARNING') }>
+ { severity }
+
+ )) }
+ Type:
+ setMonitorHistoryTypeFilter(event.target.value) }>
+ { monitorHistoryTypeOptions.map(type => (
+
+ { type }
+
+ )) }
+
+
+
+
+
+
+ Type
+ Severity
+ Trigger
+ Motivation
+ Occurred at
+
+
+
+ { !filteredMonitorHistoryRows.length &&
+
+ No log history for the current filters
+ }
+ { filteredMonitorHistoryRows.map((row, index) => (
+ openMonitorLogDetails(row.type, {
+ severity: row.category,
+ occurredAt: row.occurredAt,
+ reason: row.reason,
+ sourceLabel: row.sourceLabel,
+ sourceId: row.sourceId
+ }) }>
+ { row.type }
+ { row.category }
+ { formatMonitorSource(row.sourceLabel, row.sourceId) }
+ { row.reason }
+ { row.occurredAt }
+
+ )) }
+
+
+
+
+ }
+ { isMonitorInfoOpen &&
+
+ setIsMonitorInfoOpen(false) } />
+
+ { monitorInfoSections.map(section => (
+
+ { section.title }
+ { section.lines.map((line, index) => (
+ { line }
+ )) }
+
+ )) }
+
+ }
+ { isVariableManageOpen && !!selectedVariableDefinition &&
+
+ setIsVariableManageOpen(false) } />
+
+
+
+ { variableManageDescription }
+ Variable name: { selectedVariableDefinition.key }
+
+
requestUserVariables() }>Refresh
+
+
+
+ { variableManageTypeLabel }:
+
+ {
+ setVariableManageTypeFilter(event.target.value);
+ setVariableManagePage(1);
+ } }>
+ { variableManageTypeOptions.map(type => (
+
+ { (type === 'ALL')
+ ? ((variablesType === 'global') ? 'All entries' : 'All types')
+ : type }
+
+ )) }
+
+
+
+ Sort by:
+
+ {
+ setVariableManageSort(event.target.value);
+ setVariableManagePage(1);
+ } }>
+ { variableManageSortOptions.map(option => (
+
+ { option.label }
+
+ )) }
+
+
+
+
+
+
+
+ { variableManageCategoryHeader }
+ Name
+ { !!selectedVariableDefinition.hasCreationTime && Creation time }
+ { !!selectedVariableDefinition.hasUpdateTime && Last update time }
+ Value
+ Manage
+
+
+
+ { !pagedVariableManageEntries.length &&
+
+ No entries found for the current filters
+ }
+ { pagedVariableManageEntries.map((entry, index) => (
+
+ { entry.categoryLabel }
+ { entry.entityName }
+ { !!selectedVariableDefinition.hasCreationTime && { formatVariableTimestamp(entry.createdAt) } }
+ { !!selectedVariableDefinition.hasUpdateTime && { formatVariableTimestamp(entry.updatedAt) } }
+ { (entry.value ?? variableManageNoValueLabel) }
+
+ setSelectedManagedVariableEntry(entry) }>
+ { entry.manageLabel }
+
+
+
+ )) }
+
+
+
+
+
+ setVariableManagePage(1) }>
+ «
+
+ setVariableManagePage(value => Math.max(1, (value - 1))) }>
+ ‹
+
+
+
+ { `${ filteredVariableManageEntries.length } entr${ (filteredVariableManageEntries.length === 1) ? 'y' : 'ies' } found. Showing page` }
+
+ {
+ const nextPage = Number(event.target.value || 1);
+
+ setVariableManagePage(Math.min(variableManagePageCount, Math.max(1, nextPage)));
+ } } />
+ { `of ${ variableManagePageCount }` }
+
+
+ = variableManagePageCount }
+ type="button"
+ onClick={ () => setVariableManagePage(value => Math.min(variableManagePageCount, (value + 1))) }>
+ ›
+
+ = variableManagePageCount }
+ type="button"
+ onClick={ () => setVariableManagePage(variableManagePageCount) }>
+ »
+
+
+
+
+ }
+ { !!selectedManagedVariableEntry && !!selectedVariableDefinition &&
+
+ setSelectedManagedVariableEntry(null) } />
+
+
+
+ { managedHolderWarningText }
+
+
requestUserVariables() }>Refresh
+
+
+
Holder info:
+
+
+ { (variablesType === 'furni') && roomSession &&
+
}
+ { (variablesType === 'user') && managedHolderUserData &&
+ <>
+ { (managedHolderUserData.type === RoomObjectType.PET)
+ ?
+ :
}
+ > }
+ { (variablesType === 'global') &&
+
}
+
+
+ { managedHolderInfoLines.map((line, index) => (
+ { line }
+ )) }
+
+
+
+
+
{ (variablesType === 'global') ? 'Room variables:' : 'Assigned variables:' }
+
+
+
+ { isManagedGiveOpen &&
+
+ Variable:
+ setManagedGiveVariableItemId(Number(event.target.value)) }>
+ { !availableManagedHolderDefinitions.length && No variables available }
+ { availableManagedHolderDefinitions.map(definition => (
+
+ { definition.name }
+
+ )) }
+
+ Value:
+ setManagedGiveValue(event.target.value) } />
+ giveManagedHolderVariable() }>Create
+
}
+
removeManagedHolderVariable() }>Remove variable
+
setIsManagedGiveOpen(value => !value) }>
+ Give variable
+
+
+
+ }
+ { !!selectedMonitorErrorInfo &&
+
+
+ {
+ setSelectedMonitorErrorType(null);
+ setSelectedMonitorLogDetails(null);
+ } } />
+
+
+ { selectedMonitorErrorInfo.title }
+
+ { selectedMonitorLogDetails?.severity ?? selectedMonitorErrorInfo.severity }
+
+
+ { !!selectedMonitorLogDetails &&
+
+ Trigger: { selectedMonitorDetailSource }
+ Motivation: { selectedMonitorLogDetails.reason }
+ { !!selectedMonitorLogDetails.amount && Amount: { selectedMonitorLogDetails.amount } }
+ { !!selectedMonitorLogDetails.latest && Latest occurrence: { selectedMonitorLogDetails.latest } }
+ { !!selectedMonitorLogDetails.occurredAt && Occurred at: { selectedMonitorLogDetails.occurredAt } }
+
}
+ { selectedMonitorErrorInfo.description.map((paragraph, index) => (
+ { paragraph }
+ )) }
+
+ }
+ >
);
};
diff --git a/src/components/wired-tools/WiredToolsSettingsTabView.tsx b/src/components/wired-tools/WiredToolsSettingsTabView.tsx
new file mode 100644
index 0000000..7858112
--- /dev/null
+++ b/src/components/wired-tools/WiredToolsSettingsTabView.tsx
@@ -0,0 +1,157 @@
+import { FC } from 'react';
+import { LocalizeText } from '../../api';
+import { Button, Text } from '../../common';
+import { useNotification, useRoom, useWiredTools } from '../../hooks';
+
+const WIRED_ACCESS_EVERYONE = 1;
+const WIRED_ACCESS_USERS_WITH_RIGHTS = 2;
+const WIRED_ACCESS_GROUP_MEMBERS = 4;
+const WIRED_ACCESS_GROUP_ADMINS = 8;
+
+interface RoomAccessOption
+{
+ bit: number;
+ label: string;
+}
+
+const toggleMaskBit = (mask: number, bit: number): number => ((mask & bit) ? (mask & ~bit) : (mask | bit));
+const normalizeAccessMask = (mask: number): number => (((mask & WIRED_ACCESS_GROUP_MEMBERS) !== 0) ? (mask | WIRED_ACCESS_GROUP_ADMINS) : mask);
+
+const buildInspectOptions = (): RoomAccessOption[] => [
+ { bit: WIRED_ACCESS_EVERYONE, label: 'Everyone' },
+ { bit: WIRED_ACCESS_USERS_WITH_RIGHTS, label: 'Users with rights' },
+ { bit: WIRED_ACCESS_GROUP_MEMBERS, label: 'Group members' },
+ { bit: WIRED_ACCESS_GROUP_ADMINS, label: 'Group admins' }
+];
+
+const buildModifyOptions = (): RoomAccessOption[] => [
+ { bit: WIRED_ACCESS_USERS_WITH_RIGHTS, label: 'Users with rights' },
+ { bit: WIRED_ACCESS_GROUP_MEMBERS, label: 'Group members' },
+ { bit: WIRED_ACCESS_GROUP_ADMINS, label: 'Group admins' }
+];
+
+export const WiredToolsSettingsTabView: FC<{}> = () =>
+{
+ const { roomSession = null } = useRoom();
+ const { showConfirm = null } = useNotification();
+ const { accountPreferences, roomSettings, saveRoomSettings, updateAccountPreferences } = useWiredTools();
+
+ const canManageSettings = roomSettings.canManageSettings;
+ const canReloadRoom = !!roomSession?.isRoomOwner;
+ const inspectOptions = buildInspectOptions();
+ const modifyOptions = buildModifyOptions();
+ const serverTimeZone = roomSession?.hotelTimeZone || 'UTC';
+
+ const updateInspectMask = (bit: number) =>
+ {
+ if(!canManageSettings) return;
+
+ saveRoomSettings(normalizeAccessMask(toggleMaskBit(roomSettings.inspectMask, bit)), roomSettings.modifyMask);
+ };
+
+ const updateModifyMask = (bit: number) =>
+ {
+ if(!canManageSettings) return;
+
+ const nextModifyMask = toggleMaskBit(roomSettings.modifyMask, bit);
+ const enabledModifyBit = ((nextModifyMask & bit) !== 0);
+ const normalizedModifyMask = normalizeAccessMask(nextModifyMask);
+ const nextInspectMask = normalizeAccessMask(enabledModifyBit ? (roomSettings.inspectMask | bit) : roomSettings.inspectMask);
+
+ saveRoomSettings(nextInspectMask, normalizedModifyMask);
+ };
+
+ const renderAccessOption = (option: RoomAccessOption, mask: number, onToggle: (bit: number) => void) =>
+ {
+ const checked = ((mask & option.bit) !== 0);
+ const disabled = !roomSettings.isLoaded || !canManageSettings;
+
+ return (
+
+ onToggle(option.bit) } />
+ { option.label }
+
+ );
+ };
+
+ return (
+
+
Room settings:
+
+
+ Who can modify Wired:
+ { modifyOptions.map(option => renderAccessOption(option, roomSettings.modifyMask, updateModifyMask)) }
+
+
+ Who can inspect Wired:
+ { inspectOptions.map(option => renderAccessOption(option, roomSettings.inspectMask, updateInspectMask)) }
+
+
+ Timezone:
+
+ { serverTimeZone }
+
+
+
+
Room state:
+
+ showConfirm(
+ LocalizeText('wiredmenu.settings.room_state.reload.warning'),
+ () => roomSession?.sendChatMessage(':reload', 0, ''),
+ null,
+ LocalizeText('generic.ok'),
+ LocalizeText('generic.cancel'),
+ LocalizeText('generic.alert.title')) }>
+ Reload
+
+
+ Rollback
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/wired/WiredView.tsx b/src/components/wired/WiredView.tsx
index 0f073ab..73d81e2 100644
--- a/src/components/wired/WiredView.tsx
+++ b/src/components/wired/WiredView.tsx
@@ -1,5 +1,5 @@
import { ConditionDefinition, TriggerDefinition, WiredActionDefinition } from '@nitrots/nitro-renderer';
-import { FC } from 'react';
+import { FC, Fragment } from 'react';
import { useWired } from '../../hooks';
import { WiredActionLayoutView } from './views/actions/WiredActionLayoutView';
import { WiredConditionLayoutView } from './views/conditions/WiredConditionLayoutView';
@@ -11,11 +11,20 @@ export const WiredView: FC<{}> = props =>
if(!trigger) return null;
- if(trigger instanceof WiredActionDefinition) return WiredActionLayoutView(trigger.code);
+ if(trigger instanceof WiredActionDefinition)
+ {
+ return
{ WiredActionLayoutView(trigger.code) } ;
+ }
- if(trigger instanceof TriggerDefinition) return WiredTriggerLayoutView(trigger.code);
+ if(trigger instanceof TriggerDefinition)
+ {
+ return
{ WiredTriggerLayoutView(trigger.code) } ;
+ }
- if(trigger instanceof ConditionDefinition) return WiredConditionLayoutView(trigger.code);
+ if(trigger instanceof ConditionDefinition)
+ {
+ return
{ WiredConditionLayoutView(trigger.code) } ;
+ }
return null;
};
diff --git a/src/components/wired/views/WiredBaseView.tsx b/src/components/wired/views/WiredBaseView.tsx
index d55e5fc..0858b71 100644
--- a/src/components/wired/views/WiredBaseView.tsx
+++ b/src/components/wired/views/WiredBaseView.tsx
@@ -1,8 +1,10 @@
import { GetRoomEngine, GetSessionDataManager } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, PropsWithChildren, ReactNode, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../api';
+import wiredBgLeft from '../../../assets/images/wired/wired_bg_left.png';
+import wiredBgRight from '../../../assets/images/wired/wired_bg_right.png';
import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common';
-import { useWired } from '../../../hooks';
+import { useWired, useWiredTools } from '../../../hooks';
import { WiredFurniSelectorView } from './WiredFurniSelectorView';
export interface WiredBaseViewProps
@@ -20,11 +22,13 @@ export interface WiredBaseViewProps
export const WiredBaseView: FC
> = props =>
{
+ const WIRED_CARD_WIDTH = 244;
const { wiredType = '', requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_NONE, save = null, validate = null, children = null, hasSpecialInput = false, cardStyle = undefined, footer = null, footerCollapsible = true, selectionPreview = null } = props;
const [ wiredName, setWiredName ] = useState(null);
const [ needsSave, setNeedsSave ] = useState(false);
const [ showFooter, setShowFooter ] = useState(false);
const { trigger = null, setTrigger = null, setIntParams = null, setStringParam = null, setFurniIds = null, setAllowsFurni = null, saveWired = null } = useWired();
+ const { roomSettings } = useWiredTools();
const clearRoomAreaSelection = () =>
{
@@ -41,6 +45,8 @@ export const WiredBaseView: FC> = props =>
const onSave = () =>
{
+ if(!roomSettings.canModify) return;
+
if(validate && !validate()) return;
if(save) save();
@@ -82,24 +88,28 @@ export const WiredBaseView: FC> = props =>
setIntParams(trigger.intData);
setStringParam(trigger.stringData);
}
+ }, [ trigger, hasSpecialInput, setIntParams, setStringParam ]);
- if(requiresFurni > WiredFurniType.STUFF_SELECTION_OPTION_NONE)
+ useEffect(() =>
+ {
+ if(!trigger) return;
+
+ setFurniIds(prevValue =>
{
- setFurniIds(prevValue =>
+ if(prevValue && prevValue.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prevValue);
+
+ if(requiresFurni <= WiredFurniType.STUFF_SELECTION_OPTION_NONE) return [];
+
+ if(trigger.selectedItems && trigger.selectedItems.length)
{
- if(prevValue && prevValue.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prevValue);
+ WiredSelectionVisualizer.applySelectionShaderToFurni(trigger.selectedItems);
- if(trigger.selectedItems && trigger.selectedItems.length)
- {
- WiredSelectionVisualizer.applySelectionShaderToFurni(trigger.selectedItems);
+ return trigger.selectedItems;
+ }
- return trigger.selectedItems;
- }
-
- return [];
- });
- }
- }, [ trigger, hasSpecialInput, setIntParams, setStringParam, setFurniIds ]);
+ return [];
+ });
+ }, [ trigger, requiresFurni, setFurniIds ]);
useEffect(() =>
{
@@ -115,64 +125,53 @@ export const WiredBaseView: FC> = props =>
const resolvedCardStyle: CSSProperties = { ...cardStyle };
- if(resolvedCardStyle.width !== undefined)
- {
- if(typeof resolvedCardStyle.width === 'number')
- {
- resolvedCardStyle.maxWidth = Math.min(resolvedCardStyle.width, 324);
- }
- else if(typeof resolvedCardStyle.width === 'string')
- {
- const match = resolvedCardStyle.width.trim().match(/^(\d+(?:\.\d+)?)px$/i);
-
- resolvedCardStyle.maxWidth = match ? `${ Math.min(parseFloat(match[1]), 324) }px` : resolvedCardStyle.width;
- }
-
- delete resolvedCardStyle.width;
- }
-
- if(resolvedCardStyle.minWidth === undefined) resolvedCardStyle.minWidth = 216;
- if(resolvedCardStyle.maxWidth === undefined) resolvedCardStyle.maxWidth = 'min(90vw, 324px)';
+ resolvedCardStyle.width = WIRED_CARD_WIDTH;
+ resolvedCardStyle.minWidth = WIRED_CARD_WIDTH;
+ resolvedCardStyle.maxWidth = WIRED_CARD_WIDTH;
+ resolvedCardStyle.resize = 'none';
return (
-
+
-
-
+
+
+
{ wiredName }
- { !!children &&
}
- { !!children &&
{ children }
}
- { (requiresFurni > WiredFurniType.STUFF_SELECTION_OPTION_NONE) &&
- <>
-
-
- { selectionPreview || }
-
- > }
- { footer &&
- <>
-
-
- { footerCollapsible
- ? (
- <>
-
setShowFooter(value => !value) }>
- { LocalizeText(showFooter ? 'wiredfurni.params.sources.collapse' : 'wiredfurni.params.sources.expand') }
-
- { showFooter &&
{ footer }
}
- >
- )
- : footer }
-
- > }
-
-
-
{ LocalizeText('wiredfurni.ready') }
-
{ LocalizeText('cancel') }
+
+ { !!children &&
}
+ { !!children &&
{ children }
}
+ { (requiresFurni > WiredFurniType.STUFF_SELECTION_OPTION_NONE) &&
+ <>
+
+
+ { selectionPreview || }
+
+ > }
+ { footer &&
+ <>
+
+
+ { footerCollapsible
+ ? (
+ <>
+
setShowFooter(value => !value) }>
+ { LocalizeText(showFooter ? 'wiredfurni.params.sources.collapse' : 'wiredfurni.params.sources.expand') }
+
+ { showFooter &&
{ footer }
}
+ >
+ )
+ : footer }
+
+ > }
+
+
+ { LocalizeText('wiredfurni.ready') }
+ { LocalizeText('cancel') }
+
diff --git a/src/components/wired/views/WiredFurniSelectionSourceRow.tsx b/src/components/wired/views/WiredFurniSelectionSourceRow.tsx
index b064d80..3037205 100644
--- a/src/components/wired/views/WiredFurniSelectionSourceRow.tsx
+++ b/src/components/wired/views/WiredFurniSelectionSourceRow.tsx
@@ -1,4 +1,4 @@
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
import { FaChevronLeft, FaChevronRight, FaMousePointer } from 'react-icons/fa';
import { LocalizeText } from '../../../api';
import { Button, Text } from '../../../common';
@@ -16,13 +16,14 @@ interface WiredFurniSelectionSourceRowProps
selectionLimit: number;
selectionEnabledValues: number[];
showSelectionToggle?: boolean;
+ headerContent?: ReactNode;
onChange: (value: number) => void;
onSelectionActivate?: () => void;
}
export const WiredFurniSelectionSourceRow: FC
= props =>
{
- const { title = '', titleIsLiteral = false, options = [], value = 0, selectionKind = 'primary', selectionActive = false, selectionCount = 0, selectionLimit = 0, selectionEnabledValues = [], showSelectionToggle = true, onChange = null, onSelectionActivate = null } = props;
+ const { title = '', titleIsLiteral = false, options = [], value = 0, selectionKind = 'primary', selectionActive = false, selectionCount = 0, selectionLimit = 0, selectionEnabledValues = [], showSelectionToggle = true, headerContent = null, onChange = null, onSelectionActivate = null } = props;
const currentIndex = Math.max(0, options.findIndex(option => (option.value === value)));
const currentOption = options[currentIndex] ?? options[0];
const canActivateSelection = !!onSelectionActivate && selectionEnabledValues.includes(currentOption?.value);
@@ -45,6 +46,7 @@ export const WiredFurniSelectionSourceRow: FC
{ resolvedTitle }
+ { headerContent }
{ showSelectionToggle &&
= props =>
const { trigger = null } = useWired();
const availableUserSources = useAvailableUserSources(trigger, userSources, usersTitle, allowClickedUserSource);
const orderedFurniSources = useMemo(() => sortWiredSourceOptions(furniSources, 'furni'), [ furniSources ]);
- const orderedUserSources = useMemo(() => sortWiredSourceOptions(availableUserSources, 'users'), [ availableUserSources ]);
+ const orderedUserSources = useMemo(() =>
+ {
+ if(usersTitle === BOT_SOURCE_TITLE) return [ ...availableUserSources ];
+
+ return sortWiredSourceOptions(availableUserSources, 'users');
+ }, [ availableUserSources, usersTitle ]);
useEffect(() =>
{
diff --git a/src/components/wired/views/WiredVariablePicker.tsx b/src/components/wired/views/WiredVariablePicker.tsx
new file mode 100644
index 0000000..dff6eae
--- /dev/null
+++ b/src/components/wired/views/WiredVariablePicker.tsx
@@ -0,0 +1,413 @@
+import { FC, useEffect, useLayoutEffect, useRef, useState } from 'react';
+import { createPortal } from 'react-dom';
+import { FaChevronRight } from 'react-icons/fa';
+import allIcon from '../../../assets/images/wired/var/var_picker_all.png';
+import internalIcon from '../../../assets/images/wired/var/var_picker_internal.png';
+import recentIcon from '../../../assets/images/wired/var/var_picker_recent.png';
+import searchClearIcon from '../../../assets/images/wired/var/ar_picker_cancel_search.png';
+import searchIcon from '../../../assets/images/wired/var/var_picker_search.png';
+import smartIcon from '../../../assets/images/wired/var/var_picker_smart.png';
+import userMadeIcon from '../../../assets/images/wired/var/var_picker_usermade.png';
+import { LocalizeText } from '../../../api';
+import { Text } from '../../../common';
+import { flattenWiredVariablePickerEntries, IWiredVariablePickerEntry } from './WiredVariablePickerData';
+
+type WiredVariablePickerMode = 'all' | 'recent' | 'usermade' | 'smart' | 'internal' | 'search';
+
+interface WiredVariablePickerProps
+{
+ emptyText?: string;
+ entries: IWiredVariablePickerEntry[];
+ placeholder?: string;
+ recentScope: string;
+ selectedToken: string;
+ onSelect: (entry: IWiredVariablePickerEntry) => void;
+}
+
+const RECENT_PICKER_LIMIT = 12;
+const RECENT_STORAGE_PREFIX = 'nitro.wired.variable-picker.recent';
+
+const PICKER_MODES: Array<{ icon: string; key: WiredVariablePickerMode; }> = [
+ { key: 'all', icon: allIcon },
+ { key: 'recent', icon: recentIcon },
+ { key: 'usermade', icon: userMadeIcon },
+ { key: 'smart', icon: smartIcon },
+ { key: 'internal', icon: internalIcon },
+ { key: 'search', icon: searchIcon }
+];
+
+const normalizeSearch = (value: string) => value.trim().toLocaleLowerCase();
+
+const applyQuery = (entries: IWiredVariablePickerEntry[], query: string): IWiredVariablePickerEntry[] =>
+{
+ if(!query) return entries;
+
+ const nextEntries: IWiredVariablePickerEntry[] = [];
+
+ for(const entry of entries)
+ {
+ const ownMatch = entry.searchableText.toLocaleLowerCase().includes(query);
+ const matchingChildren = entry.children?.length ? applyQuery(entry.children, query) : [];
+
+ if(!ownMatch && !matchingChildren.length) continue;
+
+ nextEntries.push(matchingChildren.length ? { ...entry, children: matchingChildren } : entry);
+ }
+
+ return nextEntries;
+};
+
+const applyMode = (entries: IWiredVariablePickerEntry[], mode: WiredVariablePickerMode, recentTokens: string[]): IWiredVariablePickerEntry[] =>
+{
+ const recentSet = new Set(recentTokens);
+
+ const filterEntries = (items: IWiredVariablePickerEntry[]): IWiredVariablePickerEntry[] =>
+ {
+ const filtered: IWiredVariablePickerEntry[] = [];
+
+ for(const entry of items)
+ {
+ if(mode === 'smart') continue;
+
+ const nextChildren = entry.children?.length ? filterEntries(entry.children) : [];
+ const childVisible = !!nextChildren.length;
+ const selfVisible = (() =>
+ {
+ switch(mode)
+ {
+ case 'recent': return recentSet.has(entry.token);
+ case 'usermade': return entry.kind === 'custom';
+ case 'internal': return entry.kind === 'internal';
+ case 'search':
+ case 'all':
+ default:
+ return true;
+ }
+ })();
+
+ if(!selfVisible && !childVisible) continue;
+
+ filtered.push(childVisible ? { ...entry, children: nextChildren } : entry);
+ }
+
+ return filtered;
+ };
+
+ if(mode === 'recent')
+ {
+ const flatEntries = flattenWiredVariablePickerEntries(entries)
+ .filter(entry => recentSet.has(entry.token))
+ .sort((left, right) => recentTokens.indexOf(left.token) - recentTokens.indexOf(right.token))
+ .map(entry => ({ ...entry, label: entry.displayLabel }));
+
+ return flatEntries.filter(entry => !entry.children?.length);
+ }
+
+ return filterEntries(entries);
+};
+
+export const WiredVariablePicker: FC = props =>
+{
+ const { entries = [], selectedToken = '', onSelect, recentScope, placeholder = LocalizeText('wiredfurni.variable_picker.search'), emptyText = 'Nothing to display' } = props;
+ const containerRef = useRef(null);
+ const panelRef = useRef(null);
+ const searchInputRef = useRef(null);
+ const submenuRef = useRef(null);
+ const storageKey = `${ RECENT_STORAGE_PREFIX }.${ recentScope }`;
+ const [ isOpen, setIsOpen ] = useState(false);
+ const [ mode, setMode ] = useState('all');
+ const [ query, setQuery ] = useState('');
+ const [ recentTokens, setRecentTokens ] = useState([]);
+ const [ activeParentToken, setActiveParentToken ] = useState('');
+ const [ panelPosition, setPanelPosition ] = useState<{ left: number; top: number; width: number; } | null>(null);
+ const [ submenuPosition, setSubmenuPosition ] = useState<{ left: number; top: number; } | null>(null);
+
+ const allEntries = flattenWiredVariablePickerEntries(entries);
+ const selectedEntry = allEntries.find(entry => (entry.token === selectedToken)) || null;
+ const modeEntries = applyMode(entries, mode, recentTokens);
+ const filteredEntries = applyQuery(modeEntries, normalizeSearch(query));
+ const activeParent = filteredEntries.find(entry => (entry.token === activeParentToken) && entry.children?.length) || null;
+ const portalTarget = (typeof document !== 'undefined') ? (document.getElementById('draggable-windows-container') ?? document.body) : null;
+
+ useEffect(() =>
+ {
+ try
+ {
+ const rawValue = window.localStorage.getItem(storageKey);
+
+ if(!rawValue)
+ {
+ setRecentTokens([]);
+ return;
+ }
+
+ const parsedValue = JSON.parse(rawValue) as string[];
+
+ setRecentTokens(Array.isArray(parsedValue) ? parsedValue.filter(token => typeof token === 'string') : []);
+ }
+ catch
+ {
+ setRecentTokens([]);
+ }
+ }, [ storageKey ]);
+
+ useEffect(() =>
+ {
+ if(!isOpen) return;
+
+ const handleClick = (event: MouseEvent) =>
+ {
+ if(containerRef.current?.contains(event.target as Node)) return;
+ if(panelRef.current?.contains(event.target as Node)) return;
+ if(submenuRef.current?.contains(event.target as Node)) return;
+
+ setIsOpen(false);
+ setActiveParentToken('');
+ setPanelPosition(null);
+ setSubmenuPosition(null);
+ };
+
+ const handleEscape = (event: KeyboardEvent) =>
+ {
+ if(event.key !== 'Escape') return;
+
+ setIsOpen(false);
+ setActiveParentToken('');
+ setPanelPosition(null);
+ setSubmenuPosition(null);
+ };
+
+ document.addEventListener('mousedown', handleClick);
+ document.addEventListener('keydown', handleEscape);
+
+ return () =>
+ {
+ document.removeEventListener('mousedown', handleClick);
+ document.removeEventListener('keydown', handleEscape);
+ };
+ }, [ isOpen ]);
+
+ useLayoutEffect(() =>
+ {
+ if(!isOpen)
+ {
+ setPanelPosition(null);
+ return;
+ }
+
+ const updatePanelPosition = () =>
+ {
+ const triggerRect = containerRef.current?.getBoundingClientRect();
+
+ if(!triggerRect)
+ {
+ setPanelPosition(null);
+ return;
+ }
+
+ const panelWidth = Math.max(202, Math.ceil(triggerRect.width));
+ const viewportPadding = 8;
+ const left = Math.min(Math.max(viewportPadding, triggerRect.left), Math.max(viewportPadding, window.innerWidth - panelWidth - viewportPadding));
+
+ setPanelPosition({
+ left,
+ top: triggerRect.bottom + 2,
+ width: panelWidth
+ });
+ setActiveParentToken('');
+ setSubmenuPosition(null);
+ };
+
+ updatePanelPosition();
+
+ window.addEventListener('resize', updatePanelPosition);
+ window.addEventListener('scroll', updatePanelPosition, true);
+
+ return () =>
+ {
+ window.removeEventListener('resize', updatePanelPosition);
+ window.removeEventListener('scroll', updatePanelPosition, true);
+ };
+ }, [ isOpen ]);
+
+ useEffect(() =>
+ {
+ if(!isOpen) return;
+ if(mode !== 'search') return;
+
+ searchInputRef.current?.focus();
+ }, [ isOpen, mode ]);
+
+ useEffect(() =>
+ {
+ if(!activeParentToken) return;
+ if(filteredEntries.some(entry => entry.token === activeParentToken && entry.children?.length)) return;
+
+ setActiveParentToken('');
+ setSubmenuPosition(null);
+ }, [ activeParentToken, filteredEntries ]);
+
+ const rememberSelection = (token: string) =>
+ {
+ if(!token) return;
+
+ const nextRecentTokens = [ token, ...recentTokens.filter(currentToken => (currentToken !== token)) ].slice(0, RECENT_PICKER_LIMIT);
+
+ setRecentTokens(nextRecentTokens);
+
+ try
+ {
+ window.localStorage.setItem(storageKey, JSON.stringify(nextRecentTokens));
+ }
+ catch
+ {
+ }
+ };
+
+ const handleSelect = (entry: IWiredVariablePickerEntry) =>
+ {
+ if(!entry.selectable) return;
+
+ rememberSelection(entry.token);
+ onSelect(entry);
+ setIsOpen(false);
+ setActiveParentToken('');
+ setSubmenuPosition(null);
+ };
+
+ const activateParent = (entry: IWiredVariablePickerEntry, element: HTMLButtonElement) =>
+ {
+ if(!entry.children?.length)
+ {
+ setActiveParentToken('');
+ setSubmenuPosition(null);
+ return;
+ }
+
+ const rowRect = element.getBoundingClientRect();
+ const panelRect = panelRef.current?.getBoundingClientRect();
+ const submenuWidth = 140;
+ const submenuHeight = Math.min((entry.children.length * 20) + 22, 168);
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+ const leftAnchor = panelRect ? panelRect.right : rowRect.right;
+ const rightSpace = viewportWidth - leftAnchor;
+ const canOpenRight = (rightSpace >= (submenuWidth + 8));
+ const left = canOpenRight
+ ? (leftAnchor + 6)
+ : Math.max(8, (panelRect ? panelRect.left : rowRect.left) - submenuWidth - 6);
+ const top = Math.min(Math.max(8, rowRect.top), Math.max(8, viewportHeight - submenuHeight - 8));
+
+ setSubmenuPosition({
+ left,
+ top
+ });
+ setActiveParentToken(entry.token);
+ };
+
+ const renderEntry = (entry: IWiredVariablePickerEntry) =>
+ {
+ const hasChildren = !!entry.children?.length;
+
+ return (
+ activateParent(entry, event.currentTarget) }
+ onClick={ event =>
+ {
+ if(hasChildren)
+ {
+ activateParent(entry, event.currentTarget);
+ return;
+ }
+
+ if(entry.selectable) handleSelect(entry);
+ } }>
+ { entry.label }
+ { hasChildren && }
+
+ );
+ };
+
+ const renderPanel = () =>
+ {
+ if(!panelPosition) return null;
+
+ return (
+
+
+ { PICKER_MODES.map(button => (
+
+ {
+ setMode(button.key);
+ if(button.key === 'search') setTimeout(() => searchInputRef.current?.focus(), 0);
+ } }>
+
+
+ )) }
+
+
+
+
+
setQuery(event.target.value) } />
+ { !!query.length &&
+
setQuery('') }>
+
+ }
+
+
+
+ { filteredEntries.length
+ ? filteredEntries.map(renderEntry)
+ : { emptyText } }
+
+
+ );
+ };
+
+ return (
+
+
setIsOpen(value => !value) }>
+ { selectedEntry?.displayLabel || placeholder }
+
+
+ { isOpen && panelPosition && portalTarget && createPortal(
+
+ { renderPanel() }
+
+ { activeParent?.children?.length && submenuPosition &&
+
+ { activeParent.children.map(child => (
+ handleSelect(child) }>
+ { child.label }
+
+ )) }
+
}
+
,
+ portalTarget
+ ) }
+
+ );
+};
diff --git a/src/components/wired/views/WiredVariablePickerData.ts b/src/components/wired/views/WiredVariablePickerData.ts
new file mode 100644
index 0000000..9216c25
--- /dev/null
+++ b/src/components/wired/views/WiredVariablePickerData.ts
@@ -0,0 +1,423 @@
+export type WiredVariablePickerTarget = 'user' | 'furni' | 'global' | 'context';
+export type WiredVariablePickerUsage = 'give' | 'remove' | 'change-destination' | 'change-reference' | 'condition' | 'filter-main' | 'echo';
+
+export interface IWiredVariableDefinitionLike
+{
+ availability: number;
+ hasValue: boolean;
+ isReadOnly?: boolean;
+ itemId: number;
+ name: string;
+}
+
+export interface IWiredVariablePickerEntry
+{
+ id: string;
+ token: string;
+ label: string;
+ displayLabel: string;
+ searchableText: string;
+ selectable: boolean;
+ hasValue: boolean;
+ kind: 'internal' | 'custom';
+ target: WiredVariablePickerTarget;
+ children?: IWiredVariablePickerEntry[];
+}
+
+interface IInternalVariableMeta
+{
+ key: string;
+ canUseAsDestination: boolean;
+ canUseAsReference: boolean;
+}
+
+const INTERNAL_VARIABLE_ALIASES: Record = {
+ '@position.x': '@position_x',
+ '@position.y': '@position_y',
+ '@effect': '@effect_id',
+ '@handitems': '@handitem_id',
+ '@is_mute': '@is_muted',
+ '@teams.red.score': '@team_red_score',
+ '@teams.green.score': '@team_green_score',
+ '@teams.blue.score': '@team_blue_score',
+ '@teams.yellow.score': '@team_yellow_score',
+ '@teams.red.size': '@team_red_size',
+ '@teams.green.size': '@team_green_size',
+ '@teams.blue.size': '@team_blue_size',
+ '@teams.yellow.size': '@team_yellow_size'
+};
+
+const CUSTOM_TOKEN_PREFIX = 'custom:';
+const INTERNAL_TOKEN_PREFIX = 'internal:';
+const GROUP_TOKEN_PREFIX = 'group:';
+
+const createInternalMeta = (key: string, canUseAsDestination = false, canUseAsReference = false): IInternalVariableMeta =>
+({
+ key,
+ canUseAsDestination,
+ canUseAsReference
+});
+
+export const normalizeInternalVariableKey = (key: string) =>
+{
+ const normalizedKey = key?.trim();
+
+ if(!normalizedKey) return '';
+
+ return (INTERNAL_VARIABLE_ALIASES[normalizedKey] || normalizedKey);
+};
+
+const INTERNAL_VARIABLES: Record<'user' | 'furni' | 'global' | 'context', IInternalVariableMeta[]> = {
+ furni: [
+ createInternalMeta('~teleport.target_id', false, true),
+ createInternalMeta('@id', false, true),
+ createInternalMeta('@class_id', false, true),
+ createInternalMeta('@height', false, true),
+ createInternalMeta('@state', true, true),
+ createInternalMeta('@position_x', true, true),
+ createInternalMeta('@position_y', true, true),
+ createInternalMeta('@rotation', true, true),
+ createInternalMeta('@altitude', true, true),
+ createInternalMeta('@is_invisible', false, true),
+ createInternalMeta('@type', false, true),
+ createInternalMeta('@is_stackable', false, true),
+ createInternalMeta('@can_stand_on', false, true),
+ createInternalMeta('@can_sit_on', false, true),
+ createInternalMeta('@can_lay_on', false, true),
+ createInternalMeta('@wallitem_offset', false, true),
+ createInternalMeta('@dimensions.x', false, true),
+ createInternalMeta('@dimensions.y', false, true),
+ createInternalMeta('@owner_id', false, true)
+ ],
+ user: [
+ createInternalMeta('@index', false, true),
+ createInternalMeta('@type', false, true),
+ createInternalMeta('@gender', false, true),
+ createInternalMeta('@level', false, true),
+ createInternalMeta('@achievement_score', false, true),
+ createInternalMeta('@is_hc', false, true),
+ createInternalMeta('@has_rights', false, true),
+ createInternalMeta('@is_group_admin', false, true),
+ createInternalMeta('@is_owner', false, true),
+ createInternalMeta('@is_muted', false, true),
+ createInternalMeta('@is_trading', false, true),
+ createInternalMeta('@is_frozen', false, true),
+ createInternalMeta('@effect_id', false, true),
+ createInternalMeta('@team_score', false, true),
+ createInternalMeta('@team_color', false, true),
+ createInternalMeta('@team_type', false, true),
+ createInternalMeta('@sign', false, true),
+ createInternalMeta('@dance', false, true),
+ createInternalMeta('@is_idle', false, true),
+ createInternalMeta('@handitem_id', false, true),
+ createInternalMeta('@position_x', true, true),
+ createInternalMeta('@position_y', true, true),
+ createInternalMeta('@direction', true, true),
+ createInternalMeta('@altitude', false, true),
+ createInternalMeta('@favourite_group_id', false, true),
+ createInternalMeta('@room_entry.method', false, true),
+ createInternalMeta('@room_entry.teleport_id', false, true),
+ createInternalMeta('@user_id', false, true),
+ createInternalMeta('@bot_id', false, true),
+ createInternalMeta('@pet_id', false, true),
+ createInternalMeta('@pet_owner_id', false, true)
+ ],
+ global: [
+ createInternalMeta('@furni_count', false, true),
+ createInternalMeta('@user_count', false, true),
+ createInternalMeta('@wired_timer', false, true),
+ createInternalMeta('@team_red_score', false, true),
+ createInternalMeta('@team_green_score', false, true),
+ createInternalMeta('@team_blue_score', false, true),
+ createInternalMeta('@team_yellow_score', false, true),
+ createInternalMeta('@team_red_size', false, true),
+ createInternalMeta('@team_green_size', false, true),
+ createInternalMeta('@team_blue_size', false, true),
+ createInternalMeta('@team_yellow_size', false, true),
+ createInternalMeta('@room_id', false, true),
+ createInternalMeta('@group_id', false, true),
+ createInternalMeta('@timezone_server', false, true),
+ createInternalMeta('@timezone_client', false, true),
+ createInternalMeta('@current_time', false, true),
+ createInternalMeta('@current_time.millisecond_of_second', false, true),
+ createInternalMeta('@current_time.seconds_of_minute', false, true),
+ createInternalMeta('@current_time.minute_of_hour', false, true),
+ createInternalMeta('@current_time.hour_of_day', false, true),
+ createInternalMeta('@current_time.day_of_week', false, true),
+ createInternalMeta('@current_time.day_of_month', false, true),
+ createInternalMeta('@current_time.day_of_year', false, true),
+ createInternalMeta('@current_time.week_of_year', false, true),
+ createInternalMeta('@current_time.month_of_year', false, true),
+ createInternalMeta('@current_time.year', false, true)
+ ],
+ context: [
+ createInternalMeta('@selector_furni_count', false, true),
+ createInternalMeta('@selector_user_count', false, true),
+ createInternalMeta('@signal_furni_count', false, true),
+ createInternalMeta('@signal_user_count', false, true),
+ createInternalMeta('@antenna_id', false, true),
+ createInternalMeta('@chat_type', false, true),
+ createInternalMeta('@chat_style', false, true)
+ ]
+};
+
+const sortEntries = (left: IWiredVariablePickerEntry, right: IWiredVariablePickerEntry) =>
+{
+ return left.displayLabel.localeCompare(right.displayLabel, undefined, { sensitivity: 'base' });
+};
+
+const getNormalizedInternalTarget = (target: WiredVariablePickerTarget): 'user' | 'furni' | 'global' | 'context' =>
+{
+ if(target === 'furni') return 'furni';
+ if(target === 'user') return 'user';
+ if(target === 'context') return 'context';
+
+ return 'global';
+};
+
+const getInternalSelectable = (usage: WiredVariablePickerUsage, meta: IInternalVariableMeta) =>
+{
+ switch(usage)
+ {
+ case 'condition': return true;
+ case 'filter-main': return meta.canUseAsReference;
+ case 'echo': return true;
+ case 'change-destination': return meta.canUseAsDestination;
+ case 'change-reference': return meta.canUseAsReference;
+ default: return false;
+ }
+};
+
+const getCustomSelectable = (usage: WiredVariablePickerUsage, definition: IWiredVariableDefinitionLike) =>
+{
+ switch(usage)
+ {
+ case 'condition':
+ case 'filter-main':
+ return true;
+ case 'echo':
+ return definition.name.includes('.');
+ case 'change-reference':
+ return !!definition.hasValue;
+ case 'change-destination':
+ return (!!definition.hasValue && !definition.isReadOnly);
+ default:
+ return !definition.isReadOnly;
+ }
+};
+
+const getRootKey = (key: string) =>
+{
+ const separatorIndex = key.indexOf('.');
+
+ if(separatorIndex < 0) return null;
+
+ return key.slice(0, separatorIndex);
+};
+
+const createInternalEntry = (target: WiredVariablePickerTarget, usage: WiredVariablePickerUsage, meta: IInternalVariableMeta): IWiredVariablePickerEntry =>
+({
+ id: `${ INTERNAL_TOKEN_PREFIX }${ meta.key }`,
+ token: `${ INTERNAL_TOKEN_PREFIX }${ meta.key }`,
+ label: meta.key,
+ displayLabel: meta.key,
+ searchableText: meta.key,
+ selectable: getInternalSelectable(usage, meta),
+ hasValue: meta.canUseAsReference,
+ kind: 'internal',
+ target
+});
+
+const createCustomEntry = (target: WiredVariablePickerTarget, usage: WiredVariablePickerUsage, definition: IWiredVariableDefinitionLike): IWiredVariablePickerEntry =>
+({
+ id: `${ CUSTOM_TOKEN_PREFIX }${ definition.itemId }`,
+ token: `${ CUSTOM_TOKEN_PREFIX }${ definition.itemId }`,
+ label: definition.name,
+ displayLabel: definition.name,
+ searchableText: definition.name,
+ selectable: getCustomSelectable(usage, definition),
+ hasValue: !!definition.hasValue,
+ kind: 'custom',
+ target
+});
+
+const groupEntries = (entries: IWiredVariablePickerEntry[]) =>
+{
+ const groupedParents = new Map();
+
+ for(const entry of entries)
+ {
+ const displayLabel = entry.displayLabel?.trim();
+
+ if(!displayLabel?.length)
+ {
+ continue;
+ }
+
+ const rootKey = getRootKey(displayLabel) || displayLabel;
+ let group = groupedParents.get(rootKey);
+
+ if(!group)
+ {
+ group = { children: [] };
+ groupedParents.set(rootKey, group);
+ }
+
+ if(displayLabel === rootKey)
+ {
+ group.exact = {
+ ...entry,
+ label: displayLabel,
+ displayLabel,
+ searchableText: displayLabel
+ };
+ continue;
+ }
+
+ const childLabel = displayLabel.slice(rootKey.length + 1).trim();
+
+ if(!childLabel.length) continue;
+
+ group.children.push({
+ ...entry,
+ label: childLabel,
+ displayLabel,
+ searchableText: `${ displayLabel } ${ childLabel }`
+ });
+ }
+
+ const groupedEntries: IWiredVariablePickerEntry[] = [];
+
+ for(const [ rootKey, group ] of groupedParents)
+ {
+ const sortedChildren = [ ...group.children ]
+ .sort(sortEntries)
+ .filter((child, index, collection) => collection.findIndex(entry => (entry.token === child.token)) === index);
+ const shouldGroup = !!sortedChildren.length && (sortedChildren.length > 1 || !!group.exact);
+
+ if(!shouldGroup)
+ {
+ if(group.exact) groupedEntries.push(group.exact);
+ groupedEntries.push(...sortedChildren.map(child => ({ ...child, label: child.displayLabel })));
+ continue;
+ }
+
+ groupedEntries.push({
+ ...(group.exact || {
+ id: `${ GROUP_TOKEN_PREFIX }${ rootKey }`,
+ token: `${ GROUP_TOKEN_PREFIX }${ rootKey }`,
+ label: rootKey,
+ displayLabel: rootKey,
+ searchableText: rootKey,
+ selectable: false,
+ hasValue: false,
+ kind: (sortedChildren[0]?.kind || 'custom'),
+ target: (sortedChildren[0]?.target || 'user')
+ }),
+ label: rootKey,
+ displayLabel: rootKey,
+ searchableText: `${ rootKey } ${ sortedChildren.map(child => child.displayLabel).join(' ') }`,
+ children: sortedChildren
+ });
+ }
+
+ return groupedEntries
+ .filter(entry => entry.displayLabel?.trim().length)
+ .sort(sortEntries)
+ .filter((entry, index, collection) => collection.findIndex(currentEntry => (currentEntry.token === entry.token)) === index);
+};
+
+export const createCustomVariableToken = (itemId: number) => (itemId > 0 ? `${ CUSTOM_TOKEN_PREFIX }${ itemId }` : '');
+export const createInternalVariableToken = (key: string) =>
+{
+ const normalizedKey = normalizeInternalVariableKey(key);
+
+ return normalizedKey ? `${ INTERNAL_TOKEN_PREFIX }${ normalizedKey }` : '';
+};
+export const isCustomVariableToken = (token: string) => !!token && token.startsWith(CUSTOM_TOKEN_PREFIX);
+export const isInternalVariableToken = (token: string) => !!token && token.startsWith(INTERNAL_TOKEN_PREFIX);
+export const getCustomVariableItemId = (token: string) => (isCustomVariableToken(token) ? parseInt(token.slice(CUSTOM_TOKEN_PREFIX.length), 10) || 0 : 0);
+export const getInternalVariableKey = (token: string) => (isInternalVariableToken(token) ? normalizeInternalVariableKey(token.slice(INTERNAL_TOKEN_PREFIX.length)) : '');
+
+export const normalizeVariableTokenFromWire = (value: string) =>
+{
+ const normalizedValue = value?.trim();
+
+ if(!normalizedValue) return '';
+ if(isCustomVariableToken(normalizedValue)) return normalizedValue;
+ if(isInternalVariableToken(normalizedValue)) return createInternalVariableToken(normalizedValue.slice(INTERNAL_TOKEN_PREFIX.length));
+
+ const parsedValue = parseInt(normalizedValue, 10);
+
+ return (!Number.isNaN(parsedValue) && (parsedValue > 0)) ? createCustomVariableToken(parsedValue) : '';
+};
+
+export const createFallbackVariableEntry = (target: WiredVariablePickerTarget, token: string): IWiredVariablePickerEntry | null =>
+{
+ if(!token) return null;
+
+ if(isCustomVariableToken(token))
+ {
+ const itemId = getCustomVariableItemId(token);
+
+ if(itemId <= 0) return null;
+
+ return {
+ id: token,
+ token,
+ label: `#${ itemId }`,
+ displayLabel: `#${ itemId }`,
+ searchableText: `#${ itemId }`,
+ selectable: true,
+ hasValue: false,
+ kind: 'custom',
+ target
+ };
+ }
+
+ if(isInternalVariableToken(token))
+ {
+ const key = getInternalVariableKey(token);
+
+ if(!key) return null;
+
+ return {
+ id: token,
+ token,
+ label: key,
+ displayLabel: key,
+ searchableText: key,
+ selectable: false,
+ hasValue: false,
+ kind: 'internal',
+ target
+ };
+ }
+
+ return null;
+};
+
+export const buildWiredVariablePickerEntries = (target: WiredVariablePickerTarget, usage: WiredVariablePickerUsage, customDefinitions: IWiredVariableDefinitionLike[]) =>
+{
+ const internalTarget = getNormalizedInternalTarget(target);
+ const customEntries = groupEntries([ ...(customDefinitions || []) ]
+ .map(definition => createCustomEntry(target, usage, definition))
+ .sort(sortEntries));
+ const internalEntries = groupEntries(INTERNAL_VARIABLES[internalTarget].map(meta => createInternalEntry(target, usage, meta)));
+
+ return [ ...customEntries, ...internalEntries ];
+};
+
+export const flattenWiredVariablePickerEntries = (entries: IWiredVariablePickerEntry[]): IWiredVariablePickerEntry[] =>
+{
+ const flattened: IWiredVariablePickerEntry[] = [];
+
+ for(const entry of entries)
+ {
+ flattened.push(entry);
+
+ if(entry.children?.length) flattened.push(...entry.children);
+ }
+
+ return flattened;
+};
diff --git a/src/components/wired/views/actions/WiredActionChangeVariableValueView.tsx b/src/components/wired/views/actions/WiredActionChangeVariableValueView.tsx
new file mode 100644
index 0000000..e27b5ed
--- /dev/null
+++ b/src/components/wired/views/actions/WiredActionChangeVariableValueView.tsx
@@ -0,0 +1,483 @@
+import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { GetWiredTimeLocale, LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../../api';
+import contextVariableIcon from '../../../../assets/images/wired/var/icon_source_context_clean.png';
+import furniVariableIcon from '../../../../assets/images/wired/var/icon_source_furni.png';
+import globalVariableIcon from '../../../../assets/images/wired/var/icon_source_global.png';
+import userVariableIcon from '../../../../assets/images/wired/var/icon_source_user.png';
+import { Slider, Text } from '../../../../common';
+import { useWired, useWiredTools } from '../../../../hooks';
+import { NitroInput } from '../../../../layout';
+import { WiredFurniSelectionSourceRow } from '../WiredFurniSelectionSourceRow';
+import { WiredVariablePicker } from '../WiredVariablePicker';
+import { buildWiredVariablePickerEntries, createFallbackVariableEntry, flattenWiredVariablePickerEntries, normalizeVariableTokenFromWire } from '../WiredVariablePickerData';
+import { CLICKED_USER_SOURCE, FURNI_SOURCES, sortWiredSourceOptions, USER_SOURCES, useAvailableUserSources, WiredSourceOption } from '../WiredSourcesSelector';
+import { WiredActionBaseView } from './WiredActionBaseView';
+
+type VariableTargetType = 'user' | 'furni' | 'global' | 'context';
+type ReferenceMode = 'constant' | 'variable';
+type SelectionMode = 'destination' | 'reference';
+
+interface IVariableDefinition
+{
+ availability: number;
+ hasValue: boolean;
+ isReadOnly?: boolean;
+ itemId: number;
+ name: string;
+}
+
+const TARGET_USER = 0;
+const TARGET_FURNI = 1;
+const TARGET_CONTEXT = 2;
+const TARGET_GLOBAL = 3;
+const REFERENCE_CONSTANT = 0;
+const REFERENCE_VARIABLE = 1;
+const SOURCE_TRIGGER = 0;
+const SOURCE_SELECTED = 100;
+const SOURCE_SECONDARY_SELECTED = 101;
+
+const TARGET_BUTTONS: Array<{ key: VariableTargetType; icon: string; disabled?: boolean; }> = [
+ { key: 'furni', icon: furniVariableIcon },
+ { key: 'user', icon: userVariableIcon },
+ { key: 'global', icon: globalVariableIcon },
+ { key: 'context', icon: contextVariableIcon }
+];
+
+const OPERATION_OPTIONS = [ 0, 1, 2, 3, 4, 5, 6, 40, 41, 50, 60, 100, 101, 102, 103, 104, 105 ];
+
+const SECONDARY_FURNI_SOURCES: WiredSourceOption[] = sortWiredSourceOptions([
+ { value: SOURCE_TRIGGER, label: 'wiredfurni.params.sources.furni.0' },
+ { value: SOURCE_SECONDARY_SELECTED, label: 'wiredfurni.params.sources.furni.101' },
+ { value: 200, label: 'wiredfurni.params.sources.furni.200' },
+ { value: 201, label: 'wiredfurni.params.sources.furni.201' }
+], 'furni');
+
+const GLOBAL_SOURCE_OPTIONS: WiredSourceOption[] = [ { value: SOURCE_TRIGGER, label: 'wiredfurni.params.sources.global' } ];
+const CONTEXT_SOURCE_OPTIONS: WiredSourceOption[] = [ { value: SOURCE_TRIGGER, label: 'Current execution' } ];
+
+const parseIds = (value: string): number[] =>
+{
+ if(!value?.length) return [];
+
+ const ids = new Set();
+
+ for(const part of value.split(/[;,\t]/))
+ {
+ const parsedValue = parseInt(part.trim(), 10);
+
+ if(!Number.isNaN(parsedValue) && (parsedValue > 0)) ids.add(parsedValue);
+ }
+
+ return [ ...ids ];
+};
+
+const serializeIds = (ids: number[]) => (ids?.length ? ids.filter(id => (id > 0)).join(';') : '');
+const parseStringData = (value: string) => (value?.length ? value.split('\t', -1) : []);
+const serializeStringData = (destinationVariableToken: string, referenceVariableToken: string, referenceFurniIds: number[]) => `${ destinationVariableToken || '' }\t${ referenceVariableToken || '' }\t${ serializeIds(referenceFurniIds) }`;
+
+const normalizeTargetType = (value: number): VariableTargetType =>
+{
+ switch(value)
+ {
+ case TARGET_FURNI: return 'furni';
+ case TARGET_GLOBAL: return 'global';
+ case TARGET_CONTEXT: return 'context';
+ default: return 'user';
+ }
+};
+
+const getTargetValue = (value: VariableTargetType) =>
+{
+ switch(value)
+ {
+ case 'furni': return TARGET_FURNI;
+ case 'global': return TARGET_GLOBAL;
+ case 'context': return TARGET_CONTEXT;
+ default: return TARGET_USER;
+ }
+};
+
+const resolveSourceOptions = (baseOptions: WiredSourceOption[], selectedValue: number, fallbackOptions: WiredSourceOption[]) =>
+{
+ if(!baseOptions.length) return baseOptions;
+ if(baseOptions.some(option => (option.value === selectedValue))) return baseOptions;
+
+ const fallbackOption = fallbackOptions.find(option => (option.value === selectedValue));
+
+ if(!fallbackOption) return baseOptions;
+
+ return [ ...baseOptions, fallbackOption ];
+};
+
+const getTargetDefinitions = (targetType: VariableTargetType, userDefinitions: IVariableDefinition[], furniDefinitions: IVariableDefinition[], roomDefinitions: IVariableDefinition[], contextDefinitions: IVariableDefinition[]) =>
+{
+ switch(targetType)
+ {
+ case 'furni': return furniDefinitions;
+ case 'global': return roomDefinitions;
+ case 'context': return contextDefinitions;
+ default: return userDefinitions;
+ }
+};
+
+const isGlobalTarget = (targetType: VariableTargetType) => (targetType === 'global');
+const isFurniTarget = (targetType: VariableTargetType) => (targetType === 'furni');
+const isContextTarget = (targetType: VariableTargetType) => (targetType === 'context');
+
+export const WiredActionChangeVariableValueView: FC<{}> = () =>
+{
+ const { trigger = null, furniIds = [], actionDelay = 0, setActionDelay = null, setAllowsFurni = null, setFurniIds = null, setIntParams = null, setStringParam = null } = useWired();
+ const { userVariableDefinitions = [], furniVariableDefinitions = [], roomVariableDefinitions = [], contextVariableDefinitions = [] } = useWiredTools();
+ const [ destinationTargetType, setDestinationTargetType ] = useState('user');
+ const [ destinationVariableToken, setDestinationVariableToken ] = useState('');
+ const [ operation, setOperation ] = useState(0);
+ const [ referenceMode, setReferenceMode ] = useState('constant');
+ const [ referenceConstantValueInput, setReferenceConstantValueInput ] = useState('0');
+ const [ referenceTargetType, setReferenceTargetType ] = useState('user');
+ const [ referenceVariableToken, setReferenceVariableToken ] = useState('');
+ const [ destinationUserSource, setDestinationUserSource ] = useState(SOURCE_TRIGGER);
+ const [ destinationFurniSource, setDestinationFurniSource ] = useState(SOURCE_TRIGGER);
+ const [ referenceUserSource, setReferenceUserSource ] = useState(SOURCE_TRIGGER);
+ const [ referenceFurniSource, setReferenceFurniSource ] = useState(SOURCE_TRIGGER);
+ const [ destinationFurniIds, setDestinationFurniIds ] = useState([]);
+ const [ referenceFurniIds, setReferenceFurniIds ] = useState([]);
+ const [ selectionMode, setSelectionMode ] = useState('destination');
+ const highlightedIds = useRef([]);
+
+ const availableUserSources = useAvailableUserSources(trigger, USER_SOURCES);
+ const orderedUserSources = useMemo(() => sortWiredSourceOptions(availableUserSources, 'users'), [ availableUserSources ]);
+ const orderedFurniSources = useMemo(() => sortWiredSourceOptions(FURNI_SOURCES, 'furni'), []);
+ const userSourceFallbackOptions = useMemo(() => sortWiredSourceOptions([ ...USER_SOURCES, CLICKED_USER_SOURCE ], 'users'), []);
+ const destinationDefinitions = useMemo(() => getTargetDefinitions(destinationTargetType, userVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions, contextVariableDefinitions), [ contextVariableDefinitions, destinationTargetType, furniVariableDefinitions, roomVariableDefinitions, userVariableDefinitions ]);
+ const referenceDefinitions = useMemo(() => getTargetDefinitions(referenceTargetType, userVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions, contextVariableDefinitions), [ contextVariableDefinitions, furniVariableDefinitions, referenceTargetType, roomVariableDefinitions, userVariableDefinitions ]);
+ const destinationVariableEntries = useMemo(() => buildWiredVariablePickerEntries(destinationTargetType, 'change-destination', destinationDefinitions), [ destinationDefinitions, destinationTargetType ]);
+ const resolvedDestinationVariableEntries = useMemo(() =>
+ {
+ if(!destinationVariableToken) return destinationVariableEntries;
+ if(flattenWiredVariablePickerEntries(destinationVariableEntries).some(entry => (entry.token === destinationVariableToken))) return destinationVariableEntries;
+
+ const fallbackEntry = createFallbackVariableEntry(destinationTargetType, destinationVariableToken);
+
+ return fallbackEntry ? [ fallbackEntry, ...destinationVariableEntries ] : destinationVariableEntries;
+ }, [ destinationTargetType, destinationVariableEntries, destinationVariableToken ]);
+ const referenceVariableEntries = useMemo(() => buildWiredVariablePickerEntries(referenceTargetType, 'change-reference', referenceDefinitions), [ referenceDefinitions, referenceTargetType ]);
+ const resolvedReferenceVariableEntries = useMemo(() =>
+ {
+ if(!referenceVariableToken) return referenceVariableEntries;
+ if(flattenWiredVariablePickerEntries(referenceVariableEntries).some(entry => (entry.token === referenceVariableToken))) return referenceVariableEntries;
+
+ const fallbackEntry = createFallbackVariableEntry(referenceTargetType, referenceVariableToken);
+
+ return fallbackEntry ? [ fallbackEntry, ...referenceVariableEntries ] : referenceVariableEntries;
+ }, [ referenceTargetType, referenceVariableEntries, referenceVariableToken ]);
+
+ const destinationSelectionEnabled = isFurniTarget(destinationTargetType) && (destinationFurniSource === SOURCE_SELECTED);
+ const referenceSelectionEnabled = (referenceMode === 'variable') && isFurniTarget(referenceTargetType) && (referenceFurniSource === SOURCE_SECONDARY_SELECTED);
+ const destinationSelectedSourceValue = isFurniTarget(destinationTargetType) ? destinationFurniSource : (isGlobalTarget(destinationTargetType) ? SOURCE_TRIGGER : destinationUserSource);
+ const referenceSelectedSourceValue = isFurniTarget(referenceTargetType) ? referenceFurniSource : (isGlobalTarget(referenceTargetType) ? SOURCE_TRIGGER : referenceUserSource);
+
+ const destinationSourceOptions = useMemo(() =>
+ {
+ if(isContextTarget(destinationTargetType)) return CONTEXT_SOURCE_OPTIONS;
+ if(isFurniTarget(destinationTargetType)) return resolveSourceOptions(orderedFurniSources, destinationSelectedSourceValue, orderedFurniSources);
+ if(isGlobalTarget(destinationTargetType)) return GLOBAL_SOURCE_OPTIONS;
+
+ return resolveSourceOptions(orderedUserSources, destinationSelectedSourceValue, userSourceFallbackOptions);
+ }, [ destinationSelectedSourceValue, destinationTargetType, orderedFurniSources, orderedUserSources, userSourceFallbackOptions ]);
+
+ const referenceSourceOptions = useMemo(() =>
+ {
+ if(isContextTarget(referenceTargetType)) return CONTEXT_SOURCE_OPTIONS;
+ if(isFurniTarget(referenceTargetType)) return resolveSourceOptions(SECONDARY_FURNI_SOURCES, referenceSelectedSourceValue, SECONDARY_FURNI_SOURCES);
+ if(isGlobalTarget(referenceTargetType)) return GLOBAL_SOURCE_OPTIONS;
+
+ return resolveSourceOptions(orderedUserSources, referenceSelectedSourceValue, userSourceFallbackOptions);
+ }, [ orderedUserSources, referenceSelectedSourceValue, referenceTargetType, userSourceFallbackOptions ]);
+
+ const syncHighlights = useCallback((nextDestinationIds: number[], nextReferenceIds: number[]) =>
+ {
+ if(highlightedIds.current.length)
+ {
+ WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current);
+ WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current);
+ }
+
+ const secondarySet = new Set(nextReferenceIds);
+ const primaryOnlyIds = nextDestinationIds.filter(id => !secondarySet.has(id));
+
+ if(primaryOnlyIds.length) WiredSelectionVisualizer.applySelectionShaderToFurni(primaryOnlyIds);
+ if(nextReferenceIds.length) WiredSelectionVisualizer.applySecondarySelectionShaderToFurni(nextReferenceIds);
+
+ highlightedIds.current = Array.from(new Set([ ...nextDestinationIds, ...nextReferenceIds ]));
+ }, []);
+
+ const switchSelection = useCallback((mode: SelectionMode) =>
+ {
+ if(mode === 'destination' && !destinationSelectionEnabled) return;
+ if(mode === 'reference' && !referenceSelectionEnabled) return;
+
+ const nextDestinationIds = (selectionMode === 'destination') ? [ ...furniIds ] : [ ...destinationFurniIds ];
+ const nextReferenceIds = (selectionMode === 'reference') ? [ ...furniIds ] : [ ...referenceFurniIds ];
+
+ setDestinationFurniIds(nextDestinationIds);
+ setReferenceFurniIds(nextReferenceIds);
+ setSelectionMode(mode);
+ setFurniIds([ ...(mode === 'destination' ? nextDestinationIds : nextReferenceIds) ]);
+ }, [ destinationFurniIds, destinationSelectionEnabled, furniIds, referenceFurniIds, referenceSelectionEnabled, selectionMode, setFurniIds ]);
+
+ useEffect(() =>
+ {
+ if(!trigger) return;
+
+ const stringParts = parseStringData(trigger.stringData);
+ const nextDestinationTargetType = normalizeTargetType((trigger.intData.length > 0) ? trigger.intData[0] : TARGET_USER);
+ const nextReferenceTargetType = normalizeTargetType((trigger.intData.length > 4) ? trigger.intData[4] : TARGET_USER);
+ const nextDestinationFurniIds = [ ...(trigger.selectedItems ?? []) ];
+ const nextReferenceFurniIds = parseIds((stringParts.length > 2) ? stringParts[2] : '');
+
+ setDestinationTargetType(nextDestinationTargetType);
+ setDestinationVariableToken(normalizeVariableTokenFromWire((stringParts.length > 0) ? stringParts[0] : ''));
+ setOperation((trigger.intData.length > 1) ? trigger.intData[1] : 0);
+ setReferenceMode(((trigger.intData.length > 2) ? trigger.intData[2] : REFERENCE_CONSTANT) === REFERENCE_VARIABLE ? 'variable' : 'constant');
+ setReferenceConstantValueInput(((trigger.intData.length > 3) ? trigger.intData[3] : 0).toString());
+ setReferenceTargetType(nextReferenceTargetType);
+ setReferenceVariableToken(normalizeVariableTokenFromWire((stringParts.length > 1) ? stringParts[1] : ''));
+ setDestinationUserSource((trigger.intData.length > 5) ? trigger.intData[5] : SOURCE_TRIGGER);
+ setDestinationFurniSource((trigger.intData.length > 6) ? trigger.intData[6] : (nextDestinationFurniIds.length ? SOURCE_SELECTED : SOURCE_TRIGGER));
+ setReferenceUserSource((trigger.intData.length > 7) ? trigger.intData[7] : SOURCE_TRIGGER);
+ setReferenceFurniSource((trigger.intData.length > 8) ? trigger.intData[8] : (nextReferenceFurniIds.length ? SOURCE_SECONDARY_SELECTED : SOURCE_TRIGGER));
+ setDestinationFurniIds(nextDestinationFurniIds);
+ setReferenceFurniIds(nextReferenceFurniIds);
+ setSelectionMode('destination');
+ setFurniIds([ ...nextDestinationFurniIds ]);
+ }, [ setFurniIds, trigger ]);
+
+ useEffect(() =>
+ {
+ if(selectionMode === 'destination') setDestinationFurniIds([ ...furniIds ]);
+ else setReferenceFurniIds([ ...furniIds ]);
+ }, [ furniIds, selectionMode ]);
+
+ useEffect(() => syncHighlights(destinationFurniIds, referenceFurniIds), [ destinationFurniIds, referenceFurniIds, syncHighlights ]);
+
+ useEffect(() =>
+ {
+ if(selectionMode === 'destination' && !destinationSelectionEnabled && referenceSelectionEnabled)
+ {
+ switchSelection('reference');
+ return;
+ }
+
+ if(selectionMode === 'reference' && !referenceSelectionEnabled && destinationSelectionEnabled)
+ {
+ switchSelection('destination');
+ return;
+ }
+
+ const canEditSelection = (selectionMode === 'destination') ? destinationSelectionEnabled : referenceSelectionEnabled;
+
+ setAllowsFurni(canEditSelection ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID : WiredFurniType.STUFF_SELECTION_OPTION_NONE);
+ }, [ destinationSelectionEnabled, referenceSelectionEnabled, selectionMode, setAllowsFurni, switchSelection ]);
+
+ useEffect(() =>
+ {
+ return () =>
+ {
+ if(!highlightedIds.current.length) return;
+
+ WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current);
+ WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current);
+ highlightedIds.current = [];
+ };
+ }, []);
+
+ const save = () =>
+ {
+ const nextDestinationFurniIds = (selectionMode === 'destination') ? [ ...furniIds ] : [ ...destinationFurniIds ];
+ const nextReferenceFurniIds = (selectionMode === 'reference') ? [ ...furniIds ] : [ ...referenceFurniIds ];
+ const parsedReferenceConstantValue = parseInt(referenceConstantValueInput.trim(), 10);
+
+ setDestinationFurniIds(nextDestinationFurniIds);
+ setReferenceFurniIds(nextReferenceFurniIds);
+ setStringParam(serializeStringData(destinationVariableToken, referenceMode === 'variable' ? referenceVariableToken : '', nextReferenceFurniIds));
+ setIntParams([
+ getTargetValue(destinationTargetType),
+ operation,
+ referenceMode === 'variable' ? REFERENCE_VARIABLE : REFERENCE_CONSTANT,
+ Number.isFinite(parsedReferenceConstantValue) ? parsedReferenceConstantValue : 0,
+ getTargetValue(referenceTargetType),
+ destinationUserSource,
+ destinationFurniSource,
+ referenceUserSource,
+ referenceFurniSource
+ ]);
+ setFurniIds((isFurniTarget(destinationTargetType) && destinationFurniSource === SOURCE_SELECTED) ? [ ...nextDestinationFurniIds ] : []);
+ };
+
+ const validate = () =>
+ {
+ if(!destinationVariableToken) return false;
+ if(referenceMode === 'variable' && !referenceVariableToken) return false;
+
+ return true;
+ };
+
+ const selectionLimit = trigger?.maximumItemSelectionCount ?? 0;
+
+ const handleDestinationTargetChange = (targetType: VariableTargetType) =>
+ {
+ if(targetType === destinationTargetType) return;
+
+ setDestinationTargetType(targetType);
+ setDestinationVariableToken('');
+ };
+
+ const handleReferenceTargetChange = (targetType: VariableTargetType) =>
+ {
+ if(targetType === referenceTargetType) return;
+
+ setReferenceTargetType(targetType);
+ setReferenceVariableToken('');
+ };
+
+ return (
+
+
+
+
{ LocalizeText('wiredfurni.params.variables.variable_selection') }
+
+ { TARGET_BUTTONS.map(button => (
+