From 31e51297e7964c2d1aeec9191e998ae202515fae Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Fri, 27 Mar 2026 09:37:13 +0100 Subject: [PATCH 1/4] Add wired creator tools monitor and inspection UI - add the new :wired inspection/monitor panel with furni, user and global tabs - add live variables, previews, inline editing and keep-selected behavior - add global room diagnostics placeholders, monitor artwork and server/client timezone display - add editor support for wf_xtra_text_output_furni_name and related UI texts/assets --- public/UITexts.example | 24 +- public/ui-config.json | 2 +- src/api/room/widgets/AvatarInfoFurni.ts | 5 + src/api/room/widgets/AvatarInfoUtilities.ts | 12 + src/api/wired/WiredActionLayoutCode.ts | 1 + .../wiredtools/wired_global_placeholder.png | Bin 0 -> 2232 bytes .../images/wiredtools/wired_monitor.png | Bin 0 -> 34423 bytes .../wired-tools/WiredCreatorToolsView.tsx | 1581 ++++++++++++++++- .../views/actions/WiredActionLayoutView.tsx | 3 + .../WiredExtraTextOutputFurniNameView.tsx | 123 ++ 10 files changed, 1723 insertions(+), 28 deletions(-) create mode 100644 src/assets/images/wiredtools/wired_global_placeholder.png create mode 100644 src/assets/images/wiredtools/wired_monitor.png create mode 100644 src/components/wired/views/extras/WiredExtraTextOutputFurniNameView.tsx diff --git a/public/UITexts.example b/public/UITexts.example index b12e768..d2190eb 100644 --- a/public/UITexts.example +++ b/public/UITexts.example @@ -38,5 +38,27 @@ "wiredfurni.params.texts.placeholder_type": "Tipo di segnaposto:", "wiredfurni.params.texts.placeholder_type.1": "Singolo", "wiredfurni.params.texts.placeholder_type.2": "Multiplo", - "wiredfurni.params.texts.select_delimiter": "Seleziona il delimitatore:" + "wiredfurni.params.texts.select_delimiter": "Seleziona il delimitatore:", + "widget.memenu.dance1": "Ballo 1", + "widget.memenu.dance2": "Ballo 2", + "widget.memenu.dance3": "Ballo 3", + "widget.memenu.dance4": "Ballo 4", + "wiredfurni.params.action.sign.0": "Cartello 0", + "wiredfurni.params.action.sign.1": "Cartello 1", + "wiredfurni.params.action.sign.2": "Cartello 2", + "wiredfurni.params.action.sign.3": "Cartello 3", + "wiredfurni.params.action.sign.4": "Cartello 4", + "wiredfurni.params.action.sign.5": "Cartello 5", + "wiredfurni.params.action.sign.6": "Cartello 6", + "wiredfurni.params.action.sign.7": "Cartello 7", + "wiredfurni.params.action.sign.8": "Cartello 8", + "wiredfurni.params.action.sign.9": "Cartello 9", + "wiredfurni.params.action.sign.10": "Cartello 10", + "wiredfurni.params.action.sign.11": "Cartello 11", + "wiredfurni.params.action.sign.12": "Cartello 12", + "wiredfurni.params.action.sign.13": "Cartello 13", + "wiredfurni.params.action.sign.14": "Cartello 14", + "wiredfurni.params.action.sign.15": "Cartello 15", + "wiredfurni.params.action.sign.16": "Cartello 16", + "wiredfurni.params.action.sign.17": "Cartello 17" } diff --git a/public/ui-config.json b/public/ui-config.json index 19c74ac..092e900 100644 --- a/public/ui-config.json +++ b/public/ui-config.json @@ -26,7 +26,7 @@ "game.center.enabled": false, "guides.enabled": true, "toolbar.hide.quests": true, - "catalog.style.new": true, + "catalog.style.new": false, "navigator.room.models": [{ "clubLevel": 0, "tileSize": 104, diff --git a/src/api/room/widgets/AvatarInfoFurni.ts b/src/api/room/widgets/AvatarInfoFurni.ts index 47743e9..133139e 100644 --- a/src/api/room/widgets/AvatarInfoFurni.ts +++ b/src/api/room/widgets/AvatarInfoFurni.ts @@ -31,6 +31,11 @@ export class AvatarInfoFurni implements IAvatarInfo public availableForBuildersClub: boolean = false; public tileSizeX: number = 1; public tileSizeY: number = 1; + public allowStack: boolean = false; + public allowSit: boolean = false; + public allowLay: boolean = false; + public allowWalk: boolean = false; + public teleportTargetId: number = 0; constructor(public readonly type: string) {} diff --git a/src/api/room/widgets/AvatarInfoUtilities.ts b/src/api/room/widgets/AvatarInfoUtilities.ts index b8154ee..0def77a 100644 --- a/src/api/room/widgets/AvatarInfoUtilities.ts +++ b/src/api/room/widgets/AvatarInfoUtilities.ts @@ -152,6 +152,18 @@ export class AvatarInfoUtilities furniInfo.ownerId = model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID); furniInfo.ownerName = model.getValue(RoomObjectVariable.FURNITURE_OWNER_NAME); furniInfo.usagePolicy = model.getValue(RoomObjectVariable.FURNITURE_USAGE_POLICY); + furniInfo.allowStack = (model.getValue(RoomObjectVariable.FURNITURE_ALLOW_STACK) > 0); + furniInfo.allowSit = (model.getValue(RoomObjectVariable.FURNITURE_ALLOW_SIT) > 0); + furniInfo.allowLay = (model.getValue(RoomObjectVariable.FURNITURE_ALLOW_LAY) > 0); + furniInfo.allowWalk = (model.getValue(RoomObjectVariable.FURNITURE_ALLOW_WALK) > 0); + furniInfo.teleportTargetId = Number(model.getValue(RoomObjectVariable.FURNITURE_TELEPORT_TARGET_ID) ?? 0); + + const dimensionsX = model.getValue(RoomObjectVariable.FURNITURE_DIMENSIONS_X); + const dimensionsY = model.getValue(RoomObjectVariable.FURNITURE_DIMENSIONS_Y); + + if(dimensionsX > 0) furniInfo.tileSizeX = dimensionsX; + + if(dimensionsY > 0) furniInfo.tileSizeY = dimensionsY; const guildId = model.getValue(RoomObjectVariable.FURNITURE_GUILD_CUSTOMIZED_GUILD_ID); diff --git a/src/api/wired/WiredActionLayoutCode.ts b/src/api/wired/WiredActionLayoutCode.ts index 0839bb1..790f829 100644 --- a/src/api/wired/WiredActionLayoutCode.ts +++ b/src/api/wired/WiredActionLayoutCode.ts @@ -66,4 +66,5 @@ export class WiredActionLayoutCode public static EXECUTION_LIMIT_EXTRA: number = 65; public static OR_EVAL_EXTRA: number = 66; public static TEXT_OUTPUT_USERNAME_EXTRA: number = 67; + public static TEXT_OUTPUT_FURNI_NAME_EXTRA: number = 68; } diff --git a/src/assets/images/wiredtools/wired_global_placeholder.png b/src/assets/images/wiredtools/wired_global_placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..74f46a5126f5180eb003ea0571a44580139b4f64 GIT binary patch literal 2232 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 = [ { key: 'monitor', label: 'Monitor' }, { key: 'variables', label: 'Variables' }, @@ -26,14 +125,6 @@ const TABS: Array<{ key: WiredToolsTab; label: string; }> = [ { key: 'settings', label: 'Settings' } ]; -const MONITOR_STATS: MonitorStat[] = [ - { label: 'Wired usage', value: '0/10000' }, - { label: 'Is heavy', value: 'No' }, - { label: 'Floor furni', value: '0/4000' }, - { label: 'Wall furni', value: '0/4000' }, - { label: 'Permanent furni vars', value: '0/60' } -]; - const MONITOR_LOGS: MonitorLog[] = [ { type: 'EXECUTION_CAP', category: 'ERROR', amount: '0', latest: '/' }, { type: 'DELAYED_EVENTS_CAP', category: 'ERROR', amount: '0', latest: '/' }, @@ -43,10 +134,638 @@ const MONITOR_LOGS: MonitorLog[] = [ { type: 'RECURSION_TIMEOUT', category: 'ERROR', amount: '0', latest: '/' } ]; -export const WiredCreatorToolsView: FC<{}> = props => +const INSPECTION_ELEMENTS: InspectionElementButton[] = [ + { key: 'furni', label: 'Furni', icon: furniInspectionIcon }, + { key: 'user', label: 'User', icon: userInspectionIcon }, + { key: 'global', label: 'Global', icon: globalInspectionIcon } +]; + +const EDITABLE_FURNI_VARIABLES: string[] = [ '@position.x', '@position.y', '@rotation', '@altitude', '@state', '@wallitem_offset' ]; +const EDITABLE_USER_VARIABLES: string[] = [ '@position.x', '@position.y', '@direction' ]; +const USER_DIRECTION_VECTORS: Array<{ x: number; y: number; }> = [ + { x: 0, y: -1 }, + { x: 1, y: -1 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 0, y: 1 }, + { x: -1, y: 1 }, + { x: -1, y: 0 }, + { x: -1, y: -1 } +]; +const 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 HOTEL_TIME_FORMATTERS: Map = new Map(); + +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) + }; +}; + +export const WiredCreatorToolsView: FC<{}> = () => { const [ isVisible, setIsVisible ] = useState(false); - const [ activeTab, setActiveTab ] = useState('monitor'); + const [ activeTab, setActiveTab ] = useState('inspection'); + const [ inspectionType, setInspectionType ] = useState('furni'); + const [ keepSelected, setKeepSelected ] = useState(false); + const [ selectedFurni, setSelectedFurni ] = useState(null); + const [ selectedFurniLiveState, setSelectedFurniLiveState ] = useState(null); + const [ selectedUser, setSelectedUser ] = useState(null); + const [ selectedUserLiveState, setSelectedUserLiveState ] = useState(null); + const [ selectedUserActionVersion, setSelectedUserActionVersion ] = useState(0); + const [ globalClock, setGlobalClock ] = useState(Date.now()); + const [ roomEnteredAt, setRoomEnteredAt ] = useState(Date.now()); + const [ editingVariable, setEditingVariable ] = useState(null); + const [ editingValue, setEditingValue ] = useState(''); + const { roomSession = null } = useRoom(); + const { ownUser: tradeOwnUser = null, otherUser: tradeOtherUser = null, isTrading = false } = useInventoryTrade(); + + const getFurniLiveState = (objectId: number, category: number): InspectionFurniLiveState => + { + if(!roomSession) return null; + + const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, objectId, category); + + if(!roomObject) return null; + + const location = roomObject.getLocation(); + const rawRotation = Math.round(roomObject.getDirection().x / 45); + + return { + positionX: Math.round(location?.x ?? 0), + positionY: Math.round(location?.y ?? 0), + altitude: Math.round(Number(location?.z ?? 0) * 100), + rotation: ((((rawRotation % 8) + 8) % 8)), + state: Number(roomObject.getState(0) ?? 0) + }; + }; + + const parseWallLocation = (wallLocation: string): ParsedWallLocation => + { + if(!wallLocation) return null; + + const match = wallLocation.match(/^:w=(-?\d+),(-?\d+)\s+l=(-?\d+),(-?\d+)\s+([^\s]+)$/i); + + if(!match) return null; + + return { + width: parseInt(match[1], 10), + height: parseInt(match[2], 10), + localX: parseInt(match[3], 10), + localY: parseInt(match[4], 10), + direction: match[5] + }; + }; + + const getSignDisplayName = (value: number): string => + { + if(value < 0) return ''; + + const localizationKey = `wiredfurni.params.action.sign.${ value }`; + const localizedName = LocalizeText(localizationKey); + + if(localizedName && (localizedName !== localizationKey)) return localizedName; + + return `Sign ${ value }`; + }; + + const getDanceDisplayName = (value: number): string => + { + if(value <= 0) return ''; + + const localizationKey = `widget.memenu.dance${ value }`; + const localizedName = LocalizeText(localizationKey); + + if(localizedName && (localizedName !== localizationKey)) return localizedName; + + return `Dance ${ value }`; + }; + + const getHandItemDisplayName = (value: number): string => + { + if(value <= 0) return ''; + + const localizationKey = `handitem${ value }`; + const localizedName = LocalizeText(localizationKey); + + if(localizedName && (localizedName !== localizationKey)) return localizedName; + + return `Item ${ value }`; + }; + + const getEffectDisplayName = (value: number): string => + { + if(value <= 0) return ''; + + const localizationKey = `fx_${ value }`; + const localizedName = LocalizeText(localizationKey); + + if(localizedName && (localizedName !== localizationKey)) return localizedName; + + return `Effect ${ value }`; + }; + + const getTeamColorDisplayName = (value: number): string => + { + if(value <= 0) return ''; + + const localizationKey = `wiredfurni.params.team.${ value }`; + const localizedName = LocalizeText(localizationKey); + + if(localizedName && (localizedName !== localizationKey)) return localizedName; + + return TEAM_COLOR_NAMES[value] ?? `Team ${ value }`; + }; + + const getTeamTypeDisplayName = (value: number): string => + { + const localizationKey = `wiredfurni.params.team_type.${ value }`; + const localizedName = LocalizeText(localizationKey); + + if(localizedName && (localizedName !== localizationKey)) return localizedName; + + switch(value) + { + case 1: return 'Battle Banzai'; + case 2: return 'Freeze'; + default: return 'Wired'; + } + }; + + const getTeamEffectData = (effectValue: number): TeamEffectData => + { + if(!roomSession || (effectValue <= 0)) return null; + + let teamType = -1; + let teamColor = 0; + + if((effectValue >= 223) && (effectValue <= 226)) + { + teamType = 0; + teamColor = (effectValue - 222); + } + else if((effectValue >= 33) && (effectValue <= 36)) + { + teamType = 1; + teamColor = (effectValue - 32); + } + else if((effectValue >= 40) && (effectValue <= 43)) + { + teamType = 2; + teamColor = (effectValue - 39); + } + + if((teamType < 0) || !(teamColor in TEAM_COLOR_NAMES)) return null; + + return { + colorId: teamColor, + typeId: teamType + }; + }; + + const getRoomTeamScore = (colorId: number): number => + { + if(!roomSession || !(colorId in TEAM_COLOR_NAMES)) return 0; + + const classNames = [ + `battlebanzai_counter_${ TEAM_COLOR_NAMES[colorId] }`, + `freeze_counter_${ TEAM_COLOR_NAMES[colorId] }`, + `football_counter_${ TEAM_COLOR_NAMES[colorId] }` + ]; + + const roomObjects = GetRoomEngine().getRoomObjects(roomSession.roomId, RoomObjectCategory.FLOOR); + + for(const targetClassName of classNames) + { + for(const roomObject of roomObjects) + { + if(!roomObject) continue; + + const typeId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); + const furnitureData = GetSessionDataManager().getFloorItemData(typeId); + + if(!furnitureData || (furnitureData.className !== targetClassName)) continue; + + return Number(roomObject.getState(0) ?? 0); + } + } + + return 0; + }; + + const getSelectedUserTeamData = (effectValue: number): InspectionUserTeamData => + { + const teamData = getTeamEffectData(effectValue); + + if(!teamData) return null; + + return { + colorId: teamData.colorId, + typeId: teamData.typeId, + score: (teamData.typeId === 0) ? 0 : getRoomTeamScore(teamData.colorId) + }; + }; + + const createUtcDateFromHotelParts = (parts: HotelDateTimeParts): Date => + { + return new Date(Date.UTC(parts.year, (parts.month - 1), parts.day, parts.hour, parts.minute, parts.second, parts.millisecond)); + }; + + const getMondayBasedWeekday = (parts: HotelDateTimeParts): number => + { + const jsDay = createUtcDateFromHotelParts(parts).getUTCDay(); + return ((jsDay === 0) ? 7 : jsDay); + }; + + const getDayOfYear = (parts: HotelDateTimeParts): number => + { + const currentDate = createUtcDateFromHotelParts(parts); + const startOfYear = new Date(Date.UTC(parts.year, 0, 1)); + const millisecondsPerDay = 86400000; + + return Math.floor((currentDate.getTime() - startOfYear.getTime()) / millisecondsPerDay) + 1; + }; + + const getIsoWeekOfYear = (parts: HotelDateTimeParts): number => + { + const utcDate = new Date(Date.UTC(parts.year, (parts.month - 1), parts.day)); + const dayOfWeek = utcDate.getUTCDay() || 7; + + utcDate.setUTCDate(utcDate.getUTCDate() + 4 - dayOfWeek); + + const yearStart = new Date(Date.UTC(utcDate.getUTCFullYear(), 0, 1)); + + return Math.ceil((((utcDate.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); + }; + + const getUserLiveState = (roomIndex: number): InspectionUserLiveState => + { + if(!roomSession) return null; + + const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, roomIndex, RoomObjectCategory.UNIT); + + if(!roomObject) return null; + + const location = roomObject.getLocation(); + const rawDirection = Math.round(roomObject.getDirection().x / 45); + + return { + positionX: Math.round(location?.x ?? 0), + positionY: Math.round(location?.y ?? 0), + altitude: Math.round(Number(location?.z ?? 0) * 100), + direction: ((((rawDirection % 8) + 8) % 8)) + }; + }; + + const refreshSelectedUser = (roomIndex: number = selectedUser?.roomIndex) => + { + if((roomIndex === null) || (roomIndex === undefined) || !roomSession) return; + + const userData = roomSession.userDataManager.getUserDataByIndex(roomIndex); + + if(!userData) return; + + const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, roomIndex, RoomObjectCategory.UNIT); + const gender = String(userData.sex || roomObject?.model.getValue(RoomObjectVariable.GENDER) || 'U').toUpperCase(); + const isOwnUser = (userData.webID === GetSessionDataManager().userId); + const roomOwnerLevel = (isOwnUser ? roomSession.controllerLevel : Number(roomObject?.model.getValue(RoomObjectVariable.FIGURE_FLAT_CONTROL) ?? 0)); + const hasRights = (roomOwnerLevel >= RoomControllerLevel.GUEST); + const isOwner = ((isOwnUser && roomSession.isRoomOwner) || (roomOwnerLevel >= RoomControllerLevel.ROOM_OWNER)); + const roomEntryMethod = (userData.roomEntryMethod || 'unknown'); + const roomEntryTeleportId = Number(userData.roomEntryTeleportId ?? 0); + + switch(userData.type) + { + case RoomObjectType.USER: { + const info = AvatarInfoUtilities.getUserInfo(RoomObjectCategory.UNIT, userData); + + if(!info) return; + + setSelectedUser({ + kind: 'user', + roomIndex, + name: info.name, + figure: info.figure, + gender, + userId: info.webID, + level: (isOwnUser ? info.roomControllerLevel : info.targetRoomControllerLevel), + achievementScore: info.achievementScore, + isHC: (isOwnUser && (GetSessionDataManager().clubLevel > 0)), + hasRights, + isOwner, + favouriteGroupId: info.groupId, + roomEntryMethod, + roomEntryTeleportId + }); + break; + } + case RoomObjectType.BOT: { + const info = AvatarInfoUtilities.getBotInfo(RoomObjectCategory.UNIT, userData); + + if(!info) return; + + setSelectedUser({ + kind: 'bot', + roomIndex, + name: info.name, + figure: info.figure, + gender, + userId: info.webID, + level: 0, + achievementScore: 0, + isHC: false, + hasRights: false, + isOwner: false, + favouriteGroupId: 0, + roomEntryMethod, + roomEntryTeleportId + }); + break; + } + case RoomObjectType.RENTABLE_BOT: { + const info = AvatarInfoUtilities.getRentableBotInfo(RoomObjectCategory.UNIT, userData); + + if(!info) return; + + setSelectedUser({ + kind: 'rentable_bot', + roomIndex, + name: info.name, + figure: info.figure, + gender, + userId: info.webID, + level: 0, + achievementScore: 0, + isHC: false, + hasRights: false, + isOwner: false, + favouriteGroupId: 0, + roomEntryMethod, + roomEntryTeleportId + }); + break; + } + case RoomObjectType.PET: + setSelectedUser({ + kind: 'pet', + roomIndex, + name: userData.name, + figure: userData.figure, + gender, + userId: userData.webID, + level: Number(userData.petLevel ?? 0), + achievementScore: 0, + isHC: false, + hasRights: false, + isOwner: false, + favouriteGroupId: 0, + roomEntryMethod, + roomEntryTeleportId, + posture: 'std' + }); + break; + } + + setSelectedUserLiveState(getUserLiveState(roomIndex)); + }; + + const refreshSelectedFurni = (objectId: number = selectedFurni?.objectId, category: number = selectedFurni?.category) => + { + if(!objectId && (objectId !== 0)) return; + + const info = AvatarInfoUtilities.getFurniInfo(objectId, category); + + if(!info) return; + + setSelectedFurni({ + objectId, + category, + info + }); + + setSelectedFurniLiveState(getFurniLiveState(objectId, category)); + }; + + useObjectSelectedEvent(event => + { + if(keepSelected || !roomSession) return; + + if((inspectionType === 'furni') && ((event.category === RoomObjectCategory.FLOOR) || (event.category === RoomObjectCategory.WALL))) + { + refreshSelectedFurni(event.id, event.category); + + return; + } + + if((inspectionType !== 'user') || (event.category !== RoomObjectCategory.UNIT)) return; + + refreshSelectedUser(event.id); + }); + + useMessageEvent(FurnitureFloorUpdateEvent, event => + { + if(!selectedFurni) return; + + const parser = event.getParser(); + + if(parser.item.itemId !== selectedFurni.objectId) return; + + refreshSelectedFurni(selectedFurni.objectId, selectedFurni.category); + }); + + useMessageEvent(FurnitureFloorUpdateEvent, () => + { + if((inspectionType !== 'user') || !selectedUser) return; + + setSelectedUserActionVersion(previousValue => (previousValue + 1)); + }); + + useMessageEvent(FurnitureWallUpdateEvent, event => + { + if(!selectedFurni || (selectedFurni.category !== RoomObjectCategory.WALL)) return; + + const parser = event.getParser(); + + if(parser.item.itemId !== selectedFurni.objectId) return; + + refreshSelectedFurni(selectedFurni.objectId, selectedFurni.category); + }); + + useMessageEvent(RoomUnitStatusEvent, event => + { + if(!selectedUser) return; + + const parser = event.getParser(); + + if(!parser?.statuses?.some(status => status.id === selectedUser.roomIndex)) return; + + setSelectedUserLiveState(getUserLiveState(selectedUser.roomIndex)); + setSelectedUserActionVersion(previousValue => (previousValue + 1)); + }); + + useMessageEvent(RoomUnitInfoEvent, event => + { + if(!selectedUser) return; + + const parser = event.getParser(); + + if(parser.unitId !== selectedUser.roomIndex) return; + + refreshSelectedUser(selectedUser.roomIndex); + }); + + useMessageEvent(FigureUpdateEvent, () => + { + if(!selectedUser || (selectedUser.kind !== 'user') || !roomSession) return; + + if(selectedUser.roomIndex !== roomSession.ownRoomIndex) return; + + refreshSelectedUser(selectedUser.roomIndex); + }); + + useMessageEvent(RoomUnitDanceEvent, event => + { + if(!selectedUser) return; + + if(event.getParser().unitId !== selectedUser.roomIndex) return; + + setSelectedUserActionVersion(previousValue => (previousValue + 1)); + }); + + useMessageEvent(RoomUnitEffectEvent, event => + { + if(!selectedUser) return; + + if(event.getParser().unitId !== selectedUser.roomIndex) return; + + setSelectedUserActionVersion(previousValue => (previousValue + 1)); + }); + + useMessageEvent(RoomUnitHandItemEvent, event => + { + if(!selectedUser) return; + + if(event.getParser().unitId !== selectedUser.roomIndex) return; + + setSelectedUserActionVersion(previousValue => (previousValue + 1)); + }); + + useMessageEvent(RoomUnitExpressionEvent, event => + { + if(!selectedUser) return; + + if(event.getParser().unitId !== selectedUser.roomIndex) return; + + setSelectedUserActionVersion(previousValue => (previousValue + 1)); + }); + + useEffect(() => + { + if(!isVisible || (inspectionType !== 'user') || !selectedUser || !roomSession) return; + + let lastMutedValue = Number(GetRoomEngine().getRoomObject(roomSession.roomId, selectedUser.roomIndex, RoomObjectCategory.UNIT)?.model.getValue(RoomObjectVariable.FIGURE_IS_MUTED) ?? 0); + + const interval = window.setInterval(() => + { + const currentMutedValue = Number(GetRoomEngine().getRoomObject(roomSession.roomId, selectedUser.roomIndex, RoomObjectCategory.UNIT)?.model.getValue(RoomObjectVariable.FIGURE_IS_MUTED) ?? 0); + + if(currentMutedValue === lastMutedValue) return; + + lastMutedValue = currentMutedValue; + + setSelectedUserActionVersion(previousValue => (previousValue + 1)); + }, 250); + + return () => window.clearInterval(interval); + }, [ isVisible, inspectionType, selectedUser, roomSession ]); + + useEffect(() => + { + const shouldTick = isVisible && ((activeTab === 'monitor') || ((activeTab === 'inspection') && (inspectionType === 'global'))); + + if(!shouldTick) return; + + setGlobalClock(Date.now()); + + const interval = window.setInterval(() => setGlobalClock(Date.now()), 100); + + return () => window.clearInterval(interval); + }, [ isVisible, activeTab, inspectionType, roomSession?.roomId ]); + + useEffect(() => + { + if(!roomSession?.roomId) return; + + setRoomEnteredAt(Date.now()); + }, [ roomSession?.roomId ]); useEffect(() => { @@ -87,7 +806,714 @@ export const WiredCreatorToolsView: FC<{}> = props => return () => RemoveLinkEventTracker(linkTracker); }, []); + const selectedRoomObject = ((roomSession && selectedFurni) + ? GetRoomEngine().getRoomObject(roomSession.roomId, selectedFurni.objectId, selectedFurni.category) + : null); + const selectedUserRoomObject = ((roomSession && selectedUser) + ? GetRoomEngine().getRoomObject(roomSession.roomId, selectedUser.roomIndex, RoomObjectCategory.UNIT) + : null); + const currentTabLabel = useMemo(() => TABS.find(tab => tab.key === activeTab)?.label ?? 'Monitor', [ activeTab ]); + const previewPlaceholder = useMemo(() => + { + switch(inspectionType) + { + case 'furni': + return 'Select a furni'; + case 'user': + return 'Select a user'; + default: + return 'Nothing to display'; + } + }, [ inspectionType ]); + const monitorStats = useMemo(() => + { + if(!roomSession) + { + return [ + { label: 'Wired usage', value: '0/10000' }, + { label: 'Is heavy', value: 'No' }, + { label: 'Room furni', value: '0/0' }, + { label: 'Wall furni', value: '0/0' }, + { label: 'Permanent furni vars', value: '0/60' } + ]; + } + + const roomId = roomSession.roomId; + const floorObjects = GetRoomEngine().getRoomObjects(roomId, RoomObjectCategory.FLOOR); + const wallObjects = GetRoomEngine().getRoomObjects(roomId, RoomObjectCategory.WALL); + const roomFurniCount = (floorObjects.length + wallObjects.length); + const roomItemLimit = Number(roomSession.roomItemLimit ?? 0); + const roomFurniValue = (roomItemLimit > 0) ? `${ roomFurniCount }/${ roomItemLimit }` : String(roomFurniCount); + const wallFurniValue = (roomItemLimit > 0) ? `${ wallObjects.length }/${ roomItemLimit }` : String(wallObjects.length); + + return [ + { label: 'Wired usage', value: '0/10000' }, + { label: 'Is heavy', value: 'No' }, + { label: 'Room furni', value: roomFurniValue }, + { label: 'Wall furni', value: wallFurniValue }, + { label: 'Permanent furni vars', value: '0/60' } + ]; + }, [ roomSession, globalClock ]); + const selectedFurnitureData = useMemo(() => + { + if(!selectedRoomObject || !selectedFurni) return null; + + const typeId = selectedRoomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); + + if(selectedFurni.category === RoomObjectCategory.WALL) return GetSessionDataManager().getWallItemData(typeId); + + return GetSessionDataManager().getFloorItemData(typeId); + }, [ selectedRoomObject, selectedFurni ]); + const currentWallLocationString = useMemo(() => + { + if(!roomSession || !selectedFurni || (selectedFurni.category !== RoomObjectCategory.WALL) || !selectedRoomObject) return null; + + const wallGeometry = GetRoomEngine().getLegacyWallGeometry(roomSession.roomId); + + if(!wallGeometry) return null; + + const angle = ((((Math.round(selectedRoomObject.getDirection().x / 45) % 8) + 8) % 8) * 45); + + return wallGeometry.getOldLocationString(selectedRoomObject.getLocation(), angle); + }, [ roomSession, selectedFurni, selectedRoomObject, selectedFurniLiveState ]); + const parsedWallLocation = useMemo(() => parseWallLocation(currentWallLocationString), [ currentWallLocationString ]); + const wallItemOffset = useMemo(() => + { + if(!parsedWallLocation) return null; + + return `${ parsedWallLocation.localX },${ parsedWallLocation.localY }`; + }, [ parsedWallLocation ]); + const furniVariables = useMemo(() => + { + if((inspectionType !== 'furni') || !selectedFurni || !selectedRoomObject) return []; + + const classId = selectedRoomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); + const tileSizeZ = Number(selectedFurnitureData?.tileSizeZ ?? 0); + const liveState = selectedFurniLiveState ?? getFurniLiveState(selectedFurni.objectId, selectedFurni.category); + + const dynamicFlags: InspectionVariable[] = []; + + if(selectedFurni.info?.allowSit) dynamicFlags.push({ key: '@can_sit_on', value: '' }); + if(selectedFurni.info?.allowLay) dynamicFlags.push({ key: '@can_lay_on', value: '' }); + if(selectedFurni.info?.allowWalk) dynamicFlags.push({ key: '@can_stand_on', value: '' }); + if(selectedFurni.info?.allowStack) dynamicFlags.push({ key: '@is_stackable', value: '' }); + + const variables: InspectionVariable[] = [ + ...((Number(selectedFurni.info?.teleportTargetId ?? 0) > 0) + ? [ { key: '~teleport.target_id', value: String(selectedFurni.info.teleportTargetId) } ] + : []), + { key: '@id', value: String(selectedFurni.objectId) }, + { key: '@class_id', value: String(classId) }, + { key: '@height', value: String(Math.round(tileSizeZ * 100)) }, + { key: '@state', value: String(liveState?.state ?? 0), editable: true }, + { key: '@position.x', value: String(liveState?.positionX ?? 0), editable: true }, + { key: '@position.y', value: String(liveState?.positionY ?? 0), editable: true }, + { key: '@rotation', value: String(liveState?.rotation ?? 0), editable: true }, + { key: '@altitude', value: String(liveState?.altitude ?? 0), editable: true }, + ...(wallItemOffset ? [ { key: '@wallitem_offset', value: wallItemOffset, editable: true } ] : []), + { key: '@type', value: `${ (selectedFurni.category === RoomObjectCategory.WALL) ? 'wall' : 'floor' }${ selectedFurnitureData?.availableForBuildersClub ? ' (BC)' : '' }` }, + ...dynamicFlags, + { key: '@dimensions.x', value: String(selectedFurni.info?.tileSizeX ?? 0) }, + { key: '@dimensions.y', value: String(selectedFurni.info?.tileSizeY ?? 0) }, + { key: '@owner_id', value: String(selectedFurni.info?.ownerId ?? 0) } + ]; + + return variables; + }, [ inspectionType, selectedFurni, selectedFurniLiveState, selectedRoomObject, selectedFurnitureData, wallItemOffset ]); + const canEditSelectedUser = useMemo(() => + { + if(!selectedUser || !roomSession) return false; + + if(selectedUser.kind === 'pet') return true; + + return ((selectedUser.kind === 'user') && (selectedUser.roomIndex === roomSession.ownRoomIndex)); + }, [ selectedUser, roomSession ]); + const userVariables = useMemo(() => + { + if((inspectionType !== 'user') || !selectedUser) return []; + + const liveState = selectedUserLiveState ?? getUserLiveState(selectedUser.roomIndex); + const currentControllerLevel = ((selectedUser.kind === 'user') + ? ((selectedUser.roomIndex === roomSession?.ownRoomIndex) + ? (roomSession?.controllerLevel ?? selectedUser.level ?? 0) + : Number(selectedUserRoomObject?.model.getValue(RoomObjectVariable.FIGURE_FLAT_CONTROL) ?? selectedUser.level ?? 0)) + : Number(selectedUser.level ?? 0)); + const isSelectedUserOwner = ((selectedUser.kind === 'user') + ? (((selectedUser.roomIndex === roomSession?.ownRoomIndex) && !!roomSession?.isRoomOwner) || (currentControllerLevel >= RoomControllerLevel.ROOM_OWNER)) + : !!selectedUser.isOwner); + const hasSelectedUserRights = ((selectedUser.kind === 'user') + ? (currentControllerLevel >= RoomControllerLevel.GUEST) + : !!selectedUser.hasRights); + const isSelectedUserGroupAdmin = ((selectedUser.kind === 'user') && !!roomSession?.isGuildRoom && (currentControllerLevel >= RoomControllerLevel.GUILD_ADMIN)); + const isSelectedUserMuted = (Number(selectedUserRoomObject?.model.getValue(RoomObjectVariable.FIGURE_IS_MUTED) ?? 0) > 0); + const isSelectedUserTrading = (!!isTrading + && (selectedUser.kind === 'user') + && ((tradeOwnUser?.userId === selectedUser.userId) || (tradeOtherUser?.userId === selectedUser.userId))); + const signValue = Number(selectedUserRoomObject?.model.getValue(RoomObjectVariable.FIGURE_SIGN) ?? -1); + const danceValue = Number(selectedUserRoomObject?.model.getValue(RoomObjectVariable.FIGURE_DANCE) ?? 0); + const handItemValue = Number(selectedUserRoomObject?.model.getValue(RoomObjectVariable.FIGURE_CARRY_OBJECT) ?? 0); + const expressionValue = Number(selectedUserRoomObject?.model.getValue(RoomObjectVariable.FIGURE_EXPRESSION) ?? 0); + const effectValue = Number(selectedUserRoomObject?.model.getValue(RoomObjectVariable.FIGURE_EFFECT) ?? 0); + const identityKey = ((selectedUser.kind === 'pet') + ? '@pet_id' + : ((selectedUser.kind === 'bot') || (selectedUser.kind === 'rentable_bot')) + ? '@bot_id' + : '@user_id'); + const teamData = getSelectedUserTeamData(effectValue); + const dynamicUserFlags: InspectionVariable[] = []; + const dynamicUserActions: InspectionVariable[] = []; + + if(selectedUser.isHC) dynamicUserFlags.push({ key: '@is_hc', value: '' }); + if(hasSelectedUserRights) dynamicUserFlags.push({ key: '@has_rights', value: '' }); + if(isSelectedUserOwner) dynamicUserFlags.push({ key: '@is_owner', value: '' }); + if(isSelectedUserGroupAdmin) dynamicUserFlags.push({ key: '@is_group_admin', value: '' }); + if(isSelectedUserMuted) dynamicUserFlags.push({ key: '@is_mute', value: '' }); + if(isSelectedUserTrading) dynamicUserFlags.push({ key: '@is_trading', value: '' }); + if(WIRED_FREEZE_EFFECT_IDS.has(effectValue)) dynamicUserFlags.push({ key: '@is_frozen', value: '' }); + if(effectValue > 0) dynamicUserActions.push({ key: '@effect', value: `${ effectValue } (${ getEffectDisplayName(effectValue) })` }); + if(teamData) dynamicUserActions.push({ key: '@team_score', value: String(teamData.score) }); + if(teamData) dynamicUserActions.push({ key: '@team_color', value: `${ teamData.colorId } (${ getTeamColorDisplayName(teamData.colorId) })` }); + if(teamData) dynamicUserActions.push({ key: '@team_type', value: `${ teamData.typeId } (${ getTeamTypeDisplayName(teamData.typeId) })` }); + if(signValue >= 0) dynamicUserActions.push({ key: '@sign', value: `${ signValue } (${ getSignDisplayName(signValue) })` }); + if(danceValue > 0) dynamicUserActions.push({ key: '@dance', value: `${ danceValue } (${ getDanceDisplayName(danceValue) })` }); + if(expressionValue === AvatarExpressionEnum.IDLE.ordinal) dynamicUserActions.push({ key: '@is_idle', value: '' }); + if(handItemValue > 0) dynamicUserActions.push({ key: '@handitems', value: `${ handItemValue } (${ getHandItemDisplayName(handItemValue) })` }); + + return [ + { key: '@index', value: String(selectedUser.roomIndex) }, + { key: '@type', value: selectedUser.kind }, + { key: '@gender', value: (selectedUser.gender || 'U') }, + { key: '@level', value: String(currentControllerLevel) }, + { key: '@achievement_score', value: String(selectedUser.achievementScore ?? 0) }, + ...dynamicUserFlags, + ...dynamicUserActions, + { key: '@position.x', value: String(liveState?.positionX ?? 0), editable: canEditSelectedUser }, + { key: '@position.y', value: String(liveState?.positionY ?? 0), editable: canEditSelectedUser }, + { key: '@direction', value: String(liveState?.direction ?? 0), editable: canEditSelectedUser }, + { key: '@altitude', value: String(liveState?.altitude ?? 0) }, + ...((Number(selectedUser.favouriteGroupId ?? 0) > 0) + ? [ { key: '@favourite_group_id', value: String(selectedUser.favouriteGroupId) } ] + : []), + ...((selectedUser.roomEntryMethod && (selectedUser.roomEntryMethod !== 'unknown')) + ? [ { key: '@room_entry', value: selectedUser.roomEntryMethod } ] + : []), + ...(((selectedUser.roomEntryMethod === 'teleport') && (Number(selectedUser.roomEntryTeleportId ?? 0) > 0)) + ? [ { key: '@room_entry.teleport_id', value: String(selectedUser.roomEntryTeleportId) } ] + : []), + { key: identityKey, value: String(selectedUser.userId ?? 0) } + ]; + }, [ inspectionType, selectedUser, selectedUserLiveState, canEditSelectedUser, selectedUserRoomObject, selectedUserActionVersion, roomSession, isTrading, tradeOwnUser, tradeOtherUser ]); + const globalVariables = useMemo(() => + { + if((inspectionType !== 'global') || !roomSession) return []; + + const roomId = roomSession.roomId; + const unitObjects = GetRoomEngine().getRoomObjects(roomId, RoomObjectCategory.UNIT); + const floorObjects = GetRoomEngine().getRoomObjects(roomId, RoomObjectCategory.FLOOR); + const wallObjects = GetRoomEngine().getRoomObjects(roomId, RoomObjectCategory.WALL); + + const teamSizes: Record = { + 1: 0, + 2: 0, + 3: 0, + 4: 0 + }; + + let userCount = 0; + + for(const roomObject of unitObjects) + { + if(!roomObject) continue; + + const userData = roomSession.userDataManager.getUserDataByIndex(roomObject.id); + + if(!userData || (userData.type !== RoomObjectType.USER)) continue; + + userCount++; + + const effectValue = Number(roomObject.model.getValue(RoomObjectVariable.FIGURE_EFFECT) ?? 0); + const teamData = getTeamEffectData(effectValue); + + if(!teamData) continue; + + teamSizes[teamData.colorId] = (teamSizes[teamData.colorId] + 1); + } + + const hotelTimeZone = (roomSession.hotelTimeZone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'); + const hotelTimeSnapshotMs = Number(roomSession.hotelTimeSnapshotMs ?? 0); + const hotelTimeSyncMs = Number(roomSession.hotelTimeSyncMs ?? 0); + const hotelCurrentTimeMs = ((hotelTimeSnapshotMs > 0) && (hotelTimeSyncMs > 0)) + ? (hotelTimeSnapshotMs + Math.max(0, (globalClock - hotelTimeSyncMs))) + : globalClock; + const hotelNow = getHotelDateTimeParts(hotelCurrentTimeMs, hotelTimeZone); + const clientTimeZone = (Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'); + const weekdayIndex = getMondayBasedWeekday(hotelNow); + const monthIndex = hotelNow.month; + + return [ + { key: '@furni_count', value: String(floorObjects.length + wallObjects.length) }, + { key: '@user_count', value: String(userCount) }, + { key: '@wired_timer', value: String(Math.max(0, Math.floor((globalClock - roomEnteredAt) / 1000))) }, + { key: '@teams.red.score', value: String(getRoomTeamScore(1)) }, + { key: '@teams.green.score', value: String(getRoomTeamScore(2)) }, + { key: '@teams.blue.score', value: String(getRoomTeamScore(3)) }, + { key: '@teams.yellow.score', value: String(getRoomTeamScore(4)) }, + { key: '@teams.red.size', value: String(teamSizes[1]) }, + { key: '@teams.green.size', value: String(teamSizes[2]) }, + { key: '@teams.blue.size', value: String(teamSizes[3]) }, + { key: '@teams.yellow.size', value: String(teamSizes[4]) }, + { key: '@room_id', value: String(roomId) }, + { key: '@group_id', value: String(Number(roomSession.groupId ?? 0)) }, + { key: '@timezone_server', value: hotelTimeZone }, + { key: '@timezone_client', value: clientTimeZone }, + { key: '@current_time', value: 'Hidden', valueClassName: 'text-[#d97b78]' }, + { key: '@current_time.millisecond_of_second', value: String(hotelNow.millisecond) }, + { key: '@current_time.seconds_of_minute', value: String(hotelNow.second) }, + { key: '@current_time.minute_of_hour', value: String(hotelNow.minute) }, + { key: '@current_time.hour_of_day', value: String(hotelNow.hour) }, + { key: '@current_time.day_of_week', value: `${ weekdayIndex } (${ WEEKDAY_NAMES[weekdayIndex - 1] })` }, + { key: '@current_time.day_of_month', value: String(hotelNow.day) }, + { key: '@current_time.day_of_year', value: String(getDayOfYear(hotelNow)) }, + { key: '@current_time.week_of_year', value: String(getIsoWeekOfYear(hotelNow)) }, + { key: '@current_time.month_of_year', value: `${ monthIndex } (${ MONTH_NAMES[monthIndex - 1] })` }, + { key: '@current_time.year', value: String(hotelNow.year) } + ]; + }, [ inspectionType, roomSession, globalClock, roomEnteredAt ]); + const displayedVariables = ((inspectionType === 'user') + ? userVariables + : ((inspectionType === 'global') + ? globalVariables + : furniVariables)); + + const beginVariableEdit = (variable: InspectionVariable) => + { + if(!variable.editable) return; + + if((inspectionType === 'furni') && !EDITABLE_FURNI_VARIABLES.includes(variable.key)) return; + if((inspectionType === 'user') && !EDITABLE_USER_VARIABLES.includes(variable.key)) return; + + setEditingVariable(variable.key); + setEditingValue(variable.value); + }; + + const commitVariableEdit = () => + { + if((inspectionType === 'user') && selectedUser && roomSession) + { + const currentLiveState = (selectedUserLiveState ?? getUserLiveState(selectedUser.roomIndex)); + + if(!currentLiveState) + { + cancelVariableEdit(); + return; + } + + let nextX = currentLiveState.positionX; + let nextY = currentLiveState.positionY; + let nextDirection = currentLiveState.direction; + let isValid = true; + + switch(editingVariable) + { + case '@position.x': { + const parsed = parseInt(editingValue.trim(), 10); + + if(Number.isNaN(parsed)) + { + isValid = false; + break; + } + + nextX = parsed; + break; + } + case '@position.y': { + const parsed = parseInt(editingValue.trim(), 10); + + if(Number.isNaN(parsed)) + { + isValid = false; + break; + } + + nextY = parsed; + break; + } + case '@direction': { + const parsed = parseInt(editingValue.trim(), 10); + + if(Number.isNaN(parsed)) + { + isValid = false; + break; + } + + nextDirection = ((((parsed % 8) + 8) % 8)); + break; + } + default: + isValid = false; + break; + } + + if(!isValid) + { + cancelVariableEdit(); + return; + } + + if((nextX === currentLiveState.positionX) && (nextY === currentLiveState.positionY) && (nextDirection === currentLiveState.direction)) + { + cancelVariableEdit(); + return; + } + + if(selectedUser.kind === 'pet') + { + SendMessageComposer(new PetMoveComposer(selectedUser.userId, nextX, nextY, nextDirection)); + } + else if((selectedUser.kind === 'user') && (selectedUser.roomIndex === roomSession.ownRoomIndex)) + { + if(editingVariable === '@direction') + { + const directionVector = USER_DIRECTION_VECTORS[nextDirection] ?? USER_DIRECTION_VECTORS[0]; + + SendMessageComposer(new RoomUnitLookComposer((currentLiveState.positionX + directionVector.x), (currentLiveState.positionY + directionVector.y))); + } + else + { + SendMessageComposer(new RoomUnitWalkComposer(nextX, nextY)); + } + } + else + { + cancelVariableEdit(); + return; + } + + setSelectedUserLiveState({ + ...currentLiveState, + positionX: nextX, + positionY: nextY, + direction: nextDirection + }); + + setEditingVariable(null); + setEditingValue(''); + return; + } + + if(!editingVariable || !selectedFurni || !selectedRoomObject || !roomSession) return; + + const currentLiveState = (selectedFurniLiveState ?? getFurniLiveState(selectedFurni.objectId, selectedFurni.category)); + + if(!currentLiveState) + { + cancelVariableEdit(); + return; + } + + let nextX = currentLiveState.positionX; + let nextY = currentLiveState.positionY; + let nextZ = (currentLiveState.altitude / 100); + let nextRotation = currentLiveState.rotation; + let nextState: number = null; + let nextWallOffsetX: number = null; + let nextWallOffsetY: number = null; + let isValid = true; + + switch(editingVariable) + { + case '@position.x': { + const parsed = parseInt(editingValue.trim(), 10); + + if(Number.isNaN(parsed)) + { + isValid = false; + break; + } + + nextX = parsed; + break; + } + case '@position.y': { + const parsed = parseInt(editingValue.trim(), 10); + + if(Number.isNaN(parsed)) + { + isValid = false; + break; + } + + nextY = parsed; + break; + } + case '@rotation': { + const parsed = parseInt(editingValue.trim(), 10); + + if(Number.isNaN(parsed)) + { + isValid = false; + break; + } + + nextRotation = ((((parsed % 8) + 8) % 8)); + break; + } + case '@altitude': { + const parsed = parseInt(editingValue.trim(), 10); + + if(Number.isNaN(parsed)) + { + isValid = false; + break; + } + + nextZ = Math.max(0, Math.min(40, (parsed / 100))); + break; + } + case '@state': { + const parsed = parseInt(editingValue.trim(), 10); + + if(Number.isNaN(parsed)) + { + isValid = false; + break; + } + + nextState = parsed; + break; + } + case '@wallitem_offset': { + if(selectedFurni.category !== RoomObjectCategory.WALL) + { + isValid = false; + break; + } + + const match = editingValue.trim().match(/^(-?\d+)\s*,\s*(-?\d+)$/); + + if(!match) + { + isValid = false; + break; + } + + nextWallOffsetX = parseInt(match[1], 10); + nextWallOffsetY = parseInt(match[2], 10); + break; + } + } + + if(!isValid) + { + cancelVariableEdit(); + return; + } + + if(editingVariable === '@state') + { + if(nextState === currentLiveState.state) + { + cancelVariableEdit(); + return; + } + + setSelectedFurniLiveState(previousValue => + { + if(!previousValue) return previousValue; + + return { ...previousValue, state: nextState }; + }); + + if(selectedFurni.category === RoomObjectCategory.WALL) SendMessageComposer(new FurnitureWallMultiStateComposer(selectedFurni.objectId, nextState)); + else SendMessageComposer(new FurnitureMultiStateComposer(selectedFurni.objectId, nextState)); + + setEditingVariable(null); + setEditingValue(''); + return; + } + + if(editingVariable === '@wallitem_offset') + { + if((selectedFurni.category !== RoomObjectCategory.WALL) || !parsedWallLocation) + { + cancelVariableEdit(); + return; + } + + if((nextWallOffsetX === parsedWallLocation.localX) && (nextWallOffsetY === parsedWallLocation.localY)) + { + cancelVariableEdit(); + return; + } + + const wallGeometry = GetRoomEngine().getLegacyWallGeometry(roomSession.roomId); + + if(!wallGeometry) + { + cancelVariableEdit(); + return; + } + + const nextWallLocationString = `:w=${parsedWallLocation.width},${parsedWallLocation.height} l=${nextWallOffsetX},${nextWallOffsetY} ${parsedWallLocation.direction}`; + const nextLocation = wallGeometry.getLocation(parsedWallLocation.width, parsedWallLocation.height, nextWallOffsetX, nextWallOffsetY, parsedWallLocation.direction); + const nextAngle = wallGeometry.getDirection(parsedWallLocation.direction); + const currentExtra = (selectedFurni.info?.stuffData?.getLegacyString?.() ?? selectedFurni.info?.extraParam ?? '0'); + + if(!nextLocation) + { + cancelVariableEdit(); + return; + } + + GetRoomEngine().updateRoomObjectWall(roomSession.roomId, selectedFurni.objectId, nextLocation, new Vector3d(nextAngle), currentLiveState.state, currentExtra); + + setSelectedFurniLiveState(previousValue => + { + if(!previousValue) return previousValue; + + return { + ...previousValue, + positionX: Math.round(nextLocation.x), + positionY: Math.round(nextLocation.y), + altitude: Math.round(nextLocation.z * 100), + rotation: ((((Math.round(nextAngle / 45) % 8) + 8) % 8)) + }; + }); + + SendMessageComposer(new FurnitureWallUpdateComposer(selectedFurni.objectId, nextWallLocationString)); + setEditingVariable(null); + setEditingValue(''); + return; + } + + if((nextX === currentLiveState.positionX) && (nextY === currentLiveState.positionY) && (Math.round(nextZ * 100) === currentLiveState.altitude) && (nextRotation === currentLiveState.rotation)) + { + cancelVariableEdit(); + return; + } + + setSelectedFurniLiveState(previousValue => + { + if(!previousValue) return previousValue; + + return { + ...previousValue, + positionX: nextX, + positionY: nextY, + altitude: Math.round(nextZ * 100), + rotation: nextRotation + }; + }); + + if(selectedFurni.category === RoomObjectCategory.WALL) + { + const wallGeometry = GetRoomEngine().getLegacyWallGeometry(roomSession.roomId); + + if(!wallGeometry) + { + cancelVariableEdit(); + return; + } + + const currentLocation = selectedRoomObject.getLocation(); + const currentExtra = (selectedFurni.info?.stuffData?.getLegacyString?.() ?? selectedFurni.info?.extraParam ?? '0'); + const nextLocation = new Vector3d(nextX, nextY, nextZ); + const nextAngle = (nextRotation * 45); + const wallLocation = wallGeometry.getOldLocationString(nextLocation, nextAngle); + + if(!wallLocation) + { + cancelVariableEdit(); + return; + } + + GetRoomEngine().updateRoomObjectWall(roomSession.roomId, selectedFurni.objectId, nextLocation, new Vector3d(nextAngle), currentLiveState.state, currentExtra); + + if(currentLocation) + { + setSelectedFurniLiveState(previousValue => + { + if(!previousValue) return previousValue; + + return { + ...previousValue, + positionX: Math.round(nextLocation.x), + positionY: Math.round(nextLocation.y), + altitude: Math.round(nextLocation.z * 100), + rotation: nextRotation + }; + }); + } + + SendMessageComposer(new FurnitureWallUpdateComposer(selectedFurni.objectId, wallLocation)); + setEditingVariable(null); + setEditingValue(''); + return; + } + + SendMessageComposer(new UpdateFurniturePositionComposer(selectedFurni.objectId, nextX, nextY, Math.round(nextZ * 10000), nextRotation)); + + setEditingVariable(null); + setEditingValue(''); + }; + + const cancelVariableEdit = () => + { + setEditingVariable(null); + setEditingValue(''); + }; + + const onVariableInputKeyDown = (event: KeyboardEvent) => + { + event.stopPropagation(); + + if(event.nativeEvent.stopImmediatePropagation) event.nativeEvent.stopImmediatePropagation(); + + switch(event.key) + { + case 'Enter': + event.preventDefault(); + commitVariableEdit(); + return; + case 'Escape': + event.preventDefault(); + cancelVariableEdit(); + return; + } + }; + + useEffect(() => + { + setEditingVariable(null); + setEditingValue(''); + }, [ selectedFurni?.objectId, selectedUser?.roomIndex, inspectionType ]); + + useEffect(() => + { + if((inspectionType !== 'furni') || !selectedFurni) + { + setSelectedFurniLiveState(null); + + return; + } + + setSelectedFurniLiveState(getFurniLiveState(selectedFurni.objectId, selectedFurni.category)); + }, [ inspectionType, selectedFurni?.objectId, selectedFurni?.category ]); + + useEffect(() => + { + if((inspectionType !== 'user') || !selectedUser) + { + setSelectedUserLiveState(null); + + return; + } + + setSelectedUserLiveState(getUserLiveState(selectedUser.roomIndex)); + }, [ inspectionType, selectedUser?.roomIndex ]); if(!isVisible) return null; @@ -102,10 +1528,6 @@ export const WiredCreatorToolsView: FC<{}> = props => )) } -

-
- { currentTabLabel } -
{ (activeTab === 'monitor') &&
@@ -114,20 +1536,15 @@ export const WiredCreatorToolsView: FC<{}> = props =>
Statistics: - { MONITOR_STATS.map(stat => ( + { monitorStats.map(stat => (
{ stat.label }: { stat.value }
)) }
-
-
- Monitor Preview -
- Live statistics, executor health and diagnostics can be connected here next. -
-
+
+ Monitor preview
@@ -160,7 +1577,120 @@ export const WiredCreatorToolsView: FC<{}> = props =>
} + { (activeTab === 'inspection') && +
+
+
+ Element type: +
+ { INSPECTION_ELEMENTS.map(element => ( + + )) } +
+
+
+ Preview: +
+ { (inspectionType === 'furni') && selectedFurni && roomSession && +
+ +
} + { (inspectionType === 'user') && selectedUser && +
+ { (selectedUser.kind === 'pet') + ? + : } +
} + { (inspectionType === 'global') && +
+ Global placeholder +
} + { (((inspectionType === 'furni') && !selectedFurni) || ((inspectionType === 'user') && !selectedUser) || (inspectionType === 'global')) && +
+ { previewPlaceholder } +
} +
+
+ +
+
+
+ Variables: +
+
+ Variable + Value +
+ { !displayedVariables.length && +
+ Nothing to display +
} + { !!displayedVariables.length && +
+ + + { displayedVariables.map((variable, index) => ( + beginVariableEdit(variable) }> + + + + )) } + +
{ variable.key } + { (editingVariable === variable.key) && + event.stopPropagation() } + onBlur={ commitVariableEdit } + onChange={ event => setEditingValue(event.target.value) } + onKeyDownCapture={ event => + { + event.stopPropagation(); + + if(event.nativeEvent.stopImmediatePropagation) event.nativeEvent.stopImmediatePropagation(); + } } + onKeyDown={ onVariableInputKeyDown } /> } + { (editingVariable !== variable.key) && !variable.editable && { variable.value } } + { (editingVariable !== variable.key) && variable.editable && + } +
+
} +
+
+
+ + +
+
+
} { (activeTab !== 'monitor') && + (activeTab !== 'inspection') &&
{ currentTabLabel } @@ -169,7 +1699,6 @@ export const WiredCreatorToolsView: FC<{}> = props =>
} -
); diff --git a/src/components/wired/views/actions/WiredActionLayoutView.tsx b/src/components/wired/views/actions/WiredActionLayoutView.tsx index 2ebefff..5e62ce4 100644 --- a/src/components/wired/views/actions/WiredActionLayoutView.tsx +++ b/src/components/wired/views/actions/WiredActionLayoutView.tsx @@ -60,6 +60,7 @@ import { WiredExtraMoveNoAnimationView } from '../extras/WiredExtraMoveNoAnimati import { WiredExtraOrEvalView } from '../extras/WiredExtraOrEvalView'; import { WiredExtraMovePhysicsView } from '../extras/WiredExtraMovePhysicsView'; import { WiredExtraRandomView } from '../extras/WiredExtraRandomView'; +import { WiredExtraTextOutputFurniNameView } from '../extras/WiredExtraTextOutputFurniNameView'; import { WiredExtraTextOutputUsernameView } from '../extras/WiredExtraTextOutputUsernameView'; import { WiredExtraUnseenView } from '../extras/WiredExtraUnseenView'; @@ -195,6 +196,8 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.TEXT_OUTPUT_USERNAME_EXTRA: return ; + case WiredActionLayoutCode.TEXT_OUTPUT_FURNI_NAME_EXTRA: + return ; case WiredActionLayoutCode.SEND_SIGNAL: return ; } diff --git a/src/components/wired/views/extras/WiredExtraTextOutputFurniNameView.tsx b/src/components/wired/views/extras/WiredExtraTextOutputFurniNameView.tsx new file mode 100644 index 0000000..95a5234 --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraTextOutputFurniNameView.tsx @@ -0,0 +1,123 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { NitroInput } from '../../../../layout'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredExtraBaseView } from './WiredExtraBaseView'; + +const TYPE_SINGLE = 1; +const TYPE_MULTIPLE = 2; +const DEFAULT_PLACEHOLDER_NAME = ''; +const DEFAULT_DELIMITER = ', '; +const MAX_PLACEHOLDER_NAME_LENGTH = 32; +const MAX_DELIMITER_LENGTH = 16; +const PLACEHOLDER_WRAPPER_PATTERN = /^\$\((.*)\)$/; + +const normalizePlaceholderType = (value: number) => ((value === TYPE_MULTIPLE) ? TYPE_MULTIPLE : TYPE_SINGLE); +const normalizeFurniSource = (value: number) => ((value === 0) || (value === 100) || (value === 200) || (value === 201) ? value : 0); +const normalizePlaceholderName = (value: string) => +{ + let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, ''); + + if(PLACEHOLDER_WRAPPER_PATTERN.test(normalizedValue)) + { + normalizedValue = normalizedValue.substring(2, normalizedValue.length - 1).trim(); + } + + return normalizedValue.slice(0, MAX_PLACEHOLDER_NAME_LENGTH); +}; + +const normalizeDelimiter = (value: string) => +{ + if(value === undefined || value === null) return DEFAULT_DELIMITER; + + return value.replace(/[\t\r\n]/g, '').slice(0, MAX_DELIMITER_LENGTH); +}; + +const splitStringData = (value: string) => +{ + if(!value?.length) return [ DEFAULT_PLACEHOLDER_NAME, DEFAULT_DELIMITER ]; + + const parts = value.split('\t'); + + if(parts.length <= 1) return [ value, DEFAULT_DELIMITER ]; + + return [ parts[0], parts[1] ]; +}; + +const escapeHtml = (value: string) => value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +export const WiredExtraTextOutputFurniNameView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + const [ placeholderName, setPlaceholderName ] = useState(DEFAULT_PLACEHOLDER_NAME); + const [ placeholderType, setPlaceholderType ] = useState(TYPE_SINGLE); + const [ delimiter, setDelimiter ] = useState(DEFAULT_DELIMITER); + const [ furniSource, setFurniSource ] = useState(0); + + useEffect(() => + { + if(!trigger) return; + + const [ nextPlaceholderName, nextDelimiter ] = splitStringData(trigger.stringData); + + setPlaceholderName(normalizePlaceholderName(nextPlaceholderName)); + setDelimiter(normalizeDelimiter(nextDelimiter)); + setPlaceholderType(normalizePlaceholderType((trigger.intData.length > 0) ? trigger.intData[0] : TYPE_SINGLE)); + setFurniSource(normalizeFurniSource((trigger.intData.length > 1) ? trigger.intData[1] : 0)); + }, [ trigger ]); + + const previewToken = useMemo(() => + { + const effectiveName = normalizePlaceholderName(placeholderName) || 'placeholder'; + + return `$(${ effectiveName })`; + }, [ placeholderName ]); + + const previewHtml = useMemo(() => LocalizeText('wiredfurni.params.texts.placeholder_preview', [ 'placeholder' ], [ escapeHtml(previewToken) ]), [ previewToken ]); + + const save = () => + { + setIntParams([ normalizePlaceholderType(placeholderType), normalizeFurniSource(furniSource) ]); + setStringParam(`${ normalizePlaceholderName(placeholderName) }\t${ normalizeDelimiter(delimiter) }`); + }; + + return ( + setFurniSource(normalizeFurniSource(value)) } /> }> +
+
+ { LocalizeText('wiredfurni.params.texts.placeholder_name') } + setPlaceholderName(normalizePlaceholderName(event.target.value)) } /> +
+ +
+ { LocalizeText('wiredfurni.params.texts.placeholder_type') } + + +
+ { placeholderType === TYPE_MULTIPLE && +
+ { LocalizeText('wiredfurni.params.texts.select_delimiter') } + setDelimiter(normalizeDelimiter(event.target.value)) } /> +
} +
+
+ ); +}; From a5a7215e09937217a2a70fcf3c52cc002fe7a4ef Mon Sep 17 00:00:00 2001 From: Life Date: Mon, 30 Mar 2026 17:55:11 +0200 Subject: [PATCH 2/4] Change pendingActionRef to hold action and itemId Refactor pendingActionRef to store action and itemId as an object instead of a string. --- src/hooks/furni-editor/useFurniEditor.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/hooks/furni-editor/useFurniEditor.ts b/src/hooks/furni-editor/useFurniEditor.ts index 936eb4f..cd95ec4 100644 --- a/src/hooks/furni-editor/useFurniEditor.ts +++ b/src/hooks/furni-editor/useFurniEditor.ts @@ -60,7 +60,7 @@ export const useFurniEditor = () => const [ catalogItems, setCatalogItems ] = useState([]); const [ interactions, setInteractions ] = useState([]); const [ furniDataEntry, setFurniDataEntry ] = useState | null>(null); - const pendingActionRef = useRef(null); + const pendingActionRef = useRef<{ action: string; itemId: number } | null>(null); const { simpleAlert = null } = useNotification(); const clearError = useCallback(() => setError(null), []); @@ -161,7 +161,9 @@ export const useFurniEditor = () => useMessageEvent(FurniEditorResultEvent, (event: FurniEditorResultEvent) => { const parser = event.getParser(); - const action = pendingActionRef.current; + const pending = pendingActionRef.current; + const action = pending?.action ?? null; + const actionItemId = pending?.itemId ?? null; pendingActionRef.current = null; setLoading(false); @@ -182,10 +184,10 @@ export const useFurniEditor = () => if(action === 'update') { - // Auto-reload detail after update - if(selectedItem) + // Auto-reload detail after update using the ID from the original request + if(actionItemId) { - SendMessageComposer(new FurniEditorDetailComposer(selectedItem.id)); + SendMessageComposer(new FurniEditorDetailComposer(actionItemId)); } if(simpleAlert) @@ -231,7 +233,7 @@ export const useFurniEditor = () => { setLoading(true); setError(null); - pendingActionRef.current = 'update'; + pendingActionRef.current = { action: 'update', itemId: id }; SendMessageComposer(new FurniEditorUpdateComposer(id, JSON.stringify(fields))); }, []); @@ -239,7 +241,7 @@ export const useFurniEditor = () => { setLoading(true); setError(null); - pendingActionRef.current = 'delete'; + pendingActionRef.current = { action: 'delete', itemId: id }; SendMessageComposer(new FurniEditorDeleteComposer(id)); }, []); From 6609c0325f7f4988096aa94152273db8edff280b Mon Sep 17 00:00:00 2001 From: duckietm Date: Tue, 31 Mar 2026 11:41:21 +0200 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=86=99=20Fix=20Youtube=20TV's?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/furniture/useFurnitureYoutubeWidget.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/hooks/rooms/widgets/furniture/useFurnitureYoutubeWidget.ts b/src/hooks/rooms/widgets/furniture/useFurnitureYoutubeWidget.ts index 7f91c1e..0069063 100644 --- a/src/hooks/rooms/widgets/furniture/useFurnitureYoutubeWidget.ts +++ b/src/hooks/rooms/widgets/furniture/useFurnitureYoutubeWidget.ts @@ -1,5 +1,5 @@ import { ControlYoutubeDisplayPlaybackMessageComposer, GetRoomEngine, GetSessionDataManager, GetYoutubeDisplayStatusMessageComposer, RoomEngineTriggerWidgetEvent, RoomId, SecurityLevel, SetYoutubeDisplayPlaylistMessageComposer, YoutubeControlVideoMessageEvent, YoutubeDisplayPlaylist, YoutubeDisplayPlaylistsEvent, YoutubeDisplayVideoMessageEvent } from '@nitrots/nitro-renderer'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { IsOwnerOfFurniture, SendMessageComposer, YoutubeVideoPlaybackStateEnum } from '../../../../api'; import { useMessageEvent, useNitroEvent } from '../../../events'; import { useFurniRemovedEvent } from '../../engine'; @@ -13,6 +13,7 @@ const useFurnitureYoutubeWidgetState = () => { const [ objectId, setObjectId ] = useState(-1); const [ category, setCategory ] = useState(-1); + const objectIdRef = useRef(-1); const [ videoId, setVideoId ] = useState(null); const [ videoStart, setVideoStart ] = useState(null); const [ videoEnd, setVideoEnd ] = useState(null); @@ -23,6 +24,7 @@ const useFurnitureYoutubeWidgetState = () => const onClose = () => { + objectIdRef.current = -1; setObjectId(-1); setCategory(-1); setVideoId(null); @@ -64,6 +66,7 @@ const useFurnitureYoutubeWidgetState = () => if(!roomObject) return; + objectIdRef.current = event.objectId; setObjectId(event.objectId); setCategory(event.category); setHasControl(GetSessionDataManager().hasSecurity(SecurityLevel.EMPLOYEE) || IsOwnerOfFurniture(roomObject)); @@ -74,8 +77,9 @@ const useFurnitureYoutubeWidgetState = () => useMessageEvent(YoutubeDisplayVideoMessageEvent, event => { const parser = event.getParser(); + const currentObjectId = objectIdRef.current; - if((objectId === -1) || (objectId !== parser.furniId)) return; + if((currentObjectId === -1) || (currentObjectId !== parser.furniId)) return; setVideoId(parser.videoId); setVideoStart(parser.startAtSeconds); @@ -86,8 +90,9 @@ const useFurnitureYoutubeWidgetState = () => useMessageEvent(YoutubeDisplayPlaylistsEvent, event => { const parser = event.getParser(); + const currentObjectId = objectIdRef.current; - if((objectId === -1) || (objectId !== parser.furniId)) return; + if((currentObjectId === -1) || (currentObjectId !== parser.furniId)) return; setPlaylists(parser.playlists); setSelectedVideo(parser.selectedPlaylistId); @@ -100,8 +105,9 @@ const useFurnitureYoutubeWidgetState = () => useMessageEvent(YoutubeControlVideoMessageEvent, event => { const parser = event.getParser(); + const currentObjectId = objectIdRef.current; - if((objectId === -1) || (objectId !== parser.furniId)) return; + if((currentObjectId === -1) || (currentObjectId !== parser.furniId)) return; switch(parser.commandId) { From 57f6960735889bb2d24ae36da9dd285dc5d7b88a Mon Sep 17 00:00:00 2001 From: duckietm Date: Tue, 31 Mar 2026 14:55:29 +0200 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=86=99=20Fix=20the=20black=20screen?= =?UTF-8?q?=20in=20some=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../furniture/FurnitureYoutubeDisplayView.tsx | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx b/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx index 0d8dd5e..f2375ca 100644 --- a/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx +++ b/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx @@ -17,42 +17,48 @@ export const FurnitureYoutubeDisplayView: FC<{}> = FurnitureYoutubeDisplayViewPr const onStateChange = (event: { target: YouTubePlayer; data: number }) => { - setPlayer(event.target); - - if(objectId === -1) return; - - switch(event.target.getPlayerState()) + try { - case -1: - case 1: - if(currentVideoState === 2) - { - //event.target.pauseVideo(); - } + setPlayer(event.target); - if(currentVideoState !== 1) play(); - return; - case 2: - if(currentVideoState !== 2) pause(); + if(objectId === -1) return; + + switch(event.target.getPlayerState()) + { + case -1: + case 1: + if(currentVideoState !== 1) play(); + return; + case 2: + if(currentVideoState !== 2) pause(); + } } + catch(err) {} }; useEffect(() => { if((currentVideoState === null) || !player) return; - if((currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PLAYING)) + try { - player.playVideo(); + if((currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PLAYING)) + { + player.playVideo(); - return; + return; + } + + if((currentVideoState === YoutubeVideoPlaybackStateEnum.PAUSED) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PAUSED)) + { + player.pauseVideo(); + + return; + } } - - if((currentVideoState === YoutubeVideoPlaybackStateEnum.PAUSED) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PAUSED)) + catch(err) { - player.pauseVideo(); - - return; + setPlayer(null); } }, [ currentVideoState, player ]);