feat(session): rank metadata in UserPermissions snapshot

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.
This commit is contained in:
simoleo89
2026-05-19 18:37:57 +02:00
parent ce561bd5b3
commit 87e67d58df
3 changed files with 77 additions and 4 deletions
@@ -19,4 +19,13 @@ export interface IUserDataSnapshot
isSystemShutdown: boolean;
uiFlags: number;
tags: ReadonlyArray<string>;
// 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;
}
@@ -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;
}
}
+21 -4
View File
@@ -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<string[]>([...this._tags]) as ReadonlyArray<string>
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;
@@ -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();
}