Adds two new packets:
* Incoming 9102 HousekeepingBanUserEvent — reads (userId, reason,
hours). Unlike ModToolSanctionBanEvent which only accepts the four
fixed Habbo-protocol banType buckets (18h / 7d / 30d / 100y), this
one converts the hours arg straight to seconds and feeds them into
ModToolManager.ban with ModToolBanType.ACCOUNT and cfhTopic=0.
Duration is clamped to 100 years to keep it inside `int` range.
* Outgoing 9201 HousekeepingActionResultComposer — generic ack
for any HK action (ban / mute / kick / give-credits / room-close /
…). Wire shape is (actionKey, ok, actionId, message). The
actionKey lets the client filter multiple in-flight actions to
the right Promise via `accept`, so concurrent admin operations
don't cross-resolve.
actionId here is the target user id because ModToolBan doesn't
expose the `bans` autoinc id on the object — there's a TODO to swap
this for a dedicated housekeeping_log row id once that table goes in.
Same ACC_HOUSEKEEPING permission gate as the find-user packets, so
operators only need to grant the permission once.
`mvn compile` clean.
Adds Incoming 9101 HousekeepingFindUserByIdEvent, which replies on
the existing HousekeepingUserDetailComposer (Outgoing 9200) — the
composer is shape-agnostic about how the lookup was issued, so the
two find-* handlers share the same response packet.
The by-id handler uses HabboManager.getHabboInfo(int) directly, which
already covers both the online (in-memory hashmap) and offline (SQL
LIMIT 1 on users) branches in one call. The by-name path still has
to do online + offline manually because the equivalent String overload
doesn't exist as an instance method, only as a static.
Also introduces Permission.ACC_HOUSEKEEPING ("acc_housekeeping") so
the in-client housekeeping panel doesn't piggyback on ACC_SUPPORTTOOL.
Both HK handlers now gate on the new permission; the toolbar UI on
the client side was already checking `acc_housekeeping`, so this
closes the loop. Operators must add the permission to
permission_definitions for the desired rank:
INSERT INTO permission_definitions
(permission_key, max_value, comment,
rank_1, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7)
VALUES
('acc_housekeeping', 1,
'Allows access to the in-client Housekeeping admin panel ...',
0, 0, 0, 0, 0, 0, 1)
ON DUPLICATE KEY UPDATE rank_7 = 1, comment = VALUES(comment);
`mvn package` clean (Habbo-4.2.12-jar-with-dependencies.jar).
First wire-level packet for the in-client housekeeping admin panel.
Adds an incoming handler (Incoming 9100) that resolves a username to
a HabboInfo (online via the HabboManager hashmap, offline via the
users-table SQL fallback) and replies with HousekeepingUserDetail
(Outgoing 9200) containing id / username / motto / look / rank / rank
name / online / lastOnline / credits / duckets / diamonds / email /
ipLogin / isBanned. Active-mute and active-trade-lock are written as
trailing booleans (currently false) so the renderer parser can pick
them up later behind a bytesAvailable guard once those manager APIs
are surfaced offline-side.
Permission gate is ACC_SUPPORTTOOL — same one ModTools already uses.
Avoids adding a new column to the permissions table on this slice; a
dedicated ACC_HOUSEKEEPING permission can be introduced later when
the destructive HK operations (give-credits, delete-room, reset-pwd)
go in.
Reserves the 9100..9199 / 9200..9299 ID blocks for the rest of the
HK packet surface (search-prefix, find-by-id, ban/mute/kick with
arbitrary duration, room actions, economy, catalog admin, dashboard
aggregate, audit log read).
`mvn compile` clean on Habbo 4.2.12.
The User Info panel reads its CFH / Cautions / Bans / Trade locks
counters from `users_settings.cfh_send` / `cfh_warnings` / `cfh_bans`
(via totalBans) / `tradelock_amount`. Historically only `cfh_send`
was ever incremented (by `InsertModToolIssue` on CFH submit), so a
user could accumulate any number of Alert / Mute / Ban / TradeLock
sanctions without the stats reflecting it — every panel showed all
zeros even on accounts with a long sanction history visible in the
modern `sanctions` table.
The two systems aren't going away — `ModToolSanctions` (the modern
one) tracks individual sanction events with probation timestamps,
while the legacy `users_settings.cfh_*` columns are flat counters
the ModTool UI displays. Both need to stay in sync.
Wire them up:
`ModToolManager.bumpUserSettingCounter(userId, column)`
Static helper, column-whitelisted (`cfh_warnings` / `cfh_bans` /
`cfh_abusive` / `tradelock_amount`) to keep the dynamic SQL safe.
Single UPDATE per call; SQL exceptions logged, never thrown.
`ModToolSanctionAlertEvent`, `ModToolSanctionMuteEvent` → bump
`cfh_warnings`. Mute is a punitive but non-banning action; both it
and Alert are recorded as a warning on the legacy counter, matching
what the Cautions stat card represents in the new UI.
`ModToolSanctionBanEvent` → bump `cfh_bans`. The `totalBans` field
the composer sends ALREADY counts entries in the `bans` table, so
the wire field reflects reality immediately — this column bump is
a defensive duplicate so any code that reads `users_settings.cfh_bans`
directly (e.g. plugin scripts, CMS dashboards) stays in sync.
`ModToolSanctionTradeLockEvent` → bump `tradelock_amount`. Mirrors
what `AllowTradingCommand` already does for the command-line path.
`ModToolManager.closeTicketAsAbusive` → bump `cfh_abusive` for the
REPORTER (issue.senderId), not the reported user. The Abusive
counter measures false reports filed by the user, so it belongs on
whoever opened the CFH that got closed as abusive.
No client-side changes — counter columns are unchanged, only the
write paths are.
ModToolUserInfoComposer used to send three trailing fields hardcoded
to empty/zero — the client rendered placeholders for every user, on
every panel open:
appendString(""); // Trading lock expiry timestamp
appendString(""); // Last Purchase Timestamp
appendInt(0); // Number of account bans
These are useful moderation signals and the data already exists in
the live tables. Wire them up.
Last Purchase
Query MAX(timestamp) FROM logs_shop_purchases WHERE user_id = ?.
Returns the most recent purchase epoch. Rendered as yyyy-MM-dd HH:mm.
Empty when the user has never bought anything (the query returns
NULL → getInt returns 0 → formatUnixTimestamp emits "").
Trading lock expiry
Query MAX(trade_locked_until) FROM sanctions WHERE habbo_id = ? AND
trade_locked_until > <now>. Latest ACTIVE lock only — past entries
don't count. Same yyyy-MM-dd HH:mm format. Empty when no active
lock.
Identity related bans
Count of DISTINCT other user accounts that have a ban entry against
the same machine_id as the target. Self is excluded since the target's
own bans already show up in banCount. An empty machine_id (default
'') short-circuits to 0 so we never match accounts whose machine
fingerprint was never recorded.
The existing totalBans counter is extracted into a helper alongside
the three new ones — cleaner than the inline try-catch tower it used
to live in, same behaviour.
Format choice yyyy-MM-dd HH:mm matches the timestamp shown elsewhere
in moderation UI; both string fields go through the same formatter so
the empty case stays consistent (empty string, not "1970-01-01...").
No client-side changes needed — ModeratorUserInfoData already parses
both strings and the int, and the React ModToolsUserView already
renders them. They were just always empty before.
The default permission_definitions seed for acc_supporttool used the
pattern (0, 1, 1, 1, 1, 0, 1) across rank_1..rank_7 — apparently
shifted by two columns:
* rank_2 (VIP) and rank_3 (X) had ALLOWED. With acc_supporttool=1
the SecureLoginEvent path sends ModeratorInitMessageEvent on
login, which makes the React client surface the ModTools toolbar
button and let the user open room/user info windows. The actual
sanction endpoints (ModToolSanctionBanEvent, ModToolWarnEvent,
…) still gate on ACC_SUPPORTTOOL so a VIP cannot actually take
moderator action — but they can request user info, room info
and chatlogs they have no business reading.
* rank_6 (Super Mod) was DISALLOWED, which is obviously not what
the name says.
Corrected pattern: (0, 0, 0, 1, 1, 1, 1) — Support (4), Moderator
(5), Super Mod (6), Administrator (7). Matches the convention used
by the other staff-only acc_modtool_* keys.
Two changes:
- Default Database/FullDatabase.sql: fix the seed for fresh
installs.
- Database Updates/004_fix_acc_supporttool_rank.sql: idempotent
UPDATE to realign existing deployments.
Found by user report: a rank-2 (VIP) account on the live retro had
the ModTools button visible in the toolbar after login.
`PermissionsManager.reload()` rebuilds the rank table from
`permission_ranks` + `permission_definitions`, but every Habbo
currently online still holds a reference to the OLD `Rank` object
on `HabboInfo.rank`. Server-side `hasPermission()` therefore keeps
returning stale results, and any Nitro client that reads permission
state from the wire keeps gating UI on the map shipped at login
— until a relogin or `:give_rank` forces a per-user refresh.
Extend the existing `UpdatePermissionsCommand` so after `reload()`
it:
1. Iterates the online Habbos via `HabboManager.getOnlineHabbos()`.
2. Re-binds each one's `HabboInfo.rank` to the FRESH `Rank` object
returned by `PermissionsManager.getRank(currentRankId)`. Falls
back to rank 1 if the admin deleted the rank from
`permission_ranks` between sessions, so the user is never left
with a null `Rank` reference.
3. Sends a fresh `UserPermissionsComposer` to each client.
With the companion composer extension PR also merged, this
broadcasts the rank metadata + resolved permission map runtime —
the Nitro React-side `useHasPermission(key)` / `useUserRank()`
consumers re-render against the freshly-loaded tables without
requiring an F5.
The whisper feedback now reports how many connected users were
refreshed, useful for ops feedback after a large `permission_ranks`
edit.
Defensive null guards on habbo / habboInfo / client survive
transient state during the broadcast (e.g. a user disconnecting
mid-iteration).
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`).
Pairs with the CMS-side change introducing auth_ticket_expires_at (60s
expiry written on every ticket issuance). Without an emulator-side
verification the column was advisory only — this commit gates every
SELECT that resolves a user by auth_ticket on
auth_ticket = ?
AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW())
The NULL branch preserves backward-compatibility: CMS deployments that
do not yet populate the column keep working exactly like before
(every ticket passes the WHERE clause as soon as auth_ticket matches),
and the TTL takes effect automatically the moment a CMS starts writing
the expiry value.
Five SELECTs touched:
- SessionEndpoints.java (cms-issued SSO + remember-token flow)
- HabboManager.loadHabbo (game client login by ticket)
- SecureLoginEvent (legacy handshake path)
DB schema delivered both ways:
- Database Updates/Own_Database_RunFirst/020_auth_ticket_ttl.sql:
idempotent ALTER, skips if column already present (information_schema
guard so re-running the bundle is safe).
- Default Database/FullDatabase.sql: column added to the `users` table
definition for fresh installs.
Bumps the emulator version to 4.2.7.
Aligns the :furnidata in-game admin command with the split-aware gamedata
layout shipped by the Nitro V3 client. FurniDataManager now resolves the
furnidata source through three accepted shapes:
- legacy single-file path (filesystem or http URL ending in .json/.json5)
- split-mode directory (URL ending with '/') — walks core/custom/seasonal
tiers via manifest.json5 files and merges by item id, with later tiers
overriding earlier ones (same semantics as the client-side loader)
- fallback to furni.editor.asset.base.path when the renderer config is
missing or contains an unresolved placeholder
Adds a small JSON5 sanitiser (stripJson5) that removes line and block
comments and trailing commas before handing the content to Gson, so both
the renderer config and the split-mode files can be JSON or JSON5
without pulling in a JSON5 dependency. String contents are preserved
verbatim — comment-looking substrings inside strings (e.g. URLs) are
not touched.
Bumps the emulator version to 4.2.6.