Compare commits

...

43 Commits

Author SHA1 Message Date
github-actions[bot] 83d418e712 🆙 Bump version to 4.2.17 [skip ci] 2026-05-26 08:04:04 +00:00
DuckieTM 38d05e8a06 Merge pull request #122 from duckietm/dev
Dev
2026-05-26 10:03:05 +02:00
duckietm c83a3bad8e 🆙 Gift Updates 2026-05-26 09:58:22 +02:00
DuckieTM 0bd23ad244 Merge pull request #121 from Lorenzune/merge-duckie-main-2026-05-06
Improve emulator monitoring and wired stability safeguards
2026-05-25 18:41:50 +02:00
Lorenzune d8f74b2477 Improve emulator monitoring and wired stability safeguards 2026-05-25 10:10:57 +02:00
github-actions[bot] 67503aeb2a 🆙 Bump version to 4.2.16 [skip ci] 2026-05-22 09:04:30 +00:00
DuckieTM b206b32748 Merge pull request #119 from duckietm/dev
🆙 Catalog Editor, now you can also edit the text1
2026-05-22 11:03:35 +02:00
duckietm ad60861a3f 🆙 Catalog Editor, now you can also edit the text1 2026-05-22 11:03:17 +02:00
github-actions[bot] b77290f5e7 🆙 Bump version to 4.2.15 [skip ci] 2026-05-21 15:03:23 +00:00
DuckieTM b14730d37f Merge pull request #118 from duckietm/dev
Dev
2026-05-21 17:02:19 +02:00
duckietm 9126396973 🆙 Fix Catalog Edit 2026-05-21 17:01:56 +02:00
duckietm d321ff3b85 Update 003_live_required_schema.sql 2026-05-21 15:54:10 +02:00
duckietm 7f38a25eef 🆙 Small SQL update 2026-05-21 15:44:30 +02:00
github-actions[bot] 4820ab15f3 🆙 Bump version to 4.2.14 [skip ci] 2026-05-21 12:03:07 +00:00
DuckieTM 8d989e7a19 Merge pull request #117 from duckietm/dev
🆕 Redesign of HC Club buy, now also give as gift
2026-05-21 14:02:14 +02:00
duckietm 1f7ec96e1c 🆕 Redesign of HC Club buy, now also give as gift 2026-05-21 14:01:57 +02:00
github-actions[bot] 969f177108 🆙 Bump version to 4.2.13 [skip ci] 2026-05-21 07:02:08 +00:00
DuckieTM e485c2747c Merge pull request #116 from duckietm/dev
Dev
2026-05-21 09:01:07 +02:00
DuckieTM d99a51899b Merge pull request #115 from simoleo89/fix/modtool-counter-bumps
fix(modtool): bump users_settings counters on every sanction
2026-05-21 07:40:49 +02:00
DuckieTM 29677a19be Merge pull request #114 from simoleo89/feat/modtool-user-info-real-data
feat(modtool): populate lastPurchase / tradeLockExpiry / identityBans
2026-05-21 07:40:34 +02:00
DuckieTM 21ee36e089 Merge pull request #113 from simoleo89/fix/acc-supporttool-rank-pattern
fix(permissions): acc_supporttool incorrectly granted to VIP, denied to Super Mod
2026-05-21 07:40:19 +02:00
simoleo89 4e47dbee16 fix(modtool): bump users_settings counters on every sanction
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.
2026-05-20 21:54:07 +02:00
simoleo89 e7ba4d0926 feat(modtool): populate lastPurchase / tradeLockExpiry / identityBans
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.
2026-05-20 21:32:10 +02:00
simoleo89 67d2f52f64 fix(permissions): acc_supporttool incorrectly granted to VIP, denied to Super Mod
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.
2026-05-20 20:34:37 +02:00
github-actions[bot] 69d770b65e 🆙 Bump version to 4.2.12 [skip ci] 2026-05-20 09:36:00 +00:00
DuckieTM 2492569e16 Merge pull request #112 from duckietm/dev
🆙 Added the missing pet package for the borderID
2026-05-20 11:34:57 +02:00
duckietm 9c215bea6b 🆙 Added the missing pet package for the borderID 2026-05-20 11:34:33 +02:00
github-actions[bot] 7dc3581f8f 🆙 Bump version to 4.2.11 [skip ci] 2026-05-20 06:25:19 +00:00
DuckieTM f38eb32eee Merge pull request #111 from duckietm/dev
Dev
2026-05-20 08:24:20 +02:00
duckietm 222e356ff0 Merge branch 'dev' of https://github.com/duckietm/Arcturus-Morningstar-Extended into dev 2026-05-20 08:23:31 +02:00
duckietm c8022ccc45 Small update 2026-05-20 08:23:22 +02:00
DuckieTM 9579833775 Merge branch 'main' into dev 2026-05-20 08:20:56 +02:00
DuckieTM 87ad289a54 Merge pull request #110 from simoleo89/pr/update-permissions-broadcast
feat(commands): :update_permissions broadcasts refreshed permissions to every online client
2026-05-20 08:15:19 +02:00
DuckieTM fd28af5f69 Merge pull request #109 from simoleo89/pr/user-permissions-composer-extension
feat(messages): extend UserPermissionsComposer with rank metadata + resolved permission map
2026-05-20 08:15:03 +02:00
DuckieTM 99c938b98f Merge pull request #108 from Lorenzune/merge-duckie-main-2026-05-06
Add badge leaderboard API endpoint
2026-05-20 08:00:20 +02:00
simoleo89 82d90418cd feat(commands): :update_permissions broadcasts refreshed UserPermissionsComposer to every online client
`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).
2026-05-19 20:20:08 +02:00
simoleo89 8b51be4940 feat(messages): extend UserPermissionsComposer with rank metadata + resolved permission map
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`).
2026-05-19 20:18:31 +02:00
duckietm 54259f89bd 🆕 Infostand Borders 2026-05-19 16:57:34 +02:00
Lorenzune 272a9b9f42 Add badge leaderboard API and live schema update 2026-05-19 15:30:47 +02:00
duckietm 9c94402f78 🆙 Small update to the SQL 2026-05-19 11:48:33 +02:00
github-actions[bot] 7271506262 🆙 Bump version to 4.2.10 [skip ci] 2026-05-19 09:42:32 +00:00
DuckieTM 09710fc5d6 Merge pull request #107 from duckietm/dev
SMall fix for CORS
2026-05-19 11:41:32 +02:00
duckietm d958fbc0ab SMall fix for CORS 2026-05-19 11:41:17 +02:00
79 changed files with 4361 additions and 857 deletions
+10
View File
@@ -34,3 +34,13 @@ SET @ddl = IF(@col_exists = 0,
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
UPDATE emulator_settings SET `key`='ws.whitelist' WHERE `key`='websockets.whitelist';
UPDATE emulator_settings SET `key`='ws.host' WHERE `key`='ws.nitro.host';
UPDATE emulator_settings SET `key`='ws.port' WHERE `key`='ws.nitro.port';
INSERT IGNORE INTO emulator_settings (`key`, `value`)
VALUES ('ws.ip.header', 'X-Forwarded-For');
INSERT IGNORE INTO emulator_settings (`key`, `value`)
VALUES ('ws.enabled', 'true');
@@ -0,0 +1,33 @@
ALTER TABLE users
ADD COLUMN `background_border_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_id`;
ALTER TABLE infostand_backgrounds
CHANGE COLUMN `category` `category` ENUM('background', 'stand', 'overlay', 'card', 'border') NOT NULL ;
INSERT IGNORE INTO `infostand_backgrounds` (`id`, `category`, `min_rank`, `is_hc_only`, `is_ambassador_only`) VALUES
(1, 'border', 1, 0, 0),
(2, 'border', 1, 0, 0),
(3, 'border', 1, 0, 0),
(4, 'border', 1, 0, 0),
(5, 'border', 1, 0, 0),
(6, 'border', 1, 0, 0),
(7, 'border', 1, 0, 0),
(8, 'border', 1, 0, 0),
(9, 'border', 1, 0, 0),
(10, 'border', 1, 0, 0),
(11, 'border', 1, 0, 0),
(12, 'border', 1, 0, 0),
(13, 'border', 1, 0, 0),
(14, 'border', 1, 0, 0),
(15, 'border', 1, 0, 0),
(16, 'border', 1, 0, 0),
(17, 'border', 1, 0, 0),
(18, 'border', 1, 0, 0),
(19, 'border', 1, 0, 0),
(20, 'border', 1, 0, 0),
(21, 'border', 1, 0, 0),
(22, 'border', 1, 0, 0),
(23, 'border', 1, 0, 0),
(24, 'border', 1, 0, 0),
(25, 'border', 1, 0, 0);
@@ -0,0 +1,481 @@
-- ============================================================
-- Live required schema
-- ============================================================
-- Consolidated schema for the currently used Nitro/Arcturus live
-- additions. This file intentionally excludes old/unused migration
-- artifacts and dump-only data.
--
-- Scope:
-- - tables/columns currently referenced by Java code
-- - runtime settings required by secure assets/API, login, wired, and UI
-- - safe CREATE IF NOT EXISTS / ADD COLUMN IF NOT EXISTS statements
--
-- Assumes the base Arcturus database already exists.
-- Tested for MariaDB-style syntax used by this project.
-- ============================================================
SET NAMES utf8mb4;
-- ------------------------------------------------------------
-- Core settings support
-- ------------------------------------------------------------
ALTER TABLE `emulator_settings`
ADD COLUMN IF NOT EXISTS `comment` TEXT NULL DEFAULT '' AFTER `value`;
ALTER TABLE catalog_pages
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL', 'BUILDER', 'BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `includes`;
CREATE TABLE IF NOT EXISTS `wired_emulator_settings` (
`key` VARCHAR(255) NOT NULL,
`value` TEXT NOT NULL,
`comment` TEXT NULL DEFAULT '',
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
('crypto.ws.enabled', '0'),
('crypto.ws.signing.enabled', '0'),
('crypto.ws.signing.public_key', ''),
('crypto.ws.signing.private_key', ''),
('login.access.jwt.secret', ''),
('login.remember.duration.days', '30'),
('login.remember.rotate.interval.minutes', '15'),
('login.remember.jwt.secret', ''),
('login.turnstile.enabled', '0'),
('login.turnstile.sitekey', ''),
('login.turnstile.secretkey', ''),
('login.ratelimit.enabled', '1'),
('login.ratelimit.max_attempts', '5'),
('login.ratelimit.window_sec', '60'),
('login.ratelimit.lockout_sec', '120'),
('login.register.enabled', '1'),
('register.max_per_ip', '5'),
('register.default.look', 'hr-100-7.hd-180-1.ch-210-66.lg-270-82.sh-290-80'),
('register.default.motto', 'I love Habbo!'),
('password.reset.url', 'http://localhost/reset-password'),
('smtp.provider', 'own'),
('smtp.host', 'localhost'),
('smtp.port', '587'),
('smtp.username', ''),
('smtp.password', ''),
('smtp.from_address', 'no-reply@example.com'),
('smtp.from_name', 'Habbo Hotel'),
('smtp.use_tls', '1'),
('smtp.use_ssl', '0'),
('new_user_credits', '0'),
('new_user_duckets', '0'),
('new_user_diamonds', '0')
ON DUPLICATE KEY UPDATE `value` = `value`;
INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`) VALUES
('wired.engine.enabled', '1', 'Compatibility flag. The runtime uses the new wired engine.'),
('wired.engine.exclusive', '1', 'Compatibility flag. The runtime uses exclusive wired engine execution.'),
('wired.engine.maxStepsPerStack', '100', 'Maximum internal processing steps allowed for a single wired stack execution.'),
('wired.engine.debug', '0', 'Enable verbose debug logging for the wired engine.'),
('wired.custom.enabled', '0', 'Enable custom legacy wired compatibility behavior.'),
('hotel.wired.furni.selection.count', '5', 'Maximum number of furni that a wired box can store or select.'),
('hotel.wired.max_delay', '20', 'Maximum delay value accepted by wired effects that support delayed execution.'),
('hotel.wired.message.max_length', '512', 'Maximum length of wired message text fields.'),
('wired.effect.teleport.delay', '500', 'Delay in milliseconds used by wired teleport movement.'),
('wired.place.under', '0', 'Allow placing wired furniture underneath other items when room rules permit it.'),
('wired.tick.interval.ms', '50', 'Global wired tick interval in milliseconds.'),
('wired.tick.resolution', '100', 'Legacy wired tick resolution value.'),
('wired.tick.debug', '0', 'Enable verbose logging for the wired tick service.'),
('wired.tick.thread.priority', '6', 'Java thread priority used by the wired tick service.'),
('wired.highscores.displaycount', '25', 'Maximum number of wired highscore entries shown to users.'),
('wired.abuse.max.recursion.depth', '10', 'Maximum recursive wired depth before execution is stopped.'),
('wired.abuse.max.events.per.window', '100', 'Maximum identical wired events allowed inside the abuse rate-limit window.'),
('wired.abuse.rate.limit.window.ms', '10000', 'Wired abuse rate-limit window in milliseconds.'),
('wired.abuse.ban.duration.ms', '600000', 'Temporary wired ban duration after abuse detection.'),
('wired.monitor.usage.window.ms', '1000', 'Rolling window size for wired usage monitoring.'),
('wired.monitor.usage.limit', '1000', 'Maximum wired usage budget in one monitor window.'),
('wired.monitor.delayed.events.limit', '100', 'Maximum delayed wired events queued in one room.'),
('wired.monitor.overload.average.ms', '50', 'Average execution time threshold for overload tracking.'),
('wired.monitor.overload.peak.ms', '150', 'Peak execution time threshold for overload tracking.'),
('wired.monitor.overload.consecutive.windows', '2', 'Consecutive overloaded windows required before logging overload.'),
('wired.monitor.heavy.usage.percent', '70', 'Usage percentage threshold for heavy-room tracking.'),
('wired.monitor.heavy.consecutive.windows', '5', 'Consecutive windows above heavy usage threshold.'),
('wired.monitor.heavy.delayed.percent', '60', 'Delayed queue percentage threshold for heavy-room tracking.')
ON DUPLICATE KEY UPDATE
`value` = VALUES(`value`),
`comment` = VALUES(`comment`);
-- ------------------------------------------------------------
-- Login API, room templates, remember-me, and news
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `password_resets` (
`user_id` INT(11) NOT NULL,
`token` VARCHAR(128) NOT NULL,
`expires_at` TIMESTAMP NOT NULL,
`created_ip` VARCHAR(64) NOT NULL DEFAULT '',
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_password_resets_token` (`token`),
CONSTRAINT `fk_password_resets_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `users_remember_families` (
`family_id` CHAR(36) NOT NULL,
`user_id` INT(11) NOT NULL,
`current_version` INT(11) NOT NULL DEFAULT 1,
`created_at` INT(11) NOT NULL,
`expires_at` INT(11) NOT NULL,
`revoked` TINYINT(1) NOT NULL DEFAULT 0,
`last_ip` VARCHAR(45) NOT NULL DEFAULT '',
PRIMARY KEY (`family_id`),
KEY `idx_users_remember_families_user_id` (`user_id`),
KEY `idx_users_remember_families_expires_at` (`expires_at`),
CONSTRAINT `fk_users_remember_families_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `room_templates` (
`template_id` INT(11) NOT NULL AUTO_INCREMENT,
`title` VARCHAR(128) NOT NULL DEFAULT '',
`description` VARCHAR(256) NOT NULL DEFAULT '',
`thumbnail` VARCHAR(512) NOT NULL DEFAULT '',
`sort_order` INT(11) NOT NULL DEFAULT 0,
`enabled` ENUM('0','1') NOT NULL DEFAULT '1',
`name` VARCHAR(50) NOT NULL DEFAULT '',
`room_description` VARCHAR(250) NOT NULL DEFAULT '',
`model` VARCHAR(100) NOT NULL,
`password` VARCHAR(50) NOT NULL DEFAULT '',
`state` ENUM('open','locked','password','invisible') NOT NULL DEFAULT 'open',
`users_max` INT(11) NOT NULL DEFAULT 25,
`category` INT(11) NOT NULL DEFAULT 0,
`paper_floor` VARCHAR(50) NOT NULL DEFAULT '0.0',
`paper_wall` VARCHAR(50) NOT NULL DEFAULT '0.0',
`paper_landscape` VARCHAR(50) NOT NULL DEFAULT '0.0',
`thickness_wall` INT(11) NOT NULL DEFAULT 0,
`thickness_floor` INT(11) NOT NULL DEFAULT 0,
`moodlight_data` VARCHAR(2048) NOT NULL DEFAULT '',
`override_model` ENUM('0','1') NOT NULL DEFAULT '0',
`trade_mode` INT(2) NOT NULL DEFAULT 2,
`heightmap` MEDIUMTEXT NOT NULL,
`door_x` INT(11) NOT NULL DEFAULT 0,
`door_y` INT(11) NOT NULL DEFAULT 0,
`door_dir` INT(4) NOT NULL DEFAULT 2,
PRIMARY KEY (`template_id`),
KEY `idx_room_templates_enabled_sort` (`enabled`, `sort_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `room_templates_items` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`template_id` INT(11) NOT NULL,
`item_id` INT(11) UNSIGNED NOT NULL,
`wall_pos` VARCHAR(20) NOT NULL DEFAULT '',
`x` INT(11) NOT NULL DEFAULT 0,
`y` INT(11) NOT NULL DEFAULT 0,
`z` DOUBLE(10,6) NOT NULL DEFAULT 0.000000,
`rot` INT(11) NOT NULL DEFAULT 0,
`extra_data` VARCHAR(2096) NOT NULL DEFAULT '',
`wired_data` VARCHAR(4096) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_room_templates_items_template_id` (`template_id`),
KEY `idx_room_templates_items_item_id` (`item_id`),
CONSTRAINT `fk_room_templates_items_template`
FOREIGN KEY (`template_id`) REFERENCES `room_templates` (`template_id`) ON DELETE CASCADE,
CONSTRAINT `fk_room_templates_items_item_base`
FOREIGN KEY (`item_id`) REFERENCES `items_base` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `ui_news` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`title` VARCHAR(150) NOT NULL,
`body` TEXT NOT NULL,
`image` MEDIUMTEXT DEFAULT NULL,
`link_text` VARCHAR(80) NOT NULL DEFAULT '',
`link_url` VARCHAR(255) NOT NULL DEFAULT '',
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_ui_news_enabled_sort` (`enabled`, `sort_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
INSERT INTO `ui_news` (`title`, `body`, `image`, `link_text`, `link_url`, `enabled`, `sort_order`)
SELECT 'Welcome to the Hotel!', 'Catch up on the latest events, updates and competitions happening right now in the hotel.', '', '', '', 1, 0
WHERE NOT EXISTS (SELECT 1 FROM `ui_news`);
-- ------------------------------------------------------------
-- Wired runtime data
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `room_wired_settings` (
`room_id` INT(11) NOT NULL,
`inspect_mask` INT(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can open and inspect Wired in the room. 1=everyone, 2=users with rights, 4=group members, 8=group admins.',
`modify_mask` INT(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can modify Wired in the room. 2=users with rights, 4=group members, 8=group admins.',
PRIMARY KEY (`room_id`),
CONSTRAINT `fk_room_wired_settings_room`
FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `room_wired_variables` (
`room_id` INT(11) NOT NULL,
`variable_item_id` INT(11) NOT NULL,
`value` INT(11) DEFAULT NULL,
`created_at` INT(11) NOT NULL DEFAULT 0,
`updated_at` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`room_id`, `variable_item_id`),
KEY `idx_room_wired_variables_room_item` (`room_id`, `variable_item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `room_user_wired_variables` (
`room_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`variable_item_id` INT(11) NOT NULL,
`value` INT(11) DEFAULT NULL,
`created_at` INT(11) NOT NULL DEFAULT 0,
`updated_at` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`room_id`, `user_id`, `variable_item_id`),
KEY `idx_room_user_wired_variables_room_item` (`room_id`, `variable_item_id`),
KEY `idx_room_user_wired_variables_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `room_furni_wired_variables` (
`room_id` INT(11) NOT NULL,
`furni_id` INT(11) NOT NULL,
`variable_item_id` INT(11) NOT NULL,
`value` INT(11) DEFAULT NULL,
`created_at` INT(11) NOT NULL DEFAULT 0,
`updated_at` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`room_id`, `furni_id`, `variable_item_id`),
KEY `idx_room_furni_wired_variables_room_item` (`room_id`, `variable_item_id`),
KEY `idx_room_furni_wired_variables_furni` (`furni_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ------------------------------------------------------------
-- User customization: prefixes, nick icons, profile backgrounds
-- ------------------------------------------------------------
ALTER TABLE `users`
ADD COLUMN IF NOT EXISTS `background_id` INT(11) NOT NULL DEFAULT 0 AFTER `machine_id`,
ADD COLUMN IF NOT EXISTS `background_stand_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_id`,
ADD COLUMN IF NOT EXISTS `background_overlay_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_stand_id`,
ADD COLUMN IF NOT EXISTS `background_card_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_overlay_id`;
CREATE TABLE IF NOT EXISTS `infostand_backgrounds` (
`id` INT(11) NOT NULL,
`category` ENUM('background','stand','overlay','card') NOT NULL,
`min_rank` INT(11) NOT NULL DEFAULT 0,
`is_hc_only` TINYINT(1) NOT NULL DEFAULT 0,
`is_ambassador_only` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`, `category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO `infostand_backgrounds` (`id`, `category`, `min_rank`, `is_hc_only`, `is_ambassador_only`) VALUES
(0, 'background', 0, 0, 0),
(0, 'stand', 0, 0, 0),
(0, 'overlay', 0, 0, 0),
(0, 'card', 0, 0, 0);
CREATE TABLE IF NOT EXISTS `user_prefixes` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`text` VARCHAR(50) NOT NULL,
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
`icon` VARCHAR(50) NOT NULL DEFAULT '',
`effect` VARCHAR(50) NOT NULL DEFAULT '',
`font` VARCHAR(50) NOT NULL DEFAULT '',
`catalog_prefix_id` INT(11) NOT NULL DEFAULT 0,
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
`points` INT(11) NOT NULL DEFAULT 0,
`points_type` INT(11) NOT NULL DEFAULT 0,
`is_custom` TINYINT(1) NOT NULL DEFAULT 1,
`active` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_user_prefixes_user_id` (`user_id`),
KEY `idx_user_prefixes_user_active` (`user_id`, `active`),
CONSTRAINT `fk_user_prefixes_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `custom_prefixes_catalog` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
`text` VARCHAR(50) NOT NULL,
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
`icon` VARCHAR(50) NOT NULL DEFAULT '',
`effect` VARCHAR(50) NOT NULL DEFAULT '',
`font` VARCHAR(50) NOT NULL DEFAULT '',
`points` INT(11) NOT NULL DEFAULT 0,
`points_type` INT(11) NOT NULL DEFAULT 0,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `user_visual_settings` (
`user_id` INT(11) NOT NULL,
`display_order` VARCHAR(50) NOT NULL DEFAULT 'icon-prefix-name',
PRIMARY KEY (`user_id`),
CONSTRAINT `fk_user_visual_settings_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
`key_name` VARCHAR(100) NOT NULL,
`value` VARCHAR(255) NOT NULL,
PRIMARY KEY (`key_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`word` VARCHAR(100) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_custom_prefix_blacklist_word` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES
('max_length', '15'),
('min_rank_to_buy', '1'),
('price_credits', '5'),
('price_points', '0'),
('points_type', '0'),
('font_price_credits', '10'),
('font_price_points', '0'),
('font_points_type', '0');
INSERT IGNORE INTO `custom_prefixes_catalog`
(`id`, `display_name`, `text`, `color`, `icon`, `effect`, `font`, `points`, `points_type`, `enabled`, `sort_order`)
VALUES
(1, 'VIP', 'VIP', '#FFD700', '', 'glow', '', 10, 0, 1, 1),
(2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2),
(3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3);
CREATE TABLE IF NOT EXISTS `custom_nick_icons_catalog` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`icon_key` VARCHAR(50) NOT NULL,
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
`points` INT(11) NOT NULL DEFAULT 0,
`points_type` INT(11) NOT NULL DEFAULT 0,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_custom_nick_icons_catalog_icon_key` (`icon_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `user_nick_icons` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`icon_key` VARCHAR(50) NOT NULL,
`active` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_nick_icons_user_icon` (`user_id`, `icon_key`),
KEY `idx_user_nick_icons_user_id` (`user_id`),
KEY `idx_user_nick_icons_user_active` (`user_id`, `active`),
CONSTRAINT `fk_user_nick_icons_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO `custom_nick_icons_catalog` (`icon_key`, `display_name`, `points`, `points_type`, `enabled`, `sort_order`) VALUES
('1', 'Icon 1', 10, 0, 1, 1),
('2', 'Icon 2', 10, 0, 1, 2),
('3', 'Icon 3', 10, 0, 1, 3),
('4', 'Icon 4', 10, 0, 1, 4),
('5', 'Icon 5', 10, 0, 1, 5),
('6', 'Icon 6', 10, 0, 1, 6);
-- ------------------------------------------------------------
-- Custom badge maker
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `users_custom_badge_settings` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`badge_path` VARCHAR(255) NOT NULL DEFAULT '/var/www/gamedata/c_images/album1584',
`badge_url` VARCHAR(255) NOT NULL DEFAULT '/gamedata/c_images/album1584',
`price_badge` INT(11) NOT NULL DEFAULT 0,
`currency_type` INT(11) NOT NULL DEFAULT -1,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
INSERT INTO `users_custom_badge_settings` (`id`, `badge_path`, `badge_url`, `price_badge`, `currency_type`)
SELECT 1, '/var/www/gamedata/c_images/album1584', '/gamedata/c_images/album1584', 50, 5
WHERE NOT EXISTS (SELECT 1 FROM `users_custom_badge_settings` WHERE `id` = 1);
CREATE TABLE IF NOT EXISTS `user_custom_badge` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`badge_id` VARCHAR(64) NOT NULL,
`badge_name` VARCHAR(64) NOT NULL DEFAULT '',
`badge_description` VARCHAR(255) NOT NULL DEFAULT '',
`date_created` INT(11) NOT NULL DEFAULT 0,
`date_edit` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_custom_badge_badge_id` (`badge_id`),
KEY `idx_user_custom_badge_user_id` (`user_id`),
CONSTRAINT `fk_user_custom_badge_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- ------------------------------------------------------------
-- UI/catalog compatibility values used by the current client
-- ------------------------------------------------------------
INSERT INTO `chat_bubbles` (`type`, `name`, `permission`, `overridable`, `triggers_talking_furniture`) VALUES
(200, 'SHOW_MESSAGE_RED', '', 1, 0),
(201, 'SHOW_MESSAGE_GREEN', '', 1, 0),
(202, 'SHOW_MESSAGE_BLUE', '', 1, 0),
(210, 'SHOW_MESSAGE_ALERT', '', 1, 0),
(211, 'SHOW_MESSAGE_INFO', '', 1, 0),
(212, 'SHOW_MESSAGE_WARNING', '', 1, 0),
(220, 'SHOW_MESSAGE_WRONG', '', 1, 0),
(221, 'SHOW_MESSAGE_WRONG_CIRCLED', '', 1, 0),
(222, 'SHOW_MESSAGE_CORRECT', '', 1, 0),
(223, 'SHOW_MESSAGE_CORRECT_CIRCLED', '', 1, 0),
(224, 'SHOW_MESSAGE_QUESTION', '', 1, 0),
(225, 'SHOW_MESSAGE_QUESTION_CIRCLED', '', 1, 0),
(226, 'SHOW_MESSAGE_ARROW_UP', '', 1, 0),
(227, 'SHOW_MESSAGE_ARROW_UP_CIRCLED', '', 1, 0),
(228, 'SHOW_MESSAGE_ARROW_DOWN', '', 1, 0),
(229, 'SHOW_MESSAGE_ARROW_DOWN_CIRCLED', '', 1, 0),
(250, 'SHOW_MESSAGE_SKULL', '', 1, 0),
(251, 'SHOW_MESSAGE_SKULL_ALT', '', 1, 0),
(252, 'SHOW_MESSAGE_MAGNIFIER', '', 1, 0)
ON DUPLICATE KEY UPDATE
`name` = VALUES(`name`),
`permission` = VALUES(`permission`),
`overridable` = VALUES(`overridable`),
`triggers_talking_furniture` = VALUES(`triggers_talking_furniture`);
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
('commands.keys.cmd_setroom_template', 'setroom_template;set_room_template'),
('commands.succes.cmd_setroom_template.verify', 'Copy the current room "%roomname%" to room_templates? Type :setroom_template %generic.yes% to confirm.'),
('commands.succes.cmd_setroom_template', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'),
('commands.error.cmd_setroom_template', 'Could not save room as template. Check the server log for details.'),
('commands.error.cmd_setroom_template.no_room', 'You must be inside a room to use this command.'),
('commands.keys.cmd_give_prefix', 'giveprefix'),
('commands.keys.cmd_list_prefixes', 'listprefixes'),
('commands.keys.cmd_remove_prefix', 'removeprefix'),
('commands.keys.cmd_prefix_blacklist', 'prefixblacklist'),
('wiredfurni.badgereceived.body', 'You have just received a new Badge! Check your Inventory!'),
('wiredfurni.badgereceived.title', 'Badge received!');
-- Optional permission metadata for normalized permission schemas.
-- Actual rank values still belong in the permissions/permission_ranks setup.
CREATE TABLE IF NOT EXISTS `permission_definitions` (
`permission_key` VARCHAR(64) NOT NULL,
`max_value` TINYINT(3) UNSIGNED NOT NULL DEFAULT 1,
`comment` TEXT NOT NULL,
PRIMARY KEY (`permission_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`) VALUES
('cmd_setroom_template', 1, 'Allows using :setroom_template to copy a room into the login room-template table.'),
('cmd_give_prefix', 1, 'Allows granting custom prefixes to users.'),
('cmd_list_prefixes', 1, 'Allows listing custom prefixes assigned to users.'),
('cmd_remove_prefix', 1, 'Allows removing custom prefixes from users.'),
('cmd_prefix_blacklist', 1, 'Allows managing the custom prefix blacklist.')
ON DUPLICATE KEY UPDATE
`max_value` = VALUES(`max_value`),
`comment` = VALUES(`comment`);
-- ------------------------------------------------------------
-- Explicitly obsolete table from older remember-me attempts.
-- The current Java uses users_remember_families only.
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `users_remember_tokens`;
@@ -0,0 +1,31 @@
-- ============================================================
-- Fix: acc_supporttool wrongly granted to VIP / wrongly denied to Super Mod
-- ============================================================
-- The default permission_definitions seed shipped acc_supporttool
-- with rank pattern (0, 1, 1, 1, 1, 0, 1) — i.e. rank_2 (VIP) and
-- rank_3 (X, junior helper) had ALLOWED, while rank_6 (Super Mod)
-- did NOT. That's two bugs:
--
-- * VIP users see the ModTools button on the toolbar and can
-- open Room/User info windows. The actual sanction endpoints
-- still gate on ACC_SUPPORTTOOL server-side so they can't
-- actually moderate, but the UI exposure is wrong and lets a
-- VIP request user info / room info / chatlogs they have no
-- business reading.
-- * Super Mod is denied the tool entirely, which is obviously
-- unintended given the rank name.
--
-- Intended pattern: only Support (4) and up — (0, 0, 0, 1, 1, 1, 1).
--
-- Run on existing deployments to align with the corrected default
-- seed in `Default Database/FullDatabase.sql`. Idempotent.
UPDATE `permission_definitions`
SET `rank_1` = 0,
`rank_2` = 0,
`rank_3` = 0,
`rank_4` = 1,
`rank_5` = 1,
`rank_6` = 1,
`rank_7` = 1
WHERE `permission_key` = 'acc_supporttool';
+6
View File
@@ -0,0 +1,6 @@
ALTER TABLE catalog_club_offers
ADD COLUMN IF NOT EXISTS giftable ENUM('0','1') NOT NULL DEFAULT '0';
INSERT INTO emulator_texts (`key`, `value`)
VALUES ('prereg.reward.you.received', 'You have recived:'),
('generic.days', 'days');
+1 -1
View File
@@ -28598,7 +28598,7 @@ INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`,
('acc_staff_chat', 1, 'Grants access to the in-game Staff Chat group buddy: receives broadcasts from other staff and can broadcast to anyone holding this permission.', 0, 0, 0, 0, 0, 0, 1),
('acc_staff_pick', 1, 'Allows using staff item pick-up actions that bypass normal room ownership restrictions.', 0, 0, 0, 0, 0, 0, 1),
('acc_superwired', 1, 'Allows saving advanced wired data without the normal wordfilter and reward payload restrictions applied to regular users.', 0, 0, 0, 0, 0, 0, 1),
('acc_supporttool', 1, 'Allows opening and using the support/moderation tool interface.', 0, 1, 1, 1, 1, 0, 1),
('acc_supporttool', 1, 'Allows opening and using the support/moderation tool interface.', 0, 0, 0, 1, 1, 1, 1),
('acc_trade_anywhere', 1, 'Allows starting trades outside the normal trade-enabled areas.', 0, 0, 0, 0, 0, 0, 1),
('acc_unignorable', 1, 'Prevents the account from being ignored by other users through the ignore system.', 0, 0, 0, 0, 0, 0, 0),
('acc_unkickable', 1, 'Prevents the user from being kicked by normal moderation or room commands.', 0, 0, 0, 0, 0, 0, 1),
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.2.9</version>
<version>4.2.17</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -6,6 +6,7 @@ import ch.qos.logback.core.ConsoleAppender;
import com.eu.habbo.core.*;
import com.eu.habbo.core.consolecommands.ConsoleCommand;
import com.eu.habbo.database.Database;
import com.eu.habbo.gui.EmulatorDashboard;
import com.eu.habbo.habbohotel.GameEnvironment;
import com.eu.habbo.habbohotel.gameclients.SessionResumeManager;
import com.eu.habbo.networking.gameserver.GameServer;
@@ -186,6 +187,10 @@ public final class Emulator {
Emulator.isReady = true;
Emulator.timeStarted = getIntUnixTimestamp();
if (Emulator.getConfig().getBoolean("gui.enabled", true)) {
EmulatorDashboard.launch();
}
if (Emulator.getConfig().getInt("runtime.threads") < (Runtime.getRuntime().availableProcessors() * 2)) {
LOGGER.warn("Emulator settings runtime.threads ({}) can be increased to ({}) to possibly increase performance.",
Emulator.getConfig().getInt("runtime.threads"),
@@ -50,6 +50,7 @@ public class RoomUserPetComposer extends MessageComposer {
this.response.appendString("");
this.response.appendString("unknown");
this.response.appendInt(0);
this.response.appendInt(0);
return this.response;
}
@@ -0,0 +1,631 @@
package com.eu.habbo.gui;
import com.eu.habbo.Emulator;
import com.eu.habbo.monitoring.EmulatorStatsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.MatteBorder;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Path2D;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class EmulatorDashboard extends JFrame {
private static final Logger LOGGER = LoggerFactory.getLogger(EmulatorDashboard.class);
// Modern Dark Theme Colors
private static final Color COLOR_BG = new Color(18, 18, 18);
private static final Color COLOR_SURFACE = new Color(30, 30, 30);
private static final Color COLOR_SURFACE_HOVER = new Color(45, 45, 45);
private static final Color COLOR_PRIMARY = new Color(99, 102, 241);
private static final Color COLOR_PRIMARY_SOFT = new Color(99, 102, 241, 45);
private static final Color COLOR_SUCCESS = new Color(34, 197, 94);
private static final Color COLOR_WARNING = new Color(245, 158, 11);
private static final Color COLOR_TEXT = new Color(240, 240, 240);
private static final Color COLOR_TEXT_MUTED = new Color(150, 150, 150);
private static final Color COLOR_TEXT_SUBTLE = new Color(110, 110, 110);
private static final Color COLOR_BORDER = new Color(50, 50, 50);
private static final Font FONT_TITLE = new Font("Segoe UI", Font.BOLD, 26);
private static final Font FONT_SECTION = new Font("Segoe UI", Font.BOLD, 16);
private static final Font FONT_SMALL = new Font("Segoe UI", Font.PLAIN, 12);
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
private static EmulatorDashboard instance;
// Overview Tab
private final JLabel memLabel = createMetricLabel();
private final JLabel cpuLabel = createMetricLabel();
private final JLabel threadLabel = createMetricLabel();
private final JLabel usersLabel = createMetricLabel();
private final JLabel roomsLabel = createMetricLabel();
private final JLabel wiredLabel = createMetricLabel();
private final JLabel uptimeLabel = createStatusValueLabel();
private final JLabel lastUpdatedLabel = createStatusValueLabel();
private final JLabel footerStatusLabel = createStatusValueLabel();
private final MemoryGraphPanel memoryGraph = new MemoryGraphPanel();
// Tables
private final DefaultTableModel usersTableModel;
private final DefaultTableModel roomsTableModel;
private final DefaultTableModel wiredTableModel;
private final JTable usersTable;
private final JTable roomsTable;
private final JTable wiredTable;
private final JLabel usersCountLabel = createCountLabel();
private final JLabel roomsCountLabel = createCountLabel();
private final JLabel wiredCountLabel = createCountLabel();
// UI Components
private final JPanel cardsPanel;
private final CardLayout cardLayout;
private final Map<String, JPanel> navButtons = new HashMap<>();
private String selectedCardName = "Overview";
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "Dashboard-Updater");
t.setDaemon(true);
return t;
});
private EmulatorDashboard() {
setTitle("Arcturus Morningstar - System Dashboard");
setSize(1100, 700);
setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
setLocationRelativeTo(null);
getContentPane().setBackground(COLOR_BG);
setLayout(new BorderLayout());
// Setup custom Look & Feel basics to remove weird Swing borders
UIManager.put("ScrollBar.background", COLOR_BG);
UIManager.put("ScrollBar.thumb", COLOR_SURFACE_HOVER);
// Sidebar
JPanel sidebar = new JPanel();
sidebar.setLayout(new BoxLayout(sidebar, BoxLayout.Y_AXIS));
sidebar.setBackground(COLOR_SURFACE);
sidebar.setPreferredSize(new Dimension(220, 0));
sidebar.setBorder(new MatteBorder(0, 0, 0, 1, COLOR_BORDER));
// Sidebar Header
JPanel brandPanel = new JPanel(new BorderLayout());
brandPanel.setBackground(COLOR_SURFACE);
brandPanel.setBorder(new EmptyBorder(20, 20, 30, 20));
JLabel brandTitle = new JLabel("Arcturus");
brandTitle.setFont(new Font("Segoe UI", Font.BOLD, 22));
brandTitle.setForeground(COLOR_TEXT);
JLabel brandSub = new JLabel("v" + Emulator.version);
brandSub.setFont(new Font("Segoe UI", Font.PLAIN, 12));
brandSub.setForeground(COLOR_PRIMARY);
brandPanel.add(brandTitle, BorderLayout.NORTH);
brandPanel.add(brandSub, BorderLayout.SOUTH);
sidebar.add(brandPanel);
// Main Cards
cardLayout = new CardLayout();
cardsPanel = new JPanel(cardLayout);
cardsPanel.setBackground(COLOR_BG);
// Setup Tabs
usersTableModel = createTableModel(new String[]{"ID", "Username", "Rank", "Credits", "Room ID"});
roomsTableModel = createTableModel(new String[]{"Room ID", "Name", "Players", "Items", "Tickables", "CPU (ms)", "Est. RAM (KB)", "Thread"});
wiredTableModel = createTableModel(new String[]{"Room ID", "Avg Tick", "Peak Tick", "Usage %", "Delayed", "Overloaded?", "Heavy?"});
usersTable = createDashboardTable(usersTableModel);
roomsTable = createDashboardTable(roomsTableModel);
wiredTable = createDashboardTable(wiredTableModel);
cardsPanel.add(createOverviewTab(), "Overview");
cardsPanel.add(createTableTab("Online Users", "Players currently connected to the emulator.", usersTable, usersCountLabel), "Online Users");
cardsPanel.add(createTableTab("Active Rooms", "Loaded rooms with lightweight performance indicators.", roomsTable, roomsCountLabel), "Active Rooms");
cardsPanel.add(createTableTab("Wired Diagnostics", "Rooms currently using wired timing, delay and execution budget.", wiredTable, wiredCountLabel), "Wired Diagnostics");
// Sidebar Navigation
sidebar.add(createNavButton("Overview", "Overview"));
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
sidebar.add(createNavButton("Online Users", "Online Users"));
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
sidebar.add(createNavButton("Active Rooms", "Active Rooms"));
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
sidebar.add(createNavButton("Wired Diagnostics", "Wired Diagnostics"));
sidebar.add(Box.createVerticalGlue());
add(sidebar, BorderLayout.WEST);
add(cardsPanel, BorderLayout.CENTER);
add(createStatusBar(), BorderLayout.SOUTH);
addComponentListener(new ComponentAdapter() {
@Override
public void componentShown(ComponentEvent e) {
setActiveCard("Overview");
}
});
// Start updates
scheduler.scheduleAtFixedRate(this::updateMetrics, 0, 1, TimeUnit.SECONDS);
}
private DefaultTableModel createTableModel(String[] cols) {
return new DefaultTableModel(cols, 0) {
@Override public boolean isCellEditable(int row, int column) { return false; }
};
}
private JPanel createNavButton(String text, String cardName) {
JPanel btn = new JPanel(new BorderLayout());
btn.setBackground(COLOR_SURFACE);
btn.setMaximumSize(new Dimension(220, 45));
btn.setBorder(new EmptyBorder(0, 18, 0, 0));
btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
navButtons.put(cardName, btn);
JLabel lbl = new JLabel(text);
lbl.setFont(new Font("Segoe UI", Font.BOLD, 14));
lbl.setForeground(COLOR_TEXT_MUTED);
btn.add(lbl, BorderLayout.CENTER);
btn.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
btn.setBackground(COLOR_SURFACE_HOVER);
lbl.setForeground(COLOR_TEXT);
}
@Override
public void mouseExited(MouseEvent e) {
updateNavButtonStyle(cardName, btn, lbl);
}
@Override
public void mouseClicked(MouseEvent e) {
setActiveCard(cardName);
}
});
updateNavButtonStyle(cardName, btn, lbl);
return btn;
}
private JPanel createOverviewTab() {
JPanel wrapper = new JPanel(new BorderLayout());
wrapper.setBackground(COLOR_BG);
wrapper.setBorder(new EmptyBorder(30, 30, 30, 30));
JPanel header = new JPanel(new BorderLayout(0, 14));
header.setOpaque(false);
JLabel title = new JLabel("Dashboard Overview");
title.setFont(FONT_TITLE);
title.setForeground(COLOR_TEXT);
JLabel subtitle = new JLabel("Operational view of emulator health, activity and wired performance.");
subtitle.setFont(new Font("Segoe UI", Font.PLAIN, 13));
subtitle.setForeground(COLOR_TEXT_MUTED);
JPanel titleBlock = new JPanel();
titleBlock.setOpaque(false);
titleBlock.setLayout(new BoxLayout(titleBlock, BoxLayout.Y_AXIS));
titleBlock.add(title);
titleBlock.add(Box.createRigidArea(new Dimension(0, 4)));
titleBlock.add(subtitle);
header.add(titleBlock, BorderLayout.NORTH);
header.add(createOverviewMetaPanel(), BorderLayout.SOUTH);
wrapper.add(header, BorderLayout.NORTH);
JPanel content = new JPanel(new GridLayout(1, 2, 20, 20));
content.setOpaque(false);
content.setBorder(new EmptyBorder(20, 0, 0, 0));
// Left Stats
JPanel statsPanel = new JPanel(new GridLayout(3, 2, 12, 12));
statsPanel.setOpaque(false);
statsPanel.add(createMetricCard("Memory Allocation", memLabel));
statsPanel.add(createMetricCard("CPU Load", cpuLabel));
statsPanel.add(createMetricCard("Active OS Threads", threadLabel));
statsPanel.add(createMetricCard("Connected Players", usersLabel));
statsPanel.add(createMetricCard("Loaded Rooms", roomsLabel));
statsPanel.add(createMetricCard("Wired Tickables", wiredLabel));
content.add(statsPanel);
// Right Graph
JPanel graphContainer = new JPanel(new BorderLayout());
graphContainer.setBackground(COLOR_SURFACE);
graphContainer.setBorder(BorderFactory.createCompoundBorder(
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
new EmptyBorder(15, 15, 15, 15)
));
JLabel gTitle = new JLabel("Realtime Memory Usage");
gTitle.setFont(FONT_SECTION);
gTitle.setForeground(COLOR_TEXT_MUTED);
gTitle.setBorder(new EmptyBorder(0, 0, 15, 0));
graphContainer.add(gTitle, BorderLayout.NORTH);
graphContainer.add(memoryGraph, BorderLayout.CENTER);
content.add(graphContainer);
wrapper.add(content, BorderLayout.CENTER);
return wrapper;
}
private JPanel createMetricCard(String title, JLabel valueLabel) {
JPanel card = new JPanel(new BorderLayout());
card.setBackground(COLOR_SURFACE);
card.setBorder(BorderFactory.createCompoundBorder(
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
new EmptyBorder(15, 20, 15, 20)
));
JLabel tLabel = new JLabel(title);
tLabel.setFont(new Font("Segoe UI", Font.BOLD, 13));
tLabel.setForeground(COLOR_TEXT_MUTED);
card.add(tLabel, BorderLayout.NORTH);
card.add(valueLabel, BorderLayout.SOUTH);
return card;
}
private JLabel createMetricLabel() {
JLabel label = new JLabel("-");
label.setFont(new Font("Segoe UI", Font.BOLD, 28));
label.setForeground(COLOR_TEXT);
return label;
}
private JPanel createOverviewMetaPanel() {
JPanel panel = new JPanel(new GridLayout(1, 3, 12, 12));
panel.setOpaque(false);
panel.add(createStatusCard("Uptime", uptimeLabel, COLOR_PRIMARY));
panel.add(createStatusCard("Last Refresh", lastUpdatedLabel, COLOR_SUCCESS));
panel.add(createStatusCard("GUI Status", footerStatusLabel, COLOR_WARNING));
return panel;
}
private JPanel createStatusCard(String title, JLabel valueLabel, Color accent) {
JPanel panel = new JPanel(new BorderLayout());
panel.setBackground(COLOR_SURFACE);
panel.setBorder(BorderFactory.createCompoundBorder(
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
new EmptyBorder(12, 14, 12, 14)
));
JPanel accentBar = new JPanel();
accentBar.setBackground(accent);
accentBar.setPreferredSize(new Dimension(6, 0));
JPanel content = new JPanel();
content.setOpaque(false);
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
JLabel label = new JLabel(title);
label.setFont(FONT_SMALL);
label.setForeground(COLOR_TEXT_MUTED);
content.add(label);
content.add(Box.createRigidArea(new Dimension(0, 6)));
content.add(valueLabel);
panel.add(accentBar, BorderLayout.WEST);
panel.add(content, BorderLayout.CENTER);
return panel;
}
private JLabel createStatusValueLabel() {
JLabel label = new JLabel("-");
label.setFont(new Font("Segoe UI", Font.BOLD, 16));
label.setForeground(COLOR_TEXT);
return label;
}
private JLabel createCountLabel() {
JLabel label = new JLabel("0 rows");
label.setFont(FONT_SMALL);
label.setForeground(COLOR_TEXT_MUTED);
return label;
}
private JTable createDashboardTable(DefaultTableModel model) {
JTable table = new JTable(model);
table.setBackground(COLOR_SURFACE);
table.setForeground(COLOR_TEXT);
table.setGridColor(COLOR_BORDER);
table.setRowHeight(34);
table.setFont(new Font("Segoe UI", Font.PLAIN, 13));
table.setFillsViewportHeight(true);
table.setSelectionBackground(COLOR_PRIMARY);
table.setSelectionForeground(Color.WHITE);
table.setShowVerticalLines(false);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setAutoCreateRowSorter(true);
JTableHeader header = table.getTableHeader();
header.setBackground(new Color(22, 22, 22));
header.setForeground(COLOR_TEXT_MUTED);
header.setFont(new Font("Segoe UI", Font.BOLD, 13));
header.setPreferredSize(new Dimension(0, 38));
header.setBorder(BorderFactory.createMatteBorder(1, 0, 1, 0, COLOR_BORDER));
((DefaultTableCellRenderer) header.getDefaultRenderer()).setHorizontalAlignment(JLabel.LEFT);
((DefaultTableCellRenderer) header.getDefaultRenderer()).setBorder(new EmptyBorder(0, 10, 0, 0));
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
label.setBorder(new EmptyBorder(0, 10, 0, 10));
label.setForeground(isSelected ? Color.WHITE : COLOR_TEXT);
label.setBackground(isSelected ? COLOR_PRIMARY : ((row % 2 == 0) ? COLOR_SURFACE : new Color(35, 35, 35)));
return label;
}
});
return table;
}
private JPanel createTableTab(String title, String subtitle, JTable table, JLabel countLabel) {
JPanel wrapper = new JPanel(new BorderLayout());
wrapper.setBackground(COLOR_BG);
wrapper.setBorder(new EmptyBorder(30, 30, 30, 30));
JPanel titlePanel = new JPanel(new BorderLayout());
titlePanel.setOpaque(false);
JLabel titleLbl = new JLabel(title);
titleLbl.setFont(FONT_TITLE);
titleLbl.setForeground(COLOR_TEXT);
JLabel subtitleLbl = new JLabel(subtitle);
subtitleLbl.setFont(new Font("Segoe UI", Font.PLAIN, 13));
subtitleLbl.setForeground(COLOR_TEXT_MUTED);
subtitleLbl.setBorder(new EmptyBorder(6, 0, 0, 0));
JPanel textPanel = new JPanel();
textPanel.setOpaque(false);
textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.Y_AXIS));
textPanel.add(titleLbl);
textPanel.add(subtitleLbl);
titlePanel.add(textPanel, BorderLayout.WEST);
titlePanel.add(countLabel, BorderLayout.EAST);
wrapper.add(titlePanel, BorderLayout.NORTH);
JScrollPane scrollPane = new JScrollPane(table);
scrollPane.getViewport().setBackground(COLOR_SURFACE);
scrollPane.setBorder(new MatteBorder(1, 1, 1, 1, COLOR_BORDER));
scrollPane.setBorder(new CompoundBorder(
new EmptyBorder(20, 0, 0, 0),
new MatteBorder(1, 1, 1, 1, COLOR_BORDER)
));
wrapper.add(scrollPane, BorderLayout.CENTER);
return wrapper;
}
private JPanel createStatusBar() {
JPanel statusBar = new JPanel(new BorderLayout());
statusBar.setBackground(COLOR_SURFACE);
statusBar.setBorder(new CompoundBorder(
new MatteBorder(1, 0, 0, 0, COLOR_BORDER),
new EmptyBorder(8, 14, 8, 14)
));
JLabel left = new JLabel("Dashboard running locally");
left.setFont(FONT_SMALL);
left.setForeground(COLOR_TEXT_SUBTLE);
JLabel right = new JLabel("Tip: table columns are sortable");
right.setFont(FONT_SMALL);
right.setForeground(COLOR_TEXT_SUBTLE);
statusBar.add(left, BorderLayout.WEST);
statusBar.add(right, BorderLayout.EAST);
return statusBar;
}
private void setActiveCard(String cardName) {
selectedCardName = cardName;
cardLayout.show(cardsPanel, cardName);
navButtons.forEach((name, button) -> {
JLabel label = (JLabel) button.getComponent(0);
updateNavButtonStyle(name, button, label);
});
}
private void updateNavButtonStyle(String cardName, JPanel button, JLabel label) {
boolean active = cardName.equals(selectedCardName);
button.setBackground(active ? COLOR_PRIMARY_SOFT : COLOR_SURFACE);
label.setForeground(active ? COLOR_TEXT : COLOR_TEXT_MUTED);
}
private void updateMetrics() {
try {
EmulatorStatsService.Snapshot snapshot = EmulatorStatsService.collectSnapshot();
EmulatorStatsService.Overview overview = snapshot.overview;
Object[][] usersData = new Object[snapshot.users.size()][5];
for (int i = 0; i < snapshot.users.size(); i++) {
EmulatorStatsService.OnlineUserRow user = snapshot.users.get(i);
usersData[i] = new Object[]{user.id, user.username, user.rank, user.credits, user.roomId};
}
Object[][] roomsData = new Object[snapshot.rooms.size()][8];
for (int i = 0; i < snapshot.rooms.size(); i++) {
EmulatorStatsService.ActiveRoomRow room = snapshot.rooms.get(i);
roomsData[i] = new Object[]{
room.roomId,
room.name,
room.players,
room.items,
room.tickables,
String.format("%.2f", room.cpuMs),
room.estimatedRamKb,
room.thread
};
}
Object[][] wiredData = new Object[snapshot.wired.size()][7];
for (int i = 0; i < snapshot.wired.size(); i++) {
EmulatorStatsService.WiredRoomRow wiredRoom = snapshot.wired.get(i);
wiredData[i] = new Object[]{
wiredRoom.roomId,
wiredRoom.averageTickMs + " ms",
wiredRoom.peakTickMs + " ms",
wiredRoom.usagePercent + "%",
wiredRoom.delayedEventsPending,
wiredRoom.overloaded ? "YES" : "NO",
wiredRoom.heavy ? "YES" : "NO"
};
}
SwingUtilities.invokeLater(() -> {
memLabel.setText(String.format("%d MB / %d MB", overview.memoryUsedMb, overview.memoryMaxMb));
cpuLabel.setText(String.format("%.1f %%", overview.cpuLoadPercent));
threadLabel.setText(String.valueOf(overview.activeOsThreads));
usersLabel.setText(String.valueOf(overview.connectedPlayers));
roomsLabel.setText(String.valueOf(overview.loadedRooms));
wiredLabel.setText(String.valueOf(overview.wiredTickables));
uptimeLabel.setText(EmulatorStatsService.formatDuration(overview.uptimeSeconds));
lastUpdatedLabel.setText(LocalDateTime.now().format(TIME_FORMAT));
footerStatusLabel.setText(overview.guiStatus);
memoryGraph.addValue((long) overview.memoryUsedMb * 1024L * 1024L, (long) overview.memoryMaxMb * 1024L * 1024L);
usersTableModel.setDataVector(usersData, new String[]{"ID", "Username", "Rank", "Credits", "Room ID"});
roomsTableModel.setDataVector(roomsData, new String[]{"Room ID", "Name", "Players", "Items", "Tickables", "CPU (ms)", "Est. RAM (KB)", "Thread"});
wiredTableModel.setDataVector(wiredData, new String[]{"Room ID", "Avg Tick", "Peak Tick", "Usage %", "Delayed", "Overloaded?", "Heavy?"});
usersCountLabel.setText(snapshot.users.size() + " rows");
roomsCountLabel.setText(snapshot.rooms.size() + " rows");
wiredCountLabel.setText(snapshot.wired.size() + " rows");
});
} catch (Exception e) {
LOGGER.error("Error updating dashboard metrics", e);
}
}
public static void launch() {
if (instance == null) {
instance = new EmulatorDashboard();
}
SwingUtilities.invokeLater(() -> {
instance.setVisible(true);
});
}
private static class MemoryGraphPanel extends JPanel {
private final LinkedList<Double> history = new LinkedList<>();
private static final int MAX_POINTS = 100;
public MemoryGraphPanel() {
setOpaque(false);
}
public void addValue(long used, long max) {
double percent = (double) used / (double) max;
history.addLast(percent);
if (history.size() > MAX_POINTS) {
history.removeFirst();
}
repaint();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int width = getWidth();
int height = getHeight();
// Background grid and labels
g2.setFont(new Font("Segoe UI", Font.PLAIN, 10));
long maxMemRaw = Runtime.getRuntime().maxMemory();
for(int i = 0; i <= 4; i++) {
int y = i == 0 ? 15 : height * i / 4;
if (i == 4) y = height - 5;
g2.setColor(COLOR_BORDER);
g2.drawLine(0, y, width, y);
// Draw Y-axis numbers
g2.setColor(COLOR_TEXT_MUTED);
long labelVal = (long) (maxMemRaw * (1.0 - (double)i / 4.0)) / 1024 / 1024;
g2.drawString(labelVal + " MB", 5, y - 5);
}
if (history.size() < 2) return;
double dx = (double) width / (MAX_POINTS - 1);
Path2D path = new Path2D.Double();
path.moveTo(0, height);
int i = MAX_POINTS - history.size();
for (Double val : history) {
double x = i * dx;
double y = height - (val * height);
if (i == MAX_POINTS - history.size()) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
i++;
}
// Draw line
g2.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2.setColor(COLOR_PRIMARY);
g2.draw(path);
// Fill area
path.lineTo(width, height);
path.lineTo((MAX_POINTS - history.size()) * dx, height);
path.closePath();
GradientPaint fillPaint = new GradientPaint(
0, 0, new Color(COLOR_PRIMARY.getRed(), COLOR_PRIMARY.getGreen(), COLOR_PRIMARY.getBlue(), 120),
0, height, new Color(COLOR_PRIMARY.getRed(), COLOR_PRIMARY.getGreen(), COLOR_PRIMARY.getBlue(), 10)
);
g2.setPaint(fillPaint);
g2.fill(path);
Double lastValue = history.peekLast();
if (lastValue != null) {
String usageLabel = String.format("Usage %.1f%%", lastValue * 100.0);
g2.setFont(new Font("Segoe UI", Font.BOLD, 12));
FontMetrics metrics = g2.getFontMetrics();
int labelWidth = metrics.stringWidth(usageLabel) + 16;
int labelHeight = 24;
int labelX = Math.max(8, width - labelWidth - 8);
int labelY = 8;
g2.setColor(new Color(0, 0, 0, 130));
g2.fillRoundRect(labelX, labelY, labelWidth, labelHeight, 12, 12);
g2.setColor(COLOR_TEXT);
g2.drawString(usageLabel, labelX + 8, labelY + 16);
}
}
}
private static String formatDuration(long millis) {
long totalSeconds = Math.max(0L, millis / 1000L);
long hours = totalSeconds / 3600L;
long minutes = (totalSeconds % 3600L) / 60L;
long seconds = totalSeconds % 60L;
return String.format("%02dh %02dm %02ds", hours, minutes, seconds);
}
}
@@ -138,18 +138,20 @@ public class Bot implements Runnable {
@Override
public void run() {
if (this.needsUpdate) {
Room localRoom = this.room;
RoomUnit localRoomUnit = this.roomUnit;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE bots SET name = ?, motto = ?, figure = ?, gender = ?, user_id = ?, room_id = ?, x = ?, y = ?, z = ?, rot = ?, dance = ?, freeroam = ?, chat_lines = ?, chat_auto = ?, chat_random = ?, chat_delay = ?, effect = ?, bubble_id = ? WHERE id = ?")) {
statement.setString(1, this.name);
statement.setString(2, this.motto);
statement.setString(3, this.figure);
statement.setString(4, this.gender.toString());
statement.setInt(5, this.ownerId);
statement.setInt(6, this.room == null ? 0 : this.room.getId());
statement.setInt(7, this.roomUnit == null ? 0 : this.roomUnit.getX());
statement.setInt(8, this.roomUnit == null ? 0 : this.roomUnit.getY());
statement.setDouble(9, this.roomUnit == null ? 0 : this.roomUnit.getZ());
statement.setInt(10, this.roomUnit == null ? 0 : this.roomUnit.getBodyRotation().getValue());
statement.setInt(11, this.roomUnit == null ? 0 : this.roomUnit.getDanceType().getType());
statement.setInt(6, localRoom == null ? 0 : localRoom.getId());
statement.setInt(7, localRoomUnit == null ? 0 : localRoomUnit.getX());
statement.setInt(8, localRoomUnit == null ? 0 : localRoomUnit.getY());
statement.setDouble(9, localRoomUnit == null ? 0 : localRoomUnit.getZ());
statement.setInt(10, localRoomUnit == null ? 0 : localRoomUnit.getBodyRotation().getValue());
statement.setInt(11, localRoomUnit == null ? 0 : localRoomUnit.getDanceType().getType());
statement.setString(12, this.canWalk ? "1" : "0");
StringBuilder text = new StringBuilder();
for (String s : this.chatLines) {
@@ -282,7 +284,7 @@ public class Bot implements Runnable {
}
public void onPickUp(Habbo habbo, Room room) {
this.stopFollowingHabbo();
}
public void onUserSay(final RoomChatMessage message) {
@@ -20,10 +20,13 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
public class ButlerBot extends Bot {
private static final Logger LOGGER = LoggerFactory.getLogger(ButlerBot.class);
public static THashMap<THashSet<String>, Integer> serveItems = new THashMap<>();
private static final ConcurrentHashMap<Pattern, Integer> serveItemsCompiled = new ConcurrentHashMap<>();
public ButlerBot(ResultSet set) throws SQLException {
super(set);
@@ -38,6 +41,7 @@ public class ButlerBot extends Bot {
serveItems = new THashMap<>();
serveItems.clear();
serveItemsCompiled.clear();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM bot_serves")) {
while (set.next()) {
@@ -45,6 +49,17 @@ public class ButlerBot extends Bot {
THashSet<String> ks = new THashSet<>();
Collections.addAll(ks, keys);
serveItems.put(ks, set.getInt("item"));
for (String key : keys) {
if (key != null && !key.trim().isEmpty()) {
try {
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(key.toLowerCase()) + "\\b");
serveItemsCompiled.put(pattern, set.getInt("item"));
} catch (Exception e) {
LOGGER.error("Failed to compile butler bot keyword pattern: {}", key, e);
}
}
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
@@ -53,6 +68,7 @@ public class ButlerBot extends Bot {
public static void dispose() {
serveItems.clear();
serveItemsCompiled.clear();
}
@Override
@@ -66,74 +82,73 @@ public class ButlerBot extends Bot {
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.commanddistance")) {
if (message.getUnfilteredMessage() != null) {
for (Map.Entry<THashSet<String>, Integer> set : serveItems.entrySet()) {
for (String keyword : set.getKey()) {
String unfilteredLower = message.getUnfilteredMessage().toLowerCase();
for (Map.Entry<Pattern, Integer> entry : serveItemsCompiled.entrySet()) {
Pattern pattern = entry.getKey();
if (pattern.matcher(unfilteredLower).matches()) {
int itemId = entry.getValue();
String keyword = pattern.pattern().replace("\\b", "").replace("\\Q", "").replace("\\E", "");
// Check if the string contains a certain keyword using a regex.
// If keyword = tea, teapot wouldn't trigger it.
if (message.getUnfilteredMessage().toLowerCase().matches("\\b" + keyword + "\\b")) {
// Enable plugins to cancel this event
BotServerItemEvent serveEvent = new BotServerItemEvent(this, message.getHabbo(), set.getValue());
if (Emulator.getPluginManager().fireEvent(serveEvent).isCancelled()) {
return;
}
// Start give handitem process
if (this.getRoomUnit().canWalk()) {
final String key = keyword;
final Bot bot = this;
// Step 1: Look at Habbo
bot.lookAt(serveEvent.habbo);
// Step 2: Prepare tasks for when the Bot (carrying the handitem) reaches the Habbo
final List<Runnable> tasks = new ArrayList<>();
tasks.add(new RoomUnitGiveHanditem(serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
tasks.add(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), 0));
tasks.add(() -> {
if(this.getRoom() != null) {
String botMessage = Emulator.getTexts()
.getValue("bots.butler.given")
.replace("%key%", key)
.replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), botMessage)) {
bot.talk(botMessage);
}
}
});
List<Runnable> failedReached = new ArrayList<>();
failedReached.add(() -> {
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.servedistance", 8)) {
for (Runnable task : tasks) {
task.run();
}
}
});
// Give bot the handitem that it's going to give the Habbo
Emulator.getThreading().run(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
if (distanceBetweenBotAndHabbo > Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)) {
Emulator.getThreading().run(new RoomUnitWalkToRoomUnit(this.getRoomUnit(), serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), tasks, failedReached, Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)));
} else {
Emulator.getThreading().run(failedReached.get(0), 1000);
}
} else {
if(this.getRoom() != null) {
this.getRoom().giveHandItem(serveEvent.habbo, serveEvent.itemId);
String msg = Emulator.getTexts().getValue("bots.butler.given").replace("%key%", keyword).replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), msg)) {
this.talk(msg);
}
}
}
// Enable plugins to cancel this event
BotServerItemEvent serveEvent = new BotServerItemEvent(this, message.getHabbo(), itemId);
if (Emulator.getPluginManager().fireEvent(serveEvent).isCancelled()) {
return;
}
// Start give handitem process
if (this.getRoomUnit().canWalk()) {
final String key = keyword;
final Bot bot = this;
// Step 1: Look at Habbo
bot.lookAt(serveEvent.habbo);
// Step 2: Prepare tasks for when the Bot (carrying the handitem) reaches the Habbo
final List<Runnable> tasks = new ArrayList<>();
tasks.add(new RoomUnitGiveHanditem(serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
tasks.add(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), 0));
tasks.add(() -> {
if(this.getRoom() != null) {
String botMessage = Emulator.getTexts()
.getValue("bots.butler.given")
.replace("%key%", key)
.replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), botMessage)) {
bot.talk(botMessage);
}
}
});
List<Runnable> failedReached = new ArrayList<>();
failedReached.add(() -> {
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.servedistance", 8)) {
for (Runnable task : tasks) {
task.run();
}
}
});
// Give bot the handitem that it's going to give the Habbo
Emulator.getThreading().run(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
if (distanceBetweenBotAndHabbo > Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)) {
Emulator.getThreading().run(new RoomUnitWalkToRoomUnit(this.getRoomUnit(), serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), tasks, failedReached, Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)));
} else {
Emulator.getThreading().run(failedReached.get(0), 1000);
}
} else {
if(this.getRoom() != null) {
this.getRoom().giveHandItem(serveEvent.habbo, serveEvent.itemId);
String msg = Emulator.getTexts().getValue("bots.butler.given").replace("%key%", keyword).replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), msg)) {
this.talk(msg);
}
}
}
return;
}
}
}
@@ -72,7 +72,11 @@ public class ClubOffer implements ISerialize {
this.type = OfferType.fromDatabase(set.getString("type"));
this.vip = this.type == OfferType.VIP;
this.deal = set.getString("deal").equals("1");
this.giftable = set.getString("giftable").equals("1");
boolean giftable = false;
try {
giftable = "1".equals(set.getString("giftable"));
} catch (SQLException ignored) {}
this.giftable = giftable;
}
public int getId() {
@@ -196,6 +196,7 @@ public class CommandHandler {
addCommand(new EmptyInventoryCommand());
addCommand(new EmptyBotsInventoryCommand());
addCommand(new EmptyPetsInventoryCommand());
addCommand(new EmuStatsCommand());
addCommand(new EnableCommand());
addCommand(new EventCommand());
addCommand(new FacelessCommand());
@@ -0,0 +1,16 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.permissions.Permission;
public class EmuStatsCommand extends Command {
public EmuStatsCommand() {
super(Permission.ACC_MODTOOL_ROOM_INFO, new String[]{"emustats"});
}
@Override
public boolean handle(GameClient gameClient, String[] params) {
gameClient.getHabbo().whisper("Emulator stats are available in the Nitro stats window.");
return true;
}
}
@@ -2,7 +2,12 @@ package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.permissions.PermissionsManager;
import com.eu.habbo.habbohotel.permissions.Rank;
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboManager;
import com.eu.habbo.messages.outgoing.users.UserPermissionsComposer;
public class UpdatePermissionsCommand extends Command {
public UpdatePermissionsCommand() {
@@ -13,7 +18,41 @@ public class UpdatePermissionsCommand extends Command {
public boolean handle(GameClient gameClient, String[] params) throws Exception {
Emulator.getGameEnvironment().getPermissionsManager().reload();
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_update_permissions"), RoomChatMessageBubbles.ALERT);
// PermissionsManager.reload() rebuilt the rank table — each online
// Habbo's HabboInfo still references the OLD Rank object, so
// server-side hasPermission() / wire composers would keep
// reporting stale data until relogin. Re-bind every connected
// user to the freshly-loaded Rank by id, then ship the new
// UserPermissionsComposer (which carries clubLevel,
// securityLevel, isAmbassador, rank metadata and the resolved
// permission_definitions map) so Nitro clients' React-side
// useHasPermission(key) / useUserRank() / useUserPermissions()
// consumers re-render against the updated tables without an F5.
HabboManager habboManager = Emulator.getGameEnvironment().getHabboManager();
PermissionsManager permissions = Emulator.getGameEnvironment().getPermissionsManager();
int refreshed = 0;
for (Habbo habbo : habboManager.getOnlineHabbos().values()) {
if (habbo == null || habbo.getHabboInfo() == null || habbo.getClient() == null) continue;
int currentRankId = habbo.getHabboInfo().getRank().getId();
// Defensive fallback: if the admin deleted the rank from the
// permission_ranks table between sessions, fall back to rank 1
// (Member) so the user isn't stranded with a null Rank.
Rank freshRank = permissions.rankExists(currentRankId)
? permissions.getRank(currentRankId)
: permissions.getRank(1);
habbo.getHabboInfo().setRank(freshRank);
habbo.getClient().sendResponse(new UserPermissionsComposer(habbo));
refreshed++;
}
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.succes.cmd_update_permissions") + " (" + refreshed + " online refreshed)",
RoomChatMessageBubbles.ALERT
);
return true;
}
@@ -208,10 +208,10 @@ public abstract class Game implements Runnable {
this.state = GameState.IDLE;
boolean gamesActive = false;
for (HabboItem timer : room.getFloorItems()) {
if (timer instanceof InteractionGameTimer) {
if (((InteractionGameTimer) timer).isRunning())
gamesActive = true;
for (InteractionGameTimer timer : room.getRoomSpecialTypes().getGameTimers().values()) {
if (timer.isRunning()) {
gamesActive = true;
break;
}
}
@@ -6,49 +6,55 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager;
public class GamePlayer {
private final Habbo habbo;
private GameTeamColors teamColor;
private int score;
private int wiredScore;
public GamePlayer(Habbo habbo, GameTeamColors teamColor) {
this.habbo = habbo;
this.teamColor = teamColor;
}
public void reset() {
this.score = 0;
this.wiredScore = 0;
}
public synchronized void addScore(int amount) {
public void addScore(int amount) {
addScore(amount, false);
}
public synchronized void addScore(int amount, boolean isWired) {
if (habbo.getHabboInfo().getGamePlayer() != null && this.habbo.getHabboInfo().getCurrentGame() != null && this.habbo.getHabboInfo().getCurrentRoom().getGame(this.habbo.getHabboInfo().getCurrentGame()).getTeamForHabbo(this.habbo) != null) {
this.score += amount;
public void addScore(int amount, boolean isWired) {
com.eu.habbo.habbohotel.rooms.Room roomToTrigger = null;
com.eu.habbo.habbohotel.rooms.RoomUnit roomUnitToTrigger = null;
int currentScore = 0;
if (this.score < 0) this.score = 0;
synchronized (this) {
if (this.habbo.getHabboInfo().getGamePlayer() != null && this.habbo.getHabboInfo().getCurrentGame() != null && this.habbo.getHabboInfo().getCurrentRoom().getGame(this.habbo.getHabboInfo().getCurrentGame()).getTeamForHabbo(this.habbo) != null) {
this.score += amount;
if(isWired) {
this.wiredScore += amount;
if (this.score < 0) this.score = 0;
if (this.wiredScore < 0) {
this.wiredScore = 0;
if (isWired) {
this.wiredScore += amount;
if (this.wiredScore < 0) {
this.wiredScore = 0;
}
if (this.wiredScore > this.score) {
this.wiredScore = this.score;
}
}
if (this.wiredScore > this.score) {
this.wiredScore = this.score;
}
roomToTrigger = this.habbo.getHabboInfo().getCurrentRoom();
roomUnitToTrigger = this.habbo.getRoomUnit();
currentScore = this.score;
}
}
WiredManager.triggerScoreAchieved(this.habbo.getHabboInfo().getCurrentRoom(), this.habbo.getRoomUnit(), this.score, amount);
if (roomToTrigger != null && roomUnitToTrigger != null) {
WiredManager.triggerScoreAchieved(roomToTrigger, roomUnitToTrigger, currentScore, amount);
}
}
@@ -56,12 +62,10 @@ public class GamePlayer {
return this.habbo;
}
public GameTeamColors getTeamColor() {
return this.teamColor;
}
public int getScore() {
return this.score;
}
@@ -89,7 +89,9 @@ public class InteractionOneWayGate extends HabboItem {
Emulator.getThreading().run(new RoomUnitWalkToLocation(unit, tile, room, onFail, onFail));
Emulator.getThreading().run(() -> {
WiredManager.triggerUserWalksOn(room, unit, this);
if (room.isLoaded()) {
WiredManager.triggerUserWalksOn(room, unit, this);
}
}, 500);
});
@@ -93,23 +93,24 @@ public abstract class InteractionWired extends InteractionDefault {
@Override
public void run() {
if (this.needsUpdate()) {
String wiredData = this.getWiredData();
String wiredDataRaw = this.getWiredData();
final String wiredData = (wiredDataRaw == null) ? "" : wiredDataRaw;
final int currentRoomId = this.getRoomId();
final int currentId = this.getId();
if (wiredData == null) {
wiredData = "";
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE items SET wired_data = ? WHERE id = ?")) {
if (this.getRoomId() != 0) {
statement.setString(1, wiredData);
} else {
statement.setString(1, "");
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE items SET wired_data = ? WHERE id = ?")) {
if (currentRoomId != 0) {
statement.setString(1, wiredData);
} else {
statement.setString(1, "");
}
statement.setInt(2, currentId);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
statement.setInt(2, this.getId());
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
});
}
super.run();
}
@@ -216,6 +217,9 @@ public abstract class InteractionWired extends InteractionDefault {
public static WiredSettings readSettings(ClientMessage packet, boolean isEffect)
{
int intParamCount = packet.readInt();
if (intParamCount < 0 || intParamCount > 100) {
throw new IllegalArgumentException("Invalid intParamCount: " + intParamCount);
}
int[] intParams = new int[intParamCount];
for(int i = 0; i < intParamCount; i++)
@@ -226,6 +230,10 @@ public abstract class InteractionWired extends InteractionDefault {
String stringParam = packet.readString();
int itemCount = packet.readInt();
int selectionLimit = Emulator.getConfig() != null ? Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5) : 5;
if (itemCount < 0 || itemCount > selectionLimit * 20) {
throw new IllegalArgumentException("Invalid itemCount: " + itemCount + " exceeds maximum allowed limit");
}
int[] itemIds = new int[itemCount];
for(int i = 0; i < itemCount; i++)
@@ -154,6 +154,7 @@ public class InteractionGameTimer extends HabboItem {
@Override
public void onPickUp(Room room) {
this.endGame(room);
this.threadActive = false;
this.timeNow = this.getInitialTimeValue();
this.setExtradata(this.timeNow + "\t" + this.baseTime);
@@ -220,8 +221,7 @@ public class InteractionGameTimer extends HabboItem {
room.updateItem(this);
WiredManager.triggerGameStarts(room);
if (!this.threadActive) {
this.threadActive = true;
if (this.tryActivateTimerThread()) {
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
}
} else if (client != null) {
@@ -243,8 +243,7 @@ public class InteractionGameTimer extends HabboItem {
} else {
this.unpause(room);
if (!this.threadActive) {
this.threadActive = true;
if (this.tryActivateTimerThread()) {
this.scheduleTimerRunnable(this.getTimerResumeDelayMs());
}
}
@@ -257,8 +256,7 @@ public class InteractionGameTimer extends HabboItem {
this.createNewGame(room);
WiredManager.triggerGameStarts(room);
if (!this.threadActive) {
this.threadActive = true;
if (this.tryActivateTimerThread()) {
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
}
}
@@ -297,8 +295,7 @@ public class InteractionGameTimer extends HabboItem {
}
this.createNewGame(room);
WiredManager.triggerGameStarts(room);
if (!threadActive) {
threadActive = true;
if (this.tryActivateTimerThread()) {
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
}
}
@@ -321,8 +318,7 @@ public class InteractionGameTimer extends HabboItem {
this.isPaused = false;
this.unpause(room);
if (!this.threadActive) {
this.threadActive = true;
if (this.tryActivateTimerThread()) {
this.scheduleTimerRunnable(this.getTimerResumeDelayMs());
}
}
@@ -406,7 +402,9 @@ public class InteractionGameTimer extends HabboItem {
}
public void setThreadActive(boolean threadActive) {
this.threadActive = threadActive;
synchronized (this) {
this.threadActive = threadActive;
}
}
public boolean isPaused() {
@@ -428,4 +426,15 @@ public class InteractionGameTimer extends HabboItem {
public int getBaseTime() {
return this.baseTime;
}
public boolean tryActivateTimerThread() {
synchronized (this) {
if (this.threadActive) {
return false;
}
this.threadActive = true;
return true;
}
}
}
@@ -20,12 +20,13 @@ import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.incoming.wired.WiredSaveException;
import com.eu.habbo.messages.outgoing.generic.alerts.UpdateFailedComposer;
import gnu.trove.procedure.TObjectProcedure;
import gnu.trove.set.hash.THashSet;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
public class WiredEffectGiveReward extends InteractionWiredEffect {
public static final int LIMIT_ONCE = 0;
@@ -37,10 +38,10 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
public int limit;
public int limitationInterval;
public int given;
public AtomicInteger given = new AtomicInteger(0);
public int rewardTime;
public boolean uniqueRewards;
public THashSet<WiredGiveRewardItem> rewardItems = new THashSet<>();
public List<WiredGiveRewardItem> rewardItems = new CopyOnWriteArrayList<>();
public int userSource = WiredSourceUtil.SOURCE_TRIGGER;
public WiredEffectGiveReward(ResultSet set, Item baseItem) throws SQLException {
@@ -71,9 +72,8 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
@Override
public String getWiredData() {
ArrayList<WiredGiveRewardItem> rewards = new ArrayList<>(this.rewardItems);
return WiredManager.getGson().toJson(new JsonData(this.limit, this.given, this.rewardTime, this.uniqueRewards, this.limitationInterval, rewards, this.getDelay(), this.userSource));
return WiredManager.getGson().toJson(new JsonData(this.limit, this.given.get(), this.rewardTime, this.uniqueRewards, this.limitationInterval, rewards, this.getDelay(), this.userSource));
}
@Override
@@ -84,7 +84,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
this.setDelay(data.delay);
this.limit = data.limit;
this.given = data.given;
this.given.set(data.given);
this.rewardTime = data.reward_time;
this.uniqueRewards = data.unique_rewards;
this.limitationInterval = data.limit_interval;
@@ -96,7 +96,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
String[] data = wiredData.split(":");
if (data.length > 0) {
this.limit = Integer.parseInt(data[0]);
this.given = Integer.parseInt(data[1]);
this.given.set(Integer.parseInt(data[1]));
this.rewardTime = Integer.parseInt(data[2]);
this.uniqueRewards = data[3].equals("1");
this.limitationInterval = Integer.parseInt(data[4]);
@@ -127,7 +127,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
public void onPickUp() {
this.limit = 0;
this.limitationInterval = 0;
this.given = 0;
this.given.set(0);
this.rewardTime = 0;
this.uniqueRewards = false;
this.rewardItems.clear();
@@ -192,7 +192,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
this.limit = settings.getIntParams()[2];
this.limitationInterval = settings.getIntParams()[3];
this.userSource = settings.getIntParams()[4];
this.given = 0;
this.given.set(0);
String data = settings.getStringParam();
@@ -276,15 +276,15 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
}
public int getGiven() {
return this.given;
return this.given.get();
}
public void setGiven(int given) {
this.given = given;
this.given.set(given);
}
public void incrementGiven() {
this.given++;
this.given.incrementAndGet();
}
public int getRewardTime() {
@@ -303,11 +303,11 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
this.uniqueRewards = uniqueRewards;
}
public THashSet<WiredGiveRewardItem> getRewardItems() {
public List<WiredGiveRewardItem> getRewardItems() {
return this.rewardItems;
}
public void setRewardItems(THashSet<WiredGiveRewardItem> rewardItems) {
public void setRewardItems(List<WiredGiveRewardItem> rewardItems) {
this.rewardItems = rewardItems;
}
}
@@ -384,61 +384,69 @@ public final class WiredVariableReferenceSupport {
}
private static void upsertSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId, SharedUserAssignment assignment) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_user_wired_variables (room_id, user_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, userId);
statement.setInt(3, sourceVariableItemId);
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_user_wired_variables (room_id, user_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, userId);
statement.setInt(3, sourceVariableItemId);
if (assignment.getValue() == null) {
statement.setNull(4, java.sql.Types.INTEGER);
} else {
statement.setInt(4, assignment.getValue());
if (assignment.getValue() == null) {
statement.setNull(4, java.sql.Types.INTEGER);
} else {
statement.setInt(4, assignment.getValue());
}
statement.setInt(5, assignment.getCreatedAt());
statement.setInt(6, assignment.getUpdatedAt());
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to store shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
}
statement.setInt(5, assignment.getCreatedAt());
statement.setInt(6, assignment.getUpdatedAt());
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to store shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
}
});
}
private static void deleteSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ?")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, userId);
statement.setInt(3, sourceVariableItemId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to delete shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
}
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ?")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, userId);
statement.setInt(3, sourceVariableItemId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to delete shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
}
});
}
private static void upsertSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId, SharedRoomAssignment assignment) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_wired_variables (room_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, sourceVariableItemId);
statement.setInt(3, assignment.getValue());
statement.setInt(4, 0);
statement.setInt(5, assignment.getUpdatedAt());
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to store shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
}
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_wired_variables (room_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, sourceVariableItemId);
statement.setInt(3, assignment.getValue());
statement.setInt(4, 0);
statement.setInt(5, assignment.getUpdatedAt());
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to store shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
}
});
}
private static void deleteSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ?")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, sourceVariableItemId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to delete shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
}
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ?")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, sourceVariableItemId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to delete shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
}
});
}
private static String createDefinitionPrefix(int sourceRoomId, int sourceVariableItemId) {
@@ -123,7 +123,11 @@ public class WiredTriggerRepeaterLong extends InteractionWiredTrigger implements
@Override
public boolean saveData(WiredSettings settings) {
if (settings.getIntParams().length < 1) return false;
this.repeatTime = settings.getIntParams()[0] * 5000;
int interval = settings.getIntParams()[0];
if (interval < 1) {
interval = 1;
}
this.repeatTime = interval * 5000;
// No accumulated time reset needed - using global tick count
return true;
}
@@ -651,6 +651,10 @@ public class ModToolManager {
sender.getClient().sendResponse(new ModToolIssueHandledComposer(ModToolIssueHandledComposer.ABUSIVE));
}
// Reporter (the user who opened the CFH) gets their abusive
// counter bumped the legacy stat shown in the User Info table.
bumpUserSettingCounter(issue.senderId, "cfh_abusive");
this.updateTicketToMods(issue);
this.removeTicket(issue);
@@ -737,4 +741,38 @@ public class ModToolManager {
return issues;
}
/**
* Increments a single integer counter on `users_settings` for the
* given user. Used by the moderation sanction handlers to bump the
* legacy counters that `ModToolUserInfoComposer` surfaces (cfh_warnings,
* cfh_bans, cfh_abusive, tradelock_amount) historically these were
* only ever incremented by the CFH submission path, so a user could
* accumulate any number of bans/mutes without the User Info table
* reflecting it.
*
* Restricted to a whitelisted column name to keep the dynamic SQL
* safe; the caller passes a Permission-style constant.
*/
public static void bumpUserSettingCounter(int userId, String column) {
switch (column) {
case "cfh_warnings":
case "cfh_bans":
case "cfh_abusive":
case "tradelock_amount":
break;
default:
LOGGER.warn("Refusing to bump unrecognized user_settings column: {}", column);
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE users_settings SET " + column + " = " + column + " + 1 WHERE user_id = ?")) {
statement.setInt(1, userId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception bumping {} for user {}", column, userId, e);
}
}
}
@@ -115,19 +115,21 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
}
public final Object roomUnitLock = new Object();
public final ConcurrentHashMap<RoomTile, THashSet<HabboItem>> tileCache = new ConcurrentHashMap<>();
public final List<Integer> userVotes;
private final TIntArrayList rights;
private final TIntIntHashMap mutedHabbos;
private final TIntObjectHashMap<RoomBan> bannedHabbos;
private final Set<Game> games;
private final TIntObjectMap<RoomMoodlightData> moodlightData;
public volatile double lastCycleCpuMs = 0.0;
public volatile String lastCycleThread = "N/A";
private final Object loadLock = new Object();
//Use appropriately. Could potentially cause memory leaks when used incorrectly.
public volatile boolean preventUnloading = false;
public volatile boolean preventUncaching = false;
public Set<ServerMessage> scheduledComposers = ConcurrentHashMap.newKeySet();
public Set<Runnable> scheduledTasks = ConcurrentHashMap.newKeySet();
public final java.util.concurrent.ConcurrentLinkedQueue<Runnable> scheduledTasks = new java.util.concurrent.ConcurrentLinkedQueue<>();
public String wordQuiz = "";
public int noVotes = 0;
public int yesVotes = 0;
@@ -981,8 +983,6 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
this.scheduledTasks.clear();
this.scheduledComposers.clear();
this.tileCache.clear();
synchronized (this.mutedHabbos) {
this.mutedHabbos.clear();
}
@@ -1160,10 +1160,13 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
synchronized (this.loadLock) {
if (this.loaded) {
try {
long startTime = System.nanoTime();
this.lastCycleThread = Thread.currentThread().getName();
// Run cycle directly instead of scheduling on thread pool
// This ensures all cycle tasks in the same tick execute synchronously
// preventing wired desync issues
this.cycle();
this.lastCycleCpuMs = (System.nanoTime() - startTime) / 1000000.0;
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
@@ -2320,27 +2323,37 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
sanitizedInspectMask |= sanitizedModifyMask;
synchronized (this.wiredSettingsLock) {
int previousInspectMask = this.wiredInspectMask;
int previousModifyMask = this.wiredModifyMask;
final int finalInspectMask = sanitizedInspectMask;
final int finalModifyMask = sanitizedModifyMask;
final int finalId = this.id;
final int previousInspectMask = this.wiredInspectMask;
final int previousModifyMask = this.wiredModifyMask;
this.wiredInspectMask = sanitizedInspectMask;
this.wiredModifyMask = sanitizedModifyMask;
this.wiredSettingsLoaded = true;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO room_wired_settings (room_id, inspect_mask, modify_mask) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE inspect_mask = VALUES(inspect_mask), modify_mask = VALUES(modify_mask)")) {
statement.setInt(1, this.id);
statement.setInt(2, sanitizedInspectMask);
statement.setInt(3, sanitizedModifyMask);
statement.executeUpdate();
this.pushWiredSettingsToCurrentHabbos();
return true;
} catch (SQLException e) {
this.wiredInspectMask = previousInspectMask;
this.wiredModifyMask = previousModifyMask;
LOGGER.error("Caught SQL exception while saving wired room settings", e);
return false;
}
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO room_wired_settings (room_id, inspect_mask, modify_mask) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE inspect_mask = VALUES(inspect_mask), modify_mask = VALUES(modify_mask)")) {
statement.setInt(1, finalId);
statement.setInt(2, finalInspectMask);
statement.setInt(3, finalModifyMask);
statement.executeUpdate();
} catch (SQLException e) {
synchronized (this.wiredSettingsLock) {
if (this.wiredInspectMask == finalInspectMask && this.wiredModifyMask == finalModifyMask) {
this.wiredInspectMask = previousInspectMask;
this.wiredModifyMask = previousModifyMask;
}
}
LOGGER.error("Caught SQL exception while saving wired room settings", e);
}
});
this.pushWiredSettingsToCurrentHabbos();
return true;
}
}
@@ -2878,4 +2891,20 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
public Collection<RoomUnit> getRoomUnitsAt(RoomTile tile) {
return this.unitManager.getRoomUnitsAt(tile);
}
public long getEstimatedMemoryUsage() {
long bytes = 1024 * 10; // Base footprint
if (this.itemManager != null) {
bytes += this.itemManager.itemCount() * 512L;
}
bytes += this.getUserCount() * 2048L;
if (this.layout != null) {
bytes += this.layout.getMapSize() * 128L;
}
com.eu.habbo.habbohotel.wired.tick.WiredTickService wired = com.eu.habbo.habbohotel.wired.tick.WiredTickService.getInstance();
if (wired != null) {
bytes += wired.getTickableCount(this.getId()) * 256L;
}
return bytes;
}
}
@@ -313,27 +313,6 @@ public class RoomChatManager {
}
}
String wiredSayMessage = roomChatMessage.getMessage();
// Handle commands and wired
boolean suppressSaysOutput = false;
if (chatType != RoomChatType.WHISPER) {
if (CommandHandler.handleCommand(habbo.getClient(), roomChatMessage.getUnfilteredMessage())) {
WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), wiredSayMessage);
roomChatMessage.isCommand = true;
return;
}
if (!ignoreWired) {
suppressSaysOutput = WiredManager.shouldSuppressUserSaysOutput(
habbo.getHabboInfo().getCurrentRoom(),
habbo.getRoomUnit(),
wiredSayMessage,
chatType.ordinal(),
roomChatMessage.getBubble() != null ? roomChatMessage.getBubble().getType() : -1);
}
}
// Flood protection
if (!habbo.hasPermission(Permission.ACC_CHAT_NO_FLOOD)) {
final int chatCounter = habbo.getHabboStats().chatCounter.addAndGet(1);
@@ -357,6 +336,27 @@ public class RoomChatManager {
}
}
String wiredSayMessage = roomChatMessage.getMessage();
// Handle commands and wired
boolean suppressSaysOutput = false;
if (chatType != RoomChatType.WHISPER) {
if (CommandHandler.handleCommand(habbo.getClient(), roomChatMessage.getUnfilteredMessage())) {
WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), wiredSayMessage);
roomChatMessage.isCommand = true;
return;
}
if (!ignoreWired) {
suppressSaysOutput = WiredManager.shouldSuppressUserSaysOutput(
habbo.getHabboInfo().getCurrentRoom(),
habbo.getRoomUnit(),
wiredSayMessage,
chatType.ordinal(),
roomChatMessage.getBubble() != null ? roomChatMessage.getBubble().getType() : -1);
}
}
// Build prefix messages
ServerMessage prefixMessage = null;
@@ -615,6 +615,9 @@ public class RoomChatManager {
InteractionTalkingFurniture.class);
for (HabboItem item : items) {
if (item.getExtradata().equals("1")) {
continue;
}
if (this.room.getLayout().getTile(item.getX(), item.getY())
.distance(habbo.getRoomUnit().getCurrentLocation()) <= Emulator.getConfig()
.getInt("furniture.talking.range")) {
@@ -75,7 +75,6 @@ public class RoomCycleManager {
final boolean[] foundRightHolder = {false};
boolean loaded = this.room.isLoaded();
this.room.tileCache.clear();
if (loaded) {
processScheduledTasks();
@@ -164,13 +163,9 @@ public class RoomCycleManager {
* Processes scheduled tasks.
*/
private void processScheduledTasks() {
if (!this.room.scheduledTasks.isEmpty()) {
Set<Runnable> tasks = this.room.scheduledTasks;
this.room.scheduledTasks = ConcurrentHashMap.newKeySet();
for (Runnable runnable : tasks) {
Emulator.getThreading().run(runnable);
}
Runnable task;
while ((task = this.room.scheduledTasks.poll()) != null) {
Emulator.getThreading().run(task);
}
}
@@ -486,7 +481,7 @@ public class RoomCycleManager {
if (!unit.hasStatus(RoomUnitStatus.LAY)) {
BedProfile bedProfile = new BedProfile(topItem);
double layHeight = Item.getCurrentHeight(topItem) * 1.0D + bedProfile.getLayZOffset();
LOGGER.info("[BedProfile] item={} stackHeight={} isFlat={} isDouble={} X={} Y={} Z={}",
LOGGER.debug("[BedProfile] item={} stackHeight={} isFlat={} isDouble={} X={} Y={} Z={}",
topItem.getBaseItem().getName(), topItem.getBaseItem().getHeight(),
bedProfile.isFlat(), bedProfile.isDouble(),
bedProfile.getLayXOffset(), bedProfile.getLayYOffset(), bedProfile.getLayZOffset());
@@ -35,6 +35,7 @@ public class RoomFurniVariableManager {
private final Room room;
private final ConcurrentHashMap<Integer, ConcurrentHashMap<Integer, VariableAssignment>> activeAssignmentsByFurniId;
private volatile boolean permanentAssignmentsLoaded;
private final java.util.concurrent.atomic.AtomicBoolean broadcastRequested = new java.util.concurrent.atomic.AtomicBoolean(false);
public RoomFurniVariableManager(Room room) {
this.room = room;
@@ -591,7 +592,22 @@ public class RoomFurniVariableManager {
habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(this.room.getUserVariableManager().createSnapshot(), this.createSnapshot(), this.room.getRoomVariableManager().createSnapshot()));
}
public void requestBroadcast() {
if (this.broadcastRequested.compareAndSet(false, true)) {
Emulator.getThreading().run(() -> {
this.broadcastRequested.set(false);
if (this.room.isLoaded()) {
this.broadcastSnapshotRaw();
}
}, 50);
}
}
public void broadcastSnapshot() {
this.requestBroadcast();
}
public void broadcastSnapshotRaw() {
RoomUserVariableManager.Snapshot userSnapshot = this.room.getUserVariableManager().createSnapshot();
Snapshot furniSnapshot = this.createSnapshot();
RoomVariableManager.Snapshot roomSnapshot = this.room.getRoomVariableManager().createSnapshot();
@@ -148,55 +148,8 @@ public class RoomItemManager {
item = this.roomItems.get(id);
}
// Check special types if not found in main storage
RoomSpecialTypes specialTypes = this.room.getRoomSpecialTypes();
if (item == null) {
item = specialTypes.getBanzaiTeleporter(id);
}
if (item == null) {
item = specialTypes.getTrigger(id);
}
if (item == null) {
item = specialTypes.getEffect(id);
}
if (item == null) {
item = specialTypes.getCondition(id);
}
if (item == null) {
item = specialTypes.getGameGate(id);
}
if (item == null) {
item = specialTypes.getGameScorebord(id);
}
if (item == null) {
item = specialTypes.getGameTimer(id);
}
if (item == null) {
item = specialTypes.getFreezeExitTiles().get(id);
}
if (item == null) {
item = specialTypes.getRoller(id);
}
if (item == null) {
item = specialTypes.getNest(id);
}
if (item == null) {
item = specialTypes.getPetDrink(id);
}
if (item == null) {
item = specialTypes.getPetFood(id);
item = this.room.getRoomSpecialTypes().getSpecialItem(id);
}
return item;
@@ -726,7 +679,7 @@ public class RoomItemManager {
item instanceof WiredBlob ||
item instanceof InteractionTent ||
item instanceof InteractionSnowboardSlope ||
item instanceof InteractionFireworks) {
item instanceof InteractionFireworks || item instanceof InteractionVoteCounter) {
specialTypes.addUndefined(item);
}
}
@@ -899,7 +852,7 @@ public class RoomItemManager {
item instanceof InteractionStickyPole ||
item instanceof WiredBlob ||
item instanceof InteractionTent ||
item instanceof InteractionSnowboardSlope) {
item instanceof InteractionSnowboardSlope || item instanceof InteractionVoteCounter) {
specialTypes.removeUndefined(item);
}
@@ -71,6 +71,7 @@ public class RoomSpecialTypes {
private final THashMap<Integer, InteractionFreezeExitTile> freezeExitTile;
private final THashMap<Integer, HabboItem> undefined;
private final Set<ICycleable> cycleTasks;
private final ConcurrentHashMap<Integer, HabboItem> specialItemsById = new ConcurrentHashMap<>();
public RoomSpecialTypes() {
this.banzaiTeleporters = new THashMap<>(0);
@@ -115,11 +116,11 @@ public class RoomSpecialTypes {
}
public void addBanzaiTeleporter(InteractionBattleBanzaiTeleporter item) {
this.banzaiTeleporters.put(item.getId(), item);
this.banzaiTeleporters.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
}
public void removeBanzaiTeleporter(InteractionBattleBanzaiTeleporter item) {
this.banzaiTeleporters.remove(item.getId());
this.banzaiTeleporters.remove(item.getId()); this.specialItemsById.remove(item.getId());
}
public THashSet<InteractionBattleBanzaiTeleporter> getBanzaiTeleporters() {
@@ -155,11 +156,11 @@ public class RoomSpecialTypes {
}
public void addNest(InteractionNest item) {
this.nests.put(item.getId(), item);
this.nests.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
}
public void removeNest(InteractionNest item) {
this.nests.remove(item.getId());
this.nests.remove(item.getId()); this.specialItemsById.remove(item.getId());
}
public THashSet<InteractionNest> getNests() {
@@ -177,11 +178,11 @@ public class RoomSpecialTypes {
}
public void addPetDrink(InteractionPetDrink item) {
this.petDrinks.put(item.getId(), item);
this.petDrinks.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
}
public void removePetDrink(InteractionPetDrink item) {
this.petDrinks.remove(item.getId());
this.petDrinks.remove(item.getId()); this.specialItemsById.remove(item.getId());
}
public THashSet<InteractionPetDrink> getPetDrinks() {
@@ -199,11 +200,11 @@ public class RoomSpecialTypes {
}
public void addPetFood(InteractionPetFood item) {
this.petFoods.put(item.getId(), item);
this.petFoods.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
}
public void removePetFood(InteractionPetFood petFood) {
this.petFoods.remove(petFood.getId());
this.petFoods.remove(petFood.getId()); this.specialItemsById.remove(petFood.getId());
}
public THashSet<InteractionPetFood> getPetFoods() {
@@ -221,11 +222,11 @@ public class RoomSpecialTypes {
}
public void addPetToy(InteractionPetToy item) {
this.petToys.put(item.getId(), item);
this.petToys.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
}
public void removePetToy(InteractionPetToy petToy) {
this.petToys.remove(petToy.getId());
this.petToys.remove(petToy.getId()); this.specialItemsById.remove(petToy.getId());
}
public THashSet<InteractionPetToy> getPetToys() {
@@ -243,11 +244,11 @@ public class RoomSpecialTypes {
}
public void addPetTree(InteractionPetTree item) {
this.petTrees.put(item.getId(), item);
this.petTrees.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
}
public void removePetTree(InteractionPetTree petTree) {
this.petTrees.remove(petTree.getId());
this.petTrees.remove(petTree.getId()); this.specialItemsById.remove(petTree.getId());
}
public THashSet<InteractionPetTree> getPetTrees() {
@@ -270,12 +271,14 @@ public class RoomSpecialTypes {
synchronized (this.rollers) {
this.rollers.put(item.getId(), item);
}
this.specialItemsById.put(item.getId(), item);
}
public void removeRoller(InteractionRoller roller) {
synchronized (this.rollers) {
this.rollers.remove(roller.getId());
}
this.specialItemsById.remove(roller.getId());
}
public THashMap<Integer, InteractionRoller> getRollers() {
@@ -469,11 +472,11 @@ public class RoomSpecialTypes {
// Add to type-based index
this.wiredTriggers.computeIfAbsent(trigger.getType(), k -> ConcurrentHashMap.newKeySet())
.add(trigger);
// Add to spatial index
long key = coordinateKey(trigger.getX(), trigger.getY());
this.wiredTriggersByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
.add(trigger);
this.specialItemsById.put(trigger.getId(), trigger);
}
/**
@@ -489,7 +492,6 @@ public class RoomSpecialTypes {
this.wiredTriggers.remove(trigger.getType());
}
}
// Remove from spatial index
long key = coordinateKey(trigger.getX(), trigger.getY());
Set<InteractionWiredTrigger> locationTriggers = this.wiredTriggersByLocation.get(key);
@@ -499,6 +501,7 @@ public class RoomSpecialTypes {
this.wiredTriggersByLocation.remove(key);
}
}
this.specialItemsById.remove(trigger.getId());
}
/**
@@ -589,11 +592,11 @@ public class RoomSpecialTypes {
// Add to type-based index
this.wiredEffects.computeIfAbsent(effect.getType(), k -> ConcurrentHashMap.newKeySet())
.add(effect);
// Add to spatial index
long key = coordinateKey(effect.getX(), effect.getY());
this.wiredEffectsByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
.add(effect);
this.specialItemsById.put(effect.getId(), effect);
}
/**
@@ -609,7 +612,6 @@ public class RoomSpecialTypes {
this.wiredEffects.remove(effect.getType());
}
}
// Remove from spatial index
long key = coordinateKey(effect.getX(), effect.getY());
Set<InteractionWiredEffect> locationEffects = this.wiredEffectsByLocation.get(key);
@@ -619,6 +621,7 @@ public class RoomSpecialTypes {
this.wiredEffectsByLocation.remove(key);
}
}
this.specialItemsById.remove(effect.getId());
}
/**
@@ -709,11 +712,11 @@ public class RoomSpecialTypes {
// Add to type-based index
this.wiredConditions.computeIfAbsent(condition.getType(), k -> ConcurrentHashMap.newKeySet())
.add(condition);
// Add to spatial index
long key = coordinateKey(condition.getX(), condition.getY());
this.wiredConditionsByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
.add(condition);
this.specialItemsById.put(condition.getId(), condition);
}
/**
@@ -729,7 +732,6 @@ public class RoomSpecialTypes {
this.wiredConditions.remove(condition.getType());
}
}
// Remove from spatial index
long key = coordinateKey(condition.getX(), condition.getY());
Set<InteractionWiredCondition> locationConditions = this.wiredConditionsByLocation.get(key);
@@ -739,6 +741,7 @@ public class RoomSpecialTypes {
this.wiredConditionsByLocation.remove(key);
}
}
this.specialItemsById.remove(condition.getId());
}
/**
@@ -805,11 +808,11 @@ public class RoomSpecialTypes {
*/
public void addExtra(InteractionWiredExtra extra) {
this.wiredExtras.put(extra.getId(), extra);
// Add to spatial index
long key = coordinateKey(extra.getX(), extra.getY());
this.wiredExtrasByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
.add(extra);
this.specialItemsById.put(extra.getId(), extra);
}
/**
@@ -818,7 +821,6 @@ public class RoomSpecialTypes {
*/
public void removeExtra(InteractionWiredExtra extra) {
this.wiredExtras.remove(extra.getId());
// Remove from spatial index
long key = coordinateKey(extra.getX(), extra.getY());
Set<InteractionWiredExtra> locationExtras = this.wiredExtrasByLocation.get(key);
@@ -828,6 +830,7 @@ public class RoomSpecialTypes {
this.wiredExtrasByLocation.remove(key);
}
}
this.specialItemsById.remove(extra.getId());
}
/**
@@ -880,11 +883,11 @@ public class RoomSpecialTypes {
}
public void addGameScoreboard(InteractionGameScoreboard scoreboard) {
this.gameScoreboards.put(scoreboard.getId(), scoreboard);
this.gameScoreboards.put(scoreboard.getId(), scoreboard); this.specialItemsById.put(scoreboard.getId(), scoreboard);
}
public void removeScoreboard(InteractionGameScoreboard scoreboard) {
this.gameScoreboards.remove(scoreboard.getId());
this.gameScoreboards.remove(scoreboard.getId()); this.specialItemsById.remove(scoreboard.getId());
}
public THashMap<Integer, InteractionFreezeScoreboard> getFreezeScoreboards() {
@@ -980,11 +983,11 @@ public class RoomSpecialTypes {
}
public void addGameGate(InteractionGameGate gameGate) {
this.gameGates.put(gameGate.getId(), gameGate);
this.gameGates.put(gameGate.getId(), gameGate); this.specialItemsById.put(gameGate.getId(), gameGate);
}
public void removeGameGate(InteractionGameGate gameGate) {
this.gameGates.remove(gameGate.getId());
this.gameGates.remove(gameGate.getId()); this.specialItemsById.remove(gameGate.getId());
}
public THashMap<Integer, InteractionFreezeGate> getFreezeGates() {
@@ -1021,11 +1024,11 @@ public class RoomSpecialTypes {
}
public void addGameTimer(InteractionGameTimer gameTimer) {
this.gameTimers.put(gameTimer.getId(), gameTimer);
this.gameTimers.put(gameTimer.getId(), gameTimer); this.specialItemsById.put(gameTimer.getId(), gameTimer);
}
public void removeGameTimer(InteractionGameTimer gameTimer) {
this.gameTimers.remove(gameTimer.getId());
this.gameTimers.remove(gameTimer.getId()); this.specialItemsById.remove(gameTimer.getId());
}
public THashMap<Integer, InteractionGameTimer> getGameTimers() {
@@ -1043,7 +1046,7 @@ public class RoomSpecialTypes {
}
public void addFreezeExitTile(InteractionFreezeExitTile freezeExitTile) {
this.freezeExitTile.put(freezeExitTile.getId(), freezeExitTile);
this.freezeExitTile.put(freezeExitTile.getId(), freezeExitTile); this.specialItemsById.put(freezeExitTile.getId(), freezeExitTile);
}
public THashMap<Integer, InteractionFreezeExitTile> getFreezeExitTiles() {
@@ -1051,7 +1054,7 @@ public class RoomSpecialTypes {
}
public void removeFreezeExitTile(InteractionFreezeExitTile freezeExitTile) {
this.freezeExitTile.remove(freezeExitTile.getId());
this.freezeExitTile.remove(freezeExitTile.getId()); this.specialItemsById.remove(freezeExitTile.getId());
}
public boolean hasFreezeExitTile() {
@@ -1062,12 +1065,14 @@ public class RoomSpecialTypes {
synchronized (this.undefined) {
this.undefined.put(item.getId(), item);
}
this.specialItemsById.put(item.getId(), item);
}
public void removeUndefined(HabboItem item) {
synchronized (this.undefined) {
this.undefined.remove(item.getId());
}
this.specialItemsById.remove(item.getId());
}
public THashSet<HabboItem> getItemsOfType(Class<? extends HabboItem> type) {
@@ -1130,6 +1135,10 @@ public class RoomSpecialTypes {
this.cycleTasks.remove(task);
}
public HabboItem getSpecialItem(int itemId) {
return this.specialItemsById.get(itemId);
}
public synchronized void dispose() {
this.banzaiTeleporters.clear();
this.nests.clear();
@@ -1142,6 +1151,7 @@ public class RoomSpecialTypes {
this.wiredTriggers.clear();
this.wiredEffects.clear();
this.wiredConditions.clear();
this.wiredExtras.clear();
this.gameScoreboards.clear();
this.gameGates.clear();
@@ -1150,6 +1160,7 @@ public class RoomSpecialTypes {
this.freezeExitTile.clear();
this.undefined.clear();
this.cycleTasks.clear();
this.specialItemsById.clear();
}
public Rectangle tentAt(RoomTile location) {
@@ -29,7 +29,6 @@ public class RoomTileManager {
*/
public void updateTile(RoomTile tile) {
if (tile != null) {
this.room.tileCache.remove(tile);
this.room.getItemManager().tileCache.remove(tile);
tile.setStackHeight(this.getStackHeight(tile.x, tile.y, false));
tile.setState(this.calculateTileState(tile));
@@ -41,7 +40,6 @@ public class RoomTileManager {
*/
public void updateTiles(THashSet<RoomTile> tiles) {
for (RoomTile tile : tiles) {
this.room.tileCache.remove(tile);
this.room.getItemManager().tileCache.remove(tile);
tile.setStackHeight(this.getStackHeight(tile.x, tile.y, false));
tile.setState(this.calculateTileState(tile));
@@ -71,6 +71,24 @@ public class RoomUnitManager {
*/
public void clear() {
synchronized (this.room.roomUnitLock) {
for (Habbo habbo : this.currentHabbos.values()) {
if (habbo.getRoomUnit() != null) {
WiredMoveCarryHelper.cleanupRoomUnit(habbo.getRoomUnit());
WiredUserMovementHelper.cleanupRoomUnit(habbo.getRoomUnit());
}
}
for (Bot bot : this.currentBots.valueCollection()) {
if (bot.getRoomUnit() != null) {
WiredMoveCarryHelper.cleanupRoomUnit(bot.getRoomUnit());
WiredUserMovementHelper.cleanupRoomUnit(bot.getRoomUnit());
}
}
for (Pet pet : this.currentPets.valueCollection()) {
if (pet.getRoomUnit() != null) {
WiredMoveCarryHelper.cleanupRoomUnit(pet.getRoomUnit());
WiredUserMovementHelper.cleanupRoomUnit(pet.getRoomUnit());
}
}
this.unitCounter = 0;
this.currentHabbos.clear();
this.currentPets.clear();
@@ -222,6 +240,8 @@ public class RoomUnitManager {
}
if (habbo.getRoomUnit() != null) {
WiredMoveCarryHelper.cleanupRoomUnit(habbo.getRoomUnit());
WiredUserMovementHelper.cleanupRoomUnit(habbo.getRoomUnit());
WiredManager.triggerUserLeavesRoom(this.room, habbo.getRoomUnit());
if (WiredFreezeUtil.isFrozen(habbo.getRoomUnit())) {
WiredFreezeUtil.unfreeze(this.room, habbo.getRoomUnit());
@@ -646,14 +666,22 @@ public class RoomUnitManager {
public boolean removeBot(Bot bot) {
synchronized (this.currentBots) {
if (this.currentBots.containsKey(bot.getId())) {
if (bot.getRoomUnit() != null && bot.getRoomUnit().getCurrentLocation() != null) {
bot.getRoomUnit().getCurrentLocation().removeUnit(bot.getRoomUnit());
if (bot.getRoomUnit() != null) {
WiredMoveCarryHelper.cleanupRoomUnit(bot.getRoomUnit());
WiredUserMovementHelper.cleanupRoomUnit(bot.getRoomUnit());
if (bot.getRoomUnit().getCurrentLocation() != null) {
bot.getRoomUnit().getCurrentLocation().removeUnit(bot.getRoomUnit());
}
}
this.currentBots.remove(bot.getId());
bot.getRoomUnit().setInRoom(false);
if (bot.getRoomUnit() != null) {
bot.getRoomUnit().setInRoom(false);
}
bot.setRoom(null);
this.room.sendComposer(new RoomUserRemoveComposer(bot.getRoomUnit()).compose());
if (bot.getRoomUnit() != null) {
this.room.sendComposer(new RoomUserRemoveComposer(bot.getRoomUnit()).compose());
}
bot.setRoomUnit(null);
return true;
}
@@ -876,7 +904,12 @@ public class RoomUnitManager {
* Removes a Pet from the room.
*/
public Pet removePet(int petId) {
return this.currentPets.remove(petId);
Pet pet = this.currentPets.remove(petId);
if (pet != null && pet.getRoomUnit() != null) {
WiredMoveCarryHelper.cleanupRoomUnit(pet.getRoomUnit());
WiredUserMovementHelper.cleanupRoomUnit(pet.getRoomUnit());
}
return pet;
}
/**
@@ -1454,6 +1487,24 @@ public class RoomUnitManager {
}
public void dispose() {
for (Habbo habbo : this.currentHabbos.values()) {
if (habbo.getRoomUnit() != null) {
WiredMoveCarryHelper.cleanupRoomUnit(habbo.getRoomUnit());
WiredUserMovementHelper.cleanupRoomUnit(habbo.getRoomUnit());
}
}
for (Bot bot : this.currentBots.valueCollection()) {
if (bot.getRoomUnit() != null) {
WiredMoveCarryHelper.cleanupRoomUnit(bot.getRoomUnit());
WiredUserMovementHelper.cleanupRoomUnit(bot.getRoomUnit());
}
}
for (Pet pet : this.currentPets.valueCollection()) {
if (pet.getRoomUnit() != null) {
WiredMoveCarryHelper.cleanupRoomUnit(pet.getRoomUnit());
WiredUserMovementHelper.cleanupRoomUnit(pet.getRoomUnit());
}
}
this.currentHabbos.clear();
this.currentBots.clear();
this.currentPets.clear();
@@ -35,6 +35,7 @@ public class RoomUserVariableManager {
private final Room room;
private final ConcurrentHashMap<Integer, ConcurrentHashMap<Integer, VariableAssignment>> activeAssignmentsByUserId;
private final java.util.concurrent.atomic.AtomicBoolean broadcastRequested = new java.util.concurrent.atomic.AtomicBoolean(false);
public RoomUserVariableManager(Room room) {
this.room = room;
@@ -660,7 +661,22 @@ public class RoomUserVariableManager {
habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(this.createSnapshot(), this.room.getFurniVariableManager().createSnapshot(), this.room.getRoomVariableManager().createSnapshot()));
}
public void requestBroadcast() {
if (this.broadcastRequested.compareAndSet(false, true)) {
Emulator.getThreading().run(() -> {
this.broadcastRequested.set(false);
if (this.room.isLoaded()) {
this.broadcastSnapshotRaw();
}
}, 50);
}
}
public void broadcastSnapshot() {
this.requestBroadcast();
}
public void broadcastSnapshotRaw() {
Snapshot userSnapshot = this.createSnapshot();
RoomFurniVariableManager.Snapshot furniSnapshot = this.room.getFurniVariableManager().createSnapshot();
RoomVariableManager.Snapshot roomSnapshot = this.room.getRoomVariableManager().createSnapshot();
@@ -35,6 +35,7 @@ public class RoomVariableManager {
private final Room room;
private final ConcurrentHashMap<Integer, VariableAssignment> activeAssignmentsByDefinitionId;
private volatile boolean persistentValuesLoaded;
private final java.util.concurrent.atomic.AtomicBoolean broadcastRequested = new java.util.concurrent.atomic.AtomicBoolean(false);
public RoomVariableManager(Room room) {
this.room = room;
@@ -433,7 +434,22 @@ public class RoomVariableManager {
habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(this.room.getUserVariableManager().createSnapshot(), this.room.getFurniVariableManager().createSnapshot(), this.createSnapshot()));
}
public void requestBroadcast() {
if (this.broadcastRequested.compareAndSet(false, true)) {
Emulator.getThreading().run(() -> {
this.broadcastRequested.set(false);
if (this.room.isLoaded()) {
this.broadcastSnapshotRaw();
}
}, 50);
}
}
public void broadcastSnapshot() {
this.requestBroadcast();
}
public void broadcastSnapshotRaw() {
RoomUserVariableManager.Snapshot userSnapshot = this.room.getUserVariableManager().createSnapshot();
RoomFurniVariableManager.Snapshot furniSnapshot = this.room.getFurniVariableManager().createSnapshot();
Snapshot roomSnapshot = this.createSnapshot();
@@ -46,6 +46,7 @@ public class HabboInfo implements Runnable {
private int InfostandStand;
private int InfostandOverlay;
private int InfostandCardBg;
private int InfostandBorder;
private int loadingRoom;
private Room currentRoom;
private String roomEntryMethod = "door";
@@ -93,6 +94,11 @@ public class HabboInfo implements Runnable {
this.InfostandStand = set.getInt("background_stand_id");
this.InfostandOverlay = set.getInt("background_overlay_id");
this.InfostandCardBg = set.getInt("background_card_id");
try {
this.InfostandBorder = set.getInt("background_border_id");
} catch (SQLException ignored) {
this.InfostandBorder = 0;
}
this.currentRoom = null;
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
@@ -300,6 +306,15 @@ public class HabboInfo implements Runnable {
public void setInfostandCardBg(int infostandCardBg) {
InfostandCardBg = infostandCardBg;
}
public int getInfostandBorder() {
return InfostandBorder;
}
public void setInfostandBorder(int infostandBorder) {
InfostandBorder = infostandBorder;
}
public Rank getRank() {
return this.rank;
}
@@ -587,7 +602,7 @@ public class HabboInfo implements Runnable {
try {
SqlQueries.update(
"UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ?, background_card_id = ? WHERE id = ?",
"UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ?, background_card_id = ?, background_border_id = ? WHERE id = ?",
this.motto,
this.online ? "1" : "0",
this.look,
@@ -604,6 +619,7 @@ public class HabboInfo implements Runnable {
this.InfostandStand,
this.InfostandOverlay,
this.InfostandCardBg,
this.InfostandBorder,
this.id);
} catch (SqlQueries.DataAccessException e) {
LOGGER.error("Caught SQL exception", e);
@@ -38,6 +38,7 @@ public class HabboManager {
private final ConcurrentHashMap<Integer, Habbo> onlineHabbos;
private final ConcurrentHashMap<String, Habbo> onlineHabbosByName;
private final ConcurrentHashMap<Integer, String> usernameCache = new ConcurrentHashMap<>();
public HabboManager() {
long millis = System.currentTimeMillis();
@@ -158,6 +159,26 @@ public class HabboManager {
return this.getHabbo(id).getHabboInfo();
}
public String getCachedUsername(int id) {
String cached = this.usernameCache.get(id);
if (cached != null) return cached;
Habbo online = this.getHabbo(id);
if (online != null) {
String name = online.getHabboInfo().getUsername();
this.usernameCache.put(id, name);
return name;
}
HabboInfo offline = getOfflineHabboInfo(id);
if (offline != null) {
String name = offline.getUsername();
this.usernameCache.put(id, name);
return name;
}
return "Unknown";
}
public int getOnlineCount() {
return this.onlineHabbos.size();
}
@@ -24,7 +24,8 @@ public class InfostandBackgroundManager {
BACKGROUND("background"),
STAND("stand"),
OVERLAY("overlay"),
CARD("card");
CARD("card"),
BORDER("border");
public final String dbValue;
@@ -89,11 +90,12 @@ public class InfostandBackgroundManager {
this.enforce = loaded > 0;
if (this.enforce) {
LOGGER.info("InfostandBackgroundManager -> Loaded {} backgrounds, {} stands, {} overlays, {} cards from infostand_backgrounds.",
LOGGER.info("InfostandBackgroundManager -> Loaded {} backgrounds, {} stands, {} overlays, {} cards, {} borders from infostand_backgrounds.",
this.entries.get(Category.BACKGROUND).size(),
this.entries.get(Category.STAND).size(),
this.entries.get(Category.OVERLAY).size(),
this.entries.get(Category.CARD).size());
this.entries.get(Category.CARD).size(),
this.entries.get(Category.BORDER).size());
} else {
LOGGER.info("InfostandBackgroundManager -> infostand_backgrounds is empty, server-side validation disabled (only range clamp will apply).");
}
@@ -43,6 +43,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -89,11 +90,11 @@ public class WiredHandler {
long millis = System.currentTimeMillis();
List<LegacyExecutionPlan> executionPlans = new ArrayList<>();
List<RoomTile> triggeredTiles = new ArrayList<>();
LinkedHashSet<Long> triggeredTiles = new LinkedHashSet<>();
for (InteractionWiredTrigger trigger : triggers) {
RoomTile tile = room.getLayout().getTile(trigger.getX(), trigger.getY());
long coordinateKey = toTileCoordinateKey(trigger.getX(), trigger.getY());
if (triggeredTiles.contains(tile))
if (!triggeredTiles.add(coordinateKey))
continue;
LegacyExecutionPlan executionPlan = new LegacyExecutionPlan();
@@ -103,8 +104,6 @@ public class WiredHandler {
if (triggerType.equals(WiredTriggerType.SAY_SOMETHING))
talked = true;
triggeredTiles.add(tile);
}
}
@@ -139,20 +138,19 @@ public class WiredHandler {
long millis = System.currentTimeMillis();
List<LegacyExecutionPlan> executionPlans = new ArrayList<>();
List<RoomTile> triggeredTiles = new ArrayList<>();
LinkedHashSet<Long> triggeredTiles = new LinkedHashSet<>();
for (InteractionWiredTrigger trigger : triggers) {
if (trigger.getClass() != triggerType) continue;
RoomTile tile = room.getLayout().getTile(trigger.getX(), trigger.getY());
long coordinateKey = toTileCoordinateKey(trigger.getX(), trigger.getY());
if (triggeredTiles.contains(tile))
if (!triggeredTiles.add(coordinateKey))
continue;
LegacyExecutionPlan executionPlan = new LegacyExecutionPlan();
if (handle(trigger, roomUnit, room, stuff, executionPlan)) {
executionPlans.add(executionPlan);
triggeredTiles.add(tile);
}
}
@@ -187,6 +185,11 @@ public class WiredHandler {
WiredExtraExecutionLimit executionLimitExtra = null;
WiredExtraRandom randomExtra = null;
int conditionEvaluationMode = WiredExtraOrEval.MODE_ALL;
int conditionEvaluationValue = 1;
boolean hasExtraUnseen = false;
boolean hasExtraExecuteInOrder = false;
for (InteractionWiredExtra extra : extras) {
if (executionLimitExtra == null && extra instanceof WiredExtraExecutionLimit) {
executionLimitExtra = (WiredExtraExecutionLimit) extra;
@@ -195,18 +198,22 @@ public class WiredHandler {
if (randomExtra == null && extra instanceof WiredExtraRandom) {
randomExtra = (WiredExtraRandom) extra;
}
if (!hasExtraUnseen && extra instanceof WiredExtraUnseen) {
hasExtraUnseen = true;
}
if (!hasExtraExecuteInOrder && extra instanceof WiredExtraExecuteInOrder) {
hasExtraExecuteInOrder = true;
}
if (extra instanceof WiredExtraOrEval) {
conditionEvaluationMode = ((WiredExtraOrEval) extra).getEvaluationMode();
conditionEvaluationValue = ((WiredExtraOrEval) extra).getCompareValue();
}
}
if (!conditions.isEmpty()) {
int conditionEvaluationMode = WiredExtraOrEval.MODE_ALL;
int conditionEvaluationValue = 1;
for (InteractionWiredExtra extra : extras) {
if (extra instanceof WiredExtraOrEval) {
conditionEvaluationMode = ((WiredExtraOrEval) extra).getEvaluationMode();
conditionEvaluationValue = ((WiredExtraOrEval) extra).getCompareValue();
break;
}
}
if (!evaluateConditions(conditions, roomUnit, room, stuff, conditionEvaluationMode, conditionEvaluationValue)) {
for (InteractionWiredCondition condition : conditions) {
@@ -230,9 +237,6 @@ public class WiredHandler {
trigger.setCooldown(millis);
boolean hasExtraUnseen = room.getRoomSpecialTypes().hasExtraType(trigger.getX(), trigger.getY(), WiredExtraUnseen.class);
boolean hasExtraExecuteInOrder = room.getRoomSpecialTypes().hasExtraType(trigger.getX(), trigger.getY(), WiredExtraExecuteInOrder.class);
for (InteractionWiredExtra extra : extras) {
extra.activateBox(room, roomUnit, millis);
}
@@ -244,7 +248,7 @@ public class WiredHandler {
executionPlan.executeInOrder = hasExtraExecuteInOrder;
if (hasExtraUnseen) {
for (InteractionWiredExtra extra : room.getRoomSpecialTypes().getExtras(trigger.getX(), trigger.getY())) {
for (InteractionWiredExtra extra : extras) {
if (extra instanceof WiredExtraUnseen) {
extra.setExtradata(extra.getExtradata().equals("1") ? "0" : "1");
InteractionWiredEffect effect = ((WiredExtraUnseen) extra).getUnseenEffect(effectList);
@@ -357,20 +361,14 @@ public class WiredHandler {
}
}
LinkedHashSet<Integer> delays = new LinkedHashSet<>();
Map<Integer, List<InteractionWiredEffect>> delayBatches = new LinkedHashMap<>();
for (InteractionWiredEffect effect : queueableEffects) {
delays.add(effect.getDelay());
delayBatches.computeIfAbsent(effect.getDelay(), ignored -> new ArrayList<>()).add(effect);
}
for (Integer delay : delays) {
List<InteractionWiredEffect> delayBatch = new ArrayList<>();
for (InteractionWiredEffect effect : queueableEffects) {
if (effect.getDelay() == delay) {
delayBatch.add(effect);
}
}
for (Map.Entry<Integer, List<InteractionWiredEffect>> entry : delayBatches.entrySet()) {
Integer delay = entry.getKey();
List<InteractionWiredEffect> delayBatch = entry.getValue();
if (delayBatch.isEmpty()) {
continue;
}
@@ -424,11 +422,19 @@ public class WiredHandler {
public static GsonBuilder getGsonBuilder() {
if(gsonBuilder == null) {
gsonBuilder = new GsonBuilder();
synchronized (WiredHandler.class) {
if (gsonBuilder == null) {
gsonBuilder = new GsonBuilder();
}
}
}
return gsonBuilder;
}
private static long toTileCoordinateKey(int x, int y) {
return (((long) x) << 32) | (y & 0xffffffffL);
}
public static boolean executeEffectsAtTiles(THashSet<RoomTile> tiles, final RoomUnit roomUnit, final Room room, final Object[] stuff) {
for (RoomTile tile : tiles) {
if (room != null) {
@@ -470,7 +476,7 @@ public class WiredHandler {
private static void completeReward(Habbo habbo, WiredEffectGiveReward wiredBox, WiredGiveRewardItem reward, int successCode) {
if (wiredBox.limit > 0)
wiredBox.given++;
wiredBox.incrementGiven();
persistReward(wiredBox.getId(), habbo.getHabboInfo().getId(), reward.id, Emulator.getIntUnixTimestamp());
habbo.getClient().sendResponse(new WiredRewardAlertComposer(successCode));
@@ -569,93 +575,124 @@ public class WiredHandler {
public static boolean getReward(Habbo habbo, WiredEffectGiveReward wiredBox) {
if (wiredBox.limit > 0) {
if (wiredBox.limit - wiredBox.given == 0) {
if (wiredBox.limit - wiredBox.getGiven() == 0) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.LIMITED_NO_MORE_AVAILABLE));
return false;
}
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) as row_count, wired_rewards_given.* FROM wired_rewards_given WHERE user_id = ? AND wired_item = ? ORDER BY timestamp DESC LIMIT ?", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)) {
WiredGiveRewardItem rewardToGive = null;
int failureCode = -1;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM wired_rewards_given WHERE user_id = ? AND wired_item = ? ORDER BY timestamp DESC LIMIT ?", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)) {
statement.setInt(1, habbo.getHabboInfo().getId());
statement.setInt(2, wiredBox.getId());
statement.setInt(3, wiredBox.rewardItems.size());
try (ResultSet set = statement.executeQuery()) {
if (set.first()) {
if (set.getInt("row_count") >= 1) {
set.last();
int rowCount = set.getRow();
set.first();
if (rowCount >= 1) {
if (wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_ONCE) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED));
return false;
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED;
}
}
set.beforeFirst();
if (set.next()) {
if (failureCode == -1) {
if (wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_N_MINUTES) {
if (Emulator.getIntUnixTimestamp() - set.getInt("timestamp") <= 60) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_MINUTE));
return false;
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_MINUTE;
}
}
if (wiredBox.uniqueRewards) {
if (set.getInt("row_count") == wiredBox.rewardItems.size()) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALL_COLLECTED));
return false;
if (failureCode == -1 && wiredBox.uniqueRewards) {
if (rowCount == wiredBox.rewardItems.size()) {
failureCode = WiredRewardAlertComposer.REWARD_ALL_COLLECTED;
}
}
if (wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_N_HOURS) {
if (failureCode == -1 && wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_N_HOURS) {
if (!(Emulator.getIntUnixTimestamp() - set.getInt("timestamp") >= (3600 * wiredBox.limitationInterval))) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_HOUR));
return false;
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_HOUR;
}
}
if (wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_N_DAY) {
if (failureCode == -1 && wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_N_DAY) {
if (!(Emulator.getIntUnixTimestamp() - set.getInt("timestamp") >= (86400 * wiredBox.limitationInterval))) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_TODAY));
return false;
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_TODAY;
}
}
}
if (wiredBox.uniqueRewards) {
for (WiredGiveRewardItem item : wiredBox.rewardItems) {
set.beforeFirst();
boolean found = false;
if (failureCode == -1) {
if (wiredBox.uniqueRewards) {
for (WiredGiveRewardItem item : wiredBox.rewardItems) {
set.beforeFirst();
boolean found = false;
while (set.next()) {
if (set.getInt("reward_id") == item.id)
found = true;
while (set.next()) {
if (set.getInt("reward_id") == item.id)
found = true;
}
if (!found) {
rewardToGive = item;
break;
}
}
if (!found) {
return giveReward(habbo, wiredBox, item);
if (rewardToGive == null) {
failureCode = WiredRewardAlertComposer.REWARD_ALL_COLLECTED;
}
}
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALL_COLLECTED));
return false;
} else {
int randomNumber = Emulator.getRandom().nextInt(101);
int count = 0;
for (WiredGiveRewardItem item : wiredBox.rewardItems) {
if (randomNumber >= count && randomNumber <= (count + item.probability)) {
return giveReward(habbo, wiredBox, item);
}
count += item.probability;
}
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.UNLUCKY_NO_REWARD));
return false;
}
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
return false;
}
if (failureCode != -1) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(failureCode));
return false;
}
// If no unique reward was determined and there are no failures, pick a random reward or the first unique one
if (rewardToGive == null) {
if (wiredBox.uniqueRewards) {
if (!wiredBox.rewardItems.isEmpty()) {
rewardToGive = wiredBox.rewardItems.get(0);
} else {
failureCode = WiredRewardAlertComposer.REWARD_ALL_COLLECTED;
}
} else {
int randomNumber = Emulator.getRandom().nextInt(101);
int count = 0;
for (WiredGiveRewardItem item : wiredBox.rewardItems) {
if (randomNumber >= count && randomNumber <= (count + item.probability)) {
rewardToGive = item;
break;
}
count += item.probability;
}
if (rewardToGive == null) {
failureCode = WiredRewardAlertComposer.UNLUCKY_NO_REWARD;
}
}
}
if (failureCode != -1) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(failureCode));
return false;
}
if (rewardToGive != null) {
return giveReward(habbo, wiredBox, rewardToGive);
}
return false;
@@ -71,27 +71,12 @@ public final class RoomWiredStackIndex implements WiredStackIndex {
return Collections.emptyList();
}
// Check cache first
if (useCache) {
Map<WiredEvent.Type, List<WiredStack>> roomCache = cache.get(room.getId());
if (roomCache != null) {
List<WiredStack> cached = roomCache.get(type);
if (cached != null) {
return cached;
}
}
return cache.computeIfAbsent(room.getId(), k -> new ConcurrentHashMap<>())
.computeIfAbsent(type, t -> buildStacks(room, t));
} else {
return buildStacks(room, type);
}
// Build stacks for this event type
List<WiredStack> stacks = buildStacks(room, type);
// Cache the result
if (useCache) {
cache.computeIfAbsent(room.getId(), k -> new ConcurrentHashMap<>())
.put(type, stacks);
}
return stacks;
}
@Override
@@ -206,16 +191,27 @@ public final class RoomWiredStackIndex implements WiredStackIndex {
THashSet<InteractionWiredExtra> extras = specialTypes.getExtras(x, y);
int conditionEvaluationMode = WiredExtraOrEval.MODE_ALL;
int conditionEvaluationValue = 1;
boolean useRandom = specialTypes.hasExtraType(x, y, WiredExtraRandom.class);
boolean useUnseen = specialTypes.hasExtraType(x, y, WiredExtraUnseen.class);
boolean executeInOrder = specialTypes.hasExtraType(x, y, WiredExtraExecuteInOrder.class);
boolean useRandom = false;
boolean useUnseen = false;
boolean executeInOrder = false;
if (extras != null) {
for (InteractionWiredExtra extra : extras) {
if (!useRandom && extra instanceof WiredExtraRandom) {
useRandom = true;
}
if (!useUnseen && extra instanceof WiredExtraUnseen) {
useUnseen = true;
}
if (!executeInOrder && extra instanceof WiredExtraExecuteInOrder) {
executeInOrder = true;
}
if (extra instanceof WiredExtraOrEval) {
conditionEvaluationMode = ((WiredExtraOrEval) extra).getEvaluationMode();
conditionEvaluationValue = ((WiredExtraOrEval) extra).getCompareValue();
break;
}
}
}
@@ -86,10 +86,10 @@ public final class WiredEngine {
public static int MONITOR_USAGE_WINDOW_MS = 1000;
/** Monitor execution cap per room window */
public static int MONITOR_USAGE_LIMIT = 1000;
public static int MONITOR_USAGE_LIMIT = 50000;
/** Maximum delayed events allowed per room at the same time */
public static int MONITOR_DELAYED_EVENTS_LIMIT = 100;
public static int MONITOR_DELAYED_EVENTS_LIMIT = 50000;
/** Average execution threshold that marks overload */
public static int MONITOR_OVERLOAD_AVERAGE_MS = 50;
@@ -180,14 +180,19 @@ public final class WiredEngine {
int roomId = room.getId();
if (this.isRoomBanned(roomId)) {
return false;
}
// Soft rate limiting to prevent rapid-fire event spam without banning whole rooms
if (isRateLimited(roomId, room, event.getType())) {
return false;
}
// Check and increment recursion depth to prevent infinite loops
int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0);
if (currentDepth >= MAX_RECURSION_DEPTH) {
int currentDepth = roomRecursionDepth.merge(roomId, 1, Integer::sum);
if (currentDepth > MAX_RECURSION_DEPTH) {
roomRecursionDepth.merge(roomId, -1, Integer::sum);
getDiagnostics(roomId).recordRecursionTimeout(
System.currentTimeMillis(),
String.format("Recursion depth %d/%d while handling %s", currentDepth, MAX_RECURSION_DEPTH, event.getType().name()),
@@ -199,18 +204,12 @@ public final class WiredEngine {
debug(room, "RECURSION LIMIT REACHED - aborting to prevent crash");
return false;
}
roomRecursionDepth.put(roomId, currentDepth + 1);
try {
return handleEventInternal(event, room, negateConditions);
} finally {
// Decrement recursion depth
int newDepth = roomRecursionDepth.getOrDefault(roomId, 1) - 1;
if (newDepth <= 0) {
roomRecursionDepth.remove(roomId);
} else {
roomRecursionDepth.put(roomId, newDepth);
}
roomRecursionDepth.compute(roomId, (k, v) -> (v == null || v <= 1) ? null : v - 1);
}
}
@@ -234,28 +233,27 @@ public final class WiredEngine {
int roomId = room.getId();
if (this.isRoomBanned(roomId)) {
return false;
}
if (isRateLimited(roomId, room, event.getType())) {
return false;
}
int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0);
if (currentDepth >= MAX_RECURSION_DEPTH) {
int currentDepth = roomRecursionDepth.merge(roomId, 1, Integer::sum);
if (currentDepth > MAX_RECURSION_DEPTH) {
roomRecursionDepth.merge(roomId, -1, Integer::sum);
LOGGER.warn("Wired recursion limit reached in room {} (depth: {}). " +
"Possible infinite loop detected (source item execution). Aborting.", roomId, currentDepth);
debug(room, "RECURSION LIMIT REACHED - aborting source-item execution");
return false;
}
roomRecursionDepth.put(roomId, currentDepth + 1);
try {
return handleEventForSourceItemInternal(event, room, sourceItemId);
} finally {
int newDepth = roomRecursionDepth.getOrDefault(roomId, 1) - 1;
if (newDepth <= 0) {
roomRecursionDepth.remove(roomId);
} else {
roomRecursionDepth.put(roomId, newDepth);
}
roomRecursionDepth.compute(roomId, (k, v) -> (v == null || v <= 1) ? null : v - 1);
}
}
@@ -1094,7 +1092,13 @@ public final class WiredEngine {
}
private void animateFilteredSelectorBox(Room room, InteractionWiredEffect wiredEffect) {
if (room == null || wiredEffect == null || room.isHideWired()) {
if (room == null || wiredEffect == null) {
return;
}
// If wired is hidden, skip animation but ensure any stale token is cleaned up
if (room.isHideWired()) {
this.filteredSelectorAnimationTokens.remove(wiredEffect.getId());
return;
}
@@ -1364,11 +1368,10 @@ public final class WiredEngine {
? String.valueOf(stack.triggerItem().getId())
: "default";
int current = unseenIndices.getOrDefault(key, -1);
int next = (current + 1) % effectCount;
unseenIndices.put(key, next);
return next;
return unseenIndices.compute(key, (k, current) -> {
if (current == null) current = -1;
return (current + 1) % effectCount;
});
}
/**
@@ -1622,6 +1625,8 @@ public final class WiredEngine {
clearRoomRecursionDepth(roomId);
clearRoomRateLimiters(roomId);
clearRoomSourceStackCache(roomId);
clearRoomDiagnostics(roomId);
clearRoomBan(roomId);
}
/**
@@ -1684,38 +1689,46 @@ public final class WiredEngine {
* @param room the room object
*/
private void banRoom(int roomId, Room room, WiredEvent.Type eventType, int eventCount) {
long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS;
bannedRooms.put(roomId, banExpiry);
getDiagnostics(roomId).recordKilled(
System.currentTimeMillis(),
String.format("Rate limit exceeded for %s with %d event(s) in %dms", eventType.name(), eventCount, RATE_LIMIT_WINDOW_MS),
eventType.name(),
0
);
long banMinutes = WIRED_BAN_DURATION_MS / 60000;
// Send alert to all users in the room
String roomAlertMessage = Emulator.getTexts().getValue("wired.abuse.room.alert")
.replace("%minutes%", String.valueOf(banMinutes));
room.sendComposer(new GenericAlertComposer(roomAlertMessage).compose());
// Send scripter bubble alert to staff with room link
THashMap<String, String> keys = new THashMap<>();
keys.put("title", Emulator.getTexts().getValue("wired.abuse.staff.title"));
keys.put("message", Emulator.getTexts().getValue("wired.abuse.staff.message")
.replace("%roomname%", room.getName())
.replace("%owner%", room.getOwnerName())
.replace("%minutes%", String.valueOf(banMinutes)));
keys.put("linkUrl", "event:navigator/goto/" + roomId);
keys.put("linkTitle", Emulator.getTexts().getValue("wired.abuse.staff.link"));
Emulator.getGameEnvironment().getHabboManager().sendPacketToHabbosWithPermission(
new BubbleAlertComposer("admin.staffalert", keys).compose(),
"acc_modtool_room_info"
);
LOGGER.warn("Wired abuse detected in room {} ({}). Owner: {}. Wired banned for {} minutes.",
roomId, room.getName(), room.getOwnerName(), banMinutes);
// Only actually ban the room if ban duration is configured (> 0)
if (WIRED_BAN_DURATION_MS > 0) {
long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS;
bannedRooms.put(roomId, banExpiry);
long banMinutes = WIRED_BAN_DURATION_MS / 60000;
// Send alert to all users in the room
String roomAlertMessage = Emulator.getTexts().getValue("wired.abuse.room.alert")
.replace("%minutes%", String.valueOf(banMinutes));
room.sendComposer(new GenericAlertComposer(roomAlertMessage).compose());
// Send scripter bubble alert to staff with room link
THashMap<String, String> keys = new THashMap<>();
keys.put("title", Emulator.getTexts().getValue("wired.abuse.staff.title"));
keys.put("message", Emulator.getTexts().getValue("wired.abuse.staff.message")
.replace("%roomname%", room.getName())
.replace("%owner%", room.getOwnerName())
.replace("%minutes%", String.valueOf(banMinutes)));
keys.put("linkUrl", "event:navigator/goto/" + roomId);
keys.put("linkTitle", Emulator.getTexts().getValue("wired.abuse.staff.link"));
Emulator.getGameEnvironment().getHabboManager().sendPacketToHabbosWithPermission(
new BubbleAlertComposer("admin.staffalert", keys).compose(),
"acc_modtool_room_info"
);
LOGGER.warn("Wired abuse detected in room {} ({}). Owner: {}. Wired banned for {} minutes.",
roomId, room.getName(), room.getOwnerName(), banMinutes);
} else {
// Ban duration is 0 - only log, do not spam alerts or put a ban entry
LOGGER.warn("Wired rate limit exceeded in room {} ({}) for event {} ({} events). Ban disabled (wired.abuse.ban.duration.ms=0).",
roomId, room.getName(), eventType.name(), eventCount);
}
}
/**
@@ -913,10 +913,7 @@ public final class WiredManager {
if (room != null) {
room.getFurniVariableManager().clearTransientAssignments();
room.getRoomVariableManager().clearTransientAssignments();
}
if (engine != null && room != null) {
engine.clearRoomExecutionCaches(room.getId());
invalidateRoom(room);
}
}
@@ -1112,18 +1109,16 @@ public final class WiredManager {
}
private static void persistReward(int wiredId, int habboId, int rewardId, int timestamp) {
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("INSERT INTO wired_rewards_given (wired_item, user_id, reward_id, timestamp) VALUES ( ?, ?, ?, ?)")) {
statement.setInt(1, wiredId);
statement.setInt(2, habboId);
statement.setInt(3, rewardId);
statement.setInt(4, timestamp);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
});
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("INSERT INTO wired_rewards_given (wired_item, user_id, reward_id, timestamp) VALUES (?, ?, ?, ?)")) {
statement.setInt(1, wiredId);
statement.setInt(2, habboId);
statement.setInt(3, rewardId);
statement.setInt(4, timestamp);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
private static void completeReward(Habbo habbo, WiredEffectGiveReward wiredBox, WiredGiveRewardItem reward, int successCode) {
@@ -1246,96 +1241,128 @@ public final class WiredManager {
}
public static boolean getReward(Habbo habbo, WiredEffectGiveReward wiredBox) {
if (wiredBox.getLimit() > 0) {
if (wiredBox.getLimit() - wiredBox.getGiven() == 0) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.LIMITED_NO_MORE_AVAILABLE));
synchronized (wiredBox) {
if (wiredBox.getLimit() > 0) {
if (wiredBox.getLimit() - wiredBox.getGiven() == 0) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.LIMITED_NO_MORE_AVAILABLE));
return false;
}
}
WiredGiveRewardItem rewardToGive = null;
int failureCode = -1;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM wired_rewards_given WHERE user_id = ? AND wired_item = ? ORDER BY timestamp DESC LIMIT ?", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)) {
statement.setInt(1, habbo.getHabboInfo().getId());
statement.setInt(2, wiredBox.getId());
statement.setInt(3, wiredBox.getRewardItems().size());
try (ResultSet set = statement.executeQuery()) {
if (set.first()) {
set.last();
int rowCount = set.getRow();
set.first();
if (rowCount >= 1) {
if (wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_ONCE) {
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED;
}
}
if (failureCode == -1) {
if (wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_N_MINUTES) {
if (Emulator.getIntUnixTimestamp() - set.getInt("timestamp") <= 60) {
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_MINUTE;
}
}
if (failureCode == -1 && wiredBox.isUniqueRewards()) {
if (rowCount == wiredBox.getRewardItems().size()) {
failureCode = WiredRewardAlertComposer.REWARD_ALL_COLLECTED;
}
}
if (failureCode == -1 && wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_N_HOURS) {
if (!(Emulator.getIntUnixTimestamp() - set.getInt("timestamp") >= (3600 * wiredBox.getLimitationInterval()))) {
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_HOUR;
}
}
if (failureCode == -1 && wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_N_DAY) {
if (!(Emulator.getIntUnixTimestamp() - set.getInt("timestamp") >= (86400 * wiredBox.getLimitationInterval()))) {
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_TODAY;
}
}
}
if (failureCode == -1) {
if (wiredBox.isUniqueRewards()) {
for (WiredGiveRewardItem item : wiredBox.getRewardItems()) {
set.beforeFirst();
boolean found = false;
while (set.next()) {
if (set.getInt("reward_id") == item.id)
found = true;
}
if (!found) {
rewardToGive = item;
break;
}
}
if (rewardToGive == null) {
failureCode = WiredRewardAlertComposer.REWARD_ALL_COLLECTED;
}
}
}
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
return false;
}
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) as row_count, wired_rewards_given.* FROM wired_rewards_given WHERE user_id = ? AND wired_item = ? ORDER BY timestamp DESC LIMIT ?", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)) {
statement.setInt(1, habbo.getHabboInfo().getId());
statement.setInt(2, wiredBox.getId());
statement.setInt(3, wiredBox.getRewardItems().size());
if (failureCode != -1) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(failureCode));
return false;
}
try (ResultSet set = statement.executeQuery()) {
if (set.first()) {
if (set.getInt("row_count") >= 1) {
if (wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_ONCE) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED));
return false;
}
}
set.beforeFirst();
if (set.next()) {
if (wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_N_MINUTES) {
if (Emulator.getIntUnixTimestamp() - set.getInt("timestamp") <= 60) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_MINUTE));
return false;
}
}
if (wiredBox.isUniqueRewards()) {
if (set.getInt("row_count") == wiredBox.getRewardItems().size()) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALL_COLLECTED));
return false;
}
}
if (wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_N_HOURS) {
if (!(Emulator.getIntUnixTimestamp() - set.getInt("timestamp") >= (3600 * wiredBox.getLimitationInterval()))) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_HOUR));
return false;
}
}
if (wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_N_DAY) {
if (!(Emulator.getIntUnixTimestamp() - set.getInt("timestamp") >= (86400 * wiredBox.getLimitationInterval()))) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_TODAY));
return false;
}
}
}
if (wiredBox.isUniqueRewards()) {
for (WiredGiveRewardItem item : wiredBox.getRewardItems()) {
set.beforeFirst();
boolean found = false;
while (set.next()) {
if (set.getInt("reward_id") == item.id)
found = true;
}
if (!found) {
return giveReward(habbo, wiredBox, item);
}
}
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALL_COLLECTED));
return false;
if (rewardToGive == null) {
if (wiredBox.isUniqueRewards()) {
if (!wiredBox.getRewardItems().isEmpty()) {
rewardToGive = wiredBox.getRewardItems().get(0);
} else {
int randomNumber = Emulator.getRandom().nextInt(101);
int count = 0;
for (WiredGiveRewardItem item : wiredBox.getRewardItems()) {
if (randomNumber >= count && randomNumber <= (count + item.probability)) {
return giveReward(habbo, wiredBox, item);
}
count += item.probability;
failureCode = WiredRewardAlertComposer.REWARD_ALL_COLLECTED;
}
} else {
int randomNumber = Emulator.getRandom().nextInt(101);
int count = 0;
for (WiredGiveRewardItem item : wiredBox.getRewardItems()) {
if (randomNumber >= count && randomNumber <= (count + item.probability)) {
rewardToGive = item;
break;
}
count += item.probability;
}
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.UNLUCKY_NO_REWARD));
return false;
if (rewardToGive == null) {
failureCode = WiredRewardAlertComposer.UNLUCKY_NO_REWARD;
}
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
return false;
if (failureCode != -1) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(failureCode));
return false;
}
if (rewardToGive != null) {
return giveReward(habbo, wiredBox, rewardToGive);
}
return false;
}
}
}
@@ -1105,4 +1105,11 @@ public final class WiredMoveCarryHelper {
this.expiresAt = System.currentTimeMillis() + USER_FOLLOWER_TTL_MS;
}
}
public static void cleanupRoomUnit(RoomUnit roomUnit) {
if (roomUnit != null) {
SUPPRESSED_STATUS_COMPOSER_UNTIL.remove(roomUnit.getId());
ACTIVE_USER_FOLLOWERS.remove(roomUnit.getId());
}
}
}
@@ -266,19 +266,19 @@ public final class WiredRoomDiagnostics {
private final ArrayDeque<HistoryEntry> history;
private final int maxHistoryEntries;
private long windowStartedAt;
private int usageCurrentWindow;
private int delayedEventsPending;
private long totalExecutionMsCurrentWindow;
private int executionSamplesCurrentWindow;
private int averageExecutionMs;
private int peakExecutionMs;
private int consecutiveHeavyWindows;
private int consecutiveOverloadWindows;
private boolean heavy;
private String peakExecutionSourceLabel;
private int peakExecutionSourceId;
private String peakExecutionReason;
private final java.util.concurrent.atomic.AtomicLong windowStartedAt = new java.util.concurrent.atomic.AtomicLong();
private final java.util.concurrent.atomic.AtomicInteger usageCurrentWindow = new java.util.concurrent.atomic.AtomicInteger();
private final java.util.concurrent.atomic.AtomicInteger delayedEventsPending = new java.util.concurrent.atomic.AtomicInteger();
private final java.util.concurrent.atomic.AtomicLong totalExecutionMsCurrentWindow = new java.util.concurrent.atomic.AtomicLong();
private final java.util.concurrent.atomic.AtomicInteger executionSamplesCurrentWindow = new java.util.concurrent.atomic.AtomicInteger();
private volatile int averageExecutionMs;
private volatile int peakExecutionMs;
private volatile int consecutiveHeavyWindows;
private volatile int consecutiveOverloadWindows;
private volatile boolean heavy;
private volatile String peakExecutionSourceLabel;
private volatile int peakExecutionSourceId;
private volatile String peakExecutionReason;
public WiredRoomDiagnostics(int usageWindowMs, int usageLimitPerWindow, int delayedEventsLimit,
int overloadAverageThresholdMs, int overloadPeakThresholdMs,
@@ -310,67 +310,69 @@ public final class WiredRoomDiagnostics {
}
}
public synchronized boolean tryConsumeExecutionBudget(int estimatedCost, long now, String sourceLabel, int sourceId, String reason) {
rollWindow(now);
public boolean tryConsumeExecutionBudget(int estimatedCost, long now, String sourceLabel, int sourceId, String reason) {
rollWindowIfNeeded(now);
int normalizedCost = Math.max(0, estimatedCost);
if ((this.usageCurrentWindow + normalizedCost) > this.usageLimitPerWindow) {
int currentUsage = this.usageCurrentWindow.addAndGet(normalizedCost);
if (currentUsage > this.usageLimitPerWindow) {
record(Type.EXECUTION_CAP, now,
buildExecutionCapReason(normalizedCost, reason),
buildExecutionCapReason(normalizedCost, reason, currentUsage),
sourceLabel,
sourceId);
return false;
}
this.usageCurrentWindow += normalizedCost;
return true;
}
public synchronized boolean tryScheduleDelayedEvent(long now, String sourceLabel, int sourceId, String reason) {
rollWindow(now);
public boolean tryScheduleDelayedEvent(long now, String sourceLabel, int sourceId, String reason) {
rollWindowIfNeeded(now);
if ((this.delayedEventsPending + 1) > this.delayedEventsLimit) {
int currentPending = this.delayedEventsPending.incrementAndGet();
if (currentPending > this.delayedEventsLimit) {
record(Type.DELAYED_EVENTS_CAP, now,
buildDelayedCapReason(reason),
buildDelayedCapReason(reason, currentPending),
sourceLabel,
sourceId);
return false;
}
this.delayedEventsPending++;
return true;
}
public synchronized void completeDelayedEvent() {
if (this.delayedEventsPending > 0) {
this.delayedEventsPending--;
}
public void completeDelayedEvent() {
this.delayedEventsPending.updateAndGet(v -> v > 0 ? v - 1 : 0);
}
public synchronized void recordExecution(long elapsedMs, long now, String sourceLabel, int sourceId, String reason) {
rollWindow(now);
public void recordExecution(long elapsedMs, long now, String sourceLabel, int sourceId, String reason) {
rollWindowIfNeeded(now);
int normalizedElapsed = (int) Math.max(0L, elapsedMs);
this.totalExecutionMsCurrentWindow += normalizedElapsed;
this.executionSamplesCurrentWindow++;
this.averageExecutionMs = (int) Math.round(this.totalExecutionMsCurrentWindow / (double) this.executionSamplesCurrentWindow);
long total = this.totalExecutionMsCurrentWindow.addAndGet(normalizedElapsed);
int samples = this.executionSamplesCurrentWindow.incrementAndGet();
this.averageExecutionMs = (int) Math.round(total / (double) samples);
if (normalizedElapsed >= this.peakExecutionMs) {
this.peakExecutionMs = normalizedElapsed;
this.peakExecutionSourceLabel = sanitizeSourceLabel(sourceLabel);
this.peakExecutionSourceId = Math.max(0, sourceId);
this.peakExecutionReason = sanitizeReason(reason);
synchronized (this) {
if (normalizedElapsed >= this.peakExecutionMs) {
this.peakExecutionMs = normalizedElapsed;
this.peakExecutionSourceLabel = sanitizeSourceLabel(sourceLabel);
this.peakExecutionSourceId = Math.max(0, sourceId);
this.peakExecutionReason = sanitizeReason(reason);
}
}
}
}
public synchronized void recordKilled(long now, String reason, String sourceLabel, int sourceId) {
rollWindow(now);
public void recordKilled(long now, String reason, String sourceLabel, int sourceId) {
rollWindowIfNeeded(now);
record(Type.KILLED, now, reason, sourceLabel, sourceId);
}
public synchronized void recordRecursionTimeout(long now, String reason, String sourceLabel, int sourceId) {
rollWindow(now);
public void recordRecursionTimeout(long now, String reason, String sourceLabel, int sourceId) {
rollWindowIfNeeded(now);
record(Type.RECURSION_TIMEOUT, now, reason, sourceLabel, sourceId);
}
@@ -394,7 +396,7 @@ public final class WiredRoomDiagnostics {
}
public synchronized Snapshot snapshot(int recursionDepthCurrent, int recursionDepthLimit, long killedUntilMs, long now) {
rollWindow(now);
rollWindowIfNeeded(now);
List<LogEntry> logEntries = new ArrayList<>(Type.values().length);
List<HistoryEntry> historyEntries = new ArrayList<>(this.history.size());
@@ -422,10 +424,10 @@ public final class WiredRoomDiagnostics {
}
return new Snapshot(
this.usageCurrentWindow,
this.usageCurrentWindow.get(),
this.usageLimitPerWindow,
this.heavy,
this.delayedEventsPending,
this.delayedEventsPending.get(),
this.delayedEventsLimit,
this.averageExecutionMs,
this.peakExecutionMs,
@@ -444,30 +446,40 @@ public final class WiredRoomDiagnostics {
);
}
private void rollWindow(long now) {
if (this.windowStartedAt <= 0L) {
this.windowStartedAt = now;
private void rollWindowIfNeeded(long now) {
long startedAt = this.windowStartedAt.get();
if (startedAt <= 0L) {
this.windowStartedAt.compareAndSet(startedAt, now);
return;
}
while ((now - this.windowStartedAt) >= this.usageWindowMs) {
evaluateWindow(this.windowStartedAt + this.usageWindowMs);
this.windowStartedAt += this.usageWindowMs;
this.usageCurrentWindow = 0;
this.totalExecutionMsCurrentWindow = 0L;
this.executionSamplesCurrentWindow = 0;
this.averageExecutionMs = 0;
this.peakExecutionMs = 0;
this.peakExecutionSourceLabel = null;
this.peakExecutionSourceId = 0;
this.peakExecutionReason = null;
if ((now - startedAt) >= this.usageWindowMs) {
synchronized (this) {
startedAt = this.windowStartedAt.get();
if ((now - startedAt) >= this.usageWindowMs) {
while ((now - startedAt) >= this.usageWindowMs) {
evaluateWindow(startedAt + this.usageWindowMs);
startedAt += this.usageWindowMs;
this.usageCurrentWindow.set(0);
this.totalExecutionMsCurrentWindow.set(0L);
this.executionSamplesCurrentWindow.set(0);
this.averageExecutionMs = 0;
this.peakExecutionMs = 0;
this.peakExecutionSourceLabel = null;
this.peakExecutionSourceId = 0;
this.peakExecutionReason = null;
}
this.windowStartedAt.set(startedAt);
}
}
}
}
private void evaluateWindow(long now) {
int usagePercent = (int) Math.round((this.usageCurrentWindow * 100D) / this.usageLimitPerWindow);
int delayedPercent = (int) Math.round((this.delayedEventsPending * 100D) / this.delayedEventsLimit);
boolean overloadWindow = (this.executionSamplesCurrentWindow > 0)
int usagePercent = (int) Math.round((this.usageCurrentWindow.get() * 100D) / this.usageLimitPerWindow);
int delayedPercent = (int) Math.round((this.delayedEventsPending.get() * 100D) / this.delayedEventsLimit);
boolean overloadWindow = (this.executionSamplesCurrentWindow.get() > 0)
&& ((this.averageExecutionMs >= this.overloadAverageThresholdMs) || (this.peakExecutionMs >= this.overloadPeakThresholdMs));
boolean heavyWindow = (usagePercent >= this.heavyUsageThresholdPercent)
|| (delayedPercent >= this.heavyDelayedThresholdPercent)
@@ -516,22 +528,22 @@ public final class WiredRoomDiagnostics {
}
}
private String buildExecutionCapReason(int normalizedCost, String reason) {
private String buildExecutionCapReason(int normalizedCost, String reason, int currentUsage) {
return joinReason(
reason,
String.format("Estimated stack cost %d would exceed usage budget %d/%d in %dms window",
normalizedCost,
this.usageCurrentWindow,
currentUsage,
this.usageLimitPerWindow,
this.usageWindowMs)
);
}
private String buildDelayedCapReason(String reason) {
private String buildDelayedCapReason(String reason, int currentPending) {
return joinReason(
reason,
String.format("Pending delayed events would exceed queue %d/%d",
this.delayedEventsPending,
currentPending,
this.delayedEventsLimit)
);
}
@@ -544,7 +556,7 @@ public final class WiredRoomDiagnostics {
this.overloadAverageThresholdMs,
this.peakExecutionMs,
this.overloadPeakThresholdMs,
this.executionSamplesCurrentWindow,
this.executionSamplesCurrentWindow.get(),
this.usageWindowMs)
);
}
@@ -33,6 +33,8 @@ import java.util.Locale;
public final class WiredTextPlaceholderUtil {
private static final char PRESERVED_SPACE = '\u00A0';
private static final int MAX_PLACEHOLDER_EXPANSION_LENGTH = 16384;
private static final int MAX_PLACEHOLDER_REPLACEMENTS = 512;
private WiredTextPlaceholderUtil() {
}
@@ -56,13 +58,20 @@ public final class WiredTextPlaceholderUtil {
String resolvedText = text;
int replacementCount = 0;
for (InteractionWiredExtra extra : WiredExecutionOrderUtil.sort(extras)) {
if (extra instanceof WiredExtraTextOutputUsername) {
WiredExtraTextOutputUsername usernameExtra = (WiredExtraTextOutputUsername) extra;
String placeholderToken = usernameExtra.getPlaceholderToken();
if (!placeholderToken.isEmpty() && resolvedText.contains(placeholderToken)) {
resolvedText = resolvedText.replace(placeholderToken, buildUsernameReplacement(ctx, usernameExtra));
resolvedText = replaceWithBudget(resolvedText, placeholderToken, buildUsernameReplacement(ctx, usernameExtra));
replacementCount++;
}
if (shouldStopPlaceholderExpansion(resolvedText, replacementCount)) {
break;
}
continue;
@@ -73,7 +82,12 @@ public final class WiredTextPlaceholderUtil {
String placeholderToken = furniExtra.getPlaceholderToken();
if (!placeholderToken.isEmpty() && resolvedText.contains(placeholderToken)) {
resolvedText = resolvedText.replace(placeholderToken, buildFurniNameReplacement(ctx, furniExtra));
resolvedText = replaceWithBudget(resolvedText, placeholderToken, buildFurniNameReplacement(ctx, furniExtra));
replacementCount++;
}
if (shouldStopPlaceholderExpansion(resolvedText, replacementCount)) {
break;
}
continue;
@@ -84,7 +98,12 @@ public final class WiredTextPlaceholderUtil {
String placeholderToken = variableExtra.getPlaceholderToken();
if (!placeholderToken.isEmpty() && resolvedText.contains(placeholderToken)) {
resolvedText = resolvedText.replace(placeholderToken, buildVariableReplacement(ctx, variableExtra));
resolvedText = replaceWithBudget(resolvedText, placeholderToken, buildVariableReplacement(ctx, variableExtra));
replacementCount++;
}
if (shouldStopPlaceholderExpansion(resolvedText, replacementCount)) {
break;
}
}
}
@@ -92,6 +111,61 @@ public final class WiredTextPlaceholderUtil {
return preserveRepeatedSpaces(resolvedText);
}
private static boolean shouldStopPlaceholderExpansion(String resolvedText, int replacementCount) {
return replacementCount >= MAX_PLACEHOLDER_REPLACEMENTS
|| (resolvedText != null && resolvedText.length() >= MAX_PLACEHOLDER_EXPANSION_LENGTH);
}
private static String replaceWithBudget(String input, String placeholderToken, String replacement) {
if (input == null || input.isEmpty() || placeholderToken == null || placeholderToken.isEmpty()) {
return input;
}
if (replacement == null) {
replacement = "";
}
int matchIndex = input.indexOf(placeholderToken);
if (matchIndex < 0) {
return input;
}
StringBuilder builder = new StringBuilder(Math.min(MAX_PLACEHOLDER_EXPANSION_LENGTH, input.length()));
int searchIndex = 0;
while (matchIndex >= 0) {
builder.append(input, searchIndex, matchIndex);
int remainingCapacity = MAX_PLACEHOLDER_EXPANSION_LENGTH - builder.length();
if (remainingCapacity <= 0) {
return builder.substring(0, MAX_PLACEHOLDER_EXPANSION_LENGTH);
}
if (replacement.length() <= remainingCapacity) {
builder.append(replacement);
} else {
builder.append(replacement, 0, remainingCapacity);
return builder.toString();
}
searchIndex = matchIndex + placeholderToken.length();
if (builder.length() >= MAX_PLACEHOLDER_EXPANSION_LENGTH) {
return builder.substring(0, MAX_PLACEHOLDER_EXPANSION_LENGTH);
}
matchIndex = input.indexOf(placeholderToken, searchIndex);
}
int remainingCapacity = MAX_PLACEHOLDER_EXPANSION_LENGTH - builder.length();
if (remainingCapacity <= 0) {
return builder.substring(0, MAX_PLACEHOLDER_EXPANSION_LENGTH);
}
int tailLength = Math.min(input.length() - searchIndex, remainingCapacity);
builder.append(input, searchIndex, searchIndex + tailLength);
return builder.toString();
}
private static String preserveRepeatedSpaces(String text) {
if (text == null || text.length() < 2) {
return text;
@@ -587,4 +587,10 @@ public final class WiredUserMovementHelper {
}
}
}
public static void cleanupRoomUnit(RoomUnit roomUnit) {
if (roomUnit != null) {
SUPPRESSED_STATUS_COMPOSER_UNTIL.remove(roomUnit.getId());
}
}
}
@@ -16,6 +16,7 @@ import java.time.LocalTime;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.WeekFields;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -23,7 +24,7 @@ import java.util.stream.Stream;
public class WiredHighscoreManager {
private static final Logger LOGGER = LoggerFactory.getLogger(WiredHighscoreManager.class);
private final HashMap<Integer, List<WiredHighscoreDataEntry>> data = new HashMap<>();
private final ConcurrentHashMap<Integer, List<WiredHighscoreDataEntry>> data = new ConcurrentHashMap<>();
private final static String locale = (System.getProperty("user.language") != null ? System.getProperty("user.language") : "en");
private final static String country = (System.getProperty("user.country") != null ? System.getProperty("user.country") : "US");
@@ -60,15 +61,12 @@ public class WiredHighscoreManager {
private void loadHighscoreData() {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM items_highscore_data")) {
statement.setFetchSize(1000);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
WiredHighscoreDataEntry entry = new WiredHighscoreDataEntry(set);
if (!this.data.containsKey(entry.getItemId())) {
this.data.put(entry.getItemId(), new ArrayList<>());
}
this.data.get(entry.getItemId()).add(entry);
this.data.computeIfAbsent(entry.getItemId(), k -> Collections.synchronizedList(new ArrayList<>())).add(entry);
}
}
} catch (SQLException e) {
@@ -77,33 +75,39 @@ public class WiredHighscoreManager {
}
public void addHighscoreData(WiredHighscoreDataEntry entry) {
if (!this.data.containsKey(entry.getItemId())) {
this.data.put(entry.getItemId(), new ArrayList<>());
}
this.data.computeIfAbsent(entry.getItemId(), k -> Collections.synchronizedList(new ArrayList<>())).add(entry);
this.data.get(entry.getItemId()).add(entry);
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO `items_highscore_data` (`item_id`, `user_ids`, `score`, `is_win`, `timestamp`) VALUES (?, ?, ?, ?, ?)")) {
statement.setInt(1, entry.getItemId());
statement.setString(2, String.join(",", entry.getUserIds().stream().map(Object::toString).collect(Collectors.toList())));
statement.setInt(3, entry.getScore());
statement.setInt(4, entry.isWin() ? 1 : 0);
statement.setInt(5, entry.getTimestamp());
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO `items_highscore_data` (`item_id`, `user_ids`, `score`, `is_win`, `timestamp`) VALUES (?, ?, ?, ?, ?)")) {
statement.setInt(1, entry.getItemId());
statement.setString(2, String.join(",", entry.getUserIds().stream().map(Object::toString).collect(Collectors.toList())));
statement.setInt(3, entry.getScore());
statement.setInt(4, entry.isWin() ? 1 : 0);
statement.setInt(5, entry.getTimestamp());
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
});
}
public List<WiredHighscoreRow> getHighscoreRowsForItem(int itemId, WiredHighscoreClearType clearType, WiredHighscoreScoreType scoreType) {
if (!this.data.containsKey(itemId)) return null;
Stream<WiredHighscoreRow> highscores = new ArrayList<>(this.data.get(itemId)).stream()
List<WiredHighscoreDataEntry> list = this.data.get(itemId);
if (list == null) return null;
List<WiredHighscoreDataEntry> copy;
synchronized (list) {
copy = new ArrayList<>(list);
}
Stream<WiredHighscoreRow> highscores = copy.stream()
.filter(entry -> this.timeMatchesEntry(entry, clearType) && (scoreType != WiredHighscoreScoreType.MOSTWIN || entry.isWin()))
.map(entry -> new WiredHighscoreRow(
entry.getUserIds().stream()
.map(id -> Emulator.getGameEnvironment().getHabboManager().getHabboInfo(id).getUsername())
.map(id -> Emulator.getGameEnvironment().getHabboManager().getCachedUsername(id))
.collect(Collectors.toList()),
entry.getScore()
));
@@ -167,7 +171,7 @@ public class WiredHighscoreManager {
return false;
}
public HashMap<Integer, List<WiredHighscoreDataEntry>> getData() {
public Map<Integer, List<WiredHighscoreDataEntry>> getData() {
return this.data;
}
@@ -176,7 +180,7 @@ public class WiredHighscoreManager {
}
public void setEntriesForItemId(int itemId, List<WiredHighscoreDataEntry> entries) {
this.data.put(itemId, entries);
this.data.put(itemId, Collections.synchronizedList(entries));
}
private long getTodayStartTimestamp() {
@@ -60,11 +60,15 @@ public final class WiredTickService {
/** Whether a shard worker loop is currently scheduled/running. */
private AtomicBoolean[] shardScheduled;
private final ConcurrentHashMap<Integer, Set<WiredTickable>> roomTickables;
private final ConcurrentHashMap<Integer, Set<WiredTickable>>[] shardRoomTickables;
private final AtomicBoolean running;
@SuppressWarnings("unchecked")
private WiredTickService() {
this.roomTickables = new ConcurrentHashMap<>();
this.shardRoomTickables = new ConcurrentHashMap[MAX_WORKER_COUNT];
for (int i = 0; i < MAX_WORKER_COUNT; i++) {
this.shardRoomTickables[i] = new ConcurrentHashMap<>();
}
this.running = new AtomicBoolean(false);
}
@@ -232,7 +236,9 @@ public final class WiredTickService {
shardProcessedTicks = null;
shardScheduled = null;
roomTickables.clear();
for (int i = 0; i < MAX_WORKER_COUNT; i++) {
shardRoomTickables[i].clear();
}
LOGGER.info("WiredTickService stopped");
}
@@ -246,7 +252,8 @@ public final class WiredTickService {
}
int roomId = room.getId();
Set<WiredTickable> tickables = roomTickables.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet());
int shardIndex = getShardIndex(roomId);
Set<WiredTickable> tickables = shardRoomTickables[shardIndex].computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet());
if (tickables.add(tickable)) {
tickable.onRegistered(room, System.currentTimeMillis());
@@ -259,7 +266,8 @@ public final class WiredTickService {
}
int roomId = room.getId();
Set<WiredTickable> tickables = roomTickables.get(roomId);
int shardIndex = getShardIndex(roomId);
Set<WiredTickable> tickables = shardRoomTickables[shardIndex].get(roomId);
if (tickables != null) {
if (tickables.remove(tickable)) {
@@ -267,13 +275,14 @@ public final class WiredTickService {
}
if (tickables.isEmpty()) {
roomTickables.remove(roomId);
shardRoomTickables[shardIndex].remove(roomId);
}
}
}
public void unregister(int roomId, int tickableId) {
Set<WiredTickable> tickables = roomTickables.get(roomId);
int shardIndex = getShardIndex(roomId);
Set<WiredTickable> tickables = shardRoomTickables[shardIndex].get(roomId);
if (tickables != null) {
tickables.removeIf(t -> {
@@ -288,7 +297,7 @@ public final class WiredTickService {
});
if (tickables.isEmpty()) {
roomTickables.remove(roomId);
shardRoomTickables[shardIndex].remove(roomId);
}
}
}
@@ -298,11 +307,12 @@ public final class WiredTickService {
return;
}
Set<WiredTickable> tickables = roomTickables.remove(room.getId());
int roomId = room.getId();
int shardIndex = getShardIndex(roomId);
Set<WiredTickable> tickables = shardRoomTickables[shardIndex].remove(roomId);
if (tickables != null) {
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
for (WiredTickable tickable : snapshot) {
for (WiredTickable tickable : tickables) {
try {
if (tickable != null) {
tickable.onUnregistered(room);
@@ -316,7 +326,7 @@ public final class WiredTickService {
);
}
}
LOGGER.debug("Unregistered {} tickables from room {}", snapshot.length, room.getId());
LOGGER.debug("Unregistered {} tickables from room {}", tickables.size(), room.getId());
}
}
@@ -325,11 +335,12 @@ public final class WiredTickService {
return;
}
Set<WiredTickable> tickables = roomTickables.get(room.getId());
int roomId = room.getId();
int shardIndex = getShardIndex(roomId);
Set<WiredTickable> tickables = shardRoomTickables[shardIndex].get(roomId);
if (tickables != null) {
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
for (WiredTickable tickable : snapshot) {
for (WiredTickable tickable : tickables) {
try {
if (tickable != null) {
tickable.resetTimer();
@@ -347,16 +358,25 @@ public final class WiredTickService {
}
public int getTickableCount(int roomId) {
Set<WiredTickable> tickables = roomTickables.get(roomId);
int shardIndex = getShardIndex(roomId);
Set<WiredTickable> tickables = shardRoomTickables[shardIndex].get(roomId);
return tickables != null ? tickables.size() : 0;
}
public int getTotalTickableCount() {
return roomTickables.values().stream().mapToInt(Set::size).sum();
int count = 0;
for (int i = 0; i < MAX_WORKER_COUNT; i++) {
count += shardRoomTickables[i].values().stream().mapToInt(Set::size).sum();
}
return count;
}
public int getActiveRoomCount() {
return roomTickables.size();
int count = 0;
for (int i = 0; i < MAX_WORKER_COUNT; i++) {
count += shardRoomTickables[i].size();
}
return count;
}
public long getTickCount() {
@@ -396,6 +416,12 @@ public final class WiredTickService {
break;
}
// If lagging by more than 5 ticks (250ms), skip intermediate ticks to avoid CPU starvation
if (requestedTick - nextTick > 5) {
nextTick = requestedTick - 5;
shardProcessedTicks[shardIndex].set(nextTick);
}
processShardTick(shardIndex, nextTick);
shardProcessedTicks[shardIndex].set(nextTick);
}
@@ -414,12 +440,8 @@ public final class WiredTickService {
int processedTickables = 0;
int processedRooms = 0;
for (Map.Entry<Integer, Set<WiredTickable>> entry : roomTickables.entrySet()) {
for (Map.Entry<Integer, Set<WiredTickable>> entry : shardRoomTickables[shardIndex].entrySet()) {
int roomId = entry.getKey();
if (getShardIndex(roomId) != shardIndex) {
continue;
}
Set<WiredTickable> tickables = entry.getValue();
if (tickables == null || tickables.isEmpty()) {
continue;
@@ -435,14 +457,9 @@ public final class WiredTickService {
}
long roomStart = System.currentTimeMillis();
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
if (snapshot.length == 0) {
continue;
}
processedRooms++;
for (WiredTickable tickable : snapshot) {
for (WiredTickable tickable : tickables) {
long tickableStart = System.currentTimeMillis();
if (tickable == null) {
@@ -489,7 +506,7 @@ public final class WiredTickService {
shardIndex,
roomId,
currentTick,
snapshot.length,
tickables.size(),
roomDuration
);
}
@@ -2,6 +2,7 @@ package com.eu.habbo.messages;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.monitoring.EmulatorNetworkStats;
import com.eu.habbo.messages.incoming.Incoming;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.incoming.achievements.RequestAchievementConfigurationEvent;
@@ -180,6 +181,8 @@ public class PacketManager {
return;
try {
EmulatorNetworkStats.recordIncoming(packet.bytesAvailable() + 6);
if (this.isRegistered(packet.getMessageId())) {
Class<? extends MessageHandler> handlerClass = this.incoming.get(packet.getMessageId());
@@ -2,10 +2,8 @@ package com.eu.habbo.messages.incoming.catalog;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.achievements.AchievementManager;
import com.eu.habbo.habbohotel.catalog.CatalogItem;
import com.eu.habbo.habbohotel.catalog.CatalogLimitedConfiguration;
import com.eu.habbo.habbohotel.catalog.CatalogManager;
import com.eu.habbo.habbohotel.catalog.CatalogPage;
import com.eu.habbo.habbohotel.catalog.*;
import com.eu.habbo.habbohotel.catalog.layouts.*;
import com.eu.habbo.habbohotel.items.FurnitureType;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.*;
@@ -14,6 +12,7 @@ import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboBadge;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.habbohotel.users.subscriptions.Subscription;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.catalog.*;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
@@ -22,21 +21,22 @@ import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.HotelWillCloseInMinutesComposer;
import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer;
import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer;
import com.eu.habbo.messages.outgoing.users.UserClubComposer;
import com.eu.habbo.threading.runnables.ShutdownEmulator;
import gnu.trove.map.hash.THashMap;
import gnu.trove.set.hash.THashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.*;
import java.util.Calendar;
public class CatalogBuyItemAsGiftEvent extends MessageHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CatalogBuyItemAsGiftEvent.class);
private static final int USERNAME_MAX = 32;
private static final int EXTRADATA_MAX = 256;
@Override
public int getRatelimit() {
return 500;
@@ -44,61 +44,66 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
@Override
public void handle() throws Exception {
LOGGER.error("DEBUG GIFT: entered CatalogBuyItemAsGiftEvent.handle()");
if (Emulator.getIntUnixTimestamp() - this.client.getHabbo().getHabboStats().lastGiftTimestamp >= CatalogManager.PURCHASE_COOLDOWN) {
this.client.getHabbo().getHabboStats().lastGiftTimestamp = Emulator.getIntUnixTimestamp();
if (ShutdownEmulator.timestamp > 0) {
LOGGER.error("DEBUG GIFT: emulator closing");
LOGGER.debug("emulator closing");
this.client.sendResponse(new HotelWillCloseInMinutesComposer((ShutdownEmulator.timestamp - Emulator.getIntUnixTimestamp()) / 60));
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
if (this.client.getHabbo().getHabboStats().isPurchasingFurniture) {
LOGGER.error("DEBUG GIFT: isPurchasingFurniture already true");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
} else {
synchronized (this.client.getHabbo().getHabboStats()) {
if (this.client.getHabbo().getHabboStats().isPurchasingFurniture) {
LOGGER.debug("isPurchasingFurniture already true");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
this.client.getHabbo().getHabboStats().isPurchasingFurniture = true;
}
int paidCredits = 0;
int paidPoints = 0;
int paidPointsType = 0;
try {
int pageId = this.packet.readInt();
int itemId = this.packet.readInt();
String extraData = this.packet.readString();
if (extraData.length() > EXTRADATA_MAX) extraData = extraData.substring(0, EXTRADATA_MAX);
String username = this.packet.readString();
String message = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo());
if (username.length() > USERNAME_MAX) username = username.substring(0, USERNAME_MAX);
int messageMax = Emulator.getConfig().getInt("hotel.gifts.length.max", 300);
String rawMessage = this.packet.readString();
if (rawMessage.length() > messageMax) rawMessage = rawMessage.substring(0, messageMax);
String message = Emulator.getGameEnvironment().getWordFilter().filter(rawMessage, this.client.getHabbo());
int spriteId = this.packet.readInt();
int color = this.packet.readInt();
int ribbonId = this.packet.readInt();
boolean showName = this.packet.readBoolean();
LOGGER.error(
"DEBUG GIFT: pageId={}, itemId={}, extraData={}, username={}, spriteId={}, color={}, ribbonId={}, showName={}, message={}",
pageId, itemId, extraData, username, spriteId, color, ribbonId, showName, message
);
LOGGER.debug("Gift request: pageId={}, itemId={}, spriteId={}, color={}, ribbonId={}", pageId, itemId, spriteId, color, ribbonId);
int userId = 0;
CatalogPage clubGiftPage = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId);
if (this.isClubOfferPage(clubGiftPage)) {
this.handleClubOfferGift(clubGiftPage, itemId, username);
return;
}
if (!Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId)
&& !Emulator.getGameEnvironment().getCatalogManager().giftFurnis.containsKey(spriteId)) {
LOGGER.error("DEBUG GIFT: invalid spriteId for gift wrapper/furni -> {}", spriteId);
LOGGER.debug("invalid spriteId for gift wrapper/furni -> {}", spriteId);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
if (!GiftConfigurationComposer.BOX_TYPES.contains(color) || !GiftConfigurationComposer.RIBBON_TYPES.contains(ribbonId)) {
LOGGER.error("DEBUG GIFT: invalid color/ribbon -> color={}, ribbonId={}", color, ribbonId);
LOGGER.debug("invalid color/ribbon -> color={}, ribbonId={}", color, ribbonId);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
if (message.length() > Emulator.getConfig().getInt("hotel.gifts.length.max", 300)) {
message = message.substring(0, Emulator.getConfig().getInt("hotel.gifts.length.max", 300));
}
Integer iItemId = Emulator.getGameEnvironment().getCatalogManager().giftWrappers.get(spriteId);
if (iItemId == null) {
@@ -106,7 +111,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
if (iItemId == null) {
LOGGER.error("DEBUG GIFT: iItemId null for spriteId={}", spriteId);
LOGGER.debug("iItemId null for spriteId={}", spriteId);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
@@ -114,7 +119,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
Item giftItem = Emulator.getGameEnvironment().getItemManager().getItem(iItemId);
if (giftItem == null) {
LOGGER.error("DEBUG GIFT: direct giftItem null, trying random fallback. iItemId={}", iItemId);
LOGGER.debug("direct giftItem null, trying random fallback. iItemId={}", iItemId);
giftItem = Emulator.getGameEnvironment().getItemManager().getItem(
(Integer) Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray()[
Emulator.getRandom().nextInt(Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size())
@@ -122,7 +127,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
);
if (giftItem == null) {
LOGGER.error("DEBUG GIFT: fallback giftItem also null");
LOGGER.debug("fallback giftItem also null");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
@@ -132,7 +137,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
if (habbo == null) {
LOGGER.error("DEBUG GIFT: target user not online, checking DB -> {}", username);
LOGGER.debug("target user not online, checking DB -> {}", username);
try (PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE username = ?")) {
statement.setString(1, username);
@@ -149,7 +154,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
if (userId == 0) {
LOGGER.error("DEBUG GIFT: receiver not found -> {}", username);
LOGGER.debug("receiver not found -> {}", username);
this.client.sendResponse(new GiftReceiverNotFoundComposer());
return;
}
@@ -157,17 +162,13 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId);
if (page == null) {
LOGGER.error("DEBUG GIFT: page null -> {}", pageId);
LOGGER.debug("page null -> {}", pageId);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
if (page.getRank() > this.client.getHabbo().getHabboInfo().getRank().getId() || !page.isEnabled() || !page.isVisible()) {
LOGGER.error("DEBUG GIFT: page access denied. pageRank={}, userRank={}, enabled={}, visible={}",
page.getRank(),
this.client.getHabbo().getHabboInfo().getRank().getId(),
page.isEnabled(),
page.isVisible());
LOGGER.debug("page access denied. pageRank={}, userRank={}, enabled={}, visible={}", page.getRank(), this.client.getHabbo().getHabboInfo().getRank().getId(), page.isEnabled(), page.isVisible());
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
@@ -175,20 +176,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
CatalogItem item = page.getCatalogItem(itemId);
if (item == null) {
LOGGER.error("DEBUG GIFT: catalog item null -> {}", itemId);
LOGGER.debug("catalog item null -> {}", itemId);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
if (item.isClubOnly() && !this.client.getHabbo().getHabboStats().hasActiveClub()) {
LOGGER.error("DEBUG GIFT: item requires club -> itemId={}", itemId);
LOGGER.debug("item requires club -> itemId={}", itemId);
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.REQUIRES_CLUB));
return;
}
for (Item baseItem : item.getBaseItems()) {
if (!baseItem.allowGift()) {
LOGGER.error("DEBUG GIFT: base item not giftable -> baseItemId={}, name={}", baseItem.getId(), baseItem.getName());
LOGGER.debug("base item not giftable -> baseItemId={}, name={}", baseItem.getId(), baseItem.getName());
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
@@ -196,7 +197,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
if (item.isLimited()) {
if (item.getLimitedStack() == item.getLimitedSells()) {
LOGGER.error("DEBUG GIFT: LTD sold out -> itemId={}", itemId);
LOGGER.debug("LTD sold out -> itemId={}", itemId);
this.client.sendResponse(new AlertLimitedSoldOutComposer());
return;
}
@@ -205,14 +206,14 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
int totalCredits = item.getCredits();
int totalPoints = item.getPoints();
// Paid wrapping (giftWrappers) costs hotel.gifts.special.price; default furni wrap is free.
boolean isPaidWrap = Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId);
int wrapFee = isPaidWrap ? Emulator.getConfig().getInt("hotel.gifts.special.price", 0) : 0;
totalCredits += wrapFee;
if (totalCredits > this.client.getHabbo().getHabboInfo().getCredits()
|| totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType())) {
LOGGER.error("DEBUG GIFT: not enough currency. creditsNeeded={}, creditsHave={}, pointsNeeded={}, pointsHave={}, pointsType={}",
totalCredits,
this.client.getHabbo().getHabboInfo().getCredits(),
totalPoints,
this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType()),
item.getPointsType());
LOGGER.debug("not enough currency. creditsNeeded={}, pointsNeeded={}, pointsType={}", totalCredits, totalPoints, item.getPointsType());
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
@@ -223,7 +224,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
if (item.isLimited()) {
if (Emulator.getGameEnvironment().getCatalogManager().getLimitedConfig(item).available() == 0) {
LOGGER.error("DEBUG GIFT: LTD available=0 -> itemId={}", itemId);
LOGGER.debug("LTD available=0 -> itemId={}", itemId);
this.client.sendResponse(new AlertLimitedSoldOutComposer());
return;
}
@@ -231,7 +232,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) {
int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total");
if (this.client.getHabbo().getHabboStats().totalLtds() >= ltdLimit) {
LOGGER.error("DEBUG GIFT: sender reached daily total LTD limit");
LOGGER.debug("sender reached daily total LTD limit");
this.client.getHabbo().alert(
Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total")
.replace("%itemname%", item.getBaseItems().iterator().next().getFullName())
@@ -242,7 +243,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item");
if (this.client.getHabbo().getHabboStats().totalLtds(item.getId()) >= ltdLimit) {
LOGGER.error("DEBUG GIFT: sender reached daily LTD item limit");
LOGGER.debug("sender reached daily LTD item limit");
this.client.getHabbo().alert(
Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item")
.replace("%itemname%", item.getBaseItems().iterator().next().getFullName())
@@ -293,20 +294,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
if (badgeFound) {
LOGGER.error("DEBUG GIFT: receiver already has badge");
LOGGER.debug("receiver already has badge");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.ALREADY_HAVE_BADGE));
return;
}
if (item.getAmount() > 1 || item.getBaseItems().size() > 1) {
LOGGER.error("DEBUG GIFT: unsupported multi amount/baseItems. amount={}, baseItems={}", item.getAmount(), item.getBaseItems().size());
LOGGER.debug("unsupported multi amount/baseItems. amount={}, baseItems={}", item.getAmount(), item.getBaseItems().size());
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
for (Item baseItem : item.getBaseItems()) {
if (item.getItemAmount(baseItem.getId()) > 1) {
LOGGER.error("DEBUG GIFT: unsupported item amount > 1 for baseItemId={}", baseItem.getId());
LOGGER.debug("unsupported item amount > 1 for baseItemId={}", baseItem.getId());
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
@@ -330,11 +331,11 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
badgeFound = true;
}
} else if (item.getName().startsWith("rentable_bot_")) {
LOGGER.error("DEBUG GIFT: rentable bot gifts not supported");
LOGGER.debug("rentable bot gifts not supported");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
} else if (Item.isPet(baseItem)) {
LOGGER.error("DEBUG GIFT: pet gifts not supported");
LOGGER.debug("pet gifts not supported");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
} else {
@@ -370,8 +371,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
HabboItem teleportTwo = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
if (teleportOne == null || teleportTwo == null) {
LOGGER.error("DEBUG GIFT: teleport creation failed. baseItemId={}, teleportOneNull={}, teleportTwoNull={}",
baseItem.getId(), teleportOne == null, teleportTwo == null);
LOGGER.debug("teleport creation failed. baseItemId={}, teleportOneNull={}, teleportTwoNull={}", baseItem.getId(), teleportOne == null, teleportTwo == null);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
@@ -384,7 +384,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedNumber, limitedNumber, extraData);
if (habboItem == null) {
LOGGER.error("DEBUG GIFT: hopper creation failed. baseItemId={}", baseItem.getId());
LOGGER.debug("hopper creation failed. baseItemId={}", baseItem.getId());
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
@@ -397,13 +397,13 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
HabboItem createdItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
if (createdItem == null) {
LOGGER.error("DEBUG GIFT: guild item creation failed. baseItemId={}", baseItem.getId());
LOGGER.debug("guild item creation failed. baseItemId={}", baseItem.getId());
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
if (!(createdItem instanceof InteractionGuildFurni)) {
LOGGER.error("DEBUG GIFT: created guild item has wrong class -> {}", createdItem.getClass().getName());
LOGGER.debug("created guild item has wrong class -> {}", createdItem.getClass().getName());
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
@@ -428,7 +428,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
if (habboItem == null) {
LOGGER.error("DEBUG GIFT: normal item creation failed. baseItemId={}, baseItemName={}", baseItem.getId(), baseItem.getName());
LOGGER.debug("normal item creation failed. baseItemId={}, baseItemName={}", baseItem.getId(), baseItem.getName());
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
@@ -437,7 +437,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
}
} else {
LOGGER.error("DEBUG GIFT: avatar_effect not supported");
LOGGER.debug("avatar_effect not supported");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
this.client.sendResponse(new GenericAlertComposer(Emulator.getTexts().getValue("error.catalog.buy.not_yet")));
return;
@@ -446,7 +446,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
if (itemsList.isEmpty()) {
LOGGER.error("DEBUG GIFT: itemsList empty before giftData");
LOGGER.debug("itemsList empty before giftData");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
@@ -455,7 +455,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
for (HabboItem i : itemsList) {
if (i == null) {
LOGGER.error("DEBUG GIFT: null HabboItem detected inside itemsList");
LOGGER.debug("null HabboItem detected inside itemsList");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
@@ -475,10 +475,37 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
.append("\t")
.append(this.client.getHabbo().getHabboInfo().getLook());
// Deduct currency before createGift so a failure here leaves the sender unpaid rather than gifted.
if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS) && totalCredits > 0) {
this.client.getHabbo().giveCredits(-totalCredits);
paidCredits = totalCredits;
}
if (totalPoints > 0) {
if (item.getPointsType() == 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_PIXELS)) {
this.client.getHabbo().givePixels(-totalPoints);
paidPoints = totalPoints;
paidPointsType = 0;
} else if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_POINTS)) {
this.client.getHabbo().givePoints(item.getPointsType(), -totalPoints);
paidPoints = totalPoints;
paidPointsType = item.getPointsType();
}
}
HabboItem gift = Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, giftData.toString(), 0, 0);
if (gift == null) {
LOGGER.error("DEBUG GIFT: createGift returned null");
LOGGER.debug("createGift returned null");
if (paidCredits > 0) {
this.client.getHabbo().giveCredits(paidCredits);
paidCredits = 0;
}
if (paidPoints > 0) {
if (paidPointsType == 0) this.client.getHabbo().givePixels(paidPoints);
else this.client.getHabbo().givePoints(paidPointsType, paidPoints);
paidPoints = 0;
}
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
@@ -486,9 +513,8 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
if (limitedConfiguration != null) {
for (HabboItem itm : itemsList) {
if (itm == null) {
LOGGER.error("DEBUG GIFT: null item before limitedSold()");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
// Trip the catch path so the deduction is refunded.
throw new IllegalStateException("null item before limitedSold()");
}
limitedConfiguration.limitedSold(item.getId(), this.client.getHabbo(), itm);
}
@@ -526,32 +552,189 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
);
}
if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) {
if (totalCredits > 0) {
this.client.getHabbo().giveCredits(-totalCredits);
}
}
// Gift fully delivered; commit cooldown and clear refund tracking so the catch block can't double-refund.
this.client.getHabbo().getHabboStats().lastGiftTimestamp = Emulator.getIntUnixTimestamp();
paidCredits = 0;
paidPoints = 0;
if (totalPoints > 0) {
if (item.getPointsType() == 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_PIXELS)) {
this.client.getHabbo().givePixels(-totalPoints);
} else if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_POINTS)) {
this.client.getHabbo().givePoints(item.getPointsType(), -totalPoints);
}
}
LOGGER.error("DEBUG GIFT: success sending PurchaseOKComposer");
this.client.sendResponse(new PurchaseOKComposer(item));
}
} catch (Exception e) {
LOGGER.error("Exception caught", e);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
if (paidCredits > 0) this.client.getHabbo().giveCredits(paidCredits);
if (paidPoints > 0) {
if (paidPointsType == 0) this.client.getHabbo().givePixels(paidPoints);
else this.client.getHabbo().givePoints(paidPointsType, paidPoints);
}
} finally {
this.client.getHabbo().getHabboStats().isPurchasingFurniture = false;
}
} else {
LOGGER.error("DEBUG GIFT: cooldown blocked purchase");
LOGGER.debug("cooldown blocked purchase");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
}
}
private boolean isClubOfferPage(CatalogPage page) {
return page instanceof ClubBuyLayout
|| page instanceof VipBuyLayout
|| page instanceof BuildersClubFrontPageLayout
|| page instanceof BuildersClubAddonsLayout
|| page instanceof BuildersClubLoyaltyLayout;
}
private int getClubOfferWindowId(CatalogPage page) {
if (page instanceof BuildersClubAddonsLayout) {
return ClubOffer.WINDOW_BUILDERS_CLUB_ADDONS;
}
if (page instanceof BuildersClubFrontPageLayout || page instanceof BuildersClubLoyaltyLayout) {
return ClubOffer.WINDOW_BUILDERS_CLUB;
}
return ClubOffer.WINDOW_HABBO_CLUB;
}
private void handleClubOfferGift(CatalogPage page, int offerId, String username) {
ClubOffer offer = Emulator.getGameEnvironment().getCatalogManager().clubOffers.get(offerId);
if (offer == null || !offer.belongsToWindow(this.getClubOfferWindowId(page))) {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
if (!offer.isGiftable()) {
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
if (offer.isBuildersClubAddon()) {
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
int totalCredits = offer.getCredits();
int totalPoints = offer.getPoints();
if (totalCredits > this.client.getHabbo().getHabboInfo().getCredits()
|| totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(offer.getPointsType())) {
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
Habbo recipient = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
int recipientId = 0;
if (recipient != null) {
recipientId = recipient.getHabboInfo().getId();
} else {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE username = ?")) {
statement.setString(1, username);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) recipientId = set.getInt(1);
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception while resolving club gift recipient", e);
}
}
if (recipientId == 0) {
this.client.sendResponse(new GiftReceiverNotFoundComposer());
return;
}
if (recipientId == this.client.getHabbo().getHabboInfo().getId()) {
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
String subscriptionType = offer.isBuildersClubSubscription() ? Subscription.BUILDERS_CLUB : Subscription.HABBO_CLUB;
int duration = offer.getDays() * 86400;
boolean extended;
if (recipient != null) {
extended = (recipient.getHabboStats().createSubscription(subscriptionType, duration) != null);
} else {
extended = this.extendOfflineSubscription(recipientId, subscriptionType, duration);
}
if (!extended) {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
if (totalCredits > 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) {
this.client.getHabbo().giveCredits(-totalCredits);
}
if (totalPoints > 0) {
if (offer.getPointsType() == 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_PIXELS)) {
this.client.getHabbo().givePixels(-totalPoints);
} else if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_POINTS)) {
this.client.getHabbo().givePoints(offer.getPointsType(), -totalPoints);
}
}
if (recipient != null) {
recipient.getClient().sendResponse(new UserClubComposer(recipient, subscriptionType, UserClubComposer.RESPONSE_TYPE_NORMAL));
String prefix = Emulator.getTexts().getValue("prereg.reward.you.received", "You have received:");
String daysWord = Emulator.getTexts().getValue("generic.days", "days");
String clubLabel = offer.isBuildersClubSubscription() ? "Builders Club" : "HC";
String giftDescription = clubLabel + " (" + offer.getDays() + " " + daysWord + ")";
THashMap<String, String> keys = new THashMap<>();
keys.put("display", "BUBBLE");
keys.put("image", "${image.library.url}notifications/gift.gif");
keys.put("message", prefix + " " + giftDescription);
recipient.getClient().sendResponse(new BubbleAlertComposer(BubbleAlertKeys.RECEIVED_GIFT.key, keys));
}
if (this.client.getHabbo().getHabboInfo().getId() != recipientId) {
AchievementManager.progressAchievement(
this.client.getHabbo(),
Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftGiver")
);
}
this.client.sendResponse(new PurchaseOKComposer(null));
}
private boolean extendOfflineSubscription(int userId, String subscriptionType, int duration) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement select = connection.prepareStatement(
"SELECT id, duration FROM users_subscriptions WHERE user_id = ? AND subscription_type = ? AND active = 1 ORDER BY id DESC LIMIT 1")) {
select.setInt(1, userId);
select.setString(2, subscriptionType);
try (ResultSet set = select.executeQuery()) {
if (set.next()) {
int subId = set.getInt("id");
int existing = set.getInt("duration");
try (PreparedStatement update = connection.prepareStatement(
"UPDATE users_subscriptions SET duration = ? WHERE id = ?")) {
update.setInt(1, existing + duration);
update.setInt(2, subId);
update.executeUpdate();
return true;
}
}
}
}
try (PreparedStatement insert = connection.prepareStatement(
"INSERT INTO users_subscriptions (user_id, subscription_type, timestamp_start, duration, active) VALUES (?, ?, ?, ?, 1)",
Statement.RETURN_GENERATED_KEYS)) {
insert.setInt(1, userId);
insert.setString(2, subscriptionType);
insert.setInt(3, Emulator.getIntUnixTimestamp());
insert.setInt(4, duration);
insert.executeUpdate();
return true;
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception while extending offline subscription", e);
return false;
}
}
}
@@ -36,8 +36,21 @@ public class CatalogAdminCreatePageEvent extends MessageHandler {
pageLayout = CatalogPageLayouts.default_3x3;
}
if (parentId != -1 && Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(parentId) == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + parentId));
return;
}
if (iconType < 0) iconType = 0;
if (minRank < 1) minRank = 1;
if (orderNum < 0) orderNum = 0;
if (caption == null) caption = "";
if (caption2 == null) caption2 = "";
if (caption.length() > 128) caption = caption.substring(0, 128);
if (caption2.length() > 25) caption2 = caption2.substring(0, 25);
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().createCatalogPage(
caption, caption2, 0, iconType, pageLayout, minRank, parentId, pageType, catalogMode
caption, caption2, 0, iconType, pageLayout, minRank, parentId, pageType, catalogMode
);
if (page == null) {
@@ -1,6 +1,7 @@
package com.eu.habbo.messages.incoming.catalog.catalogadmin;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.catalog.CatalogPage;
import com.eu.habbo.habbohotel.catalog.CatalogPageType;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
@@ -11,6 +12,9 @@ import java.sql.PreparedStatement;
public class CatalogAdminMovePageEvent extends MessageHandler {
private static final int MAX_PARENT_WALK = 64;
private static final int ROOT_PARENT_ID = -1;
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
@@ -24,12 +28,10 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
String tableName = (pageType == CatalogPageType.BUILDER) ? "catalog_pages_bc" : "catalog_pages";
// Special values: -1 = toggle enabled, -2 = toggle visible
if (newParentId == -1) {
// Toggle enabled
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE " + tableName + " SET enabled = IF(enabled = '1', '0', '1') WHERE id = ?")) {
"UPDATE " + tableName + " SET enabled = IF(enabled = '1', '0', '1') WHERE id = ?")) {
statement.setInt(1, pageId);
statement.execute();
}
@@ -38,21 +40,43 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
}
if (newParentId == -2) {
// Toggle visible
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE " + tableName + " SET visible = IF(visible = '1', '0', '1') WHERE id = ?")) {
"UPDATE " + tableName + " SET visible = IF(visible = '1', '0', '1') WHERE id = ?")) {
statement.setInt(1, pageId);
statement.execute();
}
this.client.sendResponse(new CatalogAdminResultComposer(true, "Visibility toggled"));
return;
}
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType);
if (page == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId));
return;
}
if (newParentId == pageId) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "A page cannot be its own parent"));
return;
}
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(newParentId);
if (parent == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + newParentId));
return;
}
if (this.wouldCreateCycle(pageId, newParentId)) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Refusing to move: that would create a cycle"));
return;
}
if (newIndex < 0) newIndex = 0;
// Normal move: update parent and order
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE " + tableName + " SET parent_id = ?, order_num = ? WHERE id = ?")) {
"UPDATE " + tableName + " SET parent_id = ?, order_num = ? WHERE id = ?")) {
statement.setInt(1, newParentId);
statement.setInt(2, newIndex);
statement.setInt(3, pageId);
@@ -61,4 +85,16 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
this.client.sendResponse(new CatalogAdminResultComposer(true, "Page moved"));
}
private boolean wouldCreateCycle(int pageId, int parentId) {
int current = parentId;
for (int hops = 0; hops < MAX_PARENT_WALK; hops++) {
if (current == ROOT_PARENT_ID) return false;
if (current == pageId) return true;
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(current);
if (parent == null) return false;
current = parent.getParentId();
}
return true;
}
}
@@ -2,16 +2,35 @@ package com.eu.habbo.messages.incoming.catalog.catalogadmin;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.catalog.CatalogPage;
import com.eu.habbo.habbohotel.catalog.CatalogPageLayouts;
import com.eu.habbo.habbohotel.catalog.CatalogPageType;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer;
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
import java.sql.Connection;
import java.sql.PreparedStatement;
public class CatalogAdminSavePageEvent extends MessageHandler {
private static final int MAX_CAPTION_LENGTH = 128;
private static final int MAX_CAPTION_SAVE_LENGTH = 25;
private static final int MAX_HEADLINE_LENGTH = 1024;
private static final int MAX_TEASER_LENGTH = 64;
private static final int MAX_TEXT_LENGTH = 8192;
private static final int MAX_PARENT_WALK = 64;
private static final int ROOT_PARENT_ID = -1;
private static final Safelist PAGE_HTML_SAFELIST = new Safelist()
.addTags("b", "i", "u", "br", "span", "div", "p", "a", "strong", "em", "img")
.addAttributes("a", "href", "target", "class", "style")
.addAttributes("img", "src", "alt", "class", "style")
.addAttributes(":all", "class", "style")
.addProtocols("a", "href", "http", "https", "mailto", "#")
.addProtocols("img", "src", "http", "https", "data");
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
@@ -34,7 +53,7 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
String textDetails = this.packet.readString();
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
CatalogPageType catalogMode = CatalogPageType.fromString(this.packet.readString());
String text1 = this.packet.bytesAvailable() > 0 ? this.packet.readString() : "";
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType);
if (page == null) {
@@ -42,9 +61,55 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
return;
}
try {
CatalogPageLayouts.valueOf(layout);
} catch (IllegalArgumentException | NullPointerException e) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Invalid layout: " + layout));
return;
}
if (parentId != ROOT_PARENT_ID) {
if (parentId == pageId) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "A page cannot be its own parent"));
return;
}
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(parentId);
if (parent == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + parentId));
return;
}
if (this.wouldCreateCycle(pageId, parentId)) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Refusing to re-parent: that would create a cycle"));
return;
}
}
if (iconType < 0) iconType = 0;
if (minRank < 1) minRank = 1;
if (orderNum < 0) orderNum = 0;
headline = this.sanitizeHtml(headline);
teaser = this.sanitizeHtml(teaser);
textDetails = this.sanitizeHtml(textDetails);
text1 = this.sanitizeHtml(text1);
caption = this.clampLength(caption, MAX_CAPTION_LENGTH);
caption2 = this.clampLength(caption2, MAX_CAPTION_SAVE_LENGTH);
headline = this.clampLength(headline, MAX_HEADLINE_LENGTH);
teaser = this.clampLength(teaser, MAX_TEASER_LENGTH);
textDetails = this.clampLength(textDetails, MAX_TEXT_LENGTH);
text1 = this.clampLength(text1, MAX_TEXT_LENGTH);
if (headline.isEmpty() && page.getHeaderImage() != null) headline = page.getHeaderImage();
if (teaser.isEmpty() && page.getTeaserImage() != null) teaser = page.getTeaserImage();
if (textDetails.isEmpty() && page.getTextDetails() != null) textDetails = page.getTextDetails();
if (text1.isEmpty() && page.getTextOne() != null) text1 = page.getTextOne();
String query = (pageType == CatalogPageType.BUILDER)
? "UPDATE catalog_pages_bc SET caption = ?, page_layout = ?, icon_image = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ? WHERE id = ?"
: "UPDATE catalog_pages SET caption = ?, caption_save = ?, page_layout = ?, icon_image = ?, min_rank = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ?, catalog_mode = ? WHERE id = ?";
? "UPDATE catalog_pages_bc SET caption = ?, page_layout = ?, icon_image = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ?, page_text1 = ? WHERE id = ?"
: "UPDATE catalog_pages SET caption = ?, caption_save = ?, page_layout = ?, icon_image = ?, min_rank = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ?, page_text1 = ?, catalog_mode = ? WHERE id = ?";
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(query)) {
@@ -60,7 +125,8 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
statement.setString(8, headline);
statement.setString(9, teaser);
statement.setString(10, textDetails);
statement.setInt(11, pageId);
statement.setString(11, text1);
statement.setInt(12, pageId);
} else {
statement.setString(2, caption2);
statement.setString(3, layout);
@@ -73,8 +139,9 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
statement.setString(10, headline);
statement.setString(11, teaser);
statement.setString(12, textDetails);
statement.setString(13, catalogMode.name());
statement.setInt(14, pageId);
statement.setString(13, text1);
statement.setString(14, catalogMode.name());
statement.setInt(15, pageId);
}
statement.execute();
@@ -82,4 +149,28 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
this.client.sendResponse(new CatalogAdminResultComposer(true, "Page saved"));
}
private boolean wouldCreateCycle(int pageId, int parentId) {
int current = parentId;
for (int hops = 0; hops < MAX_PARENT_WALK; hops++) {
if (current == ROOT_PARENT_ID) return false;
if (current == pageId) return true;
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(current);
if (parent == null) return false;
current = parent.getParentId();
}
return true;
}
private String clampLength(String value, int max) {
if (value == null) return "";
if (value.length() <= max) return value;
return value.substring(0, max);
}
private String sanitizeHtml(String value) {
if (value == null || value.isEmpty()) return "";
return Jsoup.clean(value, PAGE_HTML_SAFELIST);
}
}
@@ -1,6 +1,7 @@
package com.eu.habbo.messages.incoming.modtool;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.modtool.ModToolManager;
import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem;
import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
import com.eu.habbo.habbohotel.permissions.Permission;
@@ -47,6 +48,8 @@ public class ModToolSanctionAlertEvent extends MessageHandler {
} else {
habbo.alert(message);
}
ModToolManager.bumpUserSettingCounter(userId, "cfh_warnings");
} else {
this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name"))));
}
@@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.modtool;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.modtool.ModToolBanType;
import com.eu.habbo.habbohotel.modtool.ModToolManager;
import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem;
import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
import com.eu.habbo.habbohotel.modtool.ScripterManager;
@@ -73,6 +74,7 @@ public class ModToolSanctionBanEvent extends MessageHandler {
Emulator.getGameEnvironment().getModToolManager().ban(userId, this.client.getHabbo(), message, duration, ModToolBanType.ACCOUNT, cfhTopic);
}
ModToolManager.bumpUserSettingCounter(userId, "cfh_bans");
} else {
ScripterManager.scripterDetected(this.client, Emulator.getTexts().getValue("scripter.warning.modtools.ban").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
}
@@ -1,6 +1,7 @@
package com.eu.habbo.messages.incoming.modtool;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.modtool.ModToolManager;
import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem;
import com.eu.habbo.habbohotel.modtool.ModToolSanctionLevelItem;
import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
@@ -59,6 +60,8 @@ public class ModToolSanctionMuteEvent extends MessageHandler {
habbo.alert(message);
this.client.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_mute.muted").replace("%user%", habbo.getHabboInfo().getUsername()));
}
ModToolManager.bumpUserSettingCounter(userId, "cfh_warnings");
} else {
this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name"))));
}
@@ -1,6 +1,7 @@
package com.eu.habbo.messages.incoming.modtool;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.modtool.ModToolManager;
import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem;
import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
import com.eu.habbo.habbohotel.permissions.Permission;
@@ -49,6 +50,8 @@ public class ModToolSanctionTradeLockEvent extends MessageHandler {
habbo.getHabboStats().setAllowTrade(false);
habbo.alert(message);
}
ModToolManager.bumpUserSettingCounter(userId, "tradelock_amount");
} else {
this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name"))));
}
@@ -27,9 +27,8 @@ public class RoomUserSignEvent extends MessageHandler {
WiredManager.triggerUserPerformsAction(room, this.client.getHabbo().getRoomUnit(), WiredUserActionType.SIGN, event.sign);
if(signId <= 10) {
int userId = this.client.getHabbo().getHabboInfo().getId();
for (HabboItem item : room.getFloorItems()) {
for (HabboItem item : room.getRoomSpecialTypes().getItemsOfType(InteractionVoteCounter.class)) {
if (item instanceof InteractionVoteCounter) {
((InteractionVoteCounter)item).vote(room, userId, signId);
}
@@ -37,6 +37,7 @@ public class ChangeInfostandBgEvent extends MessageHandler {
int requestedStand = sanitize(this.packet.readInt());
int requestedOverlay = sanitize(this.packet.readInt());
int requestedCard = this.packet.bytesAvailable() >= 4 ? sanitize(this.packet.readInt()) : 0;
int requestedBorder = this.packet.bytesAvailable() >= 4 ? sanitize(this.packet.readInt()) : 0;
InfostandBackgroundManager manager = Emulator.getGameEnvironment() != null ? Emulator.getGameEnvironment().getInfostandBackgroundManager() : null;
@@ -44,11 +45,13 @@ public class ChangeInfostandBgEvent extends MessageHandler {
int backgroundStand = resolve(manager, habbo, Category.STAND, requestedStand, info.getInfostandStand());
int backgroundOverlay = resolve(manager, habbo, Category.OVERLAY, requestedOverlay, info.getInfostandOverlay());
int backgroundCard = resolve(manager, habbo, Category.CARD, requestedCard, info.getInfostandCardBg());
int backgroundBorder = resolve(manager, habbo, Category.BORDER, requestedBorder, info.getInfostandBorder());
if (info.getInfostandBg() == backgroundImage
&& info.getInfostandStand() == backgroundStand
&& info.getInfostandOverlay() == backgroundOverlay
&& info.getInfostandCardBg() == backgroundCard) {
&& info.getInfostandCardBg() == backgroundCard
&& info.getInfostandBorder() == backgroundBorder) {
return;
}
@@ -56,6 +59,7 @@ public class ChangeInfostandBgEvent extends MessageHandler {
info.setInfostandStand(backgroundStand);
info.setInfostandOverlay(backgroundOverlay);
info.setInfostandCardBg(backgroundCard);
info.setInfostandBorder(backgroundBorder);
info.run();
if (info.getCurrentRoom() != null) {
@@ -41,6 +41,7 @@ public class CatalogPagesListComposer extends MessageComposer {
this.response.appendBoolean(true);
this.response.appendInt(0);
this.response.appendInt(-1);
this.response.appendInt(-1);
this.response.appendString("root");
this.response.appendString("");
this.response.appendInt(0);
@@ -68,7 +69,8 @@ public class CatalogPagesListComposer extends MessageComposer {
this.response.appendBoolean(category.isVisible());
this.response.appendInt(category.getIconImage());
this.response.appendInt(category.isEnabled() ? category.getId() : -1);
this.response.appendInt(category.isEnabled() || this.hasPermission ? category.getId() : -1);
this.response.appendInt(category.getParentId());
this.response.appendString(category.getPageName());
this.response.appendString(category.getCaption() + (this.hasPermission ? " (" + category.getId() + ")" : ""));
@@ -12,10 +12,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
public class ModToolUserInfoComposer extends MessageComposer {
private static final Logger LOGGER = LoggerFactory.getLogger(ModToolUserInfoComposer.class);
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
private final ResultSet set;
private final boolean hideMail;
@@ -29,37 +32,30 @@ public class ModToolUserInfoComposer extends MessageComposer {
protected ServerMessage composeInternal() {
this.response.init(Outgoing.ModToolUserInfoComposer);
try {
int totalBans = 0;
int userId = this.set.getInt("user_id");
String machineId = this.set.getString("machine_id");
int now = Emulator.getIntUnixTimestamp();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) AS amount FROM bans WHERE user_id = ?")) {
statement.setInt(1, this.set.getInt("user_id"));
try (ResultSet set = statement.executeQuery()) {
if (set.next()) {
totalBans = set.getInt("amount");
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
int totalBans = countBansForUser(userId);
int lastPurchaseTimestamp = fetchLastPurchaseTimestamp(userId);
int tradeLockExpiryTimestamp = fetchActiveTradeLockExpiry(userId, now);
int identityRelatedBanCount = countIdentityRelatedBans(userId, machineId);
this.response.appendInt(this.set.getInt("user_id"));
this.response.appendInt(userId);
this.response.appendString(this.set.getString("username"));
this.response.appendString(this.set.getString("look"));
this.response.appendInt((Emulator.getIntUnixTimestamp() - this.set.getInt("account_created")) / 60);
this.response.appendInt((this.set.getInt("online") == 1 ? 0 : Emulator.getIntUnixTimestamp() - this.set.getInt("last_online")) / 60);
this.response.appendInt((now - this.set.getInt("account_created")) / 60);
this.response.appendInt((this.set.getInt("online") == 1 ? 0 : now - this.set.getInt("last_online")) / 60);
this.response.appendBoolean(this.set.getInt("online") == 1);
this.response.appendInt(this.set.getInt("cfh_send"));
this.response.appendInt(this.set.getInt("cfh_abusive"));
this.response.appendInt(this.set.getInt("cfh_warnings"));
this.response.appendInt(totalBans); // Number of bans
this.response.appendInt(this.set.getInt("tradelock_amount"));
this.response.appendString(""); //Trading lock expiry timestamp
this.response.appendString(""); //Last Purchase Timestamp
this.response.appendInt(this.set.getInt("user_id")); //Personal Identification #
this.response.appendInt(0); // Number of account bans
this.response.appendString(formatUnixTimestamp(tradeLockExpiryTimestamp)); // Trading lock expiry timestamp
this.response.appendString(formatUnixTimestamp(lastPurchaseTimestamp)); // Last Purchase Timestamp
this.response.appendInt(userId); //Personal Identification #
this.response.appendInt(identityRelatedBanCount); // Number of account bans on the same machine_id
this.response.appendString(this.hideMail ? "" : this.set.getString("mail"));
this.response.appendString("Rank (" + this.set.getInt("rank_id") + "): " + this.set.getString("rank_name")); //user_class_txt
@@ -90,4 +86,87 @@ public class ModToolUserInfoComposer extends MessageComposer {
public ResultSet getSet() {
return set;
}
private static int countBansForUser(int userId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) AS amount FROM bans WHERE user_id = ?")) {
statement.setInt(1, userId);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) return set.getInt("amount");
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
return 0;
}
/**
* Most recent purchase timestamp from logs_shop_purchases for this
* user. Returns 0 when the user has never bought anything (in which
* case the wire field stays empty and the client shows the empty
* placeholder).
*/
private static int fetchLastPurchaseTimestamp(int userId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT MAX(`timestamp`) AS ts FROM logs_shop_purchases WHERE user_id = ?")) {
statement.setInt(1, userId);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) return set.getInt("ts");
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
return 0;
}
/**
* Latest active trade-lock expiry from the sanctions table. Only
* locks expiring in the future are considered past entries don't
* count. Returns 0 when no active lock exists.
*/
private static int fetchActiveTradeLockExpiry(int userId, int now) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT MAX(trade_locked_until) AS expiry FROM sanctions WHERE habbo_id = ? AND trade_locked_until > ?")) {
statement.setInt(1, userId);
statement.setInt(2, now);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) return set.getInt("expiry");
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
return 0;
}
/**
* Count of OTHER user accounts that have been banned from the same
* machine_id as this user. An empty machine_id (default '') is
* ignored never matches anything by definition. Self is excluded
* because the user's own bans are already counted under banCount.
*/
private static int countIdentityRelatedBans(int userId, String machineId) {
if (machineId == null || machineId.isEmpty()) return 0;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT COUNT(DISTINCT user_id) AS amount FROM bans WHERE machine_id = ? AND user_id != ?")) {
statement.setString(1, machineId);
statement.setInt(2, userId);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) return set.getInt("amount");
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
return 0;
}
/**
* Wire format for date fields is `yyyy-MM-dd HH:mm`. A 0 timestamp
* is rendered as an empty string so the client falls back to its
* empty-state placeholder.
*/
private static String formatUnixTimestamp(int timestamp) {
if (timestamp <= 0) return "";
return DATE_FORMAT.format(new Date(timestamp * 1000L));
}
}
@@ -62,6 +62,7 @@ public class RoomPetComposer extends MessageComposer implements TIntObjectProced
this.response.appendString("");
this.response.appendString("unknown");
this.response.appendInt(0);
this.response.appendInt(0);
return true;
}
@@ -33,6 +33,7 @@ public class RoomUserDataComposer extends MessageComposer {
this.response.appendString(customizationData.prefixEffect);
this.response.appendString(customizationData.prefixFont);
this.response.appendString(customizationData.displayOrder);
this.response.appendInt(this.habbo.getHabboInfo().getInfostandBorder());
return this.response;
}
@@ -78,6 +78,7 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendString(customizationData.displayOrder);
this.response.appendString(this.habbo.getHabboInfo().getRoomEntryMethod());
this.response.appendInt(this.habbo.getHabboInfo().getRoomEntryTeleportId());
this.response.appendInt(this.habbo.getHabboInfo().getInfostandBorder());
} else if (this.habbos != null) {
this.response.appendInt(this.habbos.size());
for (Habbo habbo : this.habbos) {
@@ -120,6 +121,7 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendString(customizationData.displayOrder);
this.response.appendString(habbo.getHabboInfo().getRoomEntryMethod());
this.response.appendInt(habbo.getHabboInfo().getRoomEntryTeleportId());
this.response.appendInt(habbo.getHabboInfo().getInfostandBorder());
}
}
} else if (this.bot != null) {
@@ -154,6 +156,7 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendShort(9);
this.response.appendString("unknown");
this.response.appendInt(0);
this.response.appendInt(0);
} else if (this.bots != null) {
this.response.appendInt(this.bots.size());
for (Bot bot : this.bots) {
@@ -187,6 +190,7 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendShort(9);
this.response.appendString("unknown");
this.response.appendInt(0);
this.response.appendInt(0);
}
}
return this.response;
@@ -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<String, Permission> rankPermissions = rank.getPermissions();
Map<String, Integer> resolved = new LinkedHashMap<>(rankPermissions.size());
for (Map.Entry<String, Permission> 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<String, Integer> 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;
}
@@ -125,10 +125,28 @@ public class UserProfileComposer extends MessageComposer {
this.response.appendString(customizationData.prefixEffect);
this.response.appendString(customizationData.prefixFont);
this.response.appendString(customizationData.displayOrder);
this.response.appendInt(this.getTotalBadges());
return this.response;
}
private int getTotalBadges() {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT COUNT(DISTINCT badge_code) AS total_badges FROM users_badges WHERE user_id = ?")) {
statement.setInt(1, this.habboInfo.getId());
try (ResultSet set = statement.executeQuery()) {
if (set.next()) {
return set.getInt("total_badges");
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception while loading total badges for extended profile", e);
}
return 0;
}
public HabboInfo getHabboInfo() {
return habboInfo;
}
@@ -0,0 +1,43 @@
package com.eu.habbo.monitoring;
import java.util.concurrent.atomic.AtomicLong;
public final class EmulatorNetworkStats {
private static final AtomicLong INCOMING_PACKETS = new AtomicLong();
private static final AtomicLong OUTGOING_PACKETS = new AtomicLong();
private static final AtomicLong INCOMING_BYTES = new AtomicLong();
private static final AtomicLong OUTGOING_BYTES = new AtomicLong();
private EmulatorNetworkStats() {
}
public static void recordIncoming(int byteCount) {
INCOMING_PACKETS.incrementAndGet();
if (byteCount > 0) {
INCOMING_BYTES.addAndGet(byteCount);
}
}
public static void recordOutgoing(int byteCount) {
OUTGOING_PACKETS.incrementAndGet();
if (byteCount > 0) {
OUTGOING_BYTES.addAndGet(byteCount);
}
}
public static long getIncomingPackets() {
return INCOMING_PACKETS.get();
}
public static long getOutgoingPackets() {
return OUTGOING_PACKETS.get();
}
public static long getIncomingBytes() {
return INCOMING_BYTES.get();
}
public static long getOutgoingBytes() {
return OUTGOING_BYTES.get();
}
}
@@ -0,0 +1,627 @@
package com.eu.habbo.monitoring;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.core.WiredRoomDiagnostics;
import com.eu.habbo.habbohotel.wired.tick.WiredTickService;
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.HikariPoolMXBean;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
public final class EmulatorStatsService {
private static final long CACHE_TTL_MS = 1_000L;
private static final int MAX_HISTORY_POINTS = 90;
private static final ArrayDeque<MemoryPoint> MEMORY_HISTORY = new ArrayDeque<>();
private static volatile Snapshot cachedSnapshot = null;
private static volatile long cachedAt = 0L;
private static volatile int peakPlayers = 0;
private static volatile int peakWebSocketSessions = 0;
private static volatile long previousIncomingPackets = 0L;
private static volatile long previousOutgoingPackets = 0L;
private static volatile long previousIncomingBytes = 0L;
private static volatile long previousOutgoingBytes = 0L;
private static volatile long previousGcCount = 0L;
private static volatile long previousGcTimeMs = 0L;
private static volatile long previousTelemetryAt = 0L;
private EmulatorStatsService() {
}
public static Snapshot collectSnapshot() {
long now = System.currentTimeMillis();
Snapshot current = cachedSnapshot;
if (current != null && (now - cachedAt) < CACHE_TTL_MS) {
return current;
}
synchronized (EmulatorStatsService.class) {
current = cachedSnapshot;
if (current != null && (now - cachedAt) < CACHE_TTL_MS) {
return current;
}
Snapshot built = buildSnapshot(now);
cachedSnapshot = built;
cachedAt = now;
return built;
}
}
public static String formatDuration(long totalSeconds) {
long hours = totalSeconds / 3600L;
long minutes = (totalSeconds % 3600L) / 60L;
long seconds = totalSeconds % 60L;
return String.format("%02dh %02dm %02ds", hours, minutes, seconds);
}
private static Snapshot buildSnapshot(long now) {
Runtime runtime = Runtime.getRuntime();
long totalMemBytes = runtime.totalMemory();
long freeMemBytes = runtime.freeMemory();
long usedMemBytes = totalMemBytes - freeMemBytes;
long maxMemBytes = runtime.maxMemory();
int usedMemMb = (int) (usedMemBytes / 1024L / 1024L);
int maxMemMb = (int) (maxMemBytes / 1024L / 1024L);
int estimatedAllocMb = (int) (totalMemBytes / 1024L / 1024L);
double memoryUsagePercent = maxMemBytes > 0
? (usedMemBytes * 100D) / maxMemBytes
: 0D;
OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
double cpuLoadPercent = 0D;
if (osBean instanceof com.sun.management.OperatingSystemMXBean managedOsBean) {
cpuLoadPercent = Math.max(0D, managedOsBean.getProcessCpuLoad() * 100D);
}
int threadCount = ManagementFactory.getThreadMXBean().getThreadCount();
List<Habbo> habbos = List.of();
List<Room> rooms = List.of();
int webSocketSessions = 0;
if (Emulator.getGameEnvironment() != null) {
if (Emulator.getGameEnvironment().getHabboManager() != null) {
habbos = Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().values().stream().toList();
}
if (Emulator.getGameEnvironment().getRoomManager() != null) {
rooms = Emulator.getGameEnvironment().getRoomManager().getActiveRooms();
}
}
if (Emulator.getGameServer() != null && Emulator.getGameServer().getGameClientManager() != null) {
webSocketSessions = Emulator.getGameServer().getGameClientManager().getSessions().size();
}
peakPlayers = Math.max(peakPlayers, habbos.size());
peakWebSocketSessions = Math.max(peakWebSocketSessions, webSocketSessions);
WiredTickService wiredTickService = WiredTickService.getInstance();
int totalTickables = (wiredTickService != null) ? wiredTickService.getTotalTickableCount() : 0;
appendMemoryHistory(now, usedMemMb, maxMemMb, memoryUsagePercent);
double averageRoomCycleMs = 0D;
double worstRoomCycleMs = 0D;
int worstRoomCycleRoomId = 0;
String worstRoomCycleRoomName = "-";
long totalDelayedEventsPending = 0L;
int overloadedWiredRooms = 0;
int heavyWiredRooms = 0;
double wiredActivityPerSecond = 0D;
List<OnlineUserRow> users = new ArrayList<>(habbos.size());
for (Habbo habbo : habbos) {
int roomId = (habbo.getHabboInfo().getCurrentRoom() != null) ? habbo.getHabboInfo().getCurrentRoom().getId() : 0;
users.add(new OnlineUserRow(
habbo.getHabboInfo().getId(),
habbo.getHabboInfo().getUsername(),
habbo.getHabboInfo().getRank().getName(),
habbo.getHabboInfo().getCurrencyAmount(0),
roomId
));
}
List<ActiveRoomRow> activeRooms = new ArrayList<>(rooms.size());
List<WiredRoomRow> wiredRooms = new ArrayList<>();
List<WiredTopRoomRow> wiredTopRooms = new ArrayList<>();
double roomCycleAccumulator = 0D;
int roomCycleSamples = 0;
for (Room room : rooms) {
int tickables = (wiredTickService != null) ? wiredTickService.getTickableCount(room.getId()) : 0;
double roomCycleMs = Math.max(0D, room.lastCycleCpuMs);
roomCycleAccumulator += roomCycleMs;
roomCycleSamples++;
if (roomCycleMs >= worstRoomCycleMs) {
worstRoomCycleMs = roomCycleMs;
worstRoomCycleRoomId = room.getId();
worstRoomCycleRoomName = room.getName();
}
activeRooms.add(new ActiveRoomRow(
room.getId(),
room.getName(),
room.getUserCount(),
room.itemCount(),
tickables,
room.lastCycleCpuMs,
room.getEstimatedMemoryUsage() / 1024L,
room.lastCycleThread
));
WiredRoomDiagnostics.Snapshot diagnostics = WiredManager.getDiagnosticsSnapshot(room.getId());
if (diagnostics == null) {
continue;
}
boolean shouldShow = diagnostics.getAverageExecutionMs() > 0
|| diagnostics.getPeakExecutionMs() > 0
|| tickables > 0
|| diagnostics.getDelayedEventsPending() > 0;
if (!shouldShow) {
continue;
}
int usagePercent = (int) Math.round((diagnostics.getUsageCurrentWindow() * 100D) / Math.max(1, diagnostics.getUsageLimitPerWindow()));
double roomActivityPerSecond = (diagnostics.getUsageCurrentWindow() * 1000D) / Math.max(1, diagnostics.getUsageWindowMs());
totalDelayedEventsPending += diagnostics.getDelayedEventsPending();
wiredActivityPerSecond += roomActivityPerSecond;
if (diagnostics.getAverageExecutionMs() >= diagnostics.getOverloadAverageThresholdMs()) {
overloadedWiredRooms++;
}
if (diagnostics.isHeavy()) {
heavyWiredRooms++;
}
wiredRooms.add(new WiredRoomRow(
room.getId(),
diagnostics.getAverageExecutionMs(),
diagnostics.getPeakExecutionMs(),
usagePercent,
diagnostics.getDelayedEventsPending(),
diagnostics.getAverageExecutionMs() >= diagnostics.getOverloadAverageThresholdMs(),
diagnostics.isHeavy()
));
wiredTopRooms.add(new WiredTopRoomRow(
room.getId(),
room.getName(),
usagePercent,
diagnostics.getAverageExecutionMs(),
diagnostics.getPeakExecutionMs(),
diagnostics.getDelayedEventsPending(),
roomActivityPerSecond,
diagnostics.isHeavy()
));
}
if (roomCycleSamples > 0) {
averageRoomCycleMs = roomCycleAccumulator / roomCycleSamples;
}
wiredTopRooms.sort(Comparator
.comparingInt((WiredTopRoomRow row) -> row.usagePercent).reversed()
.thenComparingInt(row -> row.averageTickMs).reversed()
.thenComparingInt(row -> row.peakTickMs).reversed());
if (wiredTopRooms.size() > 5) {
wiredTopRooms = new ArrayList<>(wiredTopRooms.subList(0, 5));
}
HikariPoolMetrics hikariPoolMetrics = collectHikariPoolMetrics();
SchedulerMetrics schedulerMetrics = collectSchedulerMetrics();
NetworkMetrics networkMetrics = collectNetworkMetrics(now);
GarbageCollectorMetrics garbageCollectorMetrics = collectGarbageCollectorMetrics(now);
Overview overview = new Overview(
Emulator.getOnlineTime(),
now,
cpuLoadPercent >= 80D ? "Attention needed" : "Healthy",
usedMemMb,
maxMemMb,
estimatedAllocMb,
memoryUsagePercent,
cpuLoadPercent,
threadCount,
habbos.size(),
rooms.size(),
totalTickables,
peakPlayers,
webSocketSessions,
peakWebSocketSessions,
averageRoomCycleMs,
worstRoomCycleMs,
worstRoomCycleRoomId,
worstRoomCycleRoomName,
totalDelayedEventsPending,
overloadedWiredRooms,
heavyWiredRooms,
wiredActivityPerSecond
);
return new Snapshot(
overview,
new ArrayList<>(MEMORY_HISTORY),
users,
activeRooms,
wiredRooms,
wiredTopRooms,
hikariPoolMetrics,
schedulerMetrics,
networkMetrics,
garbageCollectorMetrics
);
}
private static HikariPoolMetrics collectHikariPoolMetrics() {
HikariDataSource dataSource = (Emulator.getDatabase() != null) ? Emulator.getDatabase().getDataSource() : null;
HikariPoolMXBean poolMxBean = (dataSource != null) ? dataSource.getHikariPoolMXBean() : null;
if (poolMxBean == null) {
return new HikariPoolMetrics(0, 0, 0, 0, 0);
}
return new HikariPoolMetrics(
poolMxBean.getActiveConnections(),
poolMxBean.getIdleConnections(),
poolMxBean.getTotalConnections(),
poolMxBean.getThreadsAwaitingConnection(),
dataSource.getMaximumPoolSize()
);
}
private static SchedulerMetrics collectSchedulerMetrics() {
if (Emulator.getThreading() == null) {
return new SchedulerMetrics(0, 0, 0, 0, false);
}
if (!(Emulator.getThreading().getService() instanceof ScheduledThreadPoolExecutor executor)) {
return new SchedulerMetrics(0, 0, 0, 0, false);
}
return new SchedulerMetrics(
executor.getQueue().size(),
executor.getActiveCount(),
executor.getPoolSize(),
executor.getCompletedTaskCount(),
!executor.isShutdown()
);
}
private static NetworkMetrics collectNetworkMetrics(long now) {
long incomingPackets = EmulatorNetworkStats.getIncomingPackets();
long outgoingPackets = EmulatorNetworkStats.getOutgoingPackets();
long incomingBytes = EmulatorNetworkStats.getIncomingBytes();
long outgoingBytes = EmulatorNetworkStats.getOutgoingBytes();
long previousAt = previousTelemetryAt;
long elapsedMs = (previousAt > 0L) ? Math.max(1L, now - previousAt) : CACHE_TTL_MS;
double incomingPacketsPerSecond = ((incomingPackets - previousIncomingPackets) * 1000D) / elapsedMs;
double outgoingPacketsPerSecond = ((outgoingPackets - previousOutgoingPackets) * 1000D) / elapsedMs;
double incomingKilobytesPerSecond = ((incomingBytes - previousIncomingBytes) / 1024D) * 1000D / elapsedMs;
double outgoingKilobytesPerSecond = ((outgoingBytes - previousOutgoingBytes) / 1024D) * 1000D / elapsedMs;
previousIncomingPackets = incomingPackets;
previousOutgoingPackets = outgoingPackets;
previousIncomingBytes = incomingBytes;
previousOutgoingBytes = outgoingBytes;
previousTelemetryAt = now;
return new NetworkMetrics(
Math.max(0D, incomingPacketsPerSecond),
Math.max(0D, outgoingPacketsPerSecond),
Math.max(0D, incomingKilobytesPerSecond),
Math.max(0D, outgoingKilobytesPerSecond),
incomingPackets,
outgoingPackets
);
}
private static GarbageCollectorMetrics collectGarbageCollectorMetrics(long now) {
long totalCollections = 0L;
long totalCollectionTimeMs = 0L;
for (GarbageCollectorMXBean garbageCollectorMXBean : ManagementFactory.getGarbageCollectorMXBeans()) {
long collectionCount = garbageCollectorMXBean.getCollectionCount();
long collectionTime = garbageCollectorMXBean.getCollectionTime();
if (collectionCount > 0) {
totalCollections += collectionCount;
}
if (collectionTime > 0) {
totalCollectionTimeMs += collectionTime;
}
}
long lastObservedPauseMs = Math.max(0L, totalCollectionTimeMs - previousGcTimeMs);
long collectionsSinceLastSample = Math.max(0L, totalCollections - previousGcCount);
previousGcCount = totalCollections;
previousGcTimeMs = totalCollectionTimeMs;
return new GarbageCollectorMetrics(
totalCollections,
totalCollectionTimeMs,
collectionsSinceLastSample,
lastObservedPauseMs,
now
);
}
private static void appendMemoryHistory(long timestamp, int usedMemMb, int maxMemMb, double usagePercent) {
MEMORY_HISTORY.addLast(new MemoryPoint(timestamp, usedMemMb, maxMemMb, usagePercent));
while (MEMORY_HISTORY.size() > MAX_HISTORY_POINTS) {
MEMORY_HISTORY.removeFirst();
}
}
public static final class Snapshot {
public final Overview overview;
public final List<MemoryPoint> memoryHistory;
public final List<OnlineUserRow> users;
public final List<ActiveRoomRow> rooms;
public final List<WiredRoomRow> wired;
public final List<WiredTopRoomRow> wiredTopRooms;
public final HikariPoolMetrics databasePool;
public final SchedulerMetrics scheduler;
public final NetworkMetrics network;
public final GarbageCollectorMetrics garbageCollector;
public Snapshot(Overview overview, List<MemoryPoint> memoryHistory, List<OnlineUserRow> users, List<ActiveRoomRow> rooms, List<WiredRoomRow> wired, List<WiredTopRoomRow> wiredTopRooms, HikariPoolMetrics databasePool, SchedulerMetrics scheduler, NetworkMetrics network, GarbageCollectorMetrics garbageCollector) {
this.overview = overview;
this.memoryHistory = memoryHistory;
this.users = users;
this.rooms = rooms;
this.wired = wired;
this.wiredTopRooms = wiredTopRooms;
this.databasePool = databasePool;
this.scheduler = scheduler;
this.network = network;
this.garbageCollector = garbageCollector;
}
}
public static final class Overview {
public final long uptimeSeconds;
public final long lastRefreshEpochMs;
public final String guiStatus;
public final int memoryUsedMb;
public final int memoryMaxMb;
public final int memoryAllocatedMb;
public final double memoryUsagePercent;
public final double cpuLoadPercent;
public final int activeOsThreads;
public final int connectedPlayers;
public final int loadedRooms;
public final int wiredTickables;
public final int peakPlayers;
public final int activeWebSocketSessions;
public final int peakWebSocketSessions;
public final double averageRoomCycleMs;
public final double worstRoomCycleMs;
public final int worstRoomCycleRoomId;
public final String worstRoomCycleRoomName;
public final long delayedEventsPending;
public final int overloadedWiredRooms;
public final int heavyWiredRooms;
public final double wiredActivityPerSecond;
public Overview(long uptimeSeconds, long lastRefreshEpochMs, String guiStatus, int memoryUsedMb, int memoryMaxMb, int memoryAllocatedMb, double memoryUsagePercent, double cpuLoadPercent, int activeOsThreads, int connectedPlayers, int loadedRooms, int wiredTickables, int peakPlayers, int activeWebSocketSessions, int peakWebSocketSessions, double averageRoomCycleMs, double worstRoomCycleMs, int worstRoomCycleRoomId, String worstRoomCycleRoomName, long delayedEventsPending, int overloadedWiredRooms, int heavyWiredRooms, double wiredActivityPerSecond) {
this.uptimeSeconds = uptimeSeconds;
this.lastRefreshEpochMs = lastRefreshEpochMs;
this.guiStatus = guiStatus;
this.memoryUsedMb = memoryUsedMb;
this.memoryMaxMb = memoryMaxMb;
this.memoryAllocatedMb = memoryAllocatedMb;
this.memoryUsagePercent = memoryUsagePercent;
this.cpuLoadPercent = cpuLoadPercent;
this.activeOsThreads = activeOsThreads;
this.connectedPlayers = connectedPlayers;
this.loadedRooms = loadedRooms;
this.wiredTickables = wiredTickables;
this.peakPlayers = peakPlayers;
this.activeWebSocketSessions = activeWebSocketSessions;
this.peakWebSocketSessions = peakWebSocketSessions;
this.averageRoomCycleMs = averageRoomCycleMs;
this.worstRoomCycleMs = worstRoomCycleMs;
this.worstRoomCycleRoomId = worstRoomCycleRoomId;
this.worstRoomCycleRoomName = worstRoomCycleRoomName;
this.delayedEventsPending = delayedEventsPending;
this.overloadedWiredRooms = overloadedWiredRooms;
this.heavyWiredRooms = heavyWiredRooms;
this.wiredActivityPerSecond = wiredActivityPerSecond;
}
}
public static final class MemoryPoint {
public final long timestamp;
public final int usedMb;
public final int maxMb;
public final double usagePercent;
public MemoryPoint(long timestamp, int usedMb, int maxMb, double usagePercent) {
this.timestamp = timestamp;
this.usedMb = usedMb;
this.maxMb = maxMb;
this.usagePercent = usagePercent;
}
}
public static final class OnlineUserRow {
public final int id;
public final String username;
public final String rank;
public final int credits;
public final int roomId;
public OnlineUserRow(int id, String username, String rank, int credits, int roomId) {
this.id = id;
this.username = username;
this.rank = rank;
this.credits = credits;
this.roomId = roomId;
}
}
public static final class ActiveRoomRow {
public final int roomId;
public final String name;
public final int players;
public final int items;
public final int tickables;
public final double cpuMs;
public final long estimatedRamKb;
public final String thread;
public ActiveRoomRow(int roomId, String name, int players, int items, int tickables, double cpuMs, long estimatedRamKb, String thread) {
this.roomId = roomId;
this.name = name;
this.players = players;
this.items = items;
this.tickables = tickables;
this.cpuMs = cpuMs;
this.estimatedRamKb = estimatedRamKb;
this.thread = thread;
}
}
public static final class WiredRoomRow {
public final int roomId;
public final long averageTickMs;
public final long peakTickMs;
public final int usagePercent;
public final int delayedEventsPending;
public final boolean overloaded;
public final boolean heavy;
public WiredRoomRow(int roomId, long averageTickMs, long peakTickMs, int usagePercent, int delayedEventsPending, boolean overloaded, boolean heavy) {
this.roomId = roomId;
this.averageTickMs = averageTickMs;
this.peakTickMs = peakTickMs;
this.usagePercent = usagePercent;
this.delayedEventsPending = delayedEventsPending;
this.overloaded = overloaded;
this.heavy = heavy;
}
}
public static final class WiredTopRoomRow {
public final int roomId;
public final String name;
public final int usagePercent;
public final int averageTickMs;
public final int peakTickMs;
public final int delayedEventsPending;
public final double activityPerSecond;
public final boolean heavy;
public WiredTopRoomRow(int roomId, String name, int usagePercent, int averageTickMs, int peakTickMs, int delayedEventsPending, double activityPerSecond, boolean heavy) {
this.roomId = roomId;
this.name = name;
this.usagePercent = usagePercent;
this.averageTickMs = averageTickMs;
this.peakTickMs = peakTickMs;
this.delayedEventsPending = delayedEventsPending;
this.activityPerSecond = activityPerSecond;
this.heavy = heavy;
}
}
public static final class HikariPoolMetrics {
public final int activeConnections;
public final int idleConnections;
public final int totalConnections;
public final int waitingThreads;
public final int maxConnections;
public HikariPoolMetrics(int activeConnections, int idleConnections, int totalConnections, int waitingThreads, int maxConnections) {
this.activeConnections = activeConnections;
this.idleConnections = idleConnections;
this.totalConnections = totalConnections;
this.waitingThreads = waitingThreads;
this.maxConnections = maxConnections;
}
}
public static final class SchedulerMetrics {
public final int queuedTasks;
public final int activeThreads;
public final int poolSize;
public final long completedTasks;
public final boolean running;
public SchedulerMetrics(int queuedTasks, int activeThreads, int poolSize, long completedTasks, boolean running) {
this.queuedTasks = queuedTasks;
this.activeThreads = activeThreads;
this.poolSize = poolSize;
this.completedTasks = completedTasks;
this.running = running;
}
}
public static final class NetworkMetrics {
public final double incomingPacketsPerSecond;
public final double outgoingPacketsPerSecond;
public final double incomingKilobytesPerSecond;
public final double outgoingKilobytesPerSecond;
public final long totalIncomingPackets;
public final long totalOutgoingPackets;
public NetworkMetrics(double incomingPacketsPerSecond, double outgoingPacketsPerSecond, double incomingKilobytesPerSecond, double outgoingKilobytesPerSecond, long totalIncomingPackets, long totalOutgoingPackets) {
this.incomingPacketsPerSecond = incomingPacketsPerSecond;
this.outgoingPacketsPerSecond = outgoingPacketsPerSecond;
this.incomingKilobytesPerSecond = incomingKilobytesPerSecond;
this.outgoingKilobytesPerSecond = outgoingKilobytesPerSecond;
this.totalIncomingPackets = totalIncomingPackets;
this.totalOutgoingPackets = totalOutgoingPackets;
}
}
public static final class GarbageCollectorMetrics {
public final long totalCollections;
public final long totalCollectionTimeMs;
public final long collectionsSinceLastSample;
public final long lastObservedPauseMs;
public final long sampledAtEpochMs;
public GarbageCollectorMetrics(long totalCollections, long totalCollectionTimeMs, long collectionsSinceLastSample, long lastObservedPauseMs, long sampledAtEpochMs) {
this.totalCollections = totalCollections;
this.totalCollectionTimeMs = totalCollectionTimeMs;
this.collectionsSinceLastSample = collectionsSinceLastSample;
this.lastObservedPauseMs = lastObservedPauseMs;
this.sampledAtEpochMs = sampledAtEpochMs;
}
}
}
@@ -5,6 +5,7 @@ import com.eu.habbo.messages.PacketManager;
import com.eu.habbo.networking.gameserver.auth.AuthHttpHandler;
import com.eu.habbo.networking.gameserver.auth.NitroSecureApiHandler;
import com.eu.habbo.networking.gameserver.auth.NitroSecureAssetHandler;
import com.eu.habbo.networking.gameserver.badges.BadgeLeaderboardHttpHandler;
import com.eu.habbo.networking.gameserver.badges.BadgeHttpHandler;
import com.eu.habbo.networking.gameserver.codec.WebSocketCodec;
import com.eu.habbo.networking.gameserver.crypto.WsHandshakeHandler;
@@ -13,6 +14,7 @@ import com.eu.habbo.networking.gameserver.encoders.GameServerMessageEncoder;
import com.eu.habbo.networking.gameserver.encoders.GameServerMessageLogger;
import com.eu.habbo.networking.gameserver.handlers.IdleTimeoutHandler;
import com.eu.habbo.networking.gameserver.handlers.WebSocketHttpHandler;
import com.eu.habbo.networking.gameserver.stats.EmuStatsHttpHandler;
import com.eu.habbo.networking.gameserver.ssl.SSLCertificateLoader;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
@@ -60,6 +62,8 @@ public class WebSocketChannelInitializer extends ChannelInitializer<SocketChanne
ch.pipeline().addLast("nitroSecureApiHandler", new NitroSecureApiHandler());
ch.pipeline().addLast("authHttpHandler", new AuthHttpHandler());
ch.pipeline().addLast("badgeHttpHandler", new BadgeHttpHandler());
ch.pipeline().addLast("badgeLeaderboardHttpHandler", new BadgeLeaderboardHttpHandler());
ch.pipeline().addLast("emuStatsHttpHandler", new EmuStatsHttpHandler());
ch.pipeline().addLast("wsProtocolHandler", new WebSocketServerProtocolHandler(this.wsConfig));
ch.pipeline().addLast("wsFrameAggregator", new WebSocketFrameAggregator(MAX_FRAME_SIZE));
ch.pipeline().addLast("wsCodec", new WebSocketCodec());
@@ -0,0 +1,458 @@
package com.eu.habbo.networking.gameserver.badges;
import com.eu.habbo.Emulator;
import com.eu.habbo.networking.gameserver.auth.AccessTokenService;
import com.eu.habbo.networking.gameserver.auth.CorsOriginGate;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.*;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.*;
public class BadgeLeaderboardHttpHandler extends ChannelInboundHandlerAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(BadgeLeaderboardHttpHandler.class);
private static final String BASE_PATH = "/api/badges/leaderboard";
private static final long CACHE_TTL_MS = 15_000L;
private static final int MAX_BOARD_USERS = 100;
private static volatile Snapshot cache = null;
private static final class Snapshot {
final List<UserBadgeAggregate> badgeUsers;
final List<UserAchievementAggregate> achievementUsers;
final JsonArray badgeStats;
final long expiresAt;
Snapshot(List<UserBadgeAggregate> badgeUsers, List<UserAchievementAggregate> achievementUsers, JsonArray badgeStats, long expiresAt) {
this.badgeUsers = badgeUsers;
this.achievementUsers = achievementUsers;
this.badgeStats = badgeStats;
this.expiresAt = expiresAt;
}
}
private static final class UserBadgeAggregate {
final int userId;
final String username;
final String figure;
final int totalBadges;
final EnumMap<Rarity, Integer> counts;
UserBadgeAggregate(int userId, String username, String figure, int totalBadges, EnumMap<Rarity, Integer> counts) {
this.userId = userId;
this.username = username;
this.figure = figure;
this.totalBadges = totalBadges;
this.counts = counts;
}
}
private static final class UserAchievementAggregate {
final int userId;
final String username;
final String figure;
final int achievementScore;
UserAchievementAggregate(int userId, String username, String figure, int achievementScore) {
this.userId = userId;
this.username = username;
this.figure = figure;
this.achievementScore = achievementScore;
}
}
private static final class ViewerProfile {
final int userId;
final String username;
final String figure;
ViewerProfile(int userId, String username, String figure) {
this.userId = userId;
this.username = username;
this.figure = figure;
}
}
private enum Rarity {
COMMON("common"),
RARE("rare"),
EPIC("epic"),
LEGENDARY("legendary"),
MYTHICAL("mythical"),
UNIQUE("unique");
final String key;
Rarity(String key) {
this.key = key;
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (!(msg instanceof FullHttpRequest req)) {
super.channelRead(ctx, msg);
return;
}
String path = new QueryStringDecoder(req.uri()).path();
if (!path.equals(BASE_PATH) && !path.startsWith(BASE_PATH + "/")) {
super.channelRead(ctx, msg);
return;
}
try {
handle(ctx, req);
} finally {
ReferenceCountUtil.release(req);
}
}
private void handle(ChannelHandlerContext ctx, FullHttpRequest req) {
if (req.method() == HttpMethod.OPTIONS) {
sendCors(ctx, req);
return;
}
if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) {
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, error("Use GET."));
return;
}
try {
Snapshot snapshot = loadSnapshot();
int viewerUserId = authenticateOptional(req);
ViewerProfile viewerProfile = loadViewerProfile(viewerUserId);
JsonObject payload = new JsonObject();
payload.addProperty("viewerUserId", viewerUserId);
payload.add("badgeStats", cloneArray(snapshot.badgeStats));
payload.add("thresholds", buildThresholdsPayload());
JsonObject boards = new JsonObject();
boards.add("totalBadges", buildBadgeBoard(snapshot.badgeUsers, viewerUserId, viewerProfile, null));
boards.add("achievementLevel", buildAchievementBoard(snapshot.achievementUsers, viewerUserId, viewerProfile));
JsonObject rarityBoards = new JsonObject();
for (Rarity rarity : Rarity.values()) {
rarityBoards.add(rarity.key, buildBadgeBoard(snapshot.badgeUsers, viewerUserId, viewerProfile, rarity));
}
boards.add("rarity", rarityBoards);
payload.add("leaderboards", boards);
sendJson(ctx, req, HttpResponseStatus.OK, payload);
} catch (Exception e) {
LOGGER.error("[badges/leaderboard] unexpected error", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, error("Server error."));
}
}
private Snapshot loadSnapshot() throws Exception {
long now = System.currentTimeMillis();
Snapshot current = cache;
if (current != null && current.expiresAt >= now) return current;
synchronized (BadgeLeaderboardHttpHandler.class) {
current = cache;
if (current != null && current.expiresAt >= now) return current;
JsonArray badgeStats = new JsonArray();
List<UserBadgeAggregate> badgeUsers = new ArrayList<>();
List<UserAchievementAggregate> achievementUsers = new ArrayList<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
loadBadgeStats(connection, badgeStats);
loadBadgeUsers(connection, badgeUsers);
loadAchievementUsers(connection, achievementUsers);
}
Snapshot built = new Snapshot(badgeUsers, achievementUsers, badgeStats, now + CACHE_TTL_MS);
cache = built;
return built;
}
}
private void loadBadgeStats(Connection connection, JsonArray badgeStats) throws Exception {
try (PreparedStatement statement = connection.prepareStatement(
"SELECT badge_code, COUNT(DISTINCT user_id) AS owner_count " +
"FROM users_badges GROUP BY badge_code ORDER BY owner_count ASC, badge_code ASC")) {
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
String badgeCode = set.getString("badge_code");
int ownerCount = set.getInt("owner_count");
JsonObject entry = new JsonObject();
entry.addProperty("badgeCode", badgeCode);
entry.addProperty("ownerCount", ownerCount);
entry.addProperty("rarity", classify(ownerCount).key);
badgeStats.add(entry);
}
}
}
}
private void loadBadgeUsers(Connection connection, List<UserBadgeAggregate> badgeUsers) throws Exception {
String sql =
"SELECT u.id AS user_id, u.username, u.look, " +
"COUNT(DISTINCT ub.badge_code) AS total_badges, " +
"COUNT(DISTINCT CASE WHEN counts.owner_count > 50 THEN ub.badge_code END) AS common_count, " +
"COUNT(DISTINCT CASE WHEN counts.owner_count > 10 AND counts.owner_count <= 50 THEN ub.badge_code END) AS rare_count, " +
"COUNT(DISTINCT CASE WHEN counts.owner_count > 6 AND counts.owner_count <= 10 THEN ub.badge_code END) AS epic_count, " +
"COUNT(DISTINCT CASE WHEN counts.owner_count > 3 AND counts.owner_count <= 6 THEN ub.badge_code END) AS legendary_count, " +
"COUNT(DISTINCT CASE WHEN counts.owner_count > 1 AND counts.owner_count <= 3 THEN ub.badge_code END) AS mythical_count, " +
"COUNT(DISTINCT CASE WHEN counts.owner_count = 1 THEN ub.badge_code END) AS unique_count " +
"FROM users_badges ub " +
"INNER JOIN users u ON u.id = ub.user_id " +
"INNER JOIN (SELECT badge_code, COUNT(DISTINCT user_id) AS owner_count FROM users_badges GROUP BY badge_code) counts ON counts.badge_code = ub.badge_code " +
"GROUP BY u.id, u.username, u.look";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
EnumMap<Rarity, Integer> counts = new EnumMap<>(Rarity.class);
counts.put(Rarity.COMMON, set.getInt("common_count"));
counts.put(Rarity.RARE, set.getInt("rare_count"));
counts.put(Rarity.EPIC, set.getInt("epic_count"));
counts.put(Rarity.LEGENDARY, set.getInt("legendary_count"));
counts.put(Rarity.MYTHICAL, set.getInt("mythical_count"));
counts.put(Rarity.UNIQUE, set.getInt("unique_count"));
badgeUsers.add(new UserBadgeAggregate(
set.getInt("user_id"),
safe(set.getString("username")),
safe(set.getString("look")),
set.getInt("total_badges"),
counts
));
}
}
}
}
private void loadAchievementUsers(Connection connection, List<UserAchievementAggregate> achievementUsers) throws Exception {
try (PreparedStatement statement = connection.prepareStatement(
"SELECT u.id AS user_id, u.username, u.look, COALESCE(us.achievement_score, 0) AS achievement_score " +
"FROM users u INNER JOIN users_settings us ON us.user_id = u.id " +
"WHERE COALESCE(us.achievement_score, 0) > 0")) {
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
achievementUsers.add(new UserAchievementAggregate(
set.getInt("user_id"),
safe(set.getString("username")),
safe(set.getString("look")),
set.getInt("achievement_score")
));
}
}
}
}
private ViewerProfile loadViewerProfile(int viewerUserId) throws Exception {
if (viewerUserId <= 0) return null;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT id, username, look FROM users WHERE id = ? LIMIT 1")) {
statement.setInt(1, viewerUserId);
try (ResultSet set = statement.executeQuery()) {
if (!set.next()) return null;
return new ViewerProfile(
set.getInt("id"),
safe(set.getString("username")),
safe(set.getString("look"))
);
}
}
}
private JsonObject buildBadgeBoard(List<UserBadgeAggregate> users, int viewerUserId, ViewerProfile viewerProfile, Rarity rarity) {
List<JsonObject> ranked = new ArrayList<>();
for (UserBadgeAggregate user : users) {
int score = (rarity == null) ? user.totalBadges : user.counts.getOrDefault(rarity, 0);
if (score <= 0) continue;
ranked.add(toEntry(user.userId, user.username, user.figure, score));
}
ranked.sort((a, b) -> {
int scoreCompare = Integer.compare(b.get("score").getAsInt(), a.get("score").getAsInt());
if (scoreCompare != 0) return scoreCompare;
return Integer.compare(a.get("userId").getAsInt(), b.get("userId").getAsInt());
});
return finalizeBoard(ranked, viewerUserId, viewerProfile);
}
private JsonObject buildAchievementBoard(List<UserAchievementAggregate> users, int viewerUserId, ViewerProfile viewerProfile) {
List<JsonObject> ranked = new ArrayList<>();
for (UserAchievementAggregate user : users) {
if (user.achievementScore <= 0) continue;
ranked.add(toEntry(user.userId, user.username, user.figure, user.achievementScore));
}
ranked.sort((a, b) -> {
int scoreCompare = Integer.compare(b.get("score").getAsInt(), a.get("score").getAsInt());
if (scoreCompare != 0) return scoreCompare;
return Integer.compare(a.get("userId").getAsInt(), b.get("userId").getAsInt());
});
return finalizeBoard(ranked, viewerUserId, viewerProfile);
}
private JsonObject finalizeBoard(List<JsonObject> ranked, int viewerUserId, ViewerProfile viewerProfile) {
JsonArray entries = new JsonArray();
JsonObject viewerEntry = null;
int cappedSize = Math.min(ranked.size(), MAX_BOARD_USERS);
for (int index = 0; index < cappedSize; index++) {
JsonObject entry = ranked.get(index).deepCopy();
int rank = index + 1;
entry.addProperty("rank", rank);
entries.add(entry);
if (viewerUserId > 0 && entry.get("userId").getAsInt() == viewerUserId) viewerEntry = entry;
}
if (viewerEntry == null && viewerUserId > 0) {
for (int index = 0; index < ranked.size(); index++) {
JsonObject entry = ranked.get(index);
if (entry.get("userId").getAsInt() != viewerUserId) continue;
viewerEntry = entry.deepCopy();
viewerEntry.addProperty("rank", index + 1);
break;
}
}
if (viewerEntry == null && viewerProfile != null) {
viewerEntry = toEntry(viewerProfile.userId, viewerProfile.username, viewerProfile.figure, 0);
viewerEntry.addProperty("rank", 0);
}
JsonObject board = new JsonObject();
board.add("entries", entries);
board.addProperty("totalPlayers", cappedSize);
board.add("viewerEntry", viewerEntry != null ? viewerEntry : new JsonObject());
return board;
}
private JsonObject toEntry(int userId, String username, String figure, int score) {
JsonObject entry = new JsonObject();
entry.addProperty("userId", userId);
entry.addProperty("username", username);
entry.addProperty("figure", figure);
entry.addProperty("score", score);
return entry;
}
private JsonObject buildThresholdsPayload() {
JsonObject thresholds = new JsonObject();
thresholds.addProperty("commonMinOwners", 51);
thresholds.addProperty("rareMinOwners", 11);
thresholds.addProperty("epicMinOwners", 7);
thresholds.addProperty("legendaryMinOwners", 4);
thresholds.addProperty("mythicalMinOwners", 2);
thresholds.addProperty("uniqueOwners", 1);
return thresholds;
}
private static Rarity classify(int ownerCount) {
if (ownerCount > 50) return Rarity.COMMON;
if (ownerCount > 10) return Rarity.RARE;
if (ownerCount > 6) return Rarity.EPIC;
if (ownerCount > 3) return Rarity.LEGENDARY;
if (ownerCount > 1) return Rarity.MYTHICAL;
if (ownerCount > 0) return Rarity.UNIQUE;
return Rarity.COMMON;
}
private static int authenticateOptional(FullHttpRequest req) {
String header = req.headers().get(HttpHeaderNames.AUTHORIZATION);
if (header == null || header.isEmpty()) return 0;
String token = header.startsWith("Bearer ") ? header.substring(7).trim() : header.trim();
return AccessTokenService.verify(token);
}
private static JsonArray cloneArray(JsonArray source) {
JsonArray copy = new JsonArray();
source.forEach(element -> copy.add(element.deepCopy()));
return copy;
}
private static String safe(String value) {
return value == null ? "" : value;
}
private static JsonObject error(String message) {
JsonObject obj = new JsonObject();
obj.addProperty("error", message);
return obj;
}
private static void sendJson(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, JsonObject body) {
byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8");
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
response.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-store, no-cache, must-revalidate");
applyCors(req, response);
boolean keepAlive = isKeepAlive(req);
if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
var future = ctx.writeAndFlush(response);
if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE);
}
private static void sendCors(ChannelHandlerContext ctx, FullHttpRequest req) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT);
applyCors(req, response);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void applyCors(FullHttpRequest req, FullHttpResponse response) {
String origin = req.headers().get(HttpHeaderNames.ORIGIN);
if (origin != null && !origin.isEmpty() && CorsOriginGate.isAllowed(req)) {
response.headers().set("Access-Control-Allow-Origin", origin);
response.headers().set("Access-Control-Allow-Credentials", "true");
}
response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
String requestedHeaders = req.headers().get("Access-Control-Request-Headers");
if (requestedHeaders != null && !requestedHeaders.isEmpty()) {
response.headers().set("Access-Control-Allow-Headers", requestedHeaders);
} else {
response.headers().set("Access-Control-Allow-Headers",
"Authorization, Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api");
}
response.headers().set("Vary", "Origin, Access-Control-Request-Headers, Access-Control-Request-Method");
response.headers().set("Access-Control-Max-Age", "600");
response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp");
}
private static boolean isKeepAlive(FullHttpRequest req) {
String connection = req.headers().get(HttpHeaderNames.CONNECTION);
return connection == null || !"close".equalsIgnoreCase(connection);
}
}
@@ -1,5 +1,6 @@
package com.eu.habbo.networking.gameserver.encoders;
import com.eu.habbo.monitoring.EmulatorNetworkStats;
import com.eu.habbo.messages.ServerMessage;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
@@ -16,6 +17,7 @@ public class GameServerMessageEncoder extends MessageToByteEncoder<ServerMessage
ByteBuf buf = message.get();
try {
EmulatorNetworkStats.recordOutgoing(buf.readableBytes());
out.writeBytes(buf);
} finally {
// Release copied buffer.
@@ -8,10 +8,13 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.*;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
public class WebSocketHttpHandler extends ChannelInboundHandlerAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketHttpHandler.class);
private static final String ORIGIN_HEADER = "Origin";
@Override
@@ -27,6 +30,12 @@ public class WebSocketHttpHandler extends ChannelInboundHandlerAdapter {
}
private boolean handleHttpRequest(ChannelHandlerContext ctx, HttpMessage req) {
captureForwardedIp(ctx, req);
if (!isWebSocketUpgrade(req)) {
return true;
}
String origin = "error";
try {
@@ -38,27 +47,47 @@ public class WebSocketHttpHandler extends ChannelInboundHandlerAdapter {
String whitelist = Emulator.getConfig().getValue("ws.whitelist", "localhost");
if (!isWhitelisted(origin, whitelist.split(","))) {
LOGGER.warn("WebSocket upgrade rejected — origin '{}' not in ws.whitelist='{}'",
req.headers().get(ORIGIN_HEADER), whitelist);
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.FORBIDDEN,
Unpooled.wrappedBuffer("Origin forbidden".getBytes())
);
response.headers().set("Vary", "Origin");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
return false;
}
String ipHeader = Emulator.getConfig().getValue("ws.ip.header", "");
if (!ipHeader.isEmpty() && req.headers().contains(ipHeader)) {
String ip = req.headers().get(ipHeader);
ctx.channel().attr(GameServerAttributes.WS_IP).set(ip);
}
return true;
}
private static void captureForwardedIp(ChannelHandlerContext ctx, HttpMessage req) {
String ipHeader = Emulator.getConfig().getValue("ws.ip.header", "");
if (!ipHeader.isEmpty() && req.headers().contains(ipHeader)) {
String ip = req.headers().get(ipHeader);
ctx.channel().attr(GameServerAttributes.WS_IP).set(ip);
}
}
private static boolean isWebSocketUpgrade(HttpMessage req) {
String upgrade = req.headers().get(HttpHeaderNames.UPGRADE);
if (upgrade == null || !"websocket".equalsIgnoreCase(upgrade)) return false;
String connection = req.headers().get(HttpHeaderNames.CONNECTION);
if (connection == null) return false;
for (String token : connection.split(",")) {
if ("upgrade".equalsIgnoreCase(token.trim())) return true;
}
return false;
}
private static String getDomainNameFromUrl(String url) throws Exception {
URI uri = new URI(url);
String domain = uri.getHost();
if (domain == null) return "error";
return domain.startsWith("www.") ? domain.substring(4) : domain;
}
@@ -0,0 +1,115 @@
package com.eu.habbo.networking.gameserver.stats;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.monitoring.EmulatorStatsService;
import com.eu.habbo.networking.gameserver.auth.AccessTokenService;
import com.eu.habbo.networking.gameserver.auth.CorsOriginGate;
import com.google.gson.Gson;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.*;
import io.netty.util.ReferenceCountUtil;
import java.nio.charset.StandardCharsets;
public class EmuStatsHttpHandler extends ChannelInboundHandlerAdapter {
private static final String BASE_PATH = "/api/emustats";
private static final Gson GSON = new Gson();
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (!(msg instanceof FullHttpRequest req)) {
super.channelRead(ctx, msg);
return;
}
String path = new QueryStringDecoder(req.uri()).path();
if (!BASE_PATH.equals(path)) {
super.channelRead(ctx, msg);
return;
}
try {
handle(ctx, req);
} finally {
ReferenceCountUtil.release(req);
}
}
private void handle(ChannelHandlerContext ctx, FullHttpRequest req) {
if (req.method() == HttpMethod.OPTIONS) {
sendCors(ctx, req);
return;
}
if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) {
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, "{\"error\":\"Use GET.\"}");
return;
}
int userId = authenticate(req);
if (userId <= 0) {
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, "{\"error\":\"Unauthorized.\"}");
return;
}
Habbo habbo = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (habbo == null || !habbo.hasPermission(Permission.ACC_MODTOOL_ROOM_INFO)) {
sendJson(ctx, req, HttpResponseStatus.FORBIDDEN, "{\"error\":\"Forbidden.\"}");
return;
}
EmulatorStatsService.Snapshot snapshot = EmulatorStatsService.collectSnapshot();
sendJson(ctx, req, HttpResponseStatus.OK, GSON.toJson(snapshot));
}
private static int authenticate(FullHttpRequest req) {
String header = req.headers().get(HttpHeaderNames.AUTHORIZATION);
if (header == null || header.isBlank()) return 0;
String token = header.startsWith("Bearer ") ? header.substring(7).trim() : header.trim();
return AccessTokenService.verify(token);
}
private static void sendCors(ChannelHandlerContext ctx, FullHttpRequest req) {
if (!CorsOriginGate.isAllowed(req)) {
FullHttpResponse forbidden = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN);
forbidden.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
ctx.writeAndFlush(forbidden).addListener(ChannelFutureListener.CLOSE);
return;
}
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT);
applyCors(req, response);
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendJson(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, String json) {
boolean headRequest = req.method() == HttpMethod.HEAD;
byte[] bytes = (json == null ? "{}" : json).getBytes(StandardCharsets.UTF_8);
FullHttpResponse response = headRequest
? new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status)
: new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8");
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
response.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-store, no-cache, must-revalidate");
applyCors(req, response);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void applyCors(FullHttpRequest req, FullHttpResponse response) {
String origin = req.headers().get(HttpHeaderNames.ORIGIN);
if (origin != null && !origin.isEmpty() && CorsOriginGate.isAllowed(req)) {
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, OPTIONS");
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Authorization, Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api");
response.headers().set(HttpHeaderNames.VARY, "Origin");
}
}
}
@@ -123,8 +123,8 @@ public class PluginManager {
WiredEngine.RATE_LIMIT_WINDOW_MS = Emulator.getConfig().getInt("wired.abuse.rate.limit.window.ms", 10000);
WiredEngine.WIRED_BAN_DURATION_MS = Emulator.getConfig().getInt("wired.abuse.ban.duration.ms", 600000);
WiredEngine.MONITOR_USAGE_WINDOW_MS = Emulator.getConfig().getInt("wired.monitor.usage.window.ms", 1000);
WiredEngine.MONITOR_USAGE_LIMIT = Emulator.getConfig().getInt("wired.monitor.usage.limit", 1000);
WiredEngine.MONITOR_DELAYED_EVENTS_LIMIT = Emulator.getConfig().getInt("wired.monitor.delayed.events.limit", 100);
WiredEngine.MONITOR_USAGE_LIMIT = Emulator.getConfig().getInt("wired.monitor.usage.limit", 50000);
WiredEngine.MONITOR_DELAYED_EVENTS_LIMIT = Emulator.getConfig().getInt("wired.monitor.delayed.events.limit", 50000);
WiredEngine.MONITOR_OVERLOAD_AVERAGE_MS = Emulator.getConfig().getInt("wired.monitor.overload.average.ms", 50);
WiredEngine.MONITOR_OVERLOAD_PEAK_MS = Emulator.getConfig().getInt("wired.monitor.overload.peak.ms", 150);
WiredEngine.MONITOR_OVERLOAD_CONSECUTIVE_WINDOWS = Emulator.getConfig().getInt("wired.monitor.overload.consecutive.windows", 2);
@@ -22,6 +22,9 @@ public class BotFollowHabbo implements Runnable {
@Override
public void run() {
if (this.bot == null || this.bot.getRoom() == null || this.bot.getRoom() != this.room) {
return;
}
if (this.bot != null) {
if (this.habbo != null && this.bot.getFollowingHabboId() == this.habbo.getHabboInfo().getId()) {
if (this.habbo.getHabboInfo().getCurrentRoom() != null && this.habbo.getHabboInfo().getCurrentRoom() == this.room) {
@@ -38,6 +38,10 @@ public class RoomUnitWalkToRoomUnit implements Runnable {
@Override
public void run() {
if (this.room == null || !this.room.isLoaded() || this.walker == null || this.target == null) {
return;
}
if (this.goalTile == null) {
this.findNewLocation();
Emulator.getThreading().run(this, 500);
@@ -48,9 +52,8 @@ public class RoomUnitWalkToRoomUnit implements Runnable {
if (this.walker.getCurrentLocation().distance(this.goalTile) <= this.minDistance) {
for (Runnable r : this.targetReached) {
Emulator.getThreading().run(r);
WiredManager.triggerBotReachedHabbo(this.room, this.walker, this.target);
}
WiredManager.triggerBotReachedHabbo(this.room, this.walker, this.target);
} else {
Emulator.getThreading().run(this, 500);
}
@@ -15,6 +15,8 @@ public class GameTimer implements Runnable {
@Override
public void run() {
timer.setThreadActive(false);
if (timer.getRoomId() == 0) {
timer.setRunning(false);
return;
@@ -23,7 +25,6 @@ public class GameTimer implements Runnable {
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(timer.getRoomId());
if (room == null || !timer.isRunning() || timer.isPaused()) {
timer.setThreadActive(false);
return;
}
@@ -31,10 +32,10 @@ public class GameTimer implements Runnable {
if (timer.getTimeNow() < 0) timer.setTimeNow(0);
if (timer.getTimeNow() > 0) {
timer.setThreadActive(true);
Emulator.getThreading().run(this, 1000);
if (timer.tryActivateTimerThread()) {
Emulator.getThreading().run(this, 1000);
}
} else {
timer.setThreadActive(false);
timer.setTimeNow(0);
timer.endGame(room);
WiredManager.triggerGameEnds(room);
@@ -14,28 +14,30 @@ public class GameUpCounter implements Runnable {
@Override
public void run() {
timer.setThreadActive(false);
if (timer.getRoomId() == 0) {
timer.setRunning(false);
timer.setThreadActive(false);
return;
}
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(timer.getRoomId());
if (room == null || !timer.isRunning() || timer.isPaused()) {
timer.setThreadActive(false);
return;
}
int tickDelayMs = (int) timer.getNextTickDelayMs();
timer.advanceCounterInMs(tickDelayMs);
WiredManager.triggerClockCounter(room, timer);
if (timer.getCurrentTimeInMs() % 1000 == 0) {
WiredManager.triggerClockCounter(room, timer);
}
if (timer.getCurrentTimeInMs() < timer.getMaximumTimeInMs()) {
timer.setThreadActive(true);
Emulator.getThreading().run(this, timer.getNextTickDelayMs());
if (timer.tryActivateTimerThread()) {
Emulator.getThreading().run(this, timer.getNextTickDelayMs());
}
} else {
timer.setThreadActive(false);
timer.setCurrentTimeInMs(timer.getMaximumTimeInMs());
timer.endGame(room);
WiredManager.triggerGameEnds(room);
+9 -10
View File
@@ -27,16 +27,6 @@ rcon.host=127.0.0.1
rcon.port=3001
rcon.allowed=127.0.0.1;127.0.0.2
#WebSocket Configuration (for Nitro)
#Set ws.enabled to true to enable WebSocket connections.
ws.enabled=false
ws.host=0.0.0.0
ws.port=2096
#Comma-separated whitelist of allowed origins. Supports wildcards: *.example.com, * (allow all)
ws.whitelist=localhost
#Header name for real client IP when behind a proxy (e.g., X-Forwarded-For, CF-Connecting-IP). Leave empty if not using a proxy.
ws.ip.header=
# Databse configuration
db.pool.connection_timeout_ms = 10000
db.pool.idle_timeout_ms = 600000
@@ -69,3 +59,12 @@ login.remember.jwt.secret=
# Login news API.
login.news.limit=5
#WebSocket Configuration (for Nitro)
#Please adjust this setting in the Database !!!!
### ws.enabled=false
### ws.host=0.0.0.0
### ws.port=2096
### ws.whitelist=localhost #Comma-separated whitelist of allowed origins. Supports wildcards: *.example.com, * (allow all)
### ws.ip.header=X-Forwarded-For #Header name for real client IP when behind a proxy (e.g., X-Forwarded-For, CF-Connecting-IP). Leave empty if not using a proxy.