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; };