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:
simoleo89
2026-05-19 18:59:35 +02:00
parent 87e67d58df
commit 159c5eb6e8
9 changed files with 130 additions and 2 deletions
+46 -1
View File
@@ -1,5 +1,5 @@
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 { GetLocalizationManager } from '@nitrots/localization';
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 _permissions: Map<string, number> = new Map();
private _permissionsSnapshot: ReadonlyMap<string, number> | null = null;
constructor()
{
this.resetUserInfo();
@@ -71,6 +74,36 @@ export class SessionDataManager implements ISessionDataManager
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;
@@ -128,6 +161,7 @@ export class SessionDataManager implements ISessionDataManager
})),
GetCommunication().registerMessageEvent(new UserInfoEvent(this.onUserInfoEvent.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 PetScratchFailedMessageEvent(this.onPetRespectFailed.bind(this))),
GetCommunication().registerMessageEvent(new ChangeUserNameResultMessageEvent(this.onChangeNameUpdateEvent.bind(this))),
@@ -263,6 +297,17 @@ export class SessionDataManager implements ISessionDataManager
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
{
if(!event || !event.connection) return;