Merge pull request #93 from simoleo89/feat/furni-editor

feat(furni): live furnidata updates + Furni Editor packets (reload/edit/revert/sort/import)
This commit is contained in:
DuckieTM
2026-06-07 08:22:29 +02:00
committed by GitHub
23 changed files with 337 additions and 16 deletions
File diff suppressed because one or more lines are too long
+2
View File
@@ -22,6 +22,7 @@ export * from './messages/incoming/crafting';
export * from './messages/incoming/desktop';
export * from './messages/incoming/friendlist';
export * from './messages/incoming/furnieditor';
export * from './messages/incoming/furniture';
export * from './messages/incoming/game';
export * from './messages/incoming/game/directory';
export * from './messages/incoming/game/lobby';
@@ -181,6 +182,7 @@ export * from './messages/parser/crafting';
export * from './messages/parser/desktop';
export * from './messages/parser/friendlist';
export * from './messages/parser/furnieditor';
export * from './messages/parser/furniture';
export * from './messages/parser/game';
export * from './messages/parser/game/directory';
export * from './messages/parser/game/lobby';
@@ -493,6 +493,8 @@ export class IncomingHeader
public static FURNI_EDITOR_DETAIL_RESULT = 10041;
public static FURNI_EDITOR_INTERACTIONS_RESULT = 10043;
public static FURNI_EDITOR_RESULT = 10044;
public static FURNITURE_DATA_RELOAD = 10047;
public static FURNI_EDITOR_IMPORT_TEXT_RESULT = 10049;
// Catalog Admin
public static CATALOG_ADMIN_RESULT = 10059;
@@ -0,0 +1,16 @@
import { IMessageEvent } from '@nitrots/api';
import { MessageEvent } from '@nitrots/events';
import { FurniEditorImportTextResultMessageParser } from '../../parser';
export class FurniEditorImportTextResultEvent extends MessageEvent implements IMessageEvent
{
constructor(callBack: Function)
{
super(callBack, FurniEditorImportTextResultMessageParser);
}
public getParser(): FurniEditorImportTextResultMessageParser
{
return this.parser as FurniEditorImportTextResultMessageParser;
}
}
@@ -1,4 +1,5 @@
export * from './FurniEditorDetailResultEvent';
export * from './FurniEditorImportTextResultEvent';
export * from './FurniEditorInteractionsResultEvent';
export * from './FurniEditorResultEvent';
export * from './FurniEditorSearchResultEvent';
@@ -0,0 +1,16 @@
import { IMessageEvent } from '@nitrots/api';
import { MessageEvent } from '@nitrots/events';
import { FurnitureDataReloadParser } from '../../parser/furniture/FurnitureDataReloadParser';
export class FurnitureDataReloadEvent extends MessageEvent implements IMessageEvent
{
constructor(callBack: Function)
{
super(callBack, FurnitureDataReloadParser);
}
public getParser(): FurnitureDataReloadParser
{
return this.parser as FurnitureDataReloadParser;
}
}
@@ -0,0 +1 @@
export * from './FurnitureDataReloadEvent';
@@ -14,6 +14,7 @@ export * from './crafting';
export * from './desktop';
export * from './friendlist';
export * from './furnieditor';
export * from './furniture';
export * from './game';
export * from './game/directory';
export * from './game/lobby';
@@ -497,6 +497,9 @@ export class OutgoingHeader
public static FURNI_EDITOR_INTERACTIONS = 10043;
public static FURNI_EDITOR_UPDATE = 10044;
public static FURNI_EDITOR_DELETE = 10045;
public static FURNI_EDITOR_UPDATE_FURNIDATA = 10046;
public static FURNI_EDITOR_REVERT_FURNIDATA = 10048;
public static FURNI_EDITOR_IMPORT_TEXT = 10049;
public static CATALOG_ADMIN_SAVE_PAGE = 10050;
public static CATALOG_ADMIN_CREATE_PAGE = 10051;
@@ -0,0 +1,21 @@
import { IMessageComposer } from '@nitrots/api';
export class FurniEditorImportTextComposer implements IMessageComposer<ConstructorParameters<typeof FurniEditorImportTextComposer>>
{
private _data: ConstructorParameters<typeof FurniEditorImportTextComposer>;
constructor(itemId: number)
{
this._data = [ itemId ];
}
dispose(): void
{
this._data = null;
}
public getMessageArray()
{
return this._data;
}
}
@@ -0,0 +1,21 @@
import { IMessageComposer } from '@nitrots/api';
export class FurniEditorRevertFurnidataComposer implements IMessageComposer<ConstructorParameters<typeof FurniEditorRevertFurnidataComposer>>
{
private _data: ConstructorParameters<typeof FurniEditorRevertFurnidataComposer>;
constructor(itemId: number)
{
this._data = [ itemId ];
}
dispose(): void
{
this._data = null;
}
public getMessageArray()
{
return this._data;
}
}
@@ -4,9 +4,9 @@ export class FurniEditorSearchComposer implements IMessageComposer<ConstructorPa
{
private _data: ConstructorParameters<typeof FurniEditorSearchComposer>;
constructor(query: string, type: string, page: number)
constructor(query: string, type: string, page: number, sortField: string = 'id', sortDir: string = 'asc')
{
this._data = [ query, type, page ];
this._data = [ query, type, page, sortField, sortDir ];
}
dispose(): void
@@ -0,0 +1,21 @@
import { IMessageComposer } from '@nitrots/api';
export class FurniEditorUpdateFurnidataComposer implements IMessageComposer<ConstructorParameters<typeof FurniEditorUpdateFurnidataComposer>>
{
private _data: ConstructorParameters<typeof FurniEditorUpdateFurnidataComposer>;
constructor(itemId: number, jsonFields: string)
{
this._data = [ itemId, jsonFields ];
}
dispose(): void
{
this._data = null;
}
public getMessageArray()
{
return this._data;
}
}
@@ -1,6 +1,9 @@
export * from './FurniEditorBySpriteComposer';
export * from './FurniEditorDeleteComposer';
export * from './FurniEditorDetailComposer';
export * from './FurniEditorImportTextComposer';
export * from './FurniEditorInteractionsComposer';
export * from './FurniEditorRevertFurnidataComposer';
export * from './FurniEditorSearchComposer';
export * from './FurniEditorUpdateComposer';
export * from './FurniEditorUpdateFurnidataComposer';
@@ -0,0 +1,51 @@
import { IMessageDataWrapper, IMessageParser } from '@nitrots/api';
export class FurniEditorImportTextResultMessageParser implements IMessageParser
{
private _found: boolean;
private _name: string;
private _description: string;
private _classname: string;
public flush(): boolean
{
this._found = false;
this._name = '';
this._description = '';
this._classname = '';
return true;
}
public parse(wrapper: IMessageDataWrapper): boolean
{
if(!wrapper) return false;
this._found = wrapper.readBoolean();
this._name = wrapper.readString();
this._description = wrapper.readString();
this._classname = wrapper.readString();
return true;
}
public get found(): boolean
{
return this._found;
}
public get name(): string
{
return this._name;
}
public get description(): string
{
return this._description;
}
public get classname(): string
{
return this._classname;
}
}
@@ -1,6 +1,7 @@
export * from './CatalogRefData';
export * from './FurniDetailData';
export * from './FurniEditorDetailResultMessageParser';
export * from './FurniEditorImportTextResultMessageParser';
export * from './FurniEditorInteractionsResultMessageParser';
export * from './FurniEditorResultMessageParser';
export * from './FurniEditorSearchResultMessageParser';
@@ -0,0 +1,56 @@
import { IMessageDataWrapper, IMessageParser } from '@nitrots/api';
export interface FurnidataDeltaEntry
{
type: string; // "S" floor | "I" wall
id: number;
classname: string;
name: string;
description: string;
}
export class FurnitureDataReloadParser implements IMessageParser
{
private static readonly MAX_ENTRIES = 100000;
private _mode: number;
private _entries: FurnidataDeltaEntry[];
public flush(): boolean
{
this._mode = 0;
this._entries = [];
return true;
}
public parse(wrapper: IMessageDataWrapper): boolean
{
if(!wrapper) return false;
this._mode = wrapper.readInt();
this._entries = [];
if(this._mode === 0)
{
let count = wrapper.readInt();
if(count < 0) count = 0;
if(count > FurnitureDataReloadParser.MAX_ENTRIES) count = FurnitureDataReloadParser.MAX_ENTRIES;
for(let i = 0; i < count; i++)
{
this._entries.push({
type: wrapper.readString(),
id: wrapper.readInt(),
classname: wrapper.readString(),
name: wrapper.readString(),
description: wrapper.readString()
});
}
}
return true;
}
public get mode(): number { return this._mode; }
public get entries(): FurnidataDeltaEntry[] { return this._entries; }
}
@@ -0,0 +1 @@
export * from './FurnitureDataReloadParser';
@@ -13,6 +13,7 @@ export * from './crafting';
export * from './desktop';
export * from './friendlist';
export * from './furnieditor';
export * from './furniture';
export * from './game';
export * from './game/directory';
export * from './game/lobby';
+21 -2
View File
@@ -1,5 +1,7 @@
import { IFurnitureData, IGroupInformationManager, IMessageComposer, IMessageEvent, IProductData, ISessionDataManager, IUserDataSnapshot, NoobnessLevelEnum, SecurityLevel } from '@nitrots/api';
import { AccountSafetyLockStatusChangeMessageEvent, AccountSafetyLockStatusChangeParser, AvailabilityStatusMessageEvent, ChangeUserNameResultMessageEvent, EmailStatusResultEvent, FigureUpdateEvent, GetCommunication, GetUserTagsComposer, InClientLinkEvent, MysteryBoxKeysEvent, NoobnessLevelMessageEvent, PetRespectComposer, PetScratchFailedMessageEvent, RoomReadyMessageEvent, RoomUnitChatComposer, UserInfoEvent, UserNameChangeMessageEvent, UserPermissionsEvent, UserRespectComposer, UserTagsMessageEvent } from '@nitrots/communication';
import { AccountSafetyLockStatusChangeMessageEvent, AccountSafetyLockStatusChangeParser, AvailabilityStatusMessageEvent, ChangeUserNameResultMessageEvent, EmailStatusResultEvent, FigureUpdateEvent, FurnitureDataReloadEvent, GetCommunication, GetUserTagsComposer, InClientLinkEvent, MysteryBoxKeysEvent, NoobnessLevelMessageEvent, PetRespectComposer, PetScratchFailedMessageEvent, RoomReadyMessageEvent, RoomUnitChatComposer, UserInfoEvent, UserNameChangeMessageEvent, UserPermissionsEvent, UserRespectComposer, UserTagsMessageEvent } from '@nitrots/communication';
import type { FurnidataDeltaEntry } from '@nitrots/communication';
import { applyFurnidataDeltaTo } from './furniture/applyFurnidataDelta';
import { GetConfiguration } from '@nitrots/configuration';
import { GetLocalizationManager } from '@nitrots/localization';
import { GetEventDispatcher, MysteryBoxKeysUpdateEvent, NitroEvent, NitroEventType, NitroSettingsEvent, SessionDataPreferencesEvent, UserNameUpdateEvent } from '@nitrots/events';
@@ -171,7 +173,13 @@ export class SessionDataManager implements ISessionDataManager
GetCommunication().registerMessageEvent(new MysteryBoxKeysEvent(this.onMysteryBoxKeysEvent.bind(this))),
GetCommunication().registerMessageEvent(new NoobnessLevelMessageEvent(this.onNoobnessLevelMessageEvent.bind(this))),
GetCommunication().registerMessageEvent(new AccountSafetyLockStatusChangeMessageEvent(this.onAccountSafetyLockStatusChangeMessageEvent.bind(this))),
GetCommunication().registerMessageEvent(new EmailStatusResultEvent(this.onEmailStatus.bind(this)))
GetCommunication().registerMessageEvent(new EmailStatusResultEvent(this.onEmailStatus.bind(this))),
GetCommunication().registerMessageEvent(new FurnitureDataReloadEvent((event: FurnitureDataReloadEvent) =>
{
const parser = event.getParser();
if(parser.mode === 1) { void this.applyFurnidataReloadHint(); }
else { this.applyFurnidataDelta(parser.entries); }
}))
);
// Store event dispatcher callback for cleanup
@@ -564,6 +572,17 @@ export class SessionDataManager implements ISessionDataManager
}
}
public applyFurnidataDelta(entries: FurnidataDeltaEntry[]): void
{
applyFurnidataDeltaTo(entries, this._floorItems as any, this._wallItems as any, GetLocalizationManager(), (typeof window !== 'undefined') ? window : { dispatchEvent: () => {} } as any);
}
public async applyFurnidataReloadHint(): Promise<void>
{
await this._furnitureData.init();
if(typeof window !== 'undefined') window.dispatchEvent(new CustomEvent('nitro-localization-updated'));
}
public getBadgeUrl(name: string): string
{
return this._badgeImageManager.getBadgeUrl(name);
@@ -0,0 +1,36 @@
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { applyFurnidataDeltaTo } from '../furniture/applyFurnidataDelta';
describe('applyFurnidataDeltaTo', () => {
const setValue = vi.fn();
beforeEach(() => setValue.mockClear());
it('patches floor FurnitureData name/desc + localization keys, dispatches window event', () => {
const floor: any = { _localizedName: 'Old', _description: 'Old desc' };
const floorItems = new Map<number, any>([[ 5, floor ]]);
const dispatched: string[] = [];
const win: any = { dispatchEvent: (e: any) => dispatched.push(e.type) };
applyFurnidataDeltaTo(
[ { type: 'S', id: 5, classname: 'chair', name: 'New', description: 'New desc' } ],
floorItems, new Map(), { setValue }, win
);
expect(floor._localizedName).toBe('New');
expect(floor._description).toBe('New desc');
expect(setValue).toHaveBeenCalledWith('roomItem.name.5', 'New');
expect(setValue).toHaveBeenCalledWith('roomItem.desc.5', 'New desc');
expect(dispatched).toContain('nitro-localization-updated');
});
it('patches wall items by id', () => {
const wall: any = { _localizedName: 'W', _description: '' };
const wallItems = new Map<number, any>([[ 9, wall ]]);
applyFurnidataDeltaTo(
[ { type: 'I', id: 9, classname: 'poster', name: 'WallNew', description: 'd' } ],
new Map(), wallItems, { setValue }, { dispatchEvent: () => {} }
);
expect(wall._localizedName).toBe('WallNew');
expect(setValue).toHaveBeenCalledWith('wallItem.name.9', 'WallNew');
});
});
@@ -0,0 +1,43 @@
import type { FurnidataDeltaEntry } from '@nitrots/communication';
/**
* Pure, testable furnidata-delta patcher. Mutates the FurnitureData objects in
* the given maps (by id) and the localization keys, then dispatches the
* `nitro-localization-updated` window event so subscribed React surfaces refresh.
*/
export function applyFurnidataDeltaTo(
entries: FurnidataDeltaEntry[],
floorItems: Map<number, any>,
wallItems: Map<number, any>,
localization: { setValue: (key: string, value: string) => void },
win: { dispatchEvent: (event: any) => void }
): void
{
if(!entries || !entries.length) return;
for(const e of entries)
{
if(e.type === 'I')
{
const wall = wallItems.get(e.id);
if(wall) { wall._localizedName = e.name; wall._description = e.description; }
localization.setValue('wallItem.name.' + e.id, e.name);
localization.setValue('wallItem.desc.' + e.id, e.description);
}
else
{
const floor = floorItems.get(e.id);
if(floor) { floor._localizedName = e.name; floor._description = e.description; }
localization.setValue('roomItem.name.' + e.id, e.name);
localization.setValue('roomItem.desc.' + e.id, e.description);
}
}
if(win && typeof win.dispatchEvent === 'function')
{
const evt = (typeof CustomEvent !== 'undefined')
? new CustomEvent('nitro-localization-updated')
: { type: 'nitro-localization-updated' } as any;
win.dispatchEvent(evt);
}
}
+11 -11
View File
@@ -656,7 +656,7 @@
resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.1.70.tgz#2beaf197eb97e0020a218d8158251ee3af7ba583"
integrity sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==
"@xmldom/xmldom@^0.8.12":
"@xmldom/xmldom@^0.8.13":
version "0.8.13"
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.13.tgz#00d1dd940b218dff2e49309d410d8bb212159225"
integrity sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==
@@ -1454,10 +1454,10 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
parse-svg-path@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/parse-svg-path/-/parse-svg-path-0.1.2.tgz#7a7ec0d1eb06fa5325c7d3e009b859a09b5d49eb"
integrity sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==
parse-svg-path@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/parse-svg-path/-/parse-svg-path-0.2.0.tgz#38bc8747e7aa32327ae2579c331b161c4867a167"
integrity sha512-Tf7FFIrguPKQwzD4pWnYkR2VOv3raoHeKED80Bm+BYHI3KxC8KsgsGC5+fSMzAGDA6UEk4bHvmi+RsjmL3khpg==
parse5@^8.0.0:
version "8.0.1"
@@ -1503,20 +1503,20 @@ pixi-filters@^6.1.5:
dependencies:
"@types/gradient-parser" "^0.1.2"
pixi.js@^8.18.1, pixi.js@^8.8.1:
version "8.18.1"
resolved "https://registry.yarnpkg.com/pixi.js/-/pixi.js-8.18.1.tgz#952a528d31a20355383f46e7aeb0653927a262e2"
integrity sha512-6LUPWYgulZhp/w4kam2XHXB0QedISZIqrJbRdHLLQ3csn5a38uzKxAp6B5j6s89QFYaIJbg95kvgTRcbgpO1ow==
pixi.js@^8.19.0, pixi.js@^8.8.1:
version "8.19.0"
resolved "https://registry.yarnpkg.com/pixi.js/-/pixi.js-8.19.0.tgz#4a0e9056f88ee61293f092723c8cbc2e083c8a7f"
integrity sha512-pq1O6emA/GFjjeF+8d3Pb5t7knD8FsnfWGqQcRjYjsqFZ7QdzG1XgjLDUu0DFJRbafjV5+g8iNLFBx0b9649lg==
dependencies:
"@pixi/colord" "^2.9.6"
"@types/earcut" "^3.0.0"
"@webgpu/types" "^0.1.69"
"@xmldom/xmldom" "^0.8.12"
"@xmldom/xmldom" "^0.8.13"
earcut "^3.0.2"
eventemitter3 "^5.0.1"
gifuct-js "^2.1.2"
ismobilejs "^1.1.1"
parse-svg-path "^0.1.2"
parse-svg-path "^0.2.0"
tiny-lru "^11.4.7"
postcss@^8.5.14: