refactor(session): fold permission map into UserPermissionsEvent

Drop the separate UserPermissionsMapEvent / UserPermissionsMapParser
and the IncomingHeader.USER_PERMISSIONS_MAP = 10070 registration —
the resolved permission map now rides on the existing
UserPermissionsEvent as a third optional trailing block, after the
rank metadata one. Same wire data, one fewer packet, one fewer
event registration, one fewer handler.

Wire layout (UserPermissionsEvent / header 411):
  int     clubLevel
  int     securityLevel
  bool    isAmbassador
  --- rank metadata (Arcturus ≥ 4.2.10) ---
  int     rankId
  string  rankName
  string  rankBadge
  string  rankPrefix
  string  rankPrefixColor
  --- resolved permission map (Arcturus ≥ 4.2.10) ---
  int     count
  loop:   string permission_key + int value     (1=ALLOWED, 2=ROOM_OWNER)

Both trailing blocks are guarded by `bytesAvailable` in
UserPermissionsParser so older emulators that don't append them
still parse cleanly.

SessionDataManager.onUserPermissionsEvent is now the single handler:
- updates clubLevel/securityLevel/isAmbassador/rank* AND _permissions;
- invalidates BOTH the user-data snapshot and the permissions
  snapshot (dispatching the two distinct
  NitroEventType.SESSION_DATA_UPDATED / USER_PERMISSIONS_UPDATED
  events).

The two distinct invalidation events stay so React consumers can
subscribe granularly — useHasPermission(key) only triggers on a real
permission map flip, not on every session-data bump.

Companion Arcturus change (feat/react19-emu-update) folds
UserPermissionsMapComposer into UserPermissionsComposer and removes
the second sendResponse in HabboManager.setRank +
SecureLoginEvent.

Verification: yarn compile:fast clean, vitest 138/138.
This commit is contained in:
simoleo89
2026-05-19 19:39:49 +02:00
parent 159c5eb6e8
commit 221f186d61
8 changed files with 42 additions and 91 deletions
@@ -1,54 +0,0 @@
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;
}
}
@@ -10,6 +10,7 @@ export class UserPermissionsParser implements IMessageParser
private _rankBadge: string;
private _rankPrefix: string;
private _rankPrefixColor: string;
private _permissions: Map<string, number> = new Map();
public flush(): boolean
{
@@ -21,6 +22,7 @@ export class UserPermissionsParser implements IMessageParser
this._rankBadge = '';
this._rankPrefix = '';
this._rankPrefixColor = '';
this._permissions = new Map();
return true;
}
@@ -34,9 +36,9 @@ export class UserPermissionsParser implements IMessageParser
this._isAmbassador = wrapper.readBoolean();
// Optional trailing block (Arcturus-Morningstar-Extended ≥ 4.2.10):
// rank metadata appended in a backward-compatible way. Older
// emulators don't write these bytes so we keep the defaults
// from flush().
// rank metadata + resolved permission map appended in a
// backward-compatible way. Older emulators don't write these
// bytes so we keep the defaults from flush().
if(!wrapper.bytesAvailable) return true;
this._rankId = wrapper.readInt();
@@ -45,6 +47,25 @@ export class UserPermissionsParser implements IMessageParser
this._rankPrefix = wrapper.readString();
this._rankPrefixColor = wrapper.readString();
if(!wrapper.bytesAvailable) return true;
// Resolved permission map: int count + (string key, int value)*.
// value 1 = ALLOWED, 2 = ROOM_OWNER. Only entries with
// PermissionSetting != DISALLOWED are sent; absence on the client
// means "no" (useHasPermission(key) returns false).
const count = wrapper.readInt();
const permissions = new Map<string, number>();
for(let i = 0; i < count; i++)
{
const key = wrapper.readString();
const value = wrapper.readInt();
permissions.set(key, value);
}
this._permissions = permissions;
return true;
}
@@ -87,4 +108,9 @@ export class UserPermissionsParser implements IMessageParser
{
return this._rankPrefixColor;
}
public get permissions(): ReadonlyMap<string, number>
{
return this._permissions;
}
}
@@ -1,2 +1 @@
export * from './UserPermissionsMapParser';
export * from './UserPermissionsParser';