Files
Nitro_Render_V3/packages/session/src/SessionDataManager.ts
T

800 lines
26 KiB
TypeScript

import { IFurnitureData, IGroupInformationManager, IMessageComposer, IMessageEvent, IProductData, ISessionDataManager, IUserDataSnapshot, NoobnessLevelEnum, SecurityLevel } from '@nitrots/api';
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';
import { CreateLinkEvent, HabboWebTools, parseConfigJsonFromResponse } from '@nitrots/utils';
import { Texture } from 'pixi.js';
import { GroupInformationManager } from './GroupInformationManager';
import { IgnoredUsersManager } from './IgnoredUsersManager';
import { BadgeImageManager } from './badge/BadgeImageManager';
import { FurnitureDataLoader } from './furniture/FurnitureDataLoader';
import { ProductDataLoader } from './product/ProductDataLoader';
export class SessionDataManager implements ISessionDataManager
{
private _messageEvents: IMessageEvent[] = [];
private _settingsEventCallback: (event: NitroSettingsEvent) => void = null;
private _userId: number;
private _name: string;
private _figure: string;
private _gender: string;
private _realName: string;
private _respectsReceived: number;
private _respectsLeft: number;
private _respectsPetLeft: number;
private _canChangeName: boolean;
private _safetyLocked: boolean;
private _ignoredUsersManager: IgnoredUsersManager = new IgnoredUsersManager();
private _groupInformationManager: IGroupInformationManager = new GroupInformationManager();
private _clubLevel: number = 0;
private _securityLevel: number = 0;
private _isAmbassador: boolean = false;
private _rankId: number = 0;
private _rankName: string = '';
private _rankBadge: string = '';
private _rankPrefix: string = '';
private _rankPrefixColor: string = '';
private _noobnessLevel: number = -1;
private _isEmailVerified: boolean = false;
private _systemOpen: boolean = false;
private _systemShutdown: boolean = false;
private _isAuthenticHabbo: boolean = false;
private _isRoomCameraFollowDisabled: boolean = false;
private _uiFlags: number = 0;
private _floorItems: Map<number, IFurnitureData> = new Map();
private _wallItems: Map<number, IFurnitureData> = new Map();
private _floorItemOverrides: Map<string, Partial<IFurnitureData>> = new Map();
private _wallItemOverrides: Map<string, Partial<IFurnitureData>> = new Map();
private _products: Map<string, IProductData> = new Map();
private _furnitureData: FurnitureDataLoader = new FurnitureDataLoader(this._floorItems, this._wallItems);
private _productData: ProductDataLoader = new ProductDataLoader(this._products);
private _tags: string[] = [];
private _badgeImageManager: BadgeImageManager = new BadgeImageManager();
private _userDataSnapshot: Readonly<IUserDataSnapshot> | null = null;
private _permissions: Map<string, number> = new Map();
private _permissionsSnapshot: ReadonlyMap<string, number> | null = null;
constructor()
{
this.resetUserInfo();
}
private invalidateUserDataSnapshot(): void
{
this._userDataSnapshot = null;
GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SESSION_DATA_UPDATED));
}
private invalidatePermissionsSnapshot(): void
{
this._permissionsSnapshot = null;
GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.USER_PERMISSIONS_UPDATED));
}
/**
* Resolved permission map for the current user — mirror of
* `permission_definitions` for the user's rank, filtered to keys
* with `PermissionSetting != DISALLOWED`. Wire-fed by
* `UserPermissionsMapEvent` (Arcturus ≥ 4.2.10). Older emulators
* that don't ship the new packet leave the snapshot empty; React
* consumers via `useHasPermission(key)` then degrade gracefully
* (every gate returns false → mod UI hidden, which is the safe
* default).
*
* Referentially stable until the next
* `UserPermissionsMapEvent` arrives (e.g. after
* `HabboManager.setRank`).
*/
public getPermissionsSnapshot(): ReadonlyMap<string, number>
{
if(this._permissionsSnapshot) return this._permissionsSnapshot;
this._permissionsSnapshot = new Map(this._permissions) as ReadonlyMap<string, number>;
return this._permissionsSnapshot;
}
public getUserDataSnapshot(): Readonly<IUserDataSnapshot>
{
if(this._userDataSnapshot) return this._userDataSnapshot;
this._userDataSnapshot = Object.freeze<IUserDataSnapshot>({
userId: this._userId,
userName: this._name,
figure: this._figure,
gender: this._gender,
realName: this._realName,
respectsReceived: this._respectsReceived,
respectsLeft: this._respectsLeft,
respectsPetLeft: this._respectsPetLeft,
canChangeName: this._canChangeName,
clubLevel: this._clubLevel,
securityLevel: this._securityLevel,
isAmbassador: this._isAmbassador,
isEmailVerified: this._isEmailVerified,
isNoob: (this._noobnessLevel !== NoobnessLevelEnum.OLD_IDENTITY),
isAuthenticHabbo: this._isAuthenticHabbo,
isSystemOpen: this._systemOpen,
isSystemShutdown: this._systemShutdown,
uiFlags: this._uiFlags,
tags: Object.freeze<string[]>([...this._tags]) as ReadonlyArray<string>,
rankId: this._rankId,
rankName: this._rankName,
rankBadge: this._rankBadge,
rankPrefix: this._rankPrefix,
rankPrefixColor: this._rankPrefixColor
});
return this._userDataSnapshot;
}
public async init(): Promise<void>
{
await Promise.all([
this._furnitureData.init(),
this._productData.init(),
this._badgeImageManager.init(),
this._ignoredUsersManager.init(),
this._groupInformationManager.init()
]);
// Store message event references for cleanup
this._messageEvents.push(
GetCommunication().registerMessageEvent(new FigureUpdateEvent((event: FigureUpdateEvent) =>
{
this._figure = event.getParser().figure;
this._gender = event.getParser().gender;
HabboWebTools.updateFigure(this._figure);
this.invalidateUserDataSnapshot();
})),
GetCommunication().registerMessageEvent(new UserInfoEvent(this.onUserInfoEvent.bind(this))),
GetCommunication().registerMessageEvent(new UserPermissionsEvent(this.onUserPermissionsEvent.bind(this))),
GetCommunication().registerMessageEvent(new AvailabilityStatusMessageEvent(this.onAvailabilityStatusMessageEvent.bind(this))),
GetCommunication().registerMessageEvent(new PetScratchFailedMessageEvent(this.onPetRespectFailed.bind(this))),
GetCommunication().registerMessageEvent(new ChangeUserNameResultMessageEvent(this.onChangeNameUpdateEvent.bind(this))),
GetCommunication().registerMessageEvent(new UserNameChangeMessageEvent(this.onUserNameChangeMessageEvent.bind(this))),
GetCommunication().registerMessageEvent(new UserTagsMessageEvent(this.onUserTags.bind(this))),
GetCommunication().registerMessageEvent(new RoomReadyMessageEvent(this.onRoomModelNameEvent.bind(this))),
GetCommunication().registerMessageEvent(new InClientLinkEvent(this.onInClientLinkEvent.bind(this))),
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 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
this._settingsEventCallback = (event: NitroSettingsEvent) =>
{
this._isRoomCameraFollowDisabled = event.cameraFollow;
this._uiFlags = event.flags;
GetEventDispatcher().dispatchEvent(new SessionDataPreferencesEvent(this._uiFlags));
this.invalidateUserDataSnapshot();
};
GetEventDispatcher().addEventListener<NitroSettingsEvent>(NitroSettingsEvent.SETTINGS_UPDATED, this._settingsEventCallback);
}
public dispose(): void
{
// Remove all message events
for(const event of this._messageEvents)
{
GetCommunication().removeMessageEvent(event);
}
this._messageEvents = [];
// Remove event dispatcher listener
if(this._settingsEventCallback)
{
GetEventDispatcher().removeEventListener(NitroSettingsEvent.SETTINGS_UPDATED, this._settingsEventCallback);
this._settingsEventCallback = null;
}
}
private resetUserInfo(): void
{
this._userId = 0;
this._name = null;
this._figure = null;
this._gender = null;
this._realName = null;
this._canChangeName = false;
this._safetyLocked = false;
}
public getAllFurnitureData(): IFurnitureData[]
{
return [
...Array.from(this._floorItems.values()).map(item => this.applyFurnitureOverrides(item, this._floorItemOverrides)),
...Array.from(this._wallItems.values()).map(item => this.applyFurnitureOverrides(item, this._wallItemOverrides))
];
}
// Mergia a runtime un chunk furnidata (es. custom/imported.json5) nelle Map
// esistenti, SENZA reload del client. Ritorna gli entry aggiunti cosi il
// chiamante puo' aggiornare anche il RoomContentLoader. Fa comparire i furni
// appena importati nel catalogo in tempo reale.
public async mergeFurnitureDataFromUrl(url: string): Promise<IFurnitureData[]>
{
if(!url || !url.length) return [];
const added = await this._furnitureData.mergeFromUrl(url);
if(added && added.length) GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SESSION_DATA_UPDATED));
return added;
}
public async applyFurnitureDataOverrides(url: string): Promise<void>
{
if(!url || !url.length)
{
this.clearFurnitureDataOverrides();
return;
}
const response = await fetch(url);
if(response.status !== 200) throw new Error(`Unable to load ${ url }`);
const data = await parseConfigJsonFromResponse(response, url);
this._floorItemOverrides = this.parseFurnitureOverrides(data?.roomitemtypes?.furnitype || []);
this._wallItemOverrides = this.parseFurnitureOverrides(data?.wallitemtypes?.furnitype || []);
this.refreshFurnitureLocalizations();
}
public clearFurnitureDataOverrides(): void
{
this._floorItemOverrides.clear();
this._wallItemOverrides.clear();
this.refreshFurnitureLocalizations();
}
private onUserInfoEvent(event: UserInfoEvent): void
{
if(!event || !event.connection) return;
this.resetUserInfo();
const userInfo = event.getParser().userInfo;
if(!userInfo) return;
this._userId = userInfo.userId;
this._name = userInfo.username;
this._figure = userInfo.figure;
this._gender = userInfo.gender;
this._realName = userInfo.realName;
this._respectsReceived = userInfo.respectsReceived;
this._respectsLeft = userInfo.respectsRemaining;
this._respectsPetLeft = userInfo.respectsPetRemaining;
this._canChangeName = userInfo.canChangeName;
this._safetyLocked = userInfo.safetyLocked;
this._ignoredUsersManager.requestIgnoredUsers(userInfo.username);
this.invalidateUserDataSnapshot();
}
private onUserPermissionsEvent(event: UserPermissionsEvent): void
{
if(!event || !event.connection) return;
const parser = event.getParser();
this._clubLevel = parser.clubLevel;
this._securityLevel = parser.securityLevel;
this._isAmbassador = parser.isAmbassador;
this._rankId = parser.rankId;
this._rankName = parser.rankName;
this._rankBadge = parser.rankBadge;
this._rankPrefix = parser.rankPrefix;
this._rankPrefixColor = parser.rankPrefixColor;
// Copy into our local mutable Map so the parser's reference
// (which is overwritten on every parse() call) can't leak back
// to consumers.
this._permissions = new Map(parser.permissions);
// Invalidate BOTH snapshots: a UserPermissionsComposer push from
// the emulator refreshes user-data fields (clubLevel/securityLevel
// /rank metadata) AND the resolved permission map. Keep the two
// invalidation events distinct so React consumers can subscribe
// to just one (e.g. a widget that only cares about
// useHasPermission re-renders only when the map actually
// changes, not on every snapshot bump).
this.invalidateUserDataSnapshot();
this.invalidatePermissionsSnapshot();
}
private onAvailabilityStatusMessageEvent(event: AvailabilityStatusMessageEvent): void
{
if(!event || !event.connection) return;
const parser = event.getParser();
if(!parser) return;
this._systemOpen = parser.isOpen;
this._systemShutdown = parser.onShutdown;
this._isAuthenticHabbo = parser.isAuthenticUser;
this.invalidateUserDataSnapshot();
}
private onPetRespectFailed(event: PetScratchFailedMessageEvent): void
{
if(!event || !event.connection) return;
this._respectsPetLeft++;
this.invalidateUserDataSnapshot();
}
private onChangeNameUpdateEvent(event: ChangeUserNameResultMessageEvent): void
{
if(!event || !event.connection) return;
const parser = event.getParser();
if(!parser) return;
if(parser.resultCode !== ChangeUserNameResultMessageEvent.NAME_OK) return;
this._canChangeName = false;
GetEventDispatcher().dispatchEvent(new UserNameUpdateEvent(parser.name));
this.invalidateUserDataSnapshot();
}
private onUserNameChangeMessageEvent(event: UserNameChangeMessageEvent): void
{
if(!event || !event.connection) return;
const parser = event.getParser();
if(!parser) return;
if(parser.webId !== this.userId) return;
this._name = parser.newName;
this._canChangeName = false;
GetEventDispatcher().dispatchEvent(new UserNameUpdateEvent(this._name));
this.invalidateUserDataSnapshot();
}
private onUserTags(event: UserTagsMessageEvent): void
{
if(!event || !event.connection) return;
const parser = event.getParser();
if(!parser) return;
this._tags = parser.tags;
this.invalidateUserDataSnapshot();
}
private onRoomModelNameEvent(event: RoomReadyMessageEvent): void
{
if(!event) return;
const parser = event.getParser();
if(!parser) return;
HabboWebTools.roomVisited(parser.roomId);
}
private onInClientLinkEvent(event: InClientLinkEvent): void
{
if(!event) return;
const parser = event.getParser();
if(!parser) return;
CreateLinkEvent(parser.link);
}
private onMysteryBoxKeysEvent(event: MysteryBoxKeysEvent): void
{
if(!event) return;
const parser = event.getParser();
if(!parser) return;
GetEventDispatcher().dispatchEvent(new MysteryBoxKeysUpdateEvent(parser.boxColor, parser.keyColor));
}
private onNoobnessLevelMessageEvent(event: NoobnessLevelMessageEvent): void
{
this._noobnessLevel = event.getParser().noobnessLevel;
if(this._noobnessLevel !== NoobnessLevelEnum.OLD_IDENTITY) GetConfiguration().setValue<number>('new.identity', 1);
this.invalidateUserDataSnapshot();
}
private onAccountSafetyLockStatusChangeMessageEvent(event: AccountSafetyLockStatusChangeMessageEvent): void
{
if(!event) return;
const parser = event.getParser();
if(!parser) return;
this._safetyLocked = (parser.status === AccountSafetyLockStatusChangeParser.SAFETY_LOCK_STATUS_LOCKED);
}
private onEmailStatus(event: EmailStatusResultEvent): void
{
this._isEmailVerified = event?.getParser()?.isVerified ?? false;
this.invalidateUserDataSnapshot();
}
public getFloorItemData(id: number): IFurnitureData
{
const existing = this._floorItems.get(id);
if(!existing) return null;
return this.applyFurnitureOverrides(existing, this._floorItemOverrides);
}
public getFloorItemDataByName(name: string): IFurnitureData
{
if(!name || !this._floorItems || !this._floorItems.size) return null;
for(const item of this._floorItems.values())
{
if(!item || (item.className !== name)) continue;
return this.applyFurnitureOverrides(item, this._floorItemOverrides);
}
return null;
}
public getWallItemData(id: number): IFurnitureData
{
const existing = this._wallItems.get(id);
if(!existing) return null;
return this.applyFurnitureOverrides(existing, this._wallItemOverrides);
}
public getWallItemDataByName(name: string): IFurnitureData
{
if(!name || !this._wallItems || !this._wallItems.size) return null;
for(const item of this._wallItems.values())
{
if(!item || (item.className !== name)) continue;
return this.applyFurnitureOverrides(item, this._wallItemOverrides);
}
return null;
}
public getProductData(type: string): IProductData
{
return this._products.get(type);
}
private parseFurnitureOverrides(items: any[]): Map<string, Partial<IFurnitureData>>
{
const overrides = new Map<string, Partial<IFurnitureData>>();
for(const item of items)
{
if(!item?.classname) continue;
const className = ((item.classname as string).split('*')[0] || '').trim();
if(!className.length) continue;
overrides.set(className, {
name: item.name || '',
description: item.description || ''
});
}
return overrides;
}
private applyFurnitureOverrides(item: IFurnitureData, overrides: Map<string, Partial<IFurnitureData>>): IFurnitureData
{
if(!item) return null;
const override = overrides.get(item.className);
if(!override) return item;
const clonedItem = Object.assign(Object.create(Object.getPrototypeOf(item)), item) as any;
if(override.name !== undefined) clonedItem._localizedName = override.name;
if(override.description !== undefined) clonedItem._description = override.description;
return clonedItem as IFurnitureData;
}
private refreshFurnitureLocalizations(): void
{
const localizationManager = GetLocalizationManager();
for(const item of this._floorItems.values())
{
const resolvedItem = this.applyFurnitureOverrides(item, this._floorItemOverrides);
localizationManager.setValue(('roomItem.name.' + item.id), resolvedItem.name);
localizationManager.setValue(('roomItem.desc.' + item.id), resolvedItem.description);
}
for(const item of this._wallItems.values())
{
const resolvedItem = this.applyFurnitureOverrides(item, this._wallItemOverrides);
localizationManager.setValue(('wallItem.name.' + item.id), resolvedItem.name);
localizationManager.setValue(('wallItem.desc.' + item.id), resolvedItem.description);
}
}
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);
}
public getGroupBadgeUrl(name: string): string
{
return this._badgeImageManager.getBadgeUrl(name, BadgeImageManager.GROUP_BADGE);
}
public getBadgeImage(name: string): Texture
{
return this._badgeImageManager.getBadgeImage(name);
}
public getGroupBadgeImage(name: string): Texture
{
return this._badgeImageManager.getBadgeImage(name, BadgeImageManager.GROUP_BADGE);
}
public getUserTags(roomUnitId: number): string[]
{
if(roomUnitId < 0) return;
this.send(new GetUserTagsComposer(roomUnitId));
}
public loadBadgeImage(name: string): string
{
return this._badgeImageManager.loadBadgeImage(name);
}
public loadGroupBadgeImage(name: string): string
{
return this._badgeImageManager.loadBadgeImage(name, BadgeImageManager.GROUP_BADGE);
}
public hasSecurity(level: number): boolean
{
return (this._securityLevel >= level);
}
public giveRespect(userId: number): void
{
if((userId < 0) || (this._respectsLeft <= 0)) return;
this.send(new UserRespectComposer(userId));
this._respectsLeft--;
this.invalidateUserDataSnapshot();
}
public givePetRespect(petId: number): void
{
if((petId < 0) || (this._respectsPetLeft <= 0)) return;
this.send(new PetRespectComposer(petId));
this._respectsPetLeft--;
this.invalidateUserDataSnapshot();
}
public sendSpecialCommandMessage(text: string, styleId: number = 0): void
{
this.send(new RoomUnitChatComposer(text));
}
public ignoreUser(name: string): void
{
this._ignoredUsersManager.ignoreUser(name);
}
public unignoreUser(name: string): void
{
this._ignoredUsersManager.unignoreUser(name);
}
public isUserIgnored(name: string): boolean
{
return this._ignoredUsersManager.isIgnored(name);
}
public getGroupBadge(groupId: number): string
{
return this._groupInformationManager.getGroupBadge(groupId);
}
public send(composer: IMessageComposer<unknown[]>): void
{
GetCommunication().connection.send(composer);
}
public get userId(): number
{
return this._userId;
}
public get userName(): string
{
return this._name;
}
public get figure(): string
{
return this._figure;
}
public get gender(): string
{
return this._gender;
}
public get realName(): string
{
return this._realName;
}
public get ignoredUsersManager(): IgnoredUsersManager
{
return this._ignoredUsersManager;
}
public get groupInformationManager(): IGroupInformationManager
{
return this._groupInformationManager;
}
public get respectsReceived(): number
{
return this._respectsReceived;
}
public get respectsLeft(): number
{
return this._respectsLeft;
}
public get respectsPetLeft(): number
{
return this._respectsPetLeft;
}
public get canChangeName(): boolean
{
return this._canChangeName;
}
public get clubLevel(): number
{
return this._clubLevel;
}
public get securityLevel(): number
{
return this._securityLevel;
}
public get isAmbassador(): boolean
{
return this._isAmbassador;
}
public get isEmailVerified(): boolean
{
return this._isEmailVerified;
}
public get isNoob(): boolean
{
return (this._noobnessLevel !== NoobnessLevelEnum.OLD_IDENTITY);
}
public get isRealNoob(): boolean
{
return (this._noobnessLevel === NoobnessLevelEnum.REAL_NOOB);
}
public get isSystemOpen(): boolean
{
return this._systemOpen;
}
public get isSystemShutdown(): boolean
{
return this._systemShutdown;
}
public get isAuthenticHabbo(): boolean
{
return this._isAuthenticHabbo;
}
public get isModerator(): boolean
{
return (this._securityLevel >= SecurityLevel.MODERATOR);
}
public get isCameraFollowDisabled(): boolean
{
return this._isRoomCameraFollowDisabled;
}
public get uiFlags(): number
{
return this._uiFlags;
}
public get tags(): string[]
{
return this._tags;
}
}