From 87e67d58df3400fece85ad155e79c7bbf54036b9 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 19 May 2026 18:37:57 +0200 Subject: [PATCH] feat(session): rank metadata in UserPermissions snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the `UserPermissionsEvent` parser and `IUserDataSnapshot` with rank metadata mirrored from the Arcturus `permission_ranks` table: rankId, rankName, rankBadge, rankPrefix, rankPrefixColor. The new fields are appended to the wire payload AFTER the existing [clubLevel, securityLevel, isAmbassador] triple. The parser guards the trailing block with `if(!wrapper.bytesAvailable) return true;` so older emulators (that don't write the extension) keep working — the snapshot just exposes the defaults (rankId=0, empty strings) in that case. `SessionDataManager.onUserPermissionsEvent` stores the values; the snapshot builder includes them; existing `invalidateUserDataSnapshot()` semantics flow through unchanged, so a runtime promote/demote (via `HabboManager.setRank` → `UserPermissionsComposer`) auto-flips the React-side `useUserRank()` / `useHasRankLevel()` / `useIsRank()` consumers in the Nitro-V3 client. Companion changes: - Arcturus-Morningstar-Extended: `UserPermissionsComposer.composeInternal()` now appends the 5 extra fields (pending operator commit; see ../Arcturus-Morningstar-Extended/Emulator/src/main/java/ com/eu/habbo/messages/outgoing/users/UserPermissionsComposer.java). - Nitro-V3: `useSessionSnapshots.ts` exposes the new family (useUserRank / useHasRankLevel / useIsRank), replacing the SecurityLevel-based wrappers (useIsModerator etc.) that hardcoded the renderer enum names — those don't match the operator's `permission_ranks.rank_name` column. Verification: tsgo clean, vitest 138/138. --- .../src/nitro/session/IUserDataSnapshot.ts | 9 ++++ .../user/access/UserPermissionsParser.ts | 47 +++++++++++++++++++ packages/session/src/SessionDataManager.ts | 25 ++++++++-- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/packages/api/src/nitro/session/IUserDataSnapshot.ts b/packages/api/src/nitro/session/IUserDataSnapshot.ts index 84c971f..9d7a24f 100644 --- a/packages/api/src/nitro/session/IUserDataSnapshot.ts +++ b/packages/api/src/nitro/session/IUserDataSnapshot.ts @@ -19,4 +19,13 @@ export interface IUserDataSnapshot isSystemShutdown: boolean; uiFlags: number; tags: ReadonlyArray; + // Rank metadata mirrored from `permission_ranks` (Arcturus emulator + // ≥ 4.2.10 ships these via `UserPermissionsComposer`). Older + // emulators leave them at the defaults (rankId=0, empty strings) + // because the renderer-side parser short-circuits on bytesAvailable. + rankId: number; + rankName: string; + rankBadge: string; + rankPrefix: string; + rankPrefixColor: string; } diff --git a/packages/communication/src/messages/parser/user/access/UserPermissionsParser.ts b/packages/communication/src/messages/parser/user/access/UserPermissionsParser.ts index 4f40f45..20d5fb6 100644 --- a/packages/communication/src/messages/parser/user/access/UserPermissionsParser.ts +++ b/packages/communication/src/messages/parser/user/access/UserPermissionsParser.ts @@ -5,12 +5,22 @@ export class UserPermissionsParser implements IMessageParser private _clubLevel: number; private _securityLevel: number; private _isAmbassador: boolean; + private _rankId: number; + private _rankName: string; + private _rankBadge: string; + private _rankPrefix: string; + private _rankPrefixColor: string; public flush(): boolean { this._clubLevel = 0; this._securityLevel = 0; this._isAmbassador = false; + this._rankId = 0; + this._rankName = ''; + this._rankBadge = ''; + this._rankPrefix = ''; + this._rankPrefixColor = ''; return true; } @@ -23,6 +33,18 @@ export class UserPermissionsParser implements IMessageParser this._securityLevel = wrapper.readInt(); 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(). + if(!wrapper.bytesAvailable) return true; + + this._rankId = wrapper.readInt(); + this._rankName = wrapper.readString(); + this._rankBadge = wrapper.readString(); + this._rankPrefix = wrapper.readString(); + this._rankPrefixColor = wrapper.readString(); + return true; } @@ -40,4 +62,29 @@ export class UserPermissionsParser implements IMessageParser { return this._isAmbassador; } + + public get rankId(): number + { + return this._rankId; + } + + public get rankName(): string + { + return this._rankName; + } + + public get rankBadge(): string + { + return this._rankBadge; + } + + public get rankPrefix(): string + { + return this._rankPrefix; + } + + public get rankPrefixColor(): string + { + return this._rankPrefixColor; + } } diff --git a/packages/session/src/SessionDataManager.ts b/packages/session/src/SessionDataManager.ts index c2f15e9..c6c8b9b 100644 --- a/packages/session/src/SessionDataManager.ts +++ b/packages/session/src/SessionDataManager.ts @@ -32,6 +32,11 @@ export class SessionDataManager implements ISessionDataManager 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; @@ -89,7 +94,12 @@ export class SessionDataManager implements ISessionDataManager isSystemOpen: this._systemOpen, isSystemShutdown: this._systemShutdown, uiFlags: this._uiFlags, - tags: Object.freeze([...this._tags]) as ReadonlyArray + tags: Object.freeze([...this._tags]) as ReadonlyArray, + rankId: this._rankId, + rankName: this._rankName, + rankBadge: this._rankBadge, + rankPrefix: this._rankPrefix, + rankPrefixColor: this._rankPrefixColor }); return this._userDataSnapshot; @@ -239,9 +249,16 @@ export class SessionDataManager implements ISessionDataManager { if(!event || !event.connection) return; - this._clubLevel = event.getParser().clubLevel; - this._securityLevel = event.getParser().securityLevel; - this._isAmbassador = event.getParser().isAmbassador; + 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; this.invalidateUserDataSnapshot(); }