From 8b51be49403b73d5fdeea24b80ec558b3b92eca8 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 19 May 2026 20:18:31 +0200 Subject: [PATCH] feat(messages): extend UserPermissionsComposer with rank metadata + resolved permission map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backward-compatible wire extension of `UserPermissionsComposer` (header 411) that lets Nitro clients display per-deployment rank info and drive UI gates against the actual `permission_definitions` table instead of hardcoded SecurityLevel constants. Wire layout after this change (each trailing block is guarded by `bytesAvailable` on the client side so older Nitro builds keep parsing the prefix and stop): int clubLevel int rank.level // mapped to securityLevel on the client bool isAmbassador // existing ACC_AMBASSADOR flag --- new: rank metadata --- int rank.id string rank.name // permission_ranks.rank_name string rank.badge string rank.prefix string rank.prefixColor --- new: resolved permission map --- int count loop: string permission_key + int value // 1 = ALLOWED, 2 = ROOM_OWNER The permission map is the union of: * Rank entries whose `PermissionSetting != DISALLOWED` (value 1 for ALLOWED, 2 for ROOM_OWNER). * For every rank-DISALLOWED key, each installed `HabboPlugin.hasPermission(habbo, key)` is consulted; if any plugin grants the permission, the key lands on the wire with value 1 (plugins do not have a ROOM_OWNER concept). Iterating `rank.getPermissions().keySet()` covers every key in `permission_definitions` because `PermissionsManager.loadPermissionsNormalized()` calls `rank.setPermission(key, ...)` for every row of the table — including DISALLOWED ones. Custom keys a plugin invents that are not in `permission_definitions` stay invisible (there is no enumeration API on `HabboPlugin` to discover them); this is a rare case documented in the class-level Javadoc. The result is a client-side permission map whose semantics match exactly what `PermissionsManager.hasPermission(habbo, key)` would return server-side — including plugin-granted permissions, which were invisible to the client before. Performance: at login the loop is O(N keys × P plugins), with N ≈ 200 (size of permission_definitions) and P typically 1-5. `HabboPlugin.hasPermission` is O(1) hashset lookups in real-world implementations. Sub-millisecond at login, and the composer is only sent at login + `HabboManager.setRank` + `:update_permissions` broadcast. Backward compatibility: all new fields are appended in tail position with `bytesAvailable` guards on the parser side, so: * existing Nitro clients keep parsing only the prefix and ignore the trailing bytes (no error, no behavior change); * new Nitro clients with the matching parser extension expose the extra data via `IUserDataSnapshot` snapshot getters and the React-side `useUserRank()` / `useHasPermission(key)` / `useUserPermissions()` hooks (see companion PRs on `duckietm/Nitro_Render_V3` and `duckietm/Nitro-V3`). --- .../users/UserPermissionsComposer.java | 107 +++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserPermissionsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserPermissionsComposer.java index f67c2bfa..9e89af3d 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserPermissionsComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserPermissionsComposer.java @@ -1,11 +1,57 @@ package com.eu.habbo.messages.outgoing.users; +import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.permissions.PermissionSetting; +import com.eu.habbo.habbohotel.permissions.Rank; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.Outgoing; +import com.eu.habbo.plugin.HabboPlugin; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Sends the full per-user permission state to the connected client. + * + * Wire layout (each trailing block is guarded by `bytesAvailable` on + * the client so older Nitro builds keep parsing the prefix and stop): + * + * int clubLevel + * int rank.level // mapped to securityLevel on the client + * bool isAmbassador // legacy ACC_AMBASSADOR flag + * --- rank metadata (Arcturus ≥ 4.2.10) --- + * int rank.id + * string rank.name // permission_ranks.rank_name + * string rank.badge + * string rank.prefix + * string rank.prefixColor + * --- resolved permission map (Arcturus ≥ 4.2.10) --- + * int count + * loop: string permission_key + int value // 1 = ALLOWED, 2 = ROOM_OWNER + * + * The map is the union of: + * • rank entries with `PermissionSetting != DISALLOWED` — same data + * `Rank.hasPermission(key, isRoomOwner)` reads server-side. + * • plugin grants — for each key the rank doesn't allow, every + * installed `HabboPlugin.hasPermission(habbo, key)` is consulted; + * if any plugin grants it, the key lands on the wire with value 1 + * (plugins don't have a ROOM_OWNER concept). + * + * The React-side `useHasPermission(key)` / `useUserPermissions()` + * consumers read the map directly so UI gates follow the same + * semantics as `PermissionsManager.hasPermission(habbo, key)` + * server-side — including plugin-granted permissions, which were + * invisible to the client before this commit. + * + * Two send points: + * 1. End of `SecureLoginEvent` — client receives the full state once. + * 2. Inside `HabboManager.setRank` — runtime promote/demote refresh. + * 3. Inside `UpdatePermissionsCommand` — broadcast after + * `:update_permissions` reloads the tables at runtime. + */ public class UserPermissionsComposer extends MessageComposer { private final int clubLevel; @@ -20,11 +66,70 @@ public class UserPermissionsComposer extends MessageComposer { protected ServerMessage composeInternal() { this.response.init(Outgoing.UserPermissionsComposer); this.response.appendInt(this.clubLevel); - this.response.appendInt(this.habbo.getHabboInfo().getRank().getLevel()); + + Rank rank = this.habbo.getHabboInfo().getRank(); + + this.response.appendInt(rank.getLevel()); this.response.appendBoolean(this.habbo.hasPermission(Permission.ACC_AMBASSADOR)); + + // Rank metadata + this.response.appendInt(rank.getId()); + this.response.appendString(rank.getName()); + this.response.appendString(rank.getBadge()); + this.response.appendString(rank.getPrefix()); + this.response.appendString(rank.getPrefixColor()); + + // Build the resolved permission map. Walk rank.getPermissions() + // (Rank.permissions has every row from permission_definitions + // because PermissionsManager.loadPermissionsNormalized() calls + // rank.setPermission(key, …) for every key, including DISALLOWED + // ones) and emit the final value per key: + // ALLOWED → 1 + // ROOM_OWNER → 2 + // DISALLOWED + plugin yes → 1 + // DISALLOWED + plugin no → omit + // + // LinkedHashMap preserves the alphabetical order that the rank + // table was populated with, which is helpful for snapshotting + // and grep'ing wire dumps. + Map rankPermissions = rank.getPermissions(); + Map resolved = new LinkedHashMap<>(rankPermissions.size()); + + for (Map.Entry entry : rankPermissions.entrySet()) { + String key = entry.getKey(); + Permission rankPerm = entry.getValue(); + + if (rankPerm.setting == PermissionSetting.ALLOWED) { + resolved.put(key, 1); + } else if (rankPerm.setting == PermissionSetting.ROOM_OWNER) { + resolved.put(key, 2); + } else if (this.anyPluginGrants(key)) { + resolved.put(key, 1); + } + } + + // Plugins may also grant CUSTOM keys that aren't in + // permission_definitions — rare but legal. There's no enumeration + // API on HabboPlugin to discover them, so they stay invisible + // here. Document the limitation rather than over-engineer. + + this.response.appendInt(resolved.size()); + + for (Map.Entry entry : resolved.entrySet()) { + this.response.appendString(entry.getKey()); + this.response.appendInt(entry.getValue()); + } + return this.response; } + private boolean anyPluginGrants(String key) { + for (HabboPlugin plugin : Emulator.getPluginManager().getPlugins()) { + if (plugin.hasPermission(this.habbo, key)) return true; + } + return false; + } + public int getClubLevel() { return clubLevel; }