You've already forked Nitro_Render_V3
mirror of
https://github.com/duckietm/Nitro_Render_V3.git
synced 2026-06-19 15:06:20 +00:00
feat(session): resolved permission map snapshot (USER_PERMISSIONS_UPDATED)
Adds the wire pipeline for `Outgoing.UserPermissionsMapComposer = 10070` shipped by Arcturus-Morningstar-Extended ≥ 4.2.10. The composer sends the resolved `permission_definitions` map for the current user (filtered to ALLOWED / ROOM_OWNER entries) at login and after every `HabboManager.setRank` — so a runtime promote/demote re-derives every React-side permission gate. - NitroEventType.USER_PERMISSIONS_UPDATED — new invalidation event. - IncomingHeader.USER_PERMISSIONS_MAP = 10070. - UserPermissionsMapParser reads `int count + (string key, int value)*`. - UserPermissionsMapEvent + NitroMessages registration. - SessionDataManager._permissions Map + getPermissionsSnapshot() referentially-stable per the snapshot convention. New handler onUserPermissionsMapEvent copies the parser map into the manager (so the parser's mutable reference doesn't leak) and invalidates. - ISessionDataManager.getPermissionsSnapshot() — public contract. React-side consumers ship in the companion commit on feat/react19-modernization. The wire is backward-compatible: older emulators never send the packet, the snapshot stays empty Map, and all useHasPermission(key) gates return false (mod-only UI hidden by default = safe). Verification: tsgo clean, vitest 138/138.
This commit is contained in:
@@ -55,4 +55,11 @@ export interface ISessionDataManager
|
|||||||
uiFlags: number;
|
uiFlags: number;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
getUserDataSnapshot(): Readonly<IUserDataSnapshot>;
|
getUserDataSnapshot(): Readonly<IUserDataSnapshot>;
|
||||||
|
/**
|
||||||
|
* Referentially-stable view of the resolved permission map for
|
||||||
|
* the current user. Invalidated by `USER_PERMISSIONS_UPDATED`.
|
||||||
|
* Empty when the connected emulator doesn't ship the extended
|
||||||
|
* `UserPermissionsMapComposer` (Arcturus ≥ 4.2.10).
|
||||||
|
*/
|
||||||
|
getPermissionsSnapshot(): ReadonlyMap<string, number>;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -255,6 +255,8 @@ export class IncomingHeader
|
|||||||
public static USER_OUTFITS = 3315;
|
public static USER_OUTFITS = 3315;
|
||||||
public static USER_PERKS = 2586;
|
public static USER_PERKS = 2586;
|
||||||
public static USER_PERMISSIONS = 411;
|
public static USER_PERMISSIONS = 411;
|
||||||
|
// Resolved permission map (Arcturus extension, Outgoing.UserPermissionsMapComposer = 10070).
|
||||||
|
public static USER_PERMISSIONS_MAP = 10070;
|
||||||
public static USER_PET_ADD = 2101;
|
public static USER_PET_ADD = 2101;
|
||||||
public static USER_PET_REMOVE = 3253;
|
public static USER_PET_REMOVE = 3253;
|
||||||
public static USER_PETS = 3522;
|
public static USER_PETS = 3522;
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { IMessageEvent } from '@nitrots/api';
|
||||||
|
import { MessageEvent } from '@nitrots/events';
|
||||||
|
import { UserPermissionsMapParser } from '../../../parser';
|
||||||
|
|
||||||
|
export class UserPermissionsMapEvent extends MessageEvent implements IMessageEvent
|
||||||
|
{
|
||||||
|
constructor(callBack: Function)
|
||||||
|
{
|
||||||
|
super(callBack, UserPermissionsMapParser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getParser(): UserPermissionsMapParser
|
||||||
|
{
|
||||||
|
return this.parser as UserPermissionsMapParser;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * from './UserPermissionsEvent';
|
export * from './UserPermissionsEvent';
|
||||||
|
export * from './UserPermissionsMapEvent';
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { IMessageDataWrapper, IMessageParser } from '@nitrots/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the resolved permission map for the current user
|
||||||
|
* (Arcturus-Morningstar-Extended ≥ 4.2.10, header
|
||||||
|
* `Outgoing.UserPermissionsMapComposer = 10070`).
|
||||||
|
*
|
||||||
|
* Wire layout:
|
||||||
|
* int count
|
||||||
|
* loop: string permission_key + int value (1 = ALLOWED, 2 = ROOM_OWNER)
|
||||||
|
*
|
||||||
|
* Only permissions whose `PermissionSetting != DISALLOWED` are sent —
|
||||||
|
* absence means "no". The renderer-side `SessionDataManager` consumes
|
||||||
|
* this and exposes it via a snapshot getter; React-side
|
||||||
|
* `useHasPermission(key)` drives UI gates against the real
|
||||||
|
* `permission_definitions.permission_key` strings instead of
|
||||||
|
* deployment-specific rank IDs.
|
||||||
|
*/
|
||||||
|
export class UserPermissionsMapParser implements IMessageParser
|
||||||
|
{
|
||||||
|
private _permissions: Map<string, number> = new Map();
|
||||||
|
|
||||||
|
public flush(): boolean
|
||||||
|
{
|
||||||
|
this._permissions = new Map();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public parse(wrapper: IMessageDataWrapper): boolean
|
||||||
|
{
|
||||||
|
if(!wrapper) return false;
|
||||||
|
|
||||||
|
const count = wrapper.readInt();
|
||||||
|
const fresh = new Map<string, number>();
|
||||||
|
|
||||||
|
for(let i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
const key = wrapper.readString();
|
||||||
|
const value = wrapper.readInt();
|
||||||
|
|
||||||
|
fresh.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._permissions = fresh;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get permissions(): ReadonlyMap<string, number>
|
||||||
|
{
|
||||||
|
return this._permissions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
export * from './UserPermissionsMapParser';
|
||||||
export * from './UserPermissionsParser';
|
export * from './UserPermissionsParser';
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ export class NitroEventType
|
|||||||
public static readonly GROUP_BADGES_UPDATED = 'GROUP_BADGES_UPDATED';
|
public static readonly GROUP_BADGES_UPDATED = 'GROUP_BADGES_UPDATED';
|
||||||
public static readonly ROOM_USER_LIST_UPDATED = 'ROOM_USER_LIST_UPDATED';
|
public static readonly ROOM_USER_LIST_UPDATED = 'ROOM_USER_LIST_UPDATED';
|
||||||
public static readonly SOUND_VOLUMES_UPDATED = 'SOUND_VOLUMES_UPDATED';
|
public static readonly SOUND_VOLUMES_UPDATED = 'SOUND_VOLUMES_UPDATED';
|
||||||
|
public static readonly USER_PERMISSIONS_UPDATED = 'USER_PERMISSIONS_UPDATED';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IFurnitureData, IGroupInformationManager, IMessageComposer, IMessageEvent, IProductData, ISessionDataManager, IUserDataSnapshot, NoobnessLevelEnum, SecurityLevel } from '@nitrots/api';
|
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, GetCommunication, GetUserTagsComposer, InClientLinkEvent, MysteryBoxKeysEvent, NoobnessLevelMessageEvent, PetRespectComposer, PetScratchFailedMessageEvent, RoomReadyMessageEvent, RoomUnitChatComposer, UserInfoEvent, UserNameChangeMessageEvent, UserPermissionsEvent, UserPermissionsMapEvent, UserRespectComposer, UserTagsMessageEvent } from '@nitrots/communication';
|
||||||
import { GetConfiguration } from '@nitrots/configuration';
|
import { GetConfiguration } from '@nitrots/configuration';
|
||||||
import { GetLocalizationManager } from '@nitrots/localization';
|
import { GetLocalizationManager } from '@nitrots/localization';
|
||||||
import { GetEventDispatcher, MysteryBoxKeysUpdateEvent, NitroEvent, NitroEventType, NitroSettingsEvent, SessionDataPreferencesEvent, UserNameUpdateEvent } from '@nitrots/events';
|
import { GetEventDispatcher, MysteryBoxKeysUpdateEvent, NitroEvent, NitroEventType, NitroSettingsEvent, SessionDataPreferencesEvent, UserNameUpdateEvent } from '@nitrots/events';
|
||||||
@@ -59,6 +59,9 @@ export class SessionDataManager implements ISessionDataManager
|
|||||||
|
|
||||||
private _userDataSnapshot: Readonly<IUserDataSnapshot> | null = null;
|
private _userDataSnapshot: Readonly<IUserDataSnapshot> | null = null;
|
||||||
|
|
||||||
|
private _permissions: Map<string, number> = new Map();
|
||||||
|
private _permissionsSnapshot: ReadonlyMap<string, number> | null = null;
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
this.resetUserInfo();
|
this.resetUserInfo();
|
||||||
@@ -71,6 +74,36 @@ export class SessionDataManager implements ISessionDataManager
|
|||||||
GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SESSION_DATA_UPDATED));
|
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>
|
public getUserDataSnapshot(): Readonly<IUserDataSnapshot>
|
||||||
{
|
{
|
||||||
if(this._userDataSnapshot) return this._userDataSnapshot;
|
if(this._userDataSnapshot) return this._userDataSnapshot;
|
||||||
@@ -128,6 +161,7 @@ export class SessionDataManager implements ISessionDataManager
|
|||||||
})),
|
})),
|
||||||
GetCommunication().registerMessageEvent(new UserInfoEvent(this.onUserInfoEvent.bind(this))),
|
GetCommunication().registerMessageEvent(new UserInfoEvent(this.onUserInfoEvent.bind(this))),
|
||||||
GetCommunication().registerMessageEvent(new UserPermissionsEvent(this.onUserPermissionsEvent.bind(this))),
|
GetCommunication().registerMessageEvent(new UserPermissionsEvent(this.onUserPermissionsEvent.bind(this))),
|
||||||
|
GetCommunication().registerMessageEvent(new UserPermissionsMapEvent(this.onUserPermissionsMapEvent.bind(this))),
|
||||||
GetCommunication().registerMessageEvent(new AvailabilityStatusMessageEvent(this.onAvailabilityStatusMessageEvent.bind(this))),
|
GetCommunication().registerMessageEvent(new AvailabilityStatusMessageEvent(this.onAvailabilityStatusMessageEvent.bind(this))),
|
||||||
GetCommunication().registerMessageEvent(new PetScratchFailedMessageEvent(this.onPetRespectFailed.bind(this))),
|
GetCommunication().registerMessageEvent(new PetScratchFailedMessageEvent(this.onPetRespectFailed.bind(this))),
|
||||||
GetCommunication().registerMessageEvent(new ChangeUserNameResultMessageEvent(this.onChangeNameUpdateEvent.bind(this))),
|
GetCommunication().registerMessageEvent(new ChangeUserNameResultMessageEvent(this.onChangeNameUpdateEvent.bind(this))),
|
||||||
@@ -263,6 +297,17 @@ export class SessionDataManager implements ISessionDataManager
|
|||||||
this.invalidateUserDataSnapshot();
|
this.invalidateUserDataSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onUserPermissionsMapEvent(event: UserPermissionsMapEvent): void
|
||||||
|
{
|
||||||
|
if(!event || !event.connection) return;
|
||||||
|
|
||||||
|
// 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(event.getParser().permissions);
|
||||||
|
|
||||||
|
this.invalidatePermissionsSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
private onAvailabilityStatusMessageEvent(event: AvailabilityStatusMessageEvent): void
|
private onAvailabilityStatusMessageEvent(event: AvailabilityStatusMessageEvent): void
|
||||||
{
|
{
|
||||||
if(!event || !event.connection) return;
|
if(!event || !event.connection) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user