Compare commits

...

140 Commits

Author SHA1 Message Date
github-actions[bot] 44ea3abd4e 🆙 Bump version to 4.2.34 [skip ci] 2026-06-03 14:37:38 +00:00
DuckieTM 609cd20ab2 Merge pull request #143 from simoleo89/feat/command-autocomplete-refactor
Structure commands alert output
2026-06-03 16:36:33 +02:00
github-actions[bot] 717a7f184f 🆙 Bump version to 4.2.33 [skip ci] 2026-06-03 14:23:40 +00:00
DuckieTM 2862446686 Merge pull request #148 from duckietm/dev
🆙 More updates mentions
2026-06-03 16:22:39 +02:00
duckietm e97e680006 🆙 More updates mentions 2026-06-03 16:20:02 +02:00
github-actions[bot] 7e59dca192 🆙 Bump version to 4.2.32 [skip ci] 2026-06-03 12:20:44 +00:00
DuckieTM 1109d53720 Merge pull request #147 from duckietm/dev
Dev
2026-06-03 14:19:42 +02:00
duckietm f12363a5b7 Merge branch 'dev' of https://github.com/duckietm/Arcturus-Morningstar-Extended into dev 2026-06-03 14:17:28 +02:00
duckietm 7d4ffec74e 🆙 Small Fixes mention 2026-06-03 14:17:25 +02:00
github-actions[bot] 281fede58c 🆙 Bump version to 4.2.31 [skip ci] 2026-06-03 08:56:45 +00:00
DuckieTM edf152485b Merge pull request #145 from duckietm/dev
Dev
2026-06-03 10:55:46 +02:00
DuckieTM 18a1bfbe90 Merge branch 'main' into dev 2026-06-03 10:55:37 +02:00
duckietm 7c32bbfd2d 🆙 wordfilter to set specific settings to prefix 2026-06-03 10:39:44 +02:00
DuckieTM 4eae206b64 Merge pull request #140 from simoleo89/feat/mentions-system
feat(mentions): server-side mention detection, persistence & packets
2026-06-03 09:49:45 +02:00
github-actions[bot] 155b2202c7 🆙 Bump version to 4.2.30 [skip ci] 2026-06-03 07:48:08 +00:00
DuckieTM 10c291eb9f Merge pull request #144 from duckietm/dev
Dev
2026-06-03 09:47:03 +02:00
duckietm 349a8c727e 🆙 Update SQL 2026-06-03 09:46:43 +02:00
duckietm 68f2b71d14 🆙 Updated Prefixes : Now use wordfilter / table custom_prefix_blacklist can be droped 2026-06-03 09:42:43 +02:00
duckietm 69a6c0d060 🆙 Make group forums private, so only memeber can view it 2026-06-03 07:46:59 +02:00
simoleo89 9f36d95dbc fix(commands): structure commands alert output 2026-06-02 18:34:50 +02:00
github-actions[bot] 885bdca0c4 🆙 Bump version to 4.2.29 [skip ci] 2026-06-02 16:03:45 +00:00
DuckieTM db035294a7 Merge pull request #142 from duckietm/dev
🆙 Updated Group buy
2026-06-02 18:02:42 +02:00
duckietm 3216ba1df6 🆙 Updated Group buy 2026-06-02 18:02:25 +02:00
Life c9a47b1fac Merge branch 'duckietm:main' into feat/mentions-system 2026-06-02 17:38:25 +02:00
github-actions[bot] 8d6b969d75 🆙 Bump version to 4.2.28 [skip ci] 2026-06-02 14:06:26 +00:00
DuckieTM b9723e0298 Merge pull request #141 from duckietm/dev
🆙 Security Fix
2026-06-02 16:05:11 +02:00
duckietm c4aae676b2 🆙 Security Fix
Thanks to @Bop:

There's a group bug where you can accept anyone into a group within MS. There's no packet validation for accepting members if the group is invite only.
This is crucial because if you allow users to have rights who are group members, your rooms can be trashed. AKA YOUR EVENT ROOMS
2026-06-02 16:04:47 +02:00
simoleo89 7624d3fbc3 feat(mentions): server-side delete packet + robust direct-nick resolution 2026-06-02 14:44:08 +02:00
github-actions[bot] 585f4dd3aa 🆙 Bump version to 4.2.27 [skip ci] 2026-06-01 06:28:06 +00:00
DuckieTM afa114d511 Merge pull request #139 from duckietm/dev
Dev
2026-06-01 08:27:01 +02:00
simoleo89 e9129576a9 feat(mentions): server-side detection, persistence and packets 2026-05-31 21:47:56 +02:00
DuckieTM 0aadd01493 Merge pull request #138 from simoleo89/feat/wheel-admin-add-remove
feat(wheel): add & remove fortune-wheel prizes from the editor
2026-05-31 15:45:10 +02:00
simoleo89 9d98fbf9ee feat(wheel): support adding & removing fortune-wheel prizes from the editor
The prize editor could only update existing rows; savePrize was UPDATE-only,
so the admin panel had no way to add a new slice or remove an old one.

- WheelManager.savePrize now takes a sortOrder and inserts when id <= 0
  (returning the generated id) or updates + re-enables when id > 0, so a
  previously removed prize can be brought back. sort_order is persisted to
  match the editor's display order.
- New WheelManager.disablePrizesNotIn(keptIds) soft-deletes (enabled = 0)
  any prize absent from the saved authoritative list. Non-destructive: rows
  stay in the table and loadPrizes already filters enabled = 1.
- WheelAdminSavePrizesEvent collects the saved ids and disables the rest
  before reloading.

No schema change (wheel_prizes already has enabled + sort_order) and no
packet change (id = 0 / omission express insert / delete on the existing
wire). Pairs with the Nitro-V3 client editor add/remove buttons.
2026-05-31 10:49:10 +02:00
DuckieTM b38274e134 Merge pull request #137 from medievalshell/Dev
fix(bans): persist client machine fingerprint so machine/super bans work
2026-05-31 10:37:54 +02:00
medievalshell 02ab30180c fix(chat): relay unknown chat bubble ids instead of resetting to default
getBubble() fell back to NORMAL (bubble 0) for any id not in the registered
BUBBLES map, so custom client-side chat bubbles (e.g. ids 253+) rendered as
the default bubble for everyone. Now unknown positive ids (<=1000) pass
through as a transient bubble carrying that id, so the server relays it and
clients render their own .bubble-<id> style. No need to enumerate each one.
2026-05-31 03:39:23 +02:00
medievalshell da63439d53 fix(bans): persist client machine fingerprint so machine/super bans work
The Nitro client already sends a strong machine fingerprint (Thumbmark,
"IID-<hash>") via the UniqueID packet (header 2490 -> MachineIDEvent), but
the emulator only stored it on the GameClient and never copied it onto the
Habbo's HabboInfo, so it was never written to users.machine_id. As a result
machine/super bans (which read users.machine_id) matched nobody.

- MachineIDEvent: when the fingerprint arrives and the Habbo is already
  loaded, copy it onto HabboInfo and persist (run the Habbo save).
- SecureLoginEvent: if the fingerprint arrived before login, copy it onto
  HabboInfo right before the login save.

This makes machine/super bans effective without changing the client.
2026-05-31 00:04:00 +02:00
github-actions[bot] bf1a29a6e8 🆙 Bump version to 4.2.26 [skip ci] 2026-05-30 05:53:48 +00:00
DuckieTM 6391d721ff Merge pull request #136 from duckietm/dev
Dev
2026-05-30 07:52:43 +02:00
DuckieTM dfea6bcf83 🆙 Updated SQL 2026-05-30 07:52:02 +02:00
DuckieTM a7f207bb76 Merge pull request #134 from medievalshell/Dev
feat: persist `scale` for room ads / branding furni
2026-05-30 07:13:59 +02:00
duckietm b7915884b6 🆙 Update Rare-Value page 2026-05-29 08:28:01 +02:00
medievalshell 478f7bdba0 feat/fix: RCON wheel+soundboard reload, robust SSO reconnect behind Cloudflare
- RCON: add updatewheel/updatesoundboard (reload WheelManager/SoundboardManager live) so the CMS admin pages apply changes without an emulator restart.

- SSO ticket is no longer single-use: loadHabbo, session-resume and performFullDisconnect no longer clear auth_ticket. Behind Cloudflare the WS is dropped and the client retries with the same ticket; clearing it caused 'non-existing SSO token' and the 'refresh twice' / kicked-on-reconnect symptoms. The ticket now lives until its TTL (auth_ticket_expires_at), is overwritten by the CMS on the next /client load, or cleared on logout.

- SessionResume: restoreSsoTicket only restores when auth_ticket is empty (don't clobber a fresh CMS ticket); GameClient.dispose only parks/disconnects when the habbo is still attached to this client (a fast reconnect may have re-attached it to the new connection).
2026-05-29 04:45:34 +02:00
medievalshell c255f1e1b4 fix: guard RoomBundleLayout against null RoomManager during catalog init
CatalogManager.loadFurnitureValues() (rare-values feature) iterates every catalog page during GameEnvironment.load(); for a RoomBundleLayout this calls getRoomManager().loadRoom(), but RoomManager is constructed after CatalogManager so getRoomManager() returns null -> NullPointerException -> boot aborts. Null-guard the room load so the bundle resolves lazily at runtime instead.
2026-05-29 00:45:02 +02:00
medievalshell 9c831a9da4 feat: grant acc_wheeladmin to staff ranks for the wheel prize editor
The wheel prize editor is gated on acc_wheeladmin (client Settings button +
server WheelAdmin{Get,Save}PrizesEvent). Upstream 008_soundboard_fortune_wheel
registers the key but only grants rank_7 (its 7-rank hotel). This portable,
idempotent migration grants it to the same ranks as acc_ads_background via
dynamic SQL over the per-rank columns — no hardcoded rank ids. Apply then
:update_permissions or restart.
2026-05-28 22:47:15 +02:00
Medievalshell 08d1ae97a7 Merge branch 'duckietm:main' into Dev 2026-05-28 22:16:17 +02:00
github-actions[bot] f8fe1e3e22 🆙 Bump version to 4.2.25 [skip ci] 2026-05-28 14:37:58 +00:00
DuckieTM be77cdf4aa Merge pull request #135 from duckietm/dev
Dev
2026-05-28 16:36:43 +02:00
duckietm 1ba2e43d4d 🆙 Wheel updates 2026-05-28 16:36:22 +02:00
medievalshell 8dd5155562 feat: persist scale for room ads / branding furni
InteractionRoomAds now carries a `scale` default value (100) alongside
imageUrl/clickUrl/offsetX/Y/Z, so the image zoom set in the client's
position editor is stored and broadcast like the other branding fields.
2026-05-28 15:30:33 +02:00
DuckieTM 4f4f581371 Merge pull request #129 from medievalshell/Dev
feat: rare values + fortune wheel + in-client prize editor + feat: soundboard (room-scoped custom audio pads) + feat: version string tied to project version + "Extended" title
2026-05-28 13:50:52 +02:00
duckietm 9705b3e42a 🆕 Added the option turn in menu for BOT 2026-05-28 13:00:02 +02:00
medievalshell e626a7fc50 feat: version string tied to project version + "Extended" title
The :about / :info hotel-info title was hardcoded ("Arcturus Morningstar
4.1.0") and drifted from the real build. Now Emulator.version reads the
jar manifest's Implementation-Version (= ${project.version}, added via the
assembly plugin) and falls back to MAJOR.MINOR.BUILD only outside a jar.
Title becomes "Arcturus Morningstar Extended <version>" (e.g. 4.2.24).
2026-05-28 12:33:50 +02:00
Medievalshell d6ebb632e6 Merge branch 'duckietm:main' into Dev 2026-05-28 12:11:27 +02:00
github-actions[bot] 014ca9ca48 🆙 Bump version to 4.2.24 [skip ci] 2026-05-28 09:50:45 +00:00
DuckieTM d189d66f9e Merge pull request #133 from duckietm/dev
🆙 Update effects
2026-05-28 11:49:38 +02:00
duckietm c272a36cc5 🆙 Update effects 2026-05-28 11:49:20 +02:00
github-actions[bot] 1d6e05ee57 🆙 Bump version to 4.2.23 [skip ci] 2026-05-28 09:35:48 +00:00
DuckieTM ea44771d69 Merge pull request #132 from duckietm/dev
Update 007_Frank.sql
2026-05-28 11:34:37 +02:00
duckietm 1da783aff9 Update 007_Frank.sql 2026-05-28 11:34:19 +02:00
github-actions[bot] e772686c4b 🆙 Bump version to 4.2.22 [skip ci] 2026-05-28 09:05:33 +00:00
DuckieTM a00f7b01f5 Merge pull request #130 from duckietm/dev
Dev
2026-05-28 11:04:35 +02:00
duckietm 6b4089cace 🆙 small typo in SQL 2026-05-28 11:04:01 +02:00
duckietm 9ea7acf05c 🆙 Update for Frank 2026-05-28 10:53:50 +02:00
duckietm bab43af41e 🆕 Frank the BOT
-- VERY IMPORTANT !!!!
-- First check if the items_base ID and catalog_items ID is not in use !
-- After the SQL please go to the catalog_items table and change the page_id to where your BOTS are located
2026-05-28 10:41:25 +02:00
medievalshell 10a2b2b872 feat: soundboard (room-scoped custom audio pads)
Server side of the soundboard feature:
- rooms.soundboard_enabled flag + soundboard_sounds table (self-bootstraps
  at boot via SoundboardManager; migration 021 seeds up-front)
- SoundboardManager loads enabled sounds and persists the per-room flag
- SoundboardPlayEvent broadcasts the pressed pad to everyone in the room
- SoundboardSetEnabledEvent (owner/staff) toggles the room flag and
  pushes refreshed settings
- settings (flag + sound list) sent on room enter, alongside YouTube
2026-05-28 09:03:27 +02:00
medievalshell 458b37dbed feat: rare values + fortune wheel + in-client prize editor
Catalog-derived rare value map (diamond-priced), fortune wheel (WheelManager,
weighted RNG, lazy daily reset, rewards, recent wins) + admin prize editor
gated on acc_supporttool. Packets 9300-9305 / 9400-9404. Migration 020.
2026-05-28 02:39:01 +02:00
github-actions[bot] 55b38e7b85 🆙 Bump version to 4.2.21 [skip ci] 2026-05-27 13:39:01 +00:00
DuckieTM 4a96c5baaf Merge pull request #128 from duckietm/dev
Dev
2026-05-27 15:37:57 +02:00
duckietm 539c5b5b96 🆙 Fix BOTS in catalog and inventory 2026-05-27 13:46:17 +02:00
duckietm 7b7154e68f 🆙 Fix search and buy #1 2026-05-27 11:34:55 +02:00
duckietm 4aabb738a3 🆙 Added missing Table for the HK 2026-05-27 09:47:30 +02:00
github-actions[bot] 691dc42627 🆙 Bump version to 4.2.20 [skip ci] 2026-05-27 07:43:14 +00:00
DuckieTM 226873c1fb Merge pull request #127 from duckietm/dev
Dev
2026-05-27 09:42:21 +02:00
duckietm a06a204b39 Merge branch 'dev' of https://github.com/duckietm/Arcturus-Morningstar-Extended into dev 2026-05-27 09:37:51 +02:00
duckietm e213609609 🆕 Added Pickup furni to the floorplan 2026-05-27 09:37:49 +02:00
DuckieTM 44d38b8661 🆙 SQL update 2026-05-26 22:18:02 +02:00
github-actions[bot] ccadb81970 🆙 Bump version to 4.2.19 [skip ci] 2026-05-26 15:16:04 +00:00
DuckieTM 0a3a940946 Merge pull request #125 from duckietm/dev
🆙 Small fix floorplan
2026-05-26 17:15:07 +02:00
duckietm 4613fbe80c 🆙 Small fix floorplan 2026-05-26 17:14:49 +02:00
github-actions[bot] 9328f4a355 🆙 Bump version to 4.2.18 [skip ci] 2026-05-26 14:37:17 +00:00
DuckieTM da8b947ddf Merge pull request #124 from duckietm/dev
🆕 Brand new Floorplan
2026-05-26 16:36:17 +02:00
duckietm b9658d0407 🆕 Brand new Floorplan 2026-05-26 16:35:58 +02:00
DuckieTM 68d3731393 Merge pull request #123 from duckietm/dev
Dev
2026-05-26 12:53:07 +02:00
duckietm 4ef4ed1a96 🆙 Enable HK in client with permissions 2026-05-26 12:52:49 +02:00
DuckieTM c20e273a2c Merge pull request #120 from simoleo89/feat/housekeeping-packets
feat(housekeeping): in-client admin packet handlers
2026-05-26 10:44:13 +02:00
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
simoleo89 dac09e92d1 fix(housekeeping): hash reset password with BCrypt, not SHA-256
`HousekeepingResetUserPasswordEvent` was writing a SHA-256 hex digest
into `users.password`, but the Nitro auth path
(`SessionEndpoints` / `AccountChangeEndpoints` → `AuthHttpUtil.checkPassword`)
only does `BCrypt.checkpw`. A SHA-256 hex string doesn't start with
`$2…$`, so jbcrypt throws `IllegalArgumentException`, `checkPassword`
returns false, and operators saw "credenziali invalide" on every
account whose password had been reset from the in-client panel.

Switch to `BCrypt.hashpw(plain, BCrypt.gensalt(10))` — same idiom
already used by `SessionEndpoints.java:351` and
`AccountChangeEndpoints.java:98`. Cost 10 (vs 12 there) is fine for a
server-generated 12-char random password: gensalt(10) keeps the
operator-facing reset snappy and the output is identical-shape
(`$2a$…`) to what jbcrypt 0.4 already accepts.

Side-effects:
- drops the `MessageDigest` / `NoSuchAlgorithmException` /
  `StandardCharsets` imports and the local `sha256Hex` helper
- repurposes the existing `housekeeping.error.hash_failed` key for
  `BCrypt.gensalt`'s only failure mode (invalid cost / log_rounds out
  of range) so the client error surface is unchanged
- updates the file javadoc to stop telling future readers to "swap the
  MessageDigest constant" — Arcturus itself only verifies BCrypt

Companion of duckietm/Nitro-V3#157 (`feat/housekeeping-panel`). The
client/UI is untouched — packet 9200, the action-result reveal card,
the copy button, and the plaintext flow through `message` are all
unchanged.
2026-05-24 22:25:16 +02:00
simoleo89 fbf979419e feat(housekeeping): hotel alert + dashboard + audit log
Closes out the HK panel server-side surface.

* Incoming 9127 HousekeepingSendHotelAlertEvent — broadcast a
  StaffAlertWithLinkComposer to every online user that hasn't
  set blockStaffAlerts. Composed once, fanned out by reference;
  empty-message guard returns `housekeeping.error.alert_empty`.

* Outgoing 9206 HousekeepingDashboardComposer + Incoming 9128
  HousekeepingGetDashboardEvent — single round trip with the
  aggregated counters: online / total users + active / total
  rooms + pending support tickets + sanctions in the last 24h +
  approximate emulator uptime + a version string. Active-rooms
  is derived from RoomManager.getActiveRooms().getUserCount()>0
  to avoid counting idle preloaded rooms. Peak online today /
  all-time aren't tracked yet, so they currently echo the live
  online count as a best-effort placeholder.

* Outgoing 9207 HousekeepingActionLogComposer + Incoming 9129
  HousekeepingListActionLogEvent — read the optional
  housekeeping_log table. If the table isn't there the SQL
  exception is swallowed and an empty list goes back, so the
  panel renders a no-entries view rather than crashing. Schema
  is documented in the handler's javadoc; operators who want
  audit run a single CREATE TABLE then the HK panel populates
  from new writes (writes are a follow-up — every HK handler
  will eventually append a row).

`mvn package` clean — the final fat jar lands in
Latest_Compiled_Version/ after the build finishes.
2026-05-24 16:31:01 +02:00
simoleo89 6126c35779 feat(housekeeping): economy domain — credits/currency/items/hc
* Incoming 9117 HousekeepingGiveCreditsEvent — Habbo.giveCredits for
  online (ships UserCreditsComposer) or UPDATE users.credits for offline.

* Incoming 9118 HousekeepingGiveCurrencyEvent — generic across the
  non-credits currencies. currencyType 0 => duckets/pixels (givePixels),
  5 => diamonds (givePoints(5,n)), anything else routes through
  givePoints(type,n). Offline path INSERT ... ON DUPLICATE KEY UPDATE
  users_currency.

* Incoming 9119 HousekeepingGrantItemEvent — batch-INSERT N rows into
  the items table with item_id = base furni id. Capped at 100 per call
  so a typo can't bury the DB. Online inventory refresh deferred — the
  user picks the new items up on next hand-inventory open or relog.

* Incoming 9120 HousekeepingSetHcSubscriptionEvent — extends
  users_settings.club_expire_timestamp by `days*86400`. Stacks on top
  of the existing expiry if it's still in the future, otherwise starts
  from now. days==0 clamps to now (effective cancel).

All four reuse HousekeepingActionResultComposer (no new outgoing
composer this slice).

`mvn compile` clean.
2026-05-24 16:29:55 +02:00
simoleo89 a1749c9eda feat(housekeeping): rooms domain — find/search + open/close/mute/kick-all/transfer/delete
Eight new incoming handlers + two new outgoing composers cover the
full rooms-domain HK panel.

* Outgoing 9202 HousekeepingRoomDetailComposer — single room with a
  leading `found` boolean. Writes the IHousekeepingRoom shape via a
  static `appendRoomFields` that HousekeepingRoomListComposer shares.

* Outgoing 9203 HousekeepingRoomListComposer — `count` then N rooms.
  Used for both find-by-name (exact match, up to 50) and the prefix
  autocomplete dropdown (up to 8).

* Incoming 9110 HousekeepingFindRoomByIdEvent — loadRoom(id, false)
  covers both the in-memory cache and the offline `SELECT * FROM rooms`
  path. No `loadData` so HK doesn't pull furni/bots/pets just to
  render a summary.

* Incoming 9111 HousekeepingSearchRoomsEvent — (query, exactMatch,
  limit). Branches between `name = ?` and `name LIKE ?` so the same
  wire packet serves both the autocomplete and the exact-find flows.
  Hard-capped to 50.

* Incoming 9112 HousekeepingRoomStateEvent — (roomId, open). Toggles
  Room.setState(OPEN | LOCKED) and persists via Room.save(). One
  packet covers both the open and close API endpoints.

* Incoming 9113 HousekeepingMuteRoomEvent — (roomId, minutes). Room.
  setMuted is a boolean, so minutes==0 unmutes and minutes>0 mutes.
  A scheduled auto-unmute is left for a future slice; the wire field
  is reserved.

* Incoming 9114 HousekeepingKickAllFromRoomEvent — Room.ejectAll().

* Incoming 9115 HousekeepingTransferRoomOwnershipEvent — UPDATEs both
  rooms.owner_id and rooms.owner_name so the navigator cached name
  doesn't go stale. Validates the new owner exists via
  HabboManager.getHabboInfo before touching the row.

* Incoming 9116 HousekeepingDeleteRoomEvent — ejectAll + dispose +
  uncacheRoom + DELETE FROM rooms, mirroring the minimum-viable
  subset of RequestDeleteRoomEvent. Pets/guild/custom-layout cleanup
  is skipped on this slice (orphans don't crash the emulator).

`mvn compile` clean.
2026-05-24 16:29:55 +02:00
simoleo89 525c124fa5 feat(housekeeping): set-rank + trade-lock + reset-password
Closes out the users-domain HK actions.

* Incoming 9107 HousekeepingSetUserRankEvent — (userId, rankId).
  Validates the rank exists in `permission_ranks`, UPDATEs users.rank,
  and if the target is online rebinds their HabboInfo to the fresh
  Rank object and ships a UserPermissionsComposer so server-side
  hasPermission() and the client's useHasPermission(key) consumers
  re-render against the new permissions without a relog.

* Incoming 9108 HousekeepingTradeLockUserEvent — (userId, hours,
  reason). Writes `users_settings.trade_locked_until = now + hours*3600`
  so the lock survives logout/login. Online targets also get their
  in-memory HabboStats.allowTrade cleared and an optional alert.

* Incoming 9109 HousekeepingResetUserPasswordEvent — (userId).
  Generates a 12-char alphanumeric (SecureRandom over a curated
  ambiguity-free alphabet), writes its SHA-256 hex to users.password
  (the column is varchar(64) — already sized for SHA-256 hex) and
  blanks auth_ticket so any live SSO ticket can't bypass the reset.
  Plaintext is returned to the operator in the action-result
  message — they relay it out-of-band. If your CMS uses a hash other
  than SHA-256, swap the MessageDigest.getInstance constant.

`mvn compile` clean.
2026-05-24 16:29:55 +02:00
simoleo89 57087a31f2 fix(housekeeping): emit localizable error keys instead of bare slugs
Every HK action handler returned bare error slugs (\"invalid_input\",
\"user_offline\", \"no_active_ban\", \"target_unkickable\", \"ban_failed\",
\"user_not_found\") in HousekeepingActionResultComposer.message. The
client's `localizeOrPassthrough` only treats a value as a translation
key when it contains a dot, so those bare slugs were rendered raw in
the status banner and the toast — ugly and untranslatable.

Re-prefix all error messages with `housekeeping.error.` so the EN +
IT dictionaries can resolve them. Success path is unchanged (server
sends empty string, client falls back to `housekeeping.action.success`).

Companion dictionary entries land on the client side.
2026-05-24 16:29:55 +02:00
simoleo89 c4b3295a45 feat(housekeeping): force-disconnect-user packet
Incoming 9106 HousekeepingForceDisconnectUserEvent — (userId, reason).
Sends the optional reason as a Habbo.alert, dispatches the action ack
BEFORE calling target.disconnect() so the result lands on the wire
before the target's socket closes, then drops the session. Online-only;
offline target returns `user_offline`.

`mvn compile` clean.
2026-05-24 16:29:54 +02:00
simoleo89 418c753e6c feat(housekeeping): mute-user + kick-user packets
Incoming 9104 HousekeepingMuteUserEvent — (userId, reason, minutes).
Unlike ModToolSanctionMute which takes a fixed-bucket minutes arg
from a CFH context, this one applies an arbitrary in-session mute via
Habbo.mute(seconds, false). Mute is online-only (the live Habbo object
holds the remaining seconds), so an offline target returns ok=false
with `user_offline`. The reason string, if non-empty, is delivered via
Habbo.alert so the muted user sees why.

Incoming 9105 HousekeepingKickUserEvent — (userId, reason). Replicates
the ModToolManager.kick body (leave room + alert) locally so HK doesn't
piggyback on ACC_SUPPORTTOOL the way ModToolManager.kick does — keeps
the permission model `acc_housekeeping`-only. Respects ACC_UNKICKABLE
the same way the legacy path does.

Both reuse HousekeepingActionResultComposer with their own actionKey
(user.mute / user.kick).

`mvn compile` clean.
2026-05-24 16:29:54 +02:00
simoleo89 8419f11883 feat(housekeeping): unban-user packet
Incoming 9103 HousekeepingUnbanUserEvent — reads userId, resolves
the username via HabboManager.getHabboInfo(int) (covers both online
and offline paths in one call), then dispatches to
ModToolManager.unban(username) which clears all active rows from
the `bans` table for that user.

Reuses HousekeepingActionResultComposer with actionKey `user.unban`.
If the user never had an active ban the SQL UPDATE matches zero rows
and the handler responds with `ok: false, message: 'no_active_ban'`
— from a UI standpoint that's a no-op, not an error.

`mvn compile` clean.
2026-05-24 16:29:54 +02:00
simoleo89 1a0d783ff7 feat(housekeeping): ban-user with arbitrary duration + ack composer
Adds two new packets:

* Incoming 9102 HousekeepingBanUserEvent — reads (userId, reason,
  hours). Unlike ModToolSanctionBanEvent which only accepts the four
  fixed Habbo-protocol banType buckets (18h / 7d / 30d / 100y), this
  one converts the hours arg straight to seconds and feeds them into
  ModToolManager.ban with ModToolBanType.ACCOUNT and cfhTopic=0.
  Duration is clamped to 100 years to keep it inside `int` range.

* Outgoing 9201 HousekeepingActionResultComposer — generic ack
  for any HK action (ban / mute / kick / give-credits / room-close /
  …). Wire shape is (actionKey, ok, actionId, message). The
  actionKey lets the client filter multiple in-flight actions to
  the right Promise via `accept`, so concurrent admin operations
  don't cross-resolve.

actionId here is the target user id because ModToolBan doesn't
expose the `bans` autoinc id on the object — there's a TODO to swap
this for a dedicated housekeeping_log row id once that table goes in.

Same ACC_HOUSEKEEPING permission gate as the find-user packets, so
operators only need to grant the permission once.

`mvn compile` clean.
2026-05-24 16:29:54 +02:00
simoleo89 655e039df7 feat(housekeeping): find-user-by-id packet + acc_housekeeping gate
Adds Incoming 9101 HousekeepingFindUserByIdEvent, which replies on
the existing HousekeepingUserDetailComposer (Outgoing 9200) — the
composer is shape-agnostic about how the lookup was issued, so the
two find-* handlers share the same response packet.

The by-id handler uses HabboManager.getHabboInfo(int) directly, which
already covers both the online (in-memory hashmap) and offline (SQL
LIMIT 1 on users) branches in one call. The by-name path still has
to do online + offline manually because the equivalent String overload
doesn't exist as an instance method, only as a static.

Also introduces Permission.ACC_HOUSEKEEPING ("acc_housekeeping") so
the in-client housekeeping panel doesn't piggyback on ACC_SUPPORTTOOL.
Both HK handlers now gate on the new permission; the toolbar UI on
the client side was already checking `acc_housekeeping`, so this
closes the loop. Operators must add the permission to
permission_definitions for the desired rank:

  INSERT INTO permission_definitions
    (permission_key, max_value, comment,
     rank_1, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7)
  VALUES
    ('acc_housekeeping', 1,
     'Allows access to the in-client Housekeeping admin panel ...',
     0, 0, 0, 0, 0, 0, 1)
  ON DUPLICATE KEY UPDATE rank_7 = 1, comment = VALUES(comment);

`mvn package` clean (Habbo-4.2.12-jar-with-dependencies.jar).
2026-05-24 16:29:54 +02:00
simoleo89 7726691cde feat(housekeeping): add find-user-by-name packet pair
First wire-level packet for the in-client housekeeping admin panel.
Adds an incoming handler (Incoming 9100) that resolves a username to
a HabboInfo (online via the HabboManager hashmap, offline via the
users-table SQL fallback) and replies with HousekeepingUserDetail
(Outgoing 9200) containing id / username / motto / look / rank / rank
name / online / lastOnline / credits / duckets / diamonds / email /
ipLogin / isBanned. Active-mute and active-trade-lock are written as
trailing booleans (currently false) so the renderer parser can pick
them up later behind a bytesAvailable guard once those manager APIs
are surfaced offline-side.

Permission gate is ACC_SUPPORTTOOL — same one ModTools already uses.
Avoids adding a new column to the permissions table on this slice; a
dedicated ACC_HOUSEKEEPING permission can be introduced later when
the destructive HK operations (give-credits, delete-room, reset-pwd)
go in.

Reserves the 9100..9199 / 9200..9299 ID blocks for the rest of the
HK packet surface (search-prefix, find-by-id, ban/mute/kick with
arbitrary duration, room actions, economy, catalog admin, dashboard
aggregate, audit log read).

`mvn compile` clean on Habbo 4.2.12.
2026-05-24 16:29:54 +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
188 changed files with 9832 additions and 1362 deletions
+10
View File
@@ -34,3 +34,13 @@ SET @ddl = IF(@col_exists = 0,
PREPARE stmt FROM @ddl; PREPARE stmt FROM @ddl;
EXECUTE stmt; EXECUTE stmt;
DEALLOCATE PREPARE 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,474 @@
-- ============================================================
-- 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;
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');
@@ -0,0 +1,17 @@
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`, `rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`) VALUES ('acc_housekeeping', '1', 'Allow housekeeping in the client', '0', '0', '0', '0', '0', '0', '1');
CREATE TABLE IF NOT EXISTS `housekeeping_log` (
`id` INT NOT NULL AUTO_INCREMENT,
`timestamp` INT NOT NULL,
`actor_id` INT NOT NULL,
`actor_name` VARCHAR(64) NOT NULL DEFAULT '',
`target_type` VARCHAR(16) NOT NULL DEFAULT 'user',
`target_id` INT NOT NULL DEFAULT 0,
`target_label` VARCHAR(128) NOT NULL DEFAULT '',
`action` VARCHAR(64) NOT NULL DEFAULT '',
`detail` VARCHAR(500) NOT NULL DEFAULT '',
`success` TINYINT NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
KEY `timestamp` (`timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+70
View File
@@ -0,0 +1,70 @@
ALTER TABLE `bots`
MODIFY COLUMN `type` ENUM('generic','visitor_log','bartender','weapons_dealer','frank')
NOT NULL DEFAULT 'generic';
INSERT INTO `permission_definitions`
(`permission_key`, `max_value`, `comment`,
`rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`)
VALUES
('acc_bot_frank', 1, 'Required to purchase the Frank mascot bot from the catalog.',
0, 0, 0, 0, 0, 0, 1)
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
CREATE TABLE IF NOT EXISTS `bot_chat_responses` (
`id` INT NOT NULL AUTO_INCREMENT,
`bot_type` VARCHAR(32) NOT NULL,
`keys` VARCHAR(255) NOT NULL COMMENT 'semicolon-separated trigger words',
`responses` TEXT NOT NULL COMMENT 'newline-separated replies; bot picks one at random',
PRIMARY KEY (`id`),
KEY `bot_type` (`bot_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `bot_chat_responses` (`bot_type`, `keys`, `responses`) VALUES
('frank', '__door_triggers', 'show me the door\nkick me\ni want to leave\nlet me out'),
('frank', '__door_lines', 'Right this way - mind the step!\nAnd out you go. Come back soon!\nAllow me to escort you to the exit.\nThere''s the door. Farewell, true believer!'),
('frank', '__busy_whisper', 'Sorry, I am currently busy. Please wait until I am available.'),
('frank', 'frank', 'Hello, I''m Frank! Welcome to Habbo.'),
('frank', 'help', 'What do you need help with?'),
('frank', 'thanks;thank you', 'Just doing my job, true believer!'),
('frank', 'new', 'Welcome to Habbo! I hope you have a great time here.'),
('frank', 'rooms', 'Looking for somewhere fun? Try the Navigator - thousands of rooms to explore!'),
('frank', 'sulake', 'Sulake is the company behind Habbo. Take a look: https://www.sulake.com'),
('frank', 'vip;hc', 'VIP gets you more outfits, more furni, more everything. Worth it!'),
('frank', 'music', 'Snoop Dogg, Frank Sinatra and a little Beethoven on Sundays.'),
('frank', 'movie', 'I''m a Casablanca man. Black and white films are an underrated art.'),
('frank', 'game', 'Battleship. Always Battleship.'),
('frank', 'snowstorm', 'Honestly? I''m terrible at Snowstorm. Don''t tell anyone.'),
('frank', 'furni', 'Best furniture maker in town - hands down, the folks at Sulake.'),
('frank', 'animal;cat;pet','I have a cat called Mr. Whiskers. He runs the place, really.'),
('frank', 'miranda', 'Miranda. The love of my life. Don''t get me started.'),
('frank', 'frank black', 'Named after the man himself. Frank Black is a hero of mine.'),
('frank', 'life', 'Life is like a bowl of popcorn - warm, salty and buttery.'),
('frank', 'job;work', 'I''m sure you can find work in one of the guest rooms!'),
('frank', 'snouthill', 'Snouthill... so many memories.'),
('frank', 'wife', 'I had a wife once. She broke my stereo.'),
('frank', 'baseball', 'Oh, I used to love to go down to the old ball park and watch Christy Mathewson and Honus Wagner at bat.'),
('frank', 'mark', 'I don''t trust Mark.'),
('frank', 'vietnam', 'Vietnam? Don''t ask. Worst trip of my life.'),
('frank', 'pills;drugs', 'Drugs are bad, mmkay?');
INSERT IGNORE INTO `bot_serves` (`keys`, `item`) VALUES
('sunflower', 1002),
('cola;habbo cola', 19),
('rose', 1000),
('book', 1003),
('tea', 27),
('coffee', 8),
('migraine;headache;pills', 1015),
('radioactive liquid;radioactive', 30),
('turkey;can of turkey', 70);
-- VERY IMPORTANT !!!!
-- First check if the items_base ID and catalog_items ID is not in use !
-- After the SQL please go to the catalog_items table and change the page_id to where your BOTS are located
INSERT IGNORE INTO `items_base` (`id`, `sprite_id`, `item_name`, `public_name`, `width`, `length`, `stack_height`, `allow_stack`, `allow_sit`, `allow_lay`, `allow_walk`, `allow_gift`, `allow_trade`, `allow_recycle`, `allow_marketplace_sell`, `allow_inventory_stack`, `type`, `interaction_type`, `interaction_modes_count`, `vending_ids`, `multiheight`, `customparams`)
VALUES (19001, 0, 'bot_frank', 'Frank', 1, 1, 0.00, '0', '0', '0', '1', '0', '0', '0', '0', '0', 'r', 'default', 1, '0', '0', '0');
INSERT IGNORE INTO `catalog_items` (`item_ids`, `page_id`, `offer_id`, `catalog_name`, `cost_credits`, `cost_points`, `points_type`, `amount`, `extradata`)
VALUES ('19001', 8, 19001, 'Frank', 0, 0, 0, 1, 'name:Frank;motto:Welcome to Habbo!;figure:hr-3499-33.sh-290-90.ch-3971-72-73.lg-270-73.hd-205-1-1.fa-1206-67.ha-3409-73-72;gender:m');
@@ -0,0 +1,89 @@
ALTER TABLE `rooms`
ADD COLUMN IF NOT EXISTS `soundboard_enabled` TINYINT(1) NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS `soundboard_sounds` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(64) NOT NULL DEFAULT '', -- pad label shown in the client
`url` VARCHAR(255) NOT NULL DEFAULT '', -- audio url (uploaded via CMS, like custom badges)
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------------------------------------------------------
-- Fortune Wheel — tables
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `wheel_prizes` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`type` VARCHAR(16) NOT NULL DEFAULT 'nothing', -- item | badge | credits | points | spin | nothing
`value` VARCHAR(64) NOT NULL DEFAULT '', -- item: base item id ; badge: badge code ; others: unused
`amount` INT(11) NOT NULL DEFAULT 1, -- item qty / credits / points / extra spins
`points_type` INT(11) NOT NULL DEFAULT 5, -- for type=points (diamond default 5)
`weight` INT(11) NOT NULL DEFAULT 1, -- relative probability
`label` VARCHAR(64) NOT NULL DEFAULT '', -- slice label override (optional)
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `wheel_user_state` (
`user_id` INT(11) NOT NULL,
`free_spins` INT(11) NOT NULL DEFAULT 0, -- remaining free spins for the current day
`extra_spins` INT(11) NOT NULL DEFAULT 0, -- bought / won spins
`last_reset` INT(11) NOT NULL DEFAULT 0, -- day index of last daily reset (unix / 86400)
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `wheel_recent_wins` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`username` VARCHAR(64) NOT NULL DEFAULT '',
`look` VARCHAR(255) NOT NULL DEFAULT '',
`prize_label` VARCHAR(64) NOT NULL DEFAULT '',
`won_at` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
('wheel.free_spins_per_day', '1', 'Fortune wheel: free spins granted each day.'),
('wheel.spin_cost', '50', 'Fortune wheel: cost of one extra spin.'),
('wheel.spin_cost_type', '5', 'Fortune wheel: currency type for the spin cost (5 = diamonds; -1 = credits).')
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
INSERT INTO `wheel_prizes` (`type`, `amount`, `points_type`, `weight`, `label`, `sort_order`)
SELECT `type`, `amount`, `points_type`, `weight`, `label`, `sort_order`
FROM (
SELECT 'points' AS `type`, 25 AS `amount`, 5 AS `points_type`, 20 AS `weight`, '25 diamonds' AS `label`, 1 AS `sort_order`
UNION ALL SELECT 'points', 50, 5, 12, '50 diamonds', 2
UNION ALL SELECT 'points', 200, 5, 3, '200 diamonds', 3
UNION ALL SELECT 'credits', 100, 0, 15, '100 credits', 4
UNION ALL SELECT 'spin', 1, 0, 15, '1 Extra spin', 5
UNION ALL SELECT 'spin', 2, 0, 6, '2 Extra spins', 6
UNION ALL SELECT 'nothing', 0, 0, 29, 'Oh to bad!', 7
) AS seed
WHERE NOT EXISTS (SELECT 1 FROM `wheel_prizes`);
INSERT IGNORE INTO `permission_definitions` (`permission_key`, `max_value`, `comment`)
VALUES (
'acc_wheeladmin',
1,
'Allows opening the Fortune Wheel prize editor (FortuneWheelSettingsView) to add/edit prize slices. Gated server-side by the same key.'
);
SET @cols := NULL;
SELECT GROUP_CONCAT(CONCAT('dst.`', `column_name`, '` = src.`', `column_name`, '`') SEPARATOR ', ')
INTO @cols
FROM `information_schema`.`columns`
WHERE `table_schema` = DATABASE()
AND `table_name` = 'permission_definitions'
AND `column_name` REGEXP '^rank_[0-9]+$';
SET @sql := CONCAT(
'UPDATE `permission_definitions` dst ',
'JOIN `permission_definitions` src ON src.`permission_key` = ''acc_ads_background'' ',
'SET ', @cols, ' ',
'WHERE dst.`permission_key` = ''acc_wheeladmin'''
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
@@ -0,0 +1,76 @@
CREATE TABLE IF NOT EXISTS `habbo_mentions` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`target_user_id` INT(11) NOT NULL,
`sender_user_id` INT(11) NOT NULL,
`sender_username` VARCHAR(64) NOT NULL DEFAULT '',
`room_id` INT(11) NOT NULL DEFAULT 0,
`room_name` VARCHAR(64) NOT NULL DEFAULT '',
`message` VARCHAR(255) NOT NULL DEFAULT '',
`mention_type` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '0 = direct (@nick), 1 = broadcast (@all/@friends/@room)',
`timestamp` INT(11) NOT NULL DEFAULT 0,
`read` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_target_id` (`target_user_id`, `id`),
KEY `idx_target_unread` (`target_user_id`, `read`),
KEY `idx_target_timestamp` (`target_user_id`, `timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `permission_definitions`
(`permission_key`, `max_value`, `comment`,
`rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`, `rank_8`)
VALUES
('acc_mention_everyone', 1,
'Allow sending @all / @everyone / @tutti broadcast mentions (hotel-wide).',
0, 0, 0, 0, 1, 1, 1, 1),
('acc_mention_friends', 1,
'Allow sending @friends / @amici broadcast mentions (sender''s online buddies).',
0, 0, 0, 0, 1, 1, 1, 1),
('cmd_disablementions', 1,
'Allow toggling :disablementions to stop receiving any @mention notifications.',
1, 1, 1, 1, 1, 1, 1, 1),
('cmd_disablemassmentions', 1,
'Allow toggling :disablemassmentions to stop receiving broadcast mentions (direct @nick still works).',
1, 1, 1, 1, 1, 1, 1, 1)
ON DUPLICATE KEY UPDATE
`comment` = VALUES(`comment`);
-- ----------------------------------------------------------------------------
-- 3. Emulator settings: cooldowns, caps and alias lists
--
-- Only inserted when missing - existing tuned values are preserved.
-- ----------------------------------------------------------------------------
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
('mentions.enabled', '1',
'Master switch. 1 = process @mentions, 0 = disable the feature entirely.'),
('mentions.max.targets', '50',
'Hard cap on how many users a single broadcast (@all / @friends / @room) can fan out to.'),
('mentions.cooldown.ms', '3000',
'Per-sender cooldown between any two mentions, in milliseconds.'),
('mentions.room.cooldown.ms', '15000',
'Extra per-sender cooldown for broadcast mentions (@all / @friends / @room) on top of mentions.cooldown.ms.'),
('mentions.store.limit', '50',
'Number of mentions returned in the initial RequestMentionsList response.'),
('mentions.request.cooldown.ms', '2000',
'Per-user cooldown between RequestMentionsList packets.'),
('mentions.markread.cooldown.ms', '500',
'Per-user cooldown between mark-single-as-read packets.'),
('mentions.markall.cooldown.ms', '5000',
'Per-user cooldown between mark-all-as-read packets (bulk DB update).'),
('mentions.delete.cooldown.ms', '500',
'Per-user cooldown between delete-mention packets.'),
('mentions.everyone.aliases', 'all,everyone,tutti',
'Comma-separated aliases that trigger an @everyone broadcast (requires acc_mention_everyone).'),
('mentions.friends.aliases', 'friends,amici',
'Comma-separated aliases that trigger an @friends broadcast (requires acc_mention_friends).'),
('mentions.room.aliases', 'room,stanza',
'Comma-separated aliases that trigger an @room broadcast (no permission required, room scope only).');
ALTER TABLE `wordfilter`
ADD COLUMN `prefix_only` ENUM('0','1') NOT NULL DEFAULT '0'
COMMENT 'When 1, this word only applies to custom prefixes, not to chat/motto/guild.' AFTER `mute`;
@@ -63,15 +63,6 @@ CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
PRIMARY KEY (`key_name`) PRIMARY KEY (`key_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ------------------------------------------------------------
-- 5. Blacklist table
-- ------------------------------------------------------------
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_word` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================ -- ============================================================
-- Schema upgrades for existing installations -- Schema upgrades for existing installations
@@ -296,14 +287,6 @@ INSERT IGNORE INTO `custom_prefixes_catalog`
(2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2), (2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2),
(3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3); (3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3);
-- ============================================================
-- Example blacklist entries
-- ============================================================
INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES
('admin'),
('staff'),
('mod'),
('owner');
-- ============================================================ -- ============================================================
-- Notes -- Notes
+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_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_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_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_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_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), ('acc_unkickable', 1, 'Prevents the user from being kicked by normal moderation or room commands.', 0, 0, 0, 0, 0, 0, 1),
+2 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId> <groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId> <artifactId>Habbo</artifactId>
<version>4.2.9</version> <version>4.2.34</version>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -38,6 +38,7 @@
<archive> <archive>
<manifest> <manifest>
<mainClass>com.eu.habbo.Emulator</mainClass> <mainClass>com.eu.habbo.Emulator</mainClass>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest> </manifest>
</archive> </archive>
</configuration> </configuration>
@@ -6,6 +6,7 @@ import ch.qos.logback.core.ConsoleAppender;
import com.eu.habbo.core.*; import com.eu.habbo.core.*;
import com.eu.habbo.core.consolecommands.ConsoleCommand; import com.eu.habbo.core.consolecommands.ConsoleCommand;
import com.eu.habbo.database.Database; import com.eu.habbo.database.Database;
import com.eu.habbo.gui.EmulatorDashboard;
import com.eu.habbo.habbohotel.GameEnvironment; import com.eu.habbo.habbohotel.GameEnvironment;
import com.eu.habbo.habbohotel.gameclients.SessionResumeManager; import com.eu.habbo.habbohotel.gameclients.SessionResumeManager;
import com.eu.habbo.networking.gameserver.GameServer; import com.eu.habbo.networking.gameserver.GameServer;
@@ -38,12 +39,23 @@ public final class Emulator {
private static final String OS_NAME = (System.getProperty("os.name") != null ? System.getProperty("os.name") : "Unknown"); private static final String OS_NAME = (System.getProperty("os.name") != null ? System.getProperty("os.name") : "Unknown");
private static final String CLASS_PATH = (System.getProperty("java.class.path") != null ? System.getProperty("java.class.path") : "Unknown"); private static final String CLASS_PATH = (System.getProperty("java.class.path") != null ? System.getProperty("java.class.path") : "Unknown");
// Fallback version, only used when running outside a packaged jar (e.g. from
// the IDE). In production the version comes from the jar manifest below.
public final static int MAJOR = 4; public final static int MAJOR = 4;
public final static int MINOR = 1; public final static int MINOR = 1;
public final static int BUILD = 0; public final static int BUILD = 0;
public final static String PREVIEW = ""; public final static String PREVIEW = "";
public static final String version = "Arcturus Morningstar" + " " + MAJOR + "." + MINOR + "." + BUILD + " " + PREVIEW; // Tied to the Maven project version: read from the jar manifest
// (Implementation-Version = ${project.version}, see pom assembly plugin).
private static String resolveVersionNumber() {
String implementation = Emulator.class.getPackage().getImplementationVersion();
if (implementation != null && !implementation.isEmpty()) return implementation;
String fallback = MAJOR + "." + MINOR + "." + BUILD;
return PREVIEW.isEmpty() ? fallback : fallback + " " + PREVIEW;
}
public static final String version = "Arcturus Morningstar Extended " + resolveVersionNumber();
private static final String logo = private static final String logo =
"\n" + "\n" +
"███╗ ███╗ ██████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗████████╗ █████╗ ██████╗ \n" + "███╗ ███╗ ██████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗████████╗ █████╗ ██████╗ \n" +
@@ -186,6 +198,10 @@ public final class Emulator {
Emulator.isReady = true; Emulator.isReady = true;
Emulator.timeStarted = getIntUnixTimestamp(); Emulator.timeStarted = getIntUnixTimestamp();
if (Emulator.getConfig().getBoolean("gui.enabled", true)) {
EmulatorDashboard.launch();
}
if (Emulator.getConfig().getInt("runtime.threads") < (Runtime.getRuntime().availableProcessors() * 2)) { if (Emulator.getConfig().getInt("runtime.threads") < (Runtime.getRuntime().availableProcessors() * 2)) {
LOGGER.warn("Emulator settings runtime.threads ({}) can be increased to ({}) to possibly increase performance.", LOGGER.warn("Emulator settings runtime.threads ({}) can be increased to ({}) to possibly increase performance.",
Emulator.getConfig().getInt("runtime.threads"), Emulator.getConfig().getInt("runtime.threads"),
@@ -50,6 +50,7 @@ public class RoomUserPetComposer extends MessageComposer {
this.response.appendString(""); this.response.appendString("");
this.response.appendString("unknown"); this.response.appendString("unknown");
this.response.appendInt(0); this.response.appendInt(0);
this.response.appendInt(0);
return this.response; 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);
}
}
@@ -6,6 +6,9 @@ import com.eu.habbo.habbohotel.achievements.AchievementManager;
import com.eu.habbo.habbohotel.bots.BotManager; import com.eu.habbo.habbohotel.bots.BotManager;
import com.eu.habbo.habbohotel.campaign.calendar.CalendarManager; import com.eu.habbo.habbohotel.campaign.calendar.CalendarManager;
import com.eu.habbo.habbohotel.catalog.CatalogManager; import com.eu.habbo.habbohotel.catalog.CatalogManager;
import com.eu.habbo.habbohotel.wheel.WheelManager;
import com.eu.habbo.habbohotel.soundboard.SoundboardManager;
import com.eu.habbo.habbohotel.mentions.MentionManager;
import com.eu.habbo.habbohotel.commands.CommandHandler; import com.eu.habbo.habbohotel.commands.CommandHandler;
import com.eu.habbo.habbohotel.crafting.CraftingManager; import com.eu.habbo.habbohotel.crafting.CraftingManager;
import com.eu.habbo.habbohotel.guides.GuideManager; import com.eu.habbo.habbohotel.guides.GuideManager;
@@ -64,6 +67,9 @@ public class GameEnvironment {
private GoogleTranslateManager googleTranslateManager; private GoogleTranslateManager googleTranslateManager;
private CustomBadgeManager customBadgeManager; private CustomBadgeManager customBadgeManager;
private InfostandBackgroundManager infostandBackgroundManager; private InfostandBackgroundManager infostandBackgroundManager;
private WheelManager wheelManager;
private SoundboardManager soundboardManager;
private MentionManager mentionManager;
public void load() throws Exception { public void load() throws Exception {
LOGGER.info("GameEnvironment -> Loading..."); LOGGER.info("GameEnvironment -> Loading...");
@@ -93,6 +99,9 @@ public class GameEnvironment {
this.googleTranslateManager = new GoogleTranslateManager(); this.googleTranslateManager = new GoogleTranslateManager();
this.customBadgeManager = new CustomBadgeManager(); this.customBadgeManager = new CustomBadgeManager();
this.infostandBackgroundManager = new InfostandBackgroundManager(); this.infostandBackgroundManager = new InfostandBackgroundManager();
this.wheelManager = new WheelManager();
this.soundboardManager = new SoundboardManager();
this.mentionManager = new MentionManager();
this.roomManager.loadPublicRooms(); this.roomManager.loadPublicRooms();
this.navigatorManager.loadNavigator(); this.navigatorManager.loadNavigator();
@@ -156,6 +165,14 @@ public class GameEnvironment {
return this.catalogManager; return this.catalogManager;
} }
public WheelManager getWheelManager() {
return this.wheelManager;
}
public SoundboardManager getSoundboardManager() {
return this.soundboardManager;
}
public HotelViewManager getHotelViewManager() { public HotelViewManager getHotelViewManager() {
return this.hotelViewManager; return this.hotelViewManager;
} }
@@ -188,6 +205,10 @@ public class GameEnvironment {
return this.petManager; return this.petManager;
} }
public MentionManager getMentionManager() {
return this.mentionManager;
}
public AchievementManager getAchievementManager() { public AchievementManager getAchievementManager() {
return this.achievementManager; return this.achievementManager;
} }
@@ -138,18 +138,20 @@ public class Bot implements Runnable {
@Override @Override
public void run() { public void run() {
if (this.needsUpdate) { 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 = ?")) { 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(1, this.name);
statement.setString(2, this.motto); statement.setString(2, this.motto);
statement.setString(3, this.figure); statement.setString(3, this.figure);
statement.setString(4, this.gender.toString()); statement.setString(4, this.gender.toString());
statement.setInt(5, this.ownerId); statement.setInt(5, this.ownerId);
statement.setInt(6, this.room == null ? 0 : this.room.getId()); statement.setInt(6, localRoom == null ? 0 : localRoom.getId());
statement.setInt(7, this.roomUnit == null ? 0 : this.roomUnit.getX()); statement.setInt(7, localRoomUnit == null ? 0 : localRoomUnit.getX());
statement.setInt(8, this.roomUnit == null ? 0 : this.roomUnit.getY()); statement.setInt(8, localRoomUnit == null ? 0 : localRoomUnit.getY());
statement.setDouble(9, this.roomUnit == null ? 0 : this.roomUnit.getZ()); statement.setDouble(9, localRoomUnit == null ? 0 : localRoomUnit.getZ());
statement.setInt(10, this.roomUnit == null ? 0 : this.roomUnit.getBodyRotation().getValue()); statement.setInt(10, localRoomUnit == null ? 0 : localRoomUnit.getBodyRotation().getValue());
statement.setInt(11, this.roomUnit == null ? 0 : this.roomUnit.getDanceType().getType()); statement.setInt(11, localRoomUnit == null ? 0 : localRoomUnit.getDanceType().getType());
statement.setString(12, this.canWalk ? "1" : "0"); statement.setString(12, this.canWalk ? "1" : "0");
StringBuilder text = new StringBuilder(); StringBuilder text = new StringBuilder();
for (String s : this.chatLines) { for (String s : this.chatLines) {
@@ -187,11 +189,7 @@ public class Bot implements Runnable {
int timeOut = Emulator.getRandom().nextInt(20) * 2; int timeOut = Emulator.getRandom().nextInt(20) * 2;
this.roomUnit.setWalkTimeOut((timeOut < 10 ? 5 : timeOut) + Emulator.getIntUnixTimestamp()); this.roomUnit.setWalkTimeOut((timeOut < 10 ? 5 : timeOut) + Emulator.getIntUnixTimestamp());
} }
}/* else { }
for (RoomTile t : this.room.getLayout().getTilesAround(this.room.getLayout().getTile(this.getRoomUnit().getX(), this.getRoomUnit().getY()))) {
WiredManager.handle(WiredTriggerType.BOT_REACHED_STF, this.roomUnit, this.room, this.room.getItemsAt(t).toArray());
}
}*/
} }
if (!this.chatLines.isEmpty() && this.chatTimeOut <= Emulator.getIntUnixTimestamp() && this.chatAuto) { if (!this.chatLines.isEmpty() && this.chatTimeOut <= Emulator.getIntUnixTimestamp() && this.chatAuto) {
@@ -216,7 +214,7 @@ public class Bot implements Runnable {
} else { } else {
this.lastChatIndex++; this.lastChatIndex++;
if (this.lastChatIndex >= this.chatLines.size()) { if (this.lastChatIndex >= this.chatLines.size()) {
this.lastChatIndex = 0; // start from scratch :-3 this.lastChatIndex = 0;
} }
} }
@@ -282,7 +280,7 @@ public class Bot implements Runnable {
} }
public void onPickUp(Habbo habbo, Room room) { public void onPickUp(Habbo habbo, Room room) {
this.stopFollowingHabbo();
} }
public void onUserSay(final RoomChatMessage message) { public void onUserSay(final RoomChatMessage message) {
@@ -308,9 +306,6 @@ public class Bot implements Runnable {
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
this.needsUpdate = true; this.needsUpdate = true;
//if(this.room != null)
//this.room.sendComposer(new ChangeNameUpdatedComposer(this.getRoomUnit(), this.getName()).compose());
} }
public String getMotto() { public String getMotto() {
@@ -537,5 +532,28 @@ public class Bot implements Runnable {
} }
} }
private static final short[] DEFAULT_OWNER_ACTION_IDS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11};
public static final int ACTION_ROTATE = 11;
private static final long MIN_OWNER_ACTION_INTERVAL_MS = 200L;
private volatile long lastOwnerActionAt;
public short[] getOwnerActionIds() {
return DEFAULT_OWNER_ACTION_IDS;
}
public synchronized boolean tryAcquireOwnerActionSlot() {
long now = System.currentTimeMillis();
if (now - this.lastOwnerActionAt < MIN_OWNER_ACTION_INTERVAL_MS) {
return false;
}
this.lastOwnerActionAt = now;
return true;
}
public void onPostOwnerAction(int actionId) {
// no-op default
}
} }
@@ -41,6 +41,7 @@ public class BotManager {
addBotDefinition("generic", Bot.class); addBotDefinition("generic", Bot.class);
addBotDefinition("bartender", ButlerBot.class); addBotDefinition("bartender", ButlerBot.class);
addBotDefinition("visitor_log", VisitorBot.class); addBotDefinition("visitor_log", VisitorBot.class);
addBotDefinition(FrankBot.BOT_TYPE, FrankBot.class);
this.reload(); this.reload();
@@ -20,10 +20,13 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
public class ButlerBot extends Bot { public class ButlerBot extends Bot {
private static final Logger LOGGER = LoggerFactory.getLogger(ButlerBot.class); private static final Logger LOGGER = LoggerFactory.getLogger(ButlerBot.class);
public static THashMap<THashSet<String>, Integer> serveItems = new THashMap<>(); public static THashMap<THashSet<String>, Integer> serveItems = new THashMap<>();
private static final ConcurrentHashMap<Pattern, Integer> serveItemsCompiled = new ConcurrentHashMap<>();
public ButlerBot(ResultSet set) throws SQLException { public ButlerBot(ResultSet set) throws SQLException {
super(set); super(set);
@@ -38,6 +41,7 @@ public class ButlerBot extends Bot {
serveItems = new THashMap<>(); serveItems = new THashMap<>();
serveItems.clear(); serveItems.clear();
serveItemsCompiled.clear();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM bot_serves")) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM bot_serves")) {
while (set.next()) { while (set.next()) {
@@ -45,6 +49,17 @@ public class ButlerBot extends Bot {
THashSet<String> ks = new THashSet<>(); THashSet<String> ks = new THashSet<>();
Collections.addAll(ks, keys); Collections.addAll(ks, keys);
serveItems.put(ks, set.getInt("item")); 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) { } catch (SQLException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
@@ -53,6 +68,7 @@ public class ButlerBot extends Bot {
public static void dispose() { public static void dispose() {
serveItems.clear(); serveItems.clear();
serveItemsCompiled.clear();
} }
@Override @Override
@@ -66,74 +82,73 @@ public class ButlerBot extends Bot {
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.commanddistance")) { if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.commanddistance")) {
if (message.getUnfilteredMessage() != null) { if (message.getUnfilteredMessage() != null) {
for (Map.Entry<THashSet<String>, Integer> set : serveItems.entrySet()) { String unfilteredLower = message.getUnfilteredMessage().toLowerCase();
for (String keyword : set.getKey()) { 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. // Enable plugins to cancel this event
// If keyword = tea, teapot wouldn't trigger it. BotServerItemEvent serveEvent = new BotServerItemEvent(this, message.getHabbo(), itemId);
if (message.getUnfilteredMessage().toLowerCase().matches("\\b" + keyword + "\\b")) { if (Emulator.getPluginManager().fireEvent(serveEvent).isCancelled()) {
// 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);
}
}
}
return; 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;
} }
} }
} }
@@ -0,0 +1,455 @@
package com.eu.habbo.habbohotel.bots;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.rooms.*;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserWhisperComposer;
import com.eu.habbo.threading.runnables.RoomUnitWalkToLocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
public class FrankBot extends ButlerBot {
private static final Logger LOGGER = LoggerFactory.getLogger(FrankBot.class);
public static final String BOT_TYPE = "frank";
public static final String PERMISSION_USE = "acc_bot_frank";
private static final String KEY_DOOR_LINES = "__door_lines";
private static final String KEY_BUSY_WHISPER = "__busy_whisper";
private static final String KEY_DOOR_TRIGGERS = "__door_triggers";
private static final List<String> DEFAULT_DOOR_LINES = List.of(
"Right this way - mind the step!",
"And out you go. Come back soon!",
"Allow me to escort you to the exit.",
"There's the door. Farewell, true believer!"
);
private static final String DEFAULT_BUSY_WHISPER =
"Sorry, I am currently busy. Please wait until I am available.";
private static final Pattern DEFAULT_DOOR_PATTERN = Pattern.compile(
"\\b(show me the door|kick me|i want to leave|let me out)\\b");
private static final ConcurrentHashMap<Pattern, List<String>> chatResponses = new ConcurrentHashMap<>();
private static volatile List<String> doorLines = DEFAULT_DOOR_LINES;
private static volatile String busyWhisper = DEFAULT_BUSY_WHISPER;
private static volatile Pattern doorTriggerPattern = DEFAULT_DOOR_PATTERN;
private static final Random RANDOM = new Random();
private static final int MAX_CHAT_KEYWORDS = 256;
private static final int MAX_DOOR_TRIGGERS = 32;
private static final int MAX_MESSAGE_LEN = 256;
private static final long BUSY_WHISPER_COOLDOWN_MS = 5000L;
private volatile RoomTile homeTile;
private volatile RoomUserRotation homeRotation;
private final AtomicBoolean busy = new AtomicBoolean(false);
private final AtomicBoolean returnScheduled = new AtomicBoolean(false);
private final ConcurrentHashMap<Integer, Long> lastBusyWhisperAt = new ConcurrentHashMap<>();
public FrankBot(ResultSet set) throws SQLException {
super(set);
}
public FrankBot(Bot bot) {
super(bot);
}
@Override
public void onPlace(Habbo habbo, Room room) {
super.onPlace(habbo, room);
if (this.getRoomUnit() != null) {
this.homeTile = this.getRoomUnit().getCurrentLocation();
this.homeRotation = this.getRoomUnit().getBodyRotation();
}
}
private static final short[] FRANK_OWNER_ACTIONS = { (short) Bot.ACTION_ROTATE };
@Override
public short[] getOwnerActionIds() {
return FRANK_OWNER_ACTIONS;
}
@Override
public void onPostOwnerAction(int actionId) {
if (actionId == ACTION_ROTATE && this.getRoomUnit() != null) {
this.homeRotation = this.getRoomUnit().getBodyRotation();
}
}
public static void initialise() {
chatResponses.clear();
doorLines = DEFAULT_DOOR_LINES;
busyWhisper = DEFAULT_BUSY_WHISPER;
doorTriggerPattern = DEFAULT_DOOR_PATTERN;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
Statement statement = connection.createStatement();
ResultSet set = statement.executeQuery("SELECT `keys`, `responses` FROM bot_chat_responses WHERE bot_type = '" + BOT_TYPE + "'")) {
while (set.next()) {
String keysRaw = set.getString("keys");
String responsesRaw = set.getString("responses");
if (keysRaw == null || responsesRaw == null) continue;
List<String> responses = new ArrayList<>();
for (String line : responsesRaw.split("\n")) {
String trimmed = line.trim();
if (!trimmed.isEmpty()) responses.add(trimmed);
}
if (responses.isEmpty()) continue;
String firstKey = keysRaw.split(";", 2)[0].trim();
if (firstKey.startsWith("__")) {
switch (firstKey) {
case KEY_DOOR_LINES:
doorLines = new CopyOnWriteArrayList<>(responses);
break;
case KEY_BUSY_WHISPER:
busyWhisper = responses.get(0);
break;
case KEY_DOOR_TRIGGERS:
doorTriggerPattern = buildDoorTriggerPattern(responses);
break;
default:
LOGGER.warn("FrankBot: unknown system key '{}', ignored", firstKey);
}
continue;
}
List<String> shared = new CopyOnWriteArrayList<>(responses);
for (String key : keysRaw.split(";")) {
if (chatResponses.size() >= MAX_CHAT_KEYWORDS) {
LOGGER.warn("FrankBot: chat keyword cap ({}) reached, remaining rows ignored",
MAX_CHAT_KEYWORDS);
break;
}
String k = key == null ? "" : key.trim().toLowerCase();
if (k.isEmpty()) continue;
try {
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(k) + "\\b");
chatResponses.put(pattern, shared);
} catch (Exception e) {
LOGGER.error("Failed to compile Frank chat keyword pattern: {}", k, e);
}
}
}
} catch (SQLException e) {
LOGGER.warn("FrankBot: could not load bot_chat_responses ({}). Frank will still serve items.", e.getMessage());
}
ButlerBot.initialise();
}
public static void dispose() {
chatResponses.clear();
doorLines = DEFAULT_DOOR_LINES;
busyWhisper = DEFAULT_BUSY_WHISPER;
doorTriggerPattern = DEFAULT_DOOR_PATTERN;
ButlerBot.dispose();
}
private static Pattern buildDoorTriggerPattern(List<String> triggers) {
StringBuilder sb = new StringBuilder("\\b(");
boolean first = true;
int count = 0;
for (String trigger : triggers) {
if (count >= MAX_DOOR_TRIGGERS) {
LOGGER.warn("FrankBot: door trigger cap ({}) reached, extra entries ignored",
MAX_DOOR_TRIGGERS);
break;
}
String t = trigger == null ? "" : trigger.trim().toLowerCase();
if (t.isEmpty()) continue;
if (!first) sb.append('|');
sb.append(Pattern.quote(t));
first = false;
count++;
}
sb.append(")\\b");
if (first) return DEFAULT_DOOR_PATTERN;
try {
return Pattern.compile(sb.toString());
} catch (Exception e) {
LOGGER.error("FrankBot: failed to compile door trigger pattern from {}, falling back to default", triggers, e);
return DEFAULT_DOOR_PATTERN;
}
}
@Override
public void onUserSay(final RoomChatMessage message) {
Room currentRoom = this.getRoom();
if (currentRoom == null) return;
Habbo asker = message.getHabbo();
if (asker == null || asker.getClient() == null) return;
if (this.getRoomUnit() == null) return;
String raw = message.getUnfilteredMessage();
if (raw != null && raw.length() > MAX_MESSAGE_LEN) return;
if (this.homeTile == null) {
this.homeTile = this.getRoomUnit().getCurrentLocation();
this.homeRotation = this.getRoomUnit().getBodyRotation();
}
if (this.busy.get() || this.getRoomUnit().hasStatus(RoomUnitStatus.MOVE)) {
this.whisperThrottled(asker, busyWhisper);
return;
}
if (raw != null) {
double distance = this.getRoomUnit().getCurrentLocation().distance(asker.getRoomUnit().getCurrentLocation());
int commandDistance = Emulator.getConfig().getInt("hotel.bot.butler.commanddistance");
if (distance <= commandDistance) {
String lower = raw.toLowerCase();
if (doorTriggerPattern.matcher(lower).find()) {
if (!this.busy.compareAndSet(false, true)) {
this.whisperThrottled(asker, busyWhisper);
return;
}
this.showToTheDoor(asker);
return;
}
for (java.util.Map.Entry<Pattern, List<String>> entry : chatResponses.entrySet()) {
if (entry.getKey().matcher(lower).find()) {
List<String> options = entry.getValue();
if (options.isEmpty()) continue;
String reply = options.get(RANDOM.nextInt(options.size()));
this.talk(reply);
return;
}
}
}
}
if (!this.busy.compareAndSet(false, true)) {
this.whisperThrottled(asker, busyWhisper);
return;
}
super.onUserSay(message);
this.schedulePostServeReturn(currentRoom.getId(), 0);
}
private void whisperThrottled(Habbo target, String text) {
if (target == null || text == null || text.isEmpty() || this.getRoomUnit() == null) return;
int userId = target.getHabboInfo().getId();
long now = System.currentTimeMillis();
Long last = lastBusyWhisperAt.get(userId);
if (last != null && (now - last) < BUSY_WHISPER_COOLDOWN_MS) return;
lastBusyWhisperAt.put(userId, now);
RoomChatMessage msg = new RoomChatMessage(text, this.getRoomUnit(), RoomChatMessageBubbles.BOT);
target.getClient().sendResponse(new RoomUserWhisperComposer(msg));
}
private void showToTheDoor(final Habbo target) {
final Room room = this.getRoom();
if (room == null || room.getLayout() == null || target == null) {
this.busy.set(false);
return;
}
final RoomTile doorTile = room.getLayout().getDoorTile();
if (doorTile == null) {
this.busy.set(false);
return;
}
this.lookAt(target);
List<String> lines = doorLines;
String line = lines.isEmpty() ? DEFAULT_DOOR_LINES.get(RANDOM.nextInt(DEFAULT_DOOR_LINES.size()))
: lines.get(RANDOM.nextInt(lines.size()));
this.talk(line);
final int targetId = target.getHabboInfo().getId();
final int roomId = room.getId();
final AtomicBoolean fired = new AtomicBoolean(false);
final Runnable kickThenReturn = () -> {
if (!fired.compareAndSet(false, true)) return;
Room currentRoom = this.getRoom();
if (currentRoom == null || currentRoom.getId() != roomId) {
this.busy.set(false);
return;
}
Habbo stillHere = currentRoom.getHabbo(targetId);
if (stillHere != null) {
currentRoom.kickHabbo(stillHere, false);
}
this.scheduleReturnHome(targetId, roomId, 0);
};
if (this.getRoomUnit().canWalk() && !this.getRoomUnit().getCurrentLocation().equals(doorTile)) {
List<Runnable> onArrive = new ArrayList<>();
onArrive.add(kickThenReturn);
List<Runnable> onFail = new ArrayList<>();
onFail.add(() -> Emulator.getThreading().run(kickThenReturn, 1500));
this.getRoomUnit().setGoalLocation(doorTile);
Emulator.getThreading().run(
new RoomUnitWalkToLocation(this.getRoomUnit(), doorTile, room, onArrive, onFail));
} else {
Emulator.getThreading().run(kickThenReturn, 1500);
}
}
private static final int RETURN_HOME_POLL_MS = 500;
private static final int RETURN_HOME_MAX_WAIT_MS = 8000;
private static final int POST_SERVE_POLL_MS = 750;
private static final int POST_SERVE_MAX_WAIT_MS = 30000;
private void schedulePostServeReturn(final int roomId, final int waitedMs) {
if (waitedMs == 0 && !this.returnScheduled.compareAndSet(false, true)) {
return;
}
if (waitedMs >= POST_SERVE_MAX_WAIT_MS) {
this.returnScheduled.set(false);
this.busy.set(false);
return;
}
if (this.homeTile == null) {
this.returnScheduled.set(false);
this.busy.set(false);
return;
}
Emulator.getThreading().run(() -> {
Room r = this.getRoom();
if (r == null || r.getId() != roomId || this.getRoomUnit() == null || this.homeTile == null) {
this.returnScheduled.set(false);
this.busy.set(false);
return;
}
if (this.getRoomUnit().getCurrentLocation().equals(this.homeTile)) {
if (this.homeRotation != null && this.getRoomUnit().getBodyRotation() != this.homeRotation) {
this.getRoomUnit().setRotation(this.homeRotation);
r.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
this.persistPosition();
} else {
this.busy.set(false);
}
this.returnScheduled.set(false);
return;
}
boolean stillWalking = this.getRoomUnit().hasStatus(RoomUnitStatus.MOVE)
|| (this.getRoomUnit().getPath() != null && !this.getRoomUnit().getPath().isEmpty());
if (stillWalking) {
this.schedulePostServeReturn(roomId, waitedMs + POST_SERVE_POLL_MS);
return;
}
this.returnScheduled.set(false);
this.returnHome(-1, false);
}, POST_SERVE_POLL_MS);
}
private void scheduleReturnHome(final int kickedHabboId, final int roomId, final int waitedMs) {
Room currentRoom = this.getRoom();
if (currentRoom == null || currentRoom.getId() != roomId) return;
boolean stillEscorting = currentRoom.getHabbo(kickedHabboId) != null;
if (!stillEscorting || waitedMs >= RETURN_HOME_MAX_WAIT_MS) {
this.returnHome(kickedHabboId, true);
return;
}
Emulator.getThreading().run(
() -> this.scheduleReturnHome(kickedHabboId, roomId, waitedMs + RETURN_HOME_POLL_MS),
RETURN_HOME_POLL_MS);
}
private void returnHome(int kickedHabboId, boolean alwaysTeleport) {
final Room room = this.getRoom();
if (room == null || this.homeTile == null || this.getRoomUnit() == null) {
this.busy.set(false);
return;
}
final Runnable teleportHome = () -> {
Room r = this.getRoom();
if (r == null || this.getRoomUnit() == null) return;
double homeZ = r.getTopHeightAt(this.homeTile.x, this.homeTile.y);
this.getRoomUnit().stopWalking();
this.getRoomUnit().setZ(homeZ);
this.getRoomUnit().setLocation(this.homeTile);
this.getRoomUnit().setPreviousLocationZ(homeZ);
if (this.homeRotation != null) {
this.getRoomUnit().setRotation(this.homeRotation);
}
this.getRoomUnit().statusUpdate(true);
r.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
this.persistPosition();
};
if (this.getRoomUnit().getCurrentLocation().equals(this.homeTile)) {
if (this.homeRotation != null) {
this.getRoomUnit().setRotation(this.homeRotation);
room.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
}
this.persistPosition();
return;
}
boolean hasOtherWatchers = false;
for (Habbo h : room.getCurrentHabbos().values()) {
if (h.getHabboInfo().getId() != kickedHabboId) {
hasOtherWatchers = true;
break;
}
}
if (alwaysTeleport || !hasOtherWatchers || !this.getRoomUnit().canWalk()) {
teleportHome.run();
return;
}
List<Runnable> onArrive = new ArrayList<>();
onArrive.add(() -> {
if (this.homeRotation != null && this.getRoom() != null) {
this.getRoomUnit().setRotation(this.homeRotation);
this.getRoom().sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
}
this.persistPosition();
});
List<Runnable> onFail = new ArrayList<>();
onFail.add(teleportHome);
this.getRoomUnit().setGoalLocation(this.homeTile);
Emulator.getThreading().run(
new RoomUnitWalkToLocation(this.getRoomUnit(), this.homeTile, room, onArrive, onFail));
}
private void persistPosition() {
this.needsUpdate(true);
this.run();
this.busy.set(false);
}
}
@@ -202,6 +202,8 @@ public class CatalogManager {
public final Item ecotronItem; public final Item ecotronItem;
public final THashMap<Integer, CatalogLimitedConfiguration> limitedNumbers; public final THashMap<Integer, CatalogLimitedConfiguration> limitedNumbers;
private final List<Voucher> vouchers; private final List<Voucher> vouchers;
public final TIntObjectMap<int[]> furnitureValues;
private volatile byte[] rareValuesPayloadCache;
public CatalogManager() { public CatalogManager() {
long millis = System.currentTimeMillis(); long millis = System.currentTimeMillis();
@@ -219,6 +221,7 @@ public class CatalogManager {
this.buildersClubOfferDefs = new TIntIntHashMap(); this.buildersClubOfferDefs = new TIntIntHashMap();
this.vouchers = new ArrayList<>(); this.vouchers = new ArrayList<>();
this.limitedNumbers = new THashMap<>(); this.limitedNumbers = new THashMap<>();
this.furnitureValues = new TIntObjectHashMap<>();
this.initialize(); this.initialize();
@@ -243,6 +246,76 @@ public class CatalogManager {
this.loadClothing(); this.loadClothing();
this.loadRecycler(); this.loadRecycler();
this.loadGiftWrappers(); this.loadGiftWrappers();
this.loadFurnitureValues();
}
private synchronized void loadFurnitureValues() {
this.furnitureValues.clear();
final int diamondType = Emulator.getConfig().getInt("seasonal.currency.diamond", 5);
for (CatalogPage page : this.catalogPages.valueCollection()) {
for (CatalogItem catalogItem : page.getCatalogItems().valueCollection()) {
if (catalogItem.getAmount() != 1)
continue;
int credits = catalogItem.getCredits();
int points = catalogItem.getPoints();
int pointsType = catalogItem.getPointsType();
if (points <= 0 || pointsType != diamondType)
continue;
THashSet<Item> baseItems = catalogItem.getBaseItems();
if (baseItems.size() != 1)
continue;
for (Item item : baseItems) {
FurnitureType type = item.getType();
if (type != FurnitureType.FLOOR && type != FurnitureType.WALL)
continue;
int spriteId = item.getSpriteId();
if (spriteId > 0 && !this.furnitureValues.containsKey(spriteId)) {
this.furnitureValues.put(spriteId, new int[]{credits, points, pointsType});
}
}
}
}
this.rebuildRareValuesPayloadCache();
LOGGER.info("Furniture Values -> Loaded! ({} entries)", this.furnitureValues.size());
}
private void rebuildRareValuesPayloadCache() {
try (java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(this.furnitureValues.size() * 16 + 8);
java.io.DataOutputStream out = new java.io.DataOutputStream(baos)) {
out.writeInt(this.furnitureValues.size());
TIntObjectIterator<int[]> iterator = this.furnitureValues.iterator();
while (iterator.hasNext()) {
iterator.advance();
int[] value = iterator.value();
out.writeInt(iterator.key()); // spriteId
out.writeInt(value[0]); // credits
out.writeInt(value[1]); // points
out.writeInt(value[2]); // pointsType
}
this.rareValuesPayloadCache = baos.toByteArray();
} catch (java.io.IOException e) {
LOGGER.error("Failed to build rare values payload cache", e);
this.rareValuesPayloadCache = null;
}
}
public TIntObjectMap<int[]> getFurnitureValues() {
return this.furnitureValues;
}
public byte[] getRareValuesPayloadSnapshot() {
return this.rareValuesPayloadCache;
} }
private synchronized void loadLimitedNumbers() { private synchronized void loadLimitedNumbers() {
@@ -1046,10 +1119,19 @@ public class CatalogManager {
for (Item baseItem : item.getBaseItems()) { for (Item baseItem : item.getBaseItems()) {
for (int k = 0; k < item.getItemAmount(baseItem.getId()); k++) { for (int k = 0; k < item.getItemAmount(baseItem.getId()); k++) {
if (baseItem.getName().startsWith("rentable_bot_") || baseItem.getName().startsWith("bot_")) { if (baseItem.getName().startsWith("rentable_bot_") || baseItem.getName().startsWith("bot_")) {
String baseName = baseItem.getName();
String type = item.getName().replace("rentable_bot_", ""); String type = item.getName().replace("rentable_bot_", "");
type = type.replace("bot_", ""); type = type.replace("bot_", "");
type = type.replace("visitor_logger", "visitor_log"); type = type.replace("visitor_logger", "visitor_log");
if (("bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)
|| ("rentable_bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)) {
if (!habbo.getClient().getHabbo().hasPermission(com.eu.habbo.habbohotel.bots.FrankBot.PERMISSION_USE)) {
habbo.getClient().sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
}
THashMap<String, String> data = new THashMap<>(); THashMap<String, String> data = new THashMap<>();
for (String s : item.getExtradata().split(";")) { for (String s : item.getExtradata().split(";")) {
@@ -72,7 +72,11 @@ public class ClubOffer implements ISerialize {
this.type = OfferType.fromDatabase(set.getString("type")); this.type = OfferType.fromDatabase(set.getString("type"));
this.vip = this.type == OfferType.VIP; this.vip = this.type == OfferType.VIP;
this.deal = set.getString("deal").equals("1"); 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() { public int getId() {
@@ -42,14 +42,18 @@ public class RoomBundleLayout extends SingleBundle {
} }
if (this.room == null) { if (this.room == null) {
if (this.roomId > 0) { RoomManager roomManager = Emulator.getGameEnvironment().getRoomManager();
this.room = Emulator.getGameEnvironment().getRoomManager().loadRoom(this.roomId); if (this.roomId > 0 && roomManager != null) {
this.room = roomManager.loadRoom(this.roomId);
if (this.room != null) if (this.room != null)
this.room.preventUnloading = true; this.room.preventUnloading = true;
} else { } else if (this.roomId <= 0) {
LOGGER.error("No room id specified for room bundle {}({})", this.getPageName(), this.getId()); LOGGER.error("No room id specified for room bundle {}({})", this.getPageName(), this.getId());
} }
// roomManager can be null when CatalogManager.loadFurnitureValues() runs
// during GameEnvironment.load() before RoomManager is constructed; in that
// case skip eager room loading — the bundle resolves lazily at runtime.
} }
if (this.room == null) { if (this.room == null) {
@@ -191,11 +191,14 @@ public class CommandHandler {
addCommand(new CreditsCommand()); addCommand(new CreditsCommand());
addCommand(new DanceCommand()); addCommand(new DanceCommand());
addCommand(new DiagonalCommand()); addCommand(new DiagonalCommand());
addCommand(new DisableMassMentionsCommand());
addCommand(new DisableMentionsCommand());
addCommand(new DisconnectCommand()); addCommand(new DisconnectCommand());
addCommand(new EjectAllCommand()); addCommand(new EjectAllCommand());
addCommand(new EmptyInventoryCommand()); addCommand(new EmptyInventoryCommand());
addCommand(new EmptyBotsInventoryCommand()); addCommand(new EmptyBotsInventoryCommand());
addCommand(new EmptyPetsInventoryCommand()); addCommand(new EmptyPetsInventoryCommand());
addCommand(new EmuStatsCommand());
addCommand(new EnableCommand()); addCommand(new EnableCommand());
addCommand(new EventCommand()); addCommand(new EventCommand());
addCommand(new FacelessCommand()); addCommand(new FacelessCommand());
@@ -300,7 +303,6 @@ public class CommandHandler {
addCommand(new GivePrefixCommand()); addCommand(new GivePrefixCommand());
addCommand(new ListPrefixesCommand()); addCommand(new ListPrefixesCommand());
addCommand(new RemovePrefixCommand()); addCommand(new RemovePrefixCommand());
addCommand(new PrefixBlacklistCommand());
addCommand(new WiredCommand()); addCommand(new WiredCommand());
addCommand(new TestCommand()); addCommand(new TestCommand());
} }
@@ -17,7 +17,22 @@ public class CommandsCommand extends Command {
message.append("(").append(commands.size()).append("):\r\n"); message.append("(").append(commands.size()).append("):\r\n");
for (Command c : commands) { for (Command c : commands) {
message.append(Emulator.getTexts().getValue("commands.description." + c.permission, "commands.description." + c.permission)).append("\r"); String textKey = "commands.description." + c.permission;
String commandText = Emulator.getTexts().getValue(textKey, "");
String commandLine = ":" + c.keys[0];
String description = "";
if (commandText.startsWith(":")) {
commandLine = commandText;
} else if (!commandText.isEmpty() && !commandText.equals(textKey)) {
description = commandText;
}
message.append(commandLine).append("\r");
if (!description.isEmpty()) {
message.append(description).append("\r");
}
} }
gameClient.getHabbo().alert(new String[]{message.toString()}); gameClient.getHabbo().alert(new String[]{message.toString()});
@@ -0,0 +1,25 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.users.Habbo;
public class DisableMassMentionsCommand extends Command {
public DisableMassMentionsCommand() {
super("cmd_disablemassmentions", new String[]{"disablemassmentions", "togglemassmentions"});
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
if (gameClient == null) return true;
Habbo habbo = gameClient.getHabbo();
if (habbo == null || habbo.getHabboStats() == null) return true;
boolean newState = !habbo.getHabboStats().massMentionsEnabled();
habbo.getHabboStats().setMassMentionsEnabled(newState);
habbo.whisper(newState
? "Broadcast mentions (@all / @friends / @room) are now ENABLED for you."
: "Broadcast mentions (@all / @friends / @room) are now DISABLED for you. Direct @nick mentions still work.");
return true;
}
}
@@ -0,0 +1,25 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.users.Habbo;
public class DisableMentionsCommand extends Command {
public DisableMentionsCommand() {
super("cmd_disablementions", new String[]{"disablementions", "togglementions"});
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
if (gameClient == null) return true;
Habbo habbo = gameClient.getHabbo();
if (habbo == null || habbo.getHabboStats() == null) return true;
boolean newState = !habbo.getHabboStats().mentionsEnabled();
habbo.getHabboStats().setMentionsEnabled(newState);
habbo.whisper(newState
? "@mention notifications are now ENABLED for you."
: "@mention notifications are now DISABLED for you. You will not receive direct or broadcast mentions.");
return true;
}
}
@@ -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;
}
}
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.modtool.WordFilter; import com.eu.habbo.habbohotel.modtool.WordFilter;
import com.eu.habbo.habbohotel.modtool.WordFilterWord; import com.eu.habbo.habbohotel.modtool.WordFilterWord;
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -21,30 +22,44 @@ public class FilterWordCommand extends Command {
@Override @Override
public boolean handle(GameClient gameClient, String[] params) throws Exception { public boolean handle(GameClient gameClient, String[] params) throws Exception {
if (params.length < 2) { if (params.length < 2) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.missing_word")); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.missing_word"), RoomChatMessageBubbles.ALERT);
return true; return true;
} }
String word = params[1]; String word = params[1];
// Optional trailing "prefix" keyword marks the word as prefix-only (blocks
// custom prefixes but not chat/motto/guild). Usage:
// :filterword <word> -> everywhere, default replacement
// :filterword <word> <replacement> -> everywhere
// :filterword <word> prefix -> prefix-only, default replacement
// :filterword <word> <replacement> prefix -> prefix-only
boolean prefixOnly = false;
String replacement = WordFilter.DEFAULT_REPLACEMENT; String replacement = WordFilter.DEFAULT_REPLACEMENT;
if (params.length == 3) {
replacement = params[2]; if (params.length >= 3) {
if (params[params.length - 1].equalsIgnoreCase("prefix")) {
prefixOnly = true;
if (params.length >= 4) replacement = params[2];
} else {
replacement = params[2];
}
} }
WordFilterWord wordFilterWord = new WordFilterWord(word, replacement); WordFilterWord wordFilterWord = new WordFilterWord(word, replacement, prefixOnly);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO wordfilter (`key`, `replacement`) VALUES (?, ?)")) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO wordfilter (`key`, `replacement`, `prefix_only`) VALUES (?, ?, ?)")) {
statement.setString(1, word); statement.setString(1, word);
statement.setString(2, replacement); statement.setString(2, replacement);
statement.setString(3, prefixOnly ? "1" : "0");
statement.execute(); statement.execute();
} catch (SQLException e) { } catch (SQLException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.error")); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.error"), RoomChatMessageBubbles.ALERT);
return true; return true;
} }
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_filterword.added").replace("%word%", word).replace("%replacement%", replacement)); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_filterword.added").replace("%word%", word).replace("%replacement%", replacement) + (prefixOnly ? " [prefix-only]" : ""), RoomChatMessageBubbles.ALERT);
Emulator.getGameEnvironment().getWordFilter().addWord(wordFilterWord); Emulator.getGameEnvironment().getWordFilter().addWord(wordFilterWord);
return true; return true;
@@ -1,98 +0,0 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class PrefixBlacklistCommand extends Command {
private static final Logger LOGGER = LoggerFactory.getLogger(PrefixBlacklistCommand.class);
public PrefixBlacklistCommand() {
super("cmd_prefix_blacklist", Emulator.getTexts().getValue("commands.keys.cmd_prefix_blacklist").split(";"));
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
if (params.length < 2) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
return true;
}
String action = params[1].toLowerCase();
if (action.equals("list")) {
StringBuilder sb = new StringBuilder();
sb.append(Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.header")).append("\r");
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT word FROM custom_prefix_blacklist ORDER BY word")) {
try (ResultSet set = statement.executeQuery()) {
int count = 0;
while (set.next()) {
sb.append("- ").append(set.getString("word")).append("\r");
count++;
}
if (count == 0) {
sb.append(Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.empty"));
}
}
} catch (SQLException e) {
LOGGER.error("Error listing prefix blacklist", e);
}
gameClient.getHabbo().whisper(sb.toString(), RoomChatMessageBubbles.ALERT);
return true;
}
if (params.length < 3) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
return true;
}
String word = params[2].toLowerCase().trim();
if (word.isEmpty()) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.empty_word"), RoomChatMessageBubbles.ALERT);
return true;
}
if (action.equals("add")) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("INSERT INTO custom_prefix_blacklist (word) VALUES (?)")) {
statement.setString(1, word);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Error adding prefix blacklist word", e);
}
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.added").replace("%word%", word),
RoomChatMessageBubbles.ALERT
);
} else if (action.equals("remove")) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM custom_prefix_blacklist WHERE word = ?")) {
statement.setString(1, word);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Error removing prefix blacklist word", e);
}
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.removed").replace("%word%", word),
RoomChatMessageBubbles.ALERT
);
} else {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
}
return true;
}
}
@@ -2,7 +2,12 @@ package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient; 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.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 class UpdatePermissionsCommand extends Command {
public UpdatePermissionsCommand() { public UpdatePermissionsCommand() {
@@ -13,7 +18,41 @@ public class UpdatePermissionsCommand extends Command {
public boolean handle(GameClient gameClient, String[] params) throws Exception { public boolean handle(GameClient gameClient, String[] params) throws Exception {
Emulator.getGameEnvironment().getPermissionsManager().reload(); 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; return true;
} }
@@ -153,7 +153,13 @@ public class GameClient {
this.channel.close(); this.channel.close();
if (this.habbo != null) { if (this.habbo != null) {
if (this.habbo.isOnline()) { // Agisci sull'Habbo SOLO se è ancora attaccato a QUESTO client. Su un
// reconnect veloce (drop Cloudflare → il client riconnette) l'Habbo può
// essere già stato riassegnato alla NUOVA connessione (session resume):
// in quel caso questo dispose della vecchia connessione NON deve
// parcheggiarlo né disconnetterlo, altrimenti ucciderebbe la sessione
// appena ripristinata (era la causa del "Bye"/kick al 2° reconnect).
if (this.habbo.getClient() == this && this.habbo.isOnline()) {
// Try to park the habbo in the grace period instead of immediate disconnect // Try to park the habbo in the grace period instead of immediate disconnect
boolean parked = SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket); boolean parked = SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket);
@@ -118,16 +118,32 @@ public class SessionResumeManager {
LOGGER.error("[SessionResume] Error during deferred disconnect", e); LOGGER.error("[SessionResume] Error during deferred disconnect", e);
} }
clearSsoTicket(habbo.getHabboInfo().getId()); // NON svuotare il ticket SSO qui. Dietro Cloudflare la pagina si ricarica
// lentamente (~15s) e la grace (5s) scade prima che la nuova connessione
// arrivi: svuotando il ticket si cancellava quello NUOVO appena scritto dal
// CMS per il refresh → "non-existing SSO token" → bisognava refreshare 2 volte.
// Il ticket vive col suo TTL (auth_ticket_expires_at) e viene sovrascritto dal
// CMS al prossimo /client o azzerato al logout.
} }
private void restoreSsoTicket(int userId, String ssoTicket) { private void restoreSsoTicket(int userId, String ssoTicket) {
// Restore the old ticket ONLY if no fresh ticket has been written in the
// meantime. On a hard-refresh the CMS writes a NEW auth_ticket for the same
// user before this parking restore runs; without the guard we'd clobber it
// with the old ticket, so the new connection's SSO wouldn't be found and the
// client would get "session expired" on the first attempt. The guard means:
// normal reconnect (ticket cleared to '' after login) -> restore; hard-refresh
// (CMS already wrote a new ticket) -> leave the new ticket untouched.
try (var connection = Emulator.getDatabase().getDataSource().getConnection(); try (var connection = Emulator.getDatabase().getDataSource().getConnection();
var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) { var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? AND (auth_ticket = '' OR auth_ticket IS NULL) LIMIT 1")) {
statement.setString(1, ssoTicket); statement.setString(1, ssoTicket);
statement.setInt(2, userId); statement.setInt(2, userId);
statement.execute(); int updated = statement.executeUpdate();
LOGGER.info("[SessionResume] Restored SSO ticket for user {} during grace period", userId); if (updated > 0) {
LOGGER.info("[SessionResume] Restored SSO ticket for user {} during grace period", userId);
} else {
LOGGER.info("[SessionResume] Skipped SSO restore for user {} — a newer ticket is already present (likely a fresh login/hard-refresh)", userId);
}
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("[SessionResume] Failed to restore SSO ticket for user " + userId, e); LOGGER.error("[SessionResume] Failed to restore SSO ticket for user " + userId, e);
} }
@@ -208,10 +208,10 @@ public abstract class Game implements Runnable {
this.state = GameState.IDLE; this.state = GameState.IDLE;
boolean gamesActive = false; boolean gamesActive = false;
for (HabboItem timer : room.getFloorItems()) { for (InteractionGameTimer timer : room.getRoomSpecialTypes().getGameTimers().values()) {
if (timer instanceof InteractionGameTimer) { if (timer.isRunning()) {
if (((InteractionGameTimer) timer).isRunning()) gamesActive = true;
gamesActive = true; break;
} }
} }
@@ -6,49 +6,55 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager;
public class GamePlayer { public class GamePlayer {
private final Habbo habbo; private final Habbo habbo;
private GameTeamColors teamColor; private GameTeamColors teamColor;
private int score; private int score;
private int wiredScore; private int wiredScore;
public GamePlayer(Habbo habbo, GameTeamColors teamColor) { public GamePlayer(Habbo habbo, GameTeamColors teamColor) {
this.habbo = habbo; this.habbo = habbo;
this.teamColor = teamColor; this.teamColor = teamColor;
} }
public void reset() { public void reset() {
this.score = 0; this.score = 0;
this.wiredScore = 0; this.wiredScore = 0;
} }
public synchronized void addScore(int amount) { public void addScore(int amount) {
addScore(amount, false); addScore(amount, false);
} }
public synchronized void addScore(int amount, boolean isWired) { public 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) { com.eu.habbo.habbohotel.rooms.Room roomToTrigger = null;
this.score += amount; 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) { if (this.score < 0) this.score = 0;
this.wiredScore += amount;
if (this.wiredScore < 0) { if (isWired) {
this.wiredScore = 0; this.wiredScore += amount;
if (this.wiredScore < 0) {
this.wiredScore = 0;
}
if (this.wiredScore > this.score) {
this.wiredScore = this.score;
}
} }
if (this.wiredScore > this.score) { roomToTrigger = this.habbo.getHabboInfo().getCurrentRoom();
this.wiredScore = this.score; 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; return this.habbo;
} }
public GameTeamColors getTeamColor() { public GameTeamColors getTeamColor() {
return this.teamColor; return this.teamColor;
} }
public int getScore() { public int getScore() {
return this.score; return this.score;
} }
@@ -252,6 +252,25 @@ public class Guild implements Runnable {
return this.readForum; return this.readForum;
} }
public boolean canHabboReadForum(int habboId, GuildMember member, boolean staff) {
if (staff || this.getOwnerId() == habboId) {
return true;
}
switch (this.readForum) {
case EVERYONE:
return true;
case MEMBERS:
return member != null && member.getRank().type <= GuildRank.MEMBER.type;
case ADMINS:
return member != null && member.getRank().type < GuildRank.MEMBER.type;
case OWNER:
return false;
default:
return true;
}
}
public void setReadForum(SettingsState readForum) { public void setReadForum(SettingsState readForum) {
this.readForum = readForum; this.readForum = readForum;
} }
@@ -48,6 +48,12 @@ public class Item implements ISerialize {
return item.getName().toLowerCase().startsWith("a0 pet"); return item.getName().toLowerCase().startsWith("a0 pet");
} }
public static boolean isBot(Item item) {
if (item == null) return false;
String name = item.getName();
return name != null && (name.startsWith("bot_") || name.startsWith("rentable_bot_"));
}
public static double getCurrentHeight(HabboItem item) { public static double getCurrentHeight(HabboItem item) {
if (item instanceof InteractionMultiHeight && item.getBaseItem().getMultiHeights().length > 0) { if (item instanceof InteractionMultiHeight && item.getBaseItem().getMultiHeights().length > 0) {
if (item.getExtradata().isEmpty()) { if (item.getExtradata().isEmpty()) {
@@ -89,7 +89,9 @@ public class InteractionOneWayGate extends HabboItem {
Emulator.getThreading().run(new RoomUnitWalkToLocation(unit, tile, room, onFail, onFail)); Emulator.getThreading().run(new RoomUnitWalkToLocation(unit, tile, room, onFail, onFail));
Emulator.getThreading().run(() -> { Emulator.getThreading().run(() -> {
WiredManager.triggerUserWalksOn(room, unit, this); if (room.isLoaded()) {
WiredManager.triggerUserWalksOn(room, unit, this);
}
}, 500); }, 500);
}); });
@@ -29,6 +29,10 @@ public class InteractionRoomAds extends InteractionCustomValues {
{ {
this.put("offsetZ", "0"); this.put("offsetZ", "0");
} }
{
this.put("scale", "100");
}
}; };
public InteractionRoomAds(ResultSet set, Item baseItem) throws SQLException { public InteractionRoomAds(ResultSet set, Item baseItem) throws SQLException {
@@ -93,23 +93,24 @@ public abstract class InteractionWired extends InteractionDefault {
@Override @Override
public void run() { public void run() {
if (this.needsUpdate()) { 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) { Emulator.getThreading().run(() -> {
wiredData = ""; 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);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE items SET wired_data = ? WHERE id = ?")) { } else {
if (this.getRoomId() != 0) { statement.setString(1, "");
statement.setString(1, wiredData); }
} else { statement.setInt(2, currentId);
statement.setString(1, ""); 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(); super.run();
} }
@@ -216,6 +217,9 @@ public abstract class InteractionWired extends InteractionDefault {
public static WiredSettings readSettings(ClientMessage packet, boolean isEffect) public static WiredSettings readSettings(ClientMessage packet, boolean isEffect)
{ {
int intParamCount = packet.readInt(); int intParamCount = packet.readInt();
if (intParamCount < 0 || intParamCount > 100) {
throw new IllegalArgumentException("Invalid intParamCount: " + intParamCount);
}
int[] intParams = new int[intParamCount]; int[] intParams = new int[intParamCount];
for(int i = 0; i < intParamCount; i++) for(int i = 0; i < intParamCount; i++)
@@ -226,6 +230,10 @@ public abstract class InteractionWired extends InteractionDefault {
String stringParam = packet.readString(); String stringParam = packet.readString();
int itemCount = packet.readInt(); 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]; int[] itemIds = new int[itemCount];
for(int i = 0; i < itemCount; i++) for(int i = 0; i < itemCount; i++)
@@ -154,6 +154,7 @@ public class InteractionGameTimer extends HabboItem {
@Override @Override
public void onPickUp(Room room) { public void onPickUp(Room room) {
this.endGame(room); this.endGame(room);
this.threadActive = false;
this.timeNow = this.getInitialTimeValue(); this.timeNow = this.getInitialTimeValue();
this.setExtradata(this.timeNow + "\t" + this.baseTime); this.setExtradata(this.timeNow + "\t" + this.baseTime);
@@ -220,8 +221,7 @@ public class InteractionGameTimer extends HabboItem {
room.updateItem(this); room.updateItem(this);
WiredManager.triggerGameStarts(room); WiredManager.triggerGameStarts(room);
if (!this.threadActive) { if (this.tryActivateTimerThread()) {
this.threadActive = true;
this.scheduleTimerRunnable(this.getTimerStartDelayMs()); this.scheduleTimerRunnable(this.getTimerStartDelayMs());
} }
} else if (client != null) { } else if (client != null) {
@@ -243,8 +243,7 @@ public class InteractionGameTimer extends HabboItem {
} else { } else {
this.unpause(room); this.unpause(room);
if (!this.threadActive) { if (this.tryActivateTimerThread()) {
this.threadActive = true;
this.scheduleTimerRunnable(this.getTimerResumeDelayMs()); this.scheduleTimerRunnable(this.getTimerResumeDelayMs());
} }
} }
@@ -257,8 +256,7 @@ public class InteractionGameTimer extends HabboItem {
this.createNewGame(room); this.createNewGame(room);
WiredManager.triggerGameStarts(room); WiredManager.triggerGameStarts(room);
if (!this.threadActive) { if (this.tryActivateTimerThread()) {
this.threadActive = true;
this.scheduleTimerRunnable(this.getTimerStartDelayMs()); this.scheduleTimerRunnable(this.getTimerStartDelayMs());
} }
} }
@@ -297,8 +295,7 @@ public class InteractionGameTimer extends HabboItem {
} }
this.createNewGame(room); this.createNewGame(room);
WiredManager.triggerGameStarts(room); WiredManager.triggerGameStarts(room);
if (!threadActive) { if (this.tryActivateTimerThread()) {
threadActive = true;
this.scheduleTimerRunnable(this.getTimerStartDelayMs()); this.scheduleTimerRunnable(this.getTimerStartDelayMs());
} }
} }
@@ -321,8 +318,7 @@ public class InteractionGameTimer extends HabboItem {
this.isPaused = false; this.isPaused = false;
this.unpause(room); this.unpause(room);
if (!this.threadActive) { if (this.tryActivateTimerThread()) {
this.threadActive = true;
this.scheduleTimerRunnable(this.getTimerResumeDelayMs()); this.scheduleTimerRunnable(this.getTimerResumeDelayMs());
} }
} }
@@ -406,7 +402,9 @@ public class InteractionGameTimer extends HabboItem {
} }
public void setThreadActive(boolean threadActive) { public void setThreadActive(boolean threadActive) {
this.threadActive = threadActive; synchronized (this) {
this.threadActive = threadActive;
}
} }
public boolean isPaused() { public boolean isPaused() {
@@ -428,4 +426,15 @@ public class InteractionGameTimer extends HabboItem {
public int getBaseTime() { public int getBaseTime() {
return this.baseTime; 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.incoming.wired.WiredSaveException;
import com.eu.habbo.messages.outgoing.generic.alerts.UpdateFailedComposer; import com.eu.habbo.messages.outgoing.generic.alerts.UpdateFailedComposer;
import gnu.trove.procedure.TObjectProcedure; import gnu.trove.procedure.TObjectProcedure;
import gnu.trove.set.hash.THashSet;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
public class WiredEffectGiveReward extends InteractionWiredEffect { public class WiredEffectGiveReward extends InteractionWiredEffect {
public static final int LIMIT_ONCE = 0; public static final int LIMIT_ONCE = 0;
@@ -37,10 +38,10 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
public int limit; public int limit;
public int limitationInterval; public int limitationInterval;
public int given; public AtomicInteger given = new AtomicInteger(0);
public int rewardTime; public int rewardTime;
public boolean uniqueRewards; public boolean uniqueRewards;
public THashSet<WiredGiveRewardItem> rewardItems = new THashSet<>(); public List<WiredGiveRewardItem> rewardItems = new CopyOnWriteArrayList<>();
public int userSource = WiredSourceUtil.SOURCE_TRIGGER; public int userSource = WiredSourceUtil.SOURCE_TRIGGER;
public WiredEffectGiveReward(ResultSet set, Item baseItem) throws SQLException { public WiredEffectGiveReward(ResultSet set, Item baseItem) throws SQLException {
@@ -71,9 +72,8 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
@Override @Override
public String getWiredData() { public String getWiredData() {
ArrayList<WiredGiveRewardItem> rewards = new ArrayList<>(this.rewardItems); 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 @Override
@@ -84,7 +84,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
this.setDelay(data.delay); this.setDelay(data.delay);
this.limit = data.limit; this.limit = data.limit;
this.given = data.given; this.given.set(data.given);
this.rewardTime = data.reward_time; this.rewardTime = data.reward_time;
this.uniqueRewards = data.unique_rewards; this.uniqueRewards = data.unique_rewards;
this.limitationInterval = data.limit_interval; this.limitationInterval = data.limit_interval;
@@ -96,7 +96,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
String[] data = wiredData.split(":"); String[] data = wiredData.split(":");
if (data.length > 0) { if (data.length > 0) {
this.limit = Integer.parseInt(data[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.rewardTime = Integer.parseInt(data[2]);
this.uniqueRewards = data[3].equals("1"); this.uniqueRewards = data[3].equals("1");
this.limitationInterval = Integer.parseInt(data[4]); this.limitationInterval = Integer.parseInt(data[4]);
@@ -127,7 +127,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
public void onPickUp() { public void onPickUp() {
this.limit = 0; this.limit = 0;
this.limitationInterval = 0; this.limitationInterval = 0;
this.given = 0; this.given.set(0);
this.rewardTime = 0; this.rewardTime = 0;
this.uniqueRewards = false; this.uniqueRewards = false;
this.rewardItems.clear(); this.rewardItems.clear();
@@ -192,7 +192,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
this.limit = settings.getIntParams()[2]; this.limit = settings.getIntParams()[2];
this.limitationInterval = settings.getIntParams()[3]; this.limitationInterval = settings.getIntParams()[3];
this.userSource = settings.getIntParams()[4]; this.userSource = settings.getIntParams()[4];
this.given = 0; this.given.set(0);
String data = settings.getStringParam(); String data = settings.getStringParam();
@@ -276,15 +276,15 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
} }
public int getGiven() { public int getGiven() {
return this.given; return this.given.get();
} }
public void setGiven(int given) { public void setGiven(int given) {
this.given = given; this.given.set(given);
} }
public void incrementGiven() { public void incrementGiven() {
this.given++; this.given.incrementAndGet();
} }
public int getRewardTime() { public int getRewardTime() {
@@ -303,11 +303,11 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
this.uniqueRewards = uniqueRewards; this.uniqueRewards = uniqueRewards;
} }
public THashSet<WiredGiveRewardItem> getRewardItems() { public List<WiredGiveRewardItem> getRewardItems() {
return this.rewardItems; return this.rewardItems;
} }
public void setRewardItems(THashSet<WiredGiveRewardItem> rewardItems) { public void setRewardItems(List<WiredGiveRewardItem> rewardItems) {
this.rewardItems = rewardItems; this.rewardItems = rewardItems;
} }
} }
@@ -384,61 +384,69 @@ public final class WiredVariableReferenceSupport {
} }
private static void upsertSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId, SharedUserAssignment assignment) { private static void upsertSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId, SharedUserAssignment assignment) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Emulator.getThreading().run(() -> {
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)")) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
statement.setInt(1, sourceRoomId); 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(2, userId); statement.setInt(1, sourceRoomId);
statement.setInt(3, sourceVariableItemId); statement.setInt(2, userId);
statement.setInt(3, sourceVariableItemId);
if (assignment.getValue() == null) { if (assignment.getValue() == null) {
statement.setNull(4, java.sql.Types.INTEGER); statement.setNull(4, java.sql.Types.INTEGER);
} else { } else {
statement.setInt(4, assignment.getValue()); 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) { private static void deleteSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Emulator.getThreading().run(() -> {
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ?")) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
statement.setInt(1, sourceRoomId); PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ?")) {
statement.setInt(2, userId); statement.setInt(1, sourceRoomId);
statement.setInt(3, sourceVariableItemId); statement.setInt(2, userId);
statement.executeUpdate(); statement.setInt(3, sourceVariableItemId);
} catch (SQLException e) { statement.executeUpdate();
LOGGER.error("Failed to delete shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e); } 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) { private static void upsertSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId, SharedRoomAssignment assignment) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Emulator.getThreading().run(() -> {
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)")) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
statement.setInt(1, sourceRoomId); 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(2, sourceVariableItemId); statement.setInt(1, sourceRoomId);
statement.setInt(3, assignment.getValue()); statement.setInt(2, sourceVariableItemId);
statement.setInt(4, 0); statement.setInt(3, assignment.getValue());
statement.setInt(5, assignment.getUpdatedAt()); statement.setInt(4, 0);
statement.executeUpdate(); statement.setInt(5, assignment.getUpdatedAt());
} catch (SQLException e) { statement.executeUpdate();
LOGGER.error("Failed to store shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e); } 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) { private static void deleteSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Emulator.getThreading().run(() -> {
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ?")) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
statement.setInt(1, sourceRoomId); PreparedStatement statement = connection.prepareStatement("DELETE FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ?")) {
statement.setInt(2, sourceVariableItemId); statement.setInt(1, sourceRoomId);
statement.executeUpdate(); statement.setInt(2, sourceVariableItemId);
} catch (SQLException e) { statement.executeUpdate();
LOGGER.error("Failed to delete shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e); } 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) { private static String createDefinitionPrefix(int sourceRoomId, int sourceVariableItemId) {
@@ -123,7 +123,11 @@ public class WiredTriggerRepeaterLong extends InteractionWiredTrigger implements
@Override @Override
public boolean saveData(WiredSettings settings) { public boolean saveData(WiredSettings settings) {
if (settings.getIntParams().length < 1) return false; 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 // No accumulated time reset needed - using global tick count
return true; return true;
} }
@@ -0,0 +1,90 @@
package com.eu.habbo.habbohotel.mentions;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.Habbo;
import java.sql.ResultSet;
import java.sql.SQLException;
public class HabboMention {
public static final int TYPE_DIRECT = 0;
public static final int TYPE_ROOM = 1;
private final int id;
private final int targetUserId;
private final int senderUserId;
private final String senderUsername;
private final int roomId;
private final String roomName;
private final String message;
private final int mentionType;
private final int timestamp;
private final boolean read;
public HabboMention(ResultSet set) throws SQLException {
this.id = set.getInt("id");
this.targetUserId = set.getInt("target_user_id");
this.senderUserId = set.getInt("sender_user_id");
this.senderUsername = set.getString("sender_username");
this.roomId = set.getInt("room_id");
this.roomName = set.getString("room_name");
this.message = set.getString("message");
this.mentionType = set.getInt("mention_type");
this.timestamp = set.getInt("timestamp");
this.read = set.getInt("read") == 1;
}
public HabboMention(int targetUserId, int id, Habbo sender, Room room, String roomName, String message, int mentionType, int timestamp) {
this.id = id;
this.targetUserId = targetUserId;
this.senderUserId = sender.getHabboInfo().getId();
this.senderUsername = sender.getHabboInfo().getUsername();
this.roomId = room.getId();
this.roomName = roomName;
this.message = message;
this.mentionType = mentionType;
this.timestamp = timestamp;
this.read = false;
}
public int getId() {
return this.id;
}
public int getTargetUserId() {
return this.targetUserId;
}
public int getSenderUserId() {
return this.senderUserId;
}
public String getSenderUsername() {
return this.senderUsername;
}
public int getRoomId() {
return this.roomId;
}
public String getRoomName() {
return this.roomName;
}
public String getMessage() {
return this.message;
}
public int getMentionType() {
return this.mentionType;
}
public int getTimestamp() {
return this.timestamp;
}
public boolean isRead() {
return this.read;
}
}
@@ -0,0 +1,437 @@
package com.eu.habbo.habbohotel.mentions;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.messenger.MessengerBuddy;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomChatType;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboManager;
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.Statement;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class MentionManager {
private static final Logger LOGGER = LoggerFactory.getLogger(MentionManager.class);
private static final int ROOM_NAME_MAX_LENGTH = 64;
private static final int MESSAGE_MAX_LENGTH = 255;
private final ConcurrentHashMap<Integer, Long> cooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> roomBroadcastCooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> requestListCooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> markReadCooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> markAllCooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> deleteCooldowns = new ConcurrentHashMap<>();
private volatile long lastPrune = System.currentTimeMillis();
private static final long PRUNE_INTERVAL_MS = 5 * 60_000L;
public boolean isEnabled() {
return Emulator.getConfig().getInt("mentions.enabled", 1) == 1;
}
/** Broadcast category resolved from a mention alias. */
public enum BroadcastScope {
NONE,
ROOM,
FRIENDS,
EVERYONE
}
public static final String PERMISSION_EVERYONE = "acc_mention_everyone";
public static final String PERMISSION_FRIENDS = "acc_mention_friends";
private Set<String> parseAliases(String configKey, String defaultValue) {
Set<String> aliases = new HashSet<>();
String raw = Emulator.getConfig().getValue(configKey, defaultValue);
for (String alias : raw.split(",")) {
String trimmed = alias.trim().toLowerCase();
if (!trimmed.isEmpty()) {
aliases.add(trimmed);
}
}
return aliases;
}
private Set<String> roomAliases() {
return parseAliases("mentions.room.aliases", "room,stanza");
}
private Set<String> friendsAliases() {
return parseAliases("mentions.friends.aliases", "friends,amici");
}
private Set<String> everyoneAliases() {
return parseAliases("mentions.everyone.aliases", "all,everyone,tutti");
}
private BroadcastScope classifyAlias(String alias,
Set<String> everyone,
Set<String> friends,
Set<String> room) {
if (alias.isEmpty()) return BroadcastScope.NONE;
if (everyone.contains(alias)) return BroadcastScope.EVERYONE;
if (friends.contains(alias)) return BroadcastScope.FRIENDS;
if (room.contains(alias)) return BroadcastScope.ROOM;
return BroadcastScope.NONE;
}
public void process(Habbo sender, Room room, String message, RoomChatType type) {
try {
if (!this.isEnabled()) {
return;
}
if (sender == null || room == null || message == null) {
return;
}
if (message.isEmpty() || message.indexOf('@') < 0) {
return;
}
int senderId = sender.getHabboInfo().getId();
long now = System.currentTimeMillis();
long cooldownMs = Emulator.getConfig().getInt("mentions.cooldown.ms", 3000);
Long last = this.cooldowns.get(senderId);
if (last != null && (now - last) < cooldownMs) {
return;
}
Set<String> roomAliases = this.roomAliases();
Set<String> friendsAliases = this.friendsAliases();
Set<String> everyoneAliases = this.everyoneAliases();
BroadcastScope broadcastScope = BroadcastScope.NONE;
LinkedHashSet<String> directTokens = new LinkedHashSet<>();
for (String token : message.split("\\s+")) {
if (token.length() < 2 || token.charAt(0) != '@') {
continue;
}
String raw = token.substring(1);
String aliasCandidate = trimTrailingPunctuation(raw).toLowerCase();
BroadcastScope scope = this.classifyAlias(aliasCandidate, everyoneAliases, friendsAliases, roomAliases);
if (scope != BroadcastScope.NONE) {
if (scope.ordinal() > broadcastScope.ordinal()) {
broadcastScope = scope;
}
} else if (!raw.isEmpty()) {
directTokens.add(raw);
}
}
if (broadcastScope == BroadcastScope.EVERYONE && !sender.hasPermission(PERMISSION_EVERYONE)) {
broadcastScope = BroadcastScope.NONE;
} else if (broadcastScope == BroadcastScope.FRIENDS && !sender.hasPermission(PERMISSION_FRIENDS)) {
broadcastScope = BroadcastScope.NONE;
}
if (broadcastScope == BroadcastScope.NONE && directTokens.isEmpty()) {
return;
}
if (broadcastScope != BroadcastScope.NONE) {
long roomCooldownMs = Emulator.getConfig().getInt("mentions.room.cooldown.ms", 15000);
Long lastRoom = this.roomBroadcastCooldowns.get(senderId);
if (lastRoom != null && (now - lastRoom) < roomCooldownMs) {
return;
}
}
int maxTargets = Emulator.getConfig().getInt("mentions.max.targets", 50);
if (maxTargets <= 0) maxTargets = 1;
int maxDirectTokens = Math.min(directTokens.size(), maxTargets);
List<Habbo> targets = new ArrayList<>();
Set<Integer> seen = new HashSet<>();
switch (broadcastScope) {
case EVERYONE:
this.collectEveryoneTargets(senderId, targets, seen, maxTargets);
break;
case FRIENDS:
this.collectFriendsTargets(sender, senderId, targets, seen, maxTargets);
break;
case ROOM:
this.collectRoomTargets(room, senderId, targets, seen, maxTargets, true);
break;
case NONE:
default:
int processed = 0;
for (String token : directTokens) {
if (processed++ >= maxDirectTokens) break;
Habbo habbo = this.resolveHabbo(room, token);
if (habbo == null || habbo.getHabboInfo().getId() == senderId) {
continue;
}
if (!acceptsMention(habbo, false)) {
continue;
}
if (seen.add(habbo.getHabboInfo().getId())) {
targets.add(habbo);
}
if (targets.size() >= maxTargets) {
break;
}
}
break;
}
if (targets.isEmpty()) {
return;
}
this.cooldowns.put(senderId, now);
if (broadcastScope != BroadcastScope.NONE) this.roomBroadcastCooldowns.put(senderId, now);
this.pruneCooldownsIfDue(now);
int mentionType = (broadcastScope != BroadcastScope.NONE) ? HabboMention.TYPE_ROOM : HabboMention.TYPE_DIRECT;
int timestamp = Emulator.getIntUnixTimestamp();
String roomName = truncate(room.getName(), ROOM_NAME_MAX_LENGTH);
String storedMessage = truncate(message, MESSAGE_MAX_LENGTH);
for (Habbo target : targets) {
this.store(target, sender, room, storedMessage, mentionType, timestamp, roomName);
}
} catch (Exception e) {
LOGGER.error("Failed to process mentions.", e);
}
}
private void collectRoomTargets(Room room, int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets, boolean isBroadcast) {
for (Habbo habbo : room.getHabbos()) {
if (habbo == null || habbo.getHabboInfo().getId() == senderId) continue;
if (!acceptsMention(habbo, isBroadcast)) continue;
if (seen.add(habbo.getHabboInfo().getId())) targets.add(habbo);
if (targets.size() >= maxTargets) break;
}
}
private void collectFriendsTargets(Habbo sender, int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets) {
if (sender.getMessenger() == null) return;
HabboManager habboManager = Emulator.getGameEnvironment().getHabboManager();
for (MessengerBuddy buddy : sender.getMessenger().getFriends().values()) {
if (buddy == null) continue;
int buddyId = buddy.getId();
if (buddyId == senderId) continue;
Habbo online = habboManager.getHabbo(buddyId);
if (online == null) continue;
if (!acceptsMention(online, true)) continue;
if (seen.add(buddyId)) targets.add(online);
if (targets.size() >= maxTargets) break;
}
}
private void collectEveryoneTargets(int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets) {
for (Habbo habbo : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().values()) {
if (habbo == null || habbo.getHabboInfo().getId() == senderId) continue;
if (!acceptsMention(habbo, true)) continue;
if (seen.add(habbo.getHabboInfo().getId())) targets.add(habbo);
if (targets.size() >= maxTargets) break;
}
}
private boolean acceptsMention(Habbo recipient, boolean isBroadcast) {
if (recipient == null || recipient.getHabboStats() == null) return true;
if (!recipient.getHabboStats().mentionsEnabled()) return false;
if (isBroadcast && !recipient.getHabboStats().massMentionsEnabled()) return false;
return true;
}
private void store(Habbo target, Habbo sender, Room room, String message, int mentionType, int timestamp, String roomName) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO habbo_mentions (target_user_id, sender_user_id, sender_username, room_id, room_name, message, mention_type, timestamp, `read`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)",
Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, target.getHabboInfo().getId());
statement.setInt(2, sender.getHabboInfo().getId());
statement.setString(3, sender.getHabboInfo().getUsername());
statement.setInt(4, room.getId());
statement.setString(5, roomName);
statement.setString(6, message);
statement.setInt(7, mentionType);
statement.setInt(8, timestamp);
statement.executeUpdate();
int generatedId = 0;
try (ResultSet keys = statement.getGeneratedKeys()) {
if (keys.next()) {
generatedId = keys.getInt(1);
}
}
if (generatedId <= 0) {
return;
}
HabboMention mention = new HabboMention(target.getHabboInfo().getId(), generatedId, sender, room, roomName, message, mentionType, timestamp);
if (target.getClient() != null) {
target.getClient().sendResponse(new com.eu.habbo.messages.outgoing.mentions.MentionReceivedComposer(mention));
}
} catch (SQLException e) {
LOGGER.error("Failed to store mention.", e);
}
}
public List<HabboMention> getMentions(int userId, int limit) {
List<HabboMention> mentions = new ArrayList<>();
if (limit <= 0) limit = 50;
if (limit > 200) limit = 200;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM habbo_mentions WHERE target_user_id = ? ORDER BY id DESC LIMIT ?")) {
statement.setInt(1, userId);
statement.setInt(2, limit);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
mentions.add(new HabboMention(set));
}
}
} catch (SQLException e) {
LOGGER.error("Failed to load mentions.", e);
}
return mentions;
}
public void markRead(int userId, int mode, int mentionId) {
if (mode != 0 && mode != 1) return;
if (mode == 1 && mentionId <= 0) return;
String query = mode == 1
? "UPDATE habbo_mentions SET `read` = 1 WHERE target_user_id = ? AND id = ? AND `read` = 0"
: "UPDATE habbo_mentions SET `read` = 1 WHERE target_user_id = ? AND `read` = 0";
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(query)) {
statement.setInt(1, userId);
if (mode == 1) {
statement.setInt(2, mentionId);
}
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to mark mentions as read.", e);
}
}
public void delete(int userId, int mentionId) {
if (mentionId <= 0) return;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"DELETE FROM habbo_mentions WHERE target_user_id = ? AND id = ?")) {
statement.setInt(1, userId);
statement.setInt(2, mentionId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to delete mention.", e);
}
}
public boolean tryAcquireRequestList(int userId) {
long cooldownMs = Emulator.getConfig().getInt("mentions.request.cooldown.ms", 2000);
return tryAcquire(this.requestListCooldowns, userId, cooldownMs);
}
public boolean tryAcquireMarkRead(int userId, int mode) {
long cooldownMs;
ConcurrentHashMap<Integer, Long> bucket;
if (mode == 1) {
cooldownMs = Emulator.getConfig().getInt("mentions.markread.cooldown.ms", 500);
bucket = this.markReadCooldowns;
} else {
cooldownMs = Emulator.getConfig().getInt("mentions.markall.cooldown.ms", 5000);
bucket = this.markAllCooldowns;
}
return tryAcquire(bucket, userId, cooldownMs);
}
public boolean tryAcquireDelete(int userId) {
long cooldownMs = Emulator.getConfig().getInt("mentions.delete.cooldown.ms", 500);
return tryAcquire(this.deleteCooldowns, userId, cooldownMs);
}
private boolean tryAcquire(ConcurrentHashMap<Integer, Long> bucket, int userId, long cooldownMs) {
long now = System.currentTimeMillis();
Long last = bucket.get(userId);
if (last != null && (now - last) < cooldownMs) {
return false;
}
bucket.put(userId, now);
this.pruneCooldownsIfDue(now);
return true;
}
private void pruneCooldownsIfDue(long now) {
if (now - this.lastPrune < PRUNE_INTERVAL_MS) return;
this.lastPrune = now;
long mentionWindow = Emulator.getConfig().getInt("mentions.cooldown.ms", 3000);
long roomWindow = Emulator.getConfig().getInt("mentions.room.cooldown.ms", 15000);
long requestWindow = Emulator.getConfig().getInt("mentions.request.cooldown.ms", 2000);
long markReadWindow = Emulator.getConfig().getInt("mentions.markread.cooldown.ms", 500);
long markAllWindow = Emulator.getConfig().getInt("mentions.markall.cooldown.ms", 5000);
long deleteWindow = Emulator.getConfig().getInt("mentions.delete.cooldown.ms", 500);
prune(this.cooldowns, now, mentionWindow);
prune(this.roomBroadcastCooldowns, now, roomWindow);
prune(this.requestListCooldowns, now, requestWindow);
prune(this.markReadCooldowns, now, markReadWindow);
prune(this.markAllCooldowns, now, markAllWindow);
prune(this.deleteCooldowns, now, deleteWindow);
}
private static void prune(ConcurrentHashMap<Integer, Long> bucket, long now, long windowMs) {
Iterator<Map.Entry<Integer, Long>> it = bucket.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, Long> entry = it.next();
Long value = entry.getValue();
if (value == null || (now - value) >= windowMs) {
it.remove();
}
}
}
private static final String TRAILING_PUNCTUATION = ".,!?;:)]}\"'";
private static String trimTrailingPunctuation(String value) {
int end = value.length();
while (end > 0 && TRAILING_PUNCTUATION.indexOf(value.charAt(end - 1)) >= 0) {
end--;
}
return value.substring(0, end);
}
private static String truncate(String value, int max) {
if (value == null) return "";
if (value.length() <= max) return value;
return value.substring(0, max);
}
private Habbo resolveHabbo(Room room, String rawToken) {
Habbo habbo = room.getHabbo(rawToken);
if (habbo != null) {
return habbo;
}
String trimmed = trimTrailingPunctuation(rawToken);
if (!trimmed.isEmpty() && !trimmed.equals(rawToken)) {
return room.getHabbo(trimmed);
}
return null;
}
}
@@ -651,6 +651,10 @@ public class ModToolManager {
sender.getClient().sendResponse(new ModToolIssueHandledComposer(ModToolIssueHandledComposer.ABUSIVE)); 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.updateTicketToMods(issue);
this.removeTicket(issue); this.removeTicket(issue);
@@ -737,4 +741,38 @@ public class ModToolManager {
return issues; 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);
}
}
} }
@@ -23,7 +23,6 @@ public class WordFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(WordFilter.class); private static final Logger LOGGER = LoggerFactory.getLogger(WordFilter.class);
private static final Pattern DIACRITICS_AND_FRIENDS = Pattern.compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+"); private static final Pattern DIACRITICS_AND_FRIENDS = Pattern.compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");
//Configuration. Loaded from database & updated accordingly.
public static boolean ENABLED_FRIENDCHAT = true; public static boolean ENABLED_FRIENDCHAT = true;
public static String DEFAULT_REPLACEMENT = "bobba"; public static String DEFAULT_REPLACEMENT = "bobba";
protected THashSet<WordFilterWord> autoReportWords = new THashSet<>(); protected THashSet<WordFilterWord> autoReportWords = new THashSet<>();
@@ -63,10 +62,12 @@ public class WordFilter {
continue; continue;
} }
if (word.autoReport) if (!word.prefixOnly) {
this.autoReportWords.add(word); if (word.autoReport)
else if (word.hideMessage) this.autoReportWords.add(word);
this.hideMessageWords.add(word); else if (word.hideMessage)
this.hideMessageWords.add(word);
}
this.words.add(word); this.words.add(word);
} }
@@ -146,6 +147,8 @@ public class WordFilter {
while (iterator.hasNext()) { while (iterator.hasNext()) {
WordFilterWord word = (WordFilterWord) iterator.next(); WordFilterWord word = (WordFilterWord) iterator.next();
if (word.prefixOnly) continue;
if (StringUtils.containsIgnoreCase(filteredMessage, word.key)) { if (StringUtils.containsIgnoreCase(filteredMessage, word.key)) {
if (habbo != null) { if (habbo != null) {
if (Emulator.getPluginManager().fireEvent(new UserTriggerWordFilterEvent(habbo, word)).isCancelled()) if (Emulator.getPluginManager().fireEvent(new UserTriggerWordFilterEvent(habbo, word)).isCancelled())
@@ -179,6 +182,8 @@ public class WordFilter {
while (iterator.hasNext()) { while (iterator.hasNext()) {
WordFilterWord word = (WordFilterWord) iterator.next(); WordFilterWord word = (WordFilterWord) iterator.next();
if (word.prefixOnly) continue;
if (StringUtils.containsIgnoreCase(message, word.key)) { if (StringUtils.containsIgnoreCase(message, word.key)) {
if (habbo != null) { if (habbo != null) {
if (Emulator.getPluginManager().fireEvent(new UserTriggerWordFilterEvent(habbo, word)).isCancelled()) if (Emulator.getPluginManager().fireEvent(new UserTriggerWordFilterEvent(habbo, word)).isCancelled())
@@ -9,6 +9,7 @@ public class WordFilterWord {
public final boolean hideMessage; public final boolean hideMessage;
public final boolean autoReport; public final boolean autoReport;
public final int muteTime; public final int muteTime;
public final boolean prefixOnly;
public WordFilterWord(ResultSet set) throws SQLException { public WordFilterWord(ResultSet set) throws SQLException {
this.key = set.getString("key"); this.key = set.getString("key");
@@ -16,13 +17,27 @@ public class WordFilterWord {
this.hideMessage = set.getInt("hide") == 1; this.hideMessage = set.getInt("hide") == 1;
this.autoReport = set.getInt("report") == 1; this.autoReport = set.getInt("report") == 1;
this.muteTime = set.getInt("mute"); this.muteTime = set.getInt("mute");
this.prefixOnly = readBooleanColumn(set, "prefix_only");
} }
public WordFilterWord(String key, String replacement) { public WordFilterWord(String key, String replacement) {
this(key, replacement, false);
}
public WordFilterWord(String key, String replacement, boolean prefixOnly) {
this.key = key; this.key = key;
this.replacement = replacement; this.replacement = replacement;
this.hideMessage = false; this.hideMessage = false;
this.autoReport = false; this.autoReport = false;
this.muteTime = 0; this.muteTime = 0;
this.prefixOnly = prefixOnly;
}
private static boolean readBooleanColumn(ResultSet set, String column) {
try {
return set.getInt(column) == 1;
} catch (SQLException e) {
return false;
}
} }
} }
@@ -8,6 +8,7 @@ public class Permission {
public static String ACC_SEE_WHISPERS = "acc_see_whispers"; public static String ACC_SEE_WHISPERS = "acc_see_whispers";
public static String ACC_SEE_TENTCHAT = "acc_see_tentchat"; public static String ACC_SEE_TENTCHAT = "acc_see_tentchat";
public static String ACC_SUPERWIRED = "acc_superwired"; public static String ACC_SUPERWIRED = "acc_superwired";
public static String ACC_HOUSEKEEPING = "acc_housekeeping";
public static String ACC_SUPPORTTOOL = "acc_supporttool"; public static String ACC_SUPPORTTOOL = "acc_supporttool";
public static String ACC_UNKICKABLE = "acc_unkickable"; public static String ACC_UNKICKABLE = "acc_unkickable";
public static String ACC_GUILDGATE = "acc_guildgate"; public static String ACC_GUILDGATE = "acc_guildgate";
@@ -115,19 +115,21 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
public final Object roomUnitLock = new Object(); public final Object roomUnitLock = new Object();
public final ConcurrentHashMap<RoomTile, THashSet<HabboItem>> tileCache = new ConcurrentHashMap<>();
public final List<Integer> userVotes; public final List<Integer> userVotes;
private final TIntArrayList rights; private final TIntArrayList rights;
private final TIntIntHashMap mutedHabbos; private final TIntIntHashMap mutedHabbos;
private final TIntObjectHashMap<RoomBan> bannedHabbos; private final TIntObjectHashMap<RoomBan> bannedHabbos;
private final Set<Game> games; private final Set<Game> games;
private final TIntObjectMap<RoomMoodlightData> moodlightData; private final TIntObjectMap<RoomMoodlightData> moodlightData;
public volatile double lastCycleCpuMs = 0.0;
public volatile String lastCycleThread = "N/A";
private final Object loadLock = new Object(); private final Object loadLock = new Object();
//Use appropriately. Could potentially cause memory leaks when used incorrectly. //Use appropriately. Could potentially cause memory leaks when used incorrectly.
public volatile boolean preventUnloading = false; public volatile boolean preventUnloading = false;
public volatile boolean preventUncaching = false; public volatile boolean preventUncaching = false;
public Set<ServerMessage> scheduledComposers = ConcurrentHashMap.newKeySet(); 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 String wordQuiz = "";
public int noVotes = 0; public int noVotes = 0;
public int yesVotes = 0; public int yesVotes = 0;
@@ -195,6 +197,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
private int wiredInspectMask = WIRED_ACCESS_DEFAULT_INSPECT_MASK; private int wiredInspectMask = WIRED_ACCESS_DEFAULT_INSPECT_MASK;
private int wiredModifyMask = WIRED_ACCESS_DEFAULT_MODIFY_MASK; private int wiredModifyMask = WIRED_ACCESS_DEFAULT_MODIFY_MASK;
private boolean youtubeEnabled = false; private boolean youtubeEnabled = false;
private boolean soundboardEnabled = false;
private String youtubeCurrentVideo = ""; private String youtubeCurrentVideo = "";
private String youtubeSenderName = ""; private String youtubeSenderName = "";
private final java.util.List<String> youtubePlaylist = new java.util.concurrent.CopyOnWriteArrayList<>(); private final java.util.List<String> youtubePlaylist = new java.util.concurrent.CopyOnWriteArrayList<>();
@@ -202,6 +205,8 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
public boolean isYoutubeEnabled() { return this.youtubeEnabled; } public boolean isYoutubeEnabled() { return this.youtubeEnabled; }
public void setYoutubeEnabled(boolean enabled) { this.youtubeEnabled = enabled; } public void setYoutubeEnabled(boolean enabled) { this.youtubeEnabled = enabled; }
public boolean isSoundboardEnabled() { return this.soundboardEnabled; }
public void setSoundboardEnabled(boolean enabled) { this.soundboardEnabled = enabled; }
public String getYoutubeCurrentVideo() { return this.youtubeCurrentVideo; } public String getYoutubeCurrentVideo() { return this.youtubeCurrentVideo; }
public String getYoutubeSenderName() { return this.youtubeSenderName; } public String getYoutubeSenderName() { return this.youtubeSenderName; }
public java.util.List<String> getYoutubePlaylist() { return this.youtubePlaylist; } public java.util.List<String> getYoutubePlaylist() { return this.youtubePlaylist; }
@@ -248,6 +253,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
this.allowWalkthrough = set.getBoolean("allow_walkthrough"); this.allowWalkthrough = set.getBoolean("allow_walkthrough");
this.hideWall = set.getBoolean("allow_hidewall"); this.hideWall = set.getBoolean("allow_hidewall");
try { this.youtubeEnabled = set.getBoolean("youtube_enabled"); } catch (Exception e) { this.youtubeEnabled = false; } try { this.youtubeEnabled = set.getBoolean("youtube_enabled"); } catch (Exception e) { this.youtubeEnabled = false; }
try { this.soundboardEnabled = set.getBoolean("soundboard_enabled"); } catch (Exception e) { this.soundboardEnabled = false; }
this.chatMode = set.getInt("chat_mode"); this.chatMode = set.getInt("chat_mode");
this.chatWeight = set.getInt("chat_weight"); this.chatWeight = set.getInt("chat_weight");
this.chatSpeed = set.getInt("chat_speed"); this.chatSpeed = set.getInt("chat_speed");
@@ -981,8 +987,6 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
this.scheduledTasks.clear(); this.scheduledTasks.clear();
this.scheduledComposers.clear(); this.scheduledComposers.clear();
this.tileCache.clear();
synchronized (this.mutedHabbos) { synchronized (this.mutedHabbos) {
this.mutedHabbos.clear(); this.mutedHabbos.clear();
} }
@@ -1160,10 +1164,13 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
synchronized (this.loadLock) { synchronized (this.loadLock) {
if (this.loaded) { if (this.loaded) {
try { try {
long startTime = System.nanoTime();
this.lastCycleThread = Thread.currentThread().getName();
// Run cycle directly instead of scheduling on thread pool // Run cycle directly instead of scheduling on thread pool
// This ensures all cycle tasks in the same tick execute synchronously // This ensures all cycle tasks in the same tick execute synchronously
// preventing wired desync issues // preventing wired desync issues
this.cycle(); this.cycle();
this.lastCycleCpuMs = (System.nanoTime() - startTime) / 1000000.0;
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Caught exception", e); LOGGER.error("Caught exception", e);
} }
@@ -2320,27 +2327,37 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
sanitizedInspectMask |= sanitizedModifyMask; sanitizedInspectMask |= sanitizedModifyMask;
synchronized (this.wiredSettingsLock) { synchronized (this.wiredSettingsLock) {
int previousInspectMask = this.wiredInspectMask; final int finalInspectMask = sanitizedInspectMask;
int previousModifyMask = this.wiredModifyMask; final int finalModifyMask = sanitizedModifyMask;
final int finalId = this.id;
final int previousInspectMask = this.wiredInspectMask;
final int previousModifyMask = this.wiredModifyMask;
this.wiredInspectMask = sanitizedInspectMask; this.wiredInspectMask = sanitizedInspectMask;
this.wiredModifyMask = sanitizedModifyMask; this.wiredModifyMask = sanitizedModifyMask;
this.wiredSettingsLoaded = true; this.wiredSettingsLoaded = true;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Emulator.getThreading().run(() -> {
PreparedStatement statement = connection.prepareStatement( try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
"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)")) { PreparedStatement statement = connection.prepareStatement(
statement.setInt(1, this.id); "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(2, sanitizedInspectMask); statement.setInt(1, finalId);
statement.setInt(3, sanitizedModifyMask); statement.setInt(2, finalInspectMask);
statement.executeUpdate(); statement.setInt(3, finalModifyMask);
this.pushWiredSettingsToCurrentHabbos(); statement.executeUpdate();
return true; } catch (SQLException e) {
} catch (SQLException e) { synchronized (this.wiredSettingsLock) {
this.wiredInspectMask = previousInspectMask; if (this.wiredInspectMask == finalInspectMask && this.wiredModifyMask == finalModifyMask) {
this.wiredModifyMask = previousModifyMask; this.wiredInspectMask = previousInspectMask;
LOGGER.error("Caught SQL exception while saving wired room settings", e); this.wiredModifyMask = previousModifyMask;
return false; }
} }
LOGGER.error("Caught SQL exception while saving wired room settings", e);
}
});
this.pushWiredSettingsToCurrentHabbos();
return true;
} }
} }
@@ -2878,4 +2895,20 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
public Collection<RoomUnit> getRoomUnitsAt(RoomTile tile) { public Collection<RoomUnit> getRoomUnitsAt(RoomTile tile) {
return this.unitManager.getRoomUnitsAt(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 // Flood protection
if (!habbo.hasPermission(Permission.ACC_CHAT_NO_FLOOD)) { if (!habbo.hasPermission(Permission.ACC_CHAT_NO_FLOOD)) {
final int chatCounter = habbo.getHabboStats().chatCounter.addAndGet(1); 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 // Build prefix messages
ServerMessage prefixMessage = null; ServerMessage prefixMessage = null;
@@ -615,6 +615,9 @@ public class RoomChatManager {
InteractionTalkingFurniture.class); InteractionTalkingFurniture.class);
for (HabboItem item : items) { for (HabboItem item : items) {
if (item.getExtradata().equals("1")) {
continue;
}
if (this.room.getLayout().getTile(item.getX(), item.getY()) if (this.room.getLayout().getTile(item.getX(), item.getY())
.distance(habbo.getRoomUnit().getCurrentLocation()) <= Emulator.getConfig() .distance(habbo.getRoomUnit().getCurrentLocation()) <= Emulator.getConfig()
.getInt("furniture.talking.range")) { .getInt("furniture.talking.range")) {
@@ -134,7 +134,18 @@ public class RoomChatMessageBubbles {
} }
public static RoomChatMessageBubbles getBubble(int id) { public static RoomChatMessageBubbles getBubble(int id) {
return BUBBLES.getOrDefault(id, NORMAL); RoomChatMessageBubbles bubble = BUBBLES.get(id);
if (bubble != null) return bubble;
// Custom chat bubbles (client-side only, e.g. ids 253+) are not registered
// above. Instead of falling back to NORMAL (which made them render as the
// default bubble), pass the id through so the server relays it as-is and
// the client renders its own .bubble-<id> style. Capped to avoid abuse.
if (id > 0 && id <= 1000) {
return new RoomChatMessageBubbles(id, "CUSTOM_" + id, "", true, false);
}
return NORMAL;
} }
private static void registerBubble(RoomChatMessageBubbles bubble) { private static void registerBubble(RoomChatMessageBubbles bubble) {
@@ -75,7 +75,6 @@ public class RoomCycleManager {
final boolean[] foundRightHolder = {false}; final boolean[] foundRightHolder = {false};
boolean loaded = this.room.isLoaded(); boolean loaded = this.room.isLoaded();
this.room.tileCache.clear();
if (loaded) { if (loaded) {
processScheduledTasks(); processScheduledTasks();
@@ -164,13 +163,9 @@ public class RoomCycleManager {
* Processes scheduled tasks. * Processes scheduled tasks.
*/ */
private void processScheduledTasks() { private void processScheduledTasks() {
if (!this.room.scheduledTasks.isEmpty()) { Runnable task;
Set<Runnable> tasks = this.room.scheduledTasks; while ((task = this.room.scheduledTasks.poll()) != null) {
this.room.scheduledTasks = ConcurrentHashMap.newKeySet(); Emulator.getThreading().run(task);
for (Runnable runnable : tasks) {
Emulator.getThreading().run(runnable);
}
} }
} }
@@ -486,7 +481,7 @@ public class RoomCycleManager {
if (!unit.hasStatus(RoomUnitStatus.LAY)) { if (!unit.hasStatus(RoomUnitStatus.LAY)) {
BedProfile bedProfile = new BedProfile(topItem); BedProfile bedProfile = new BedProfile(topItem);
double layHeight = Item.getCurrentHeight(topItem) * 1.0D + bedProfile.getLayZOffset(); 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(), topItem.getBaseItem().getName(), topItem.getBaseItem().getHeight(),
bedProfile.isFlat(), bedProfile.isDouble(), bedProfile.isFlat(), bedProfile.isDouble(),
bedProfile.getLayXOffset(), bedProfile.getLayYOffset(), bedProfile.getLayZOffset()); bedProfile.getLayXOffset(), bedProfile.getLayYOffset(), bedProfile.getLayZOffset());
@@ -35,6 +35,7 @@ public class RoomFurniVariableManager {
private final Room room; private final Room room;
private final ConcurrentHashMap<Integer, ConcurrentHashMap<Integer, VariableAssignment>> activeAssignmentsByFurniId; private final ConcurrentHashMap<Integer, ConcurrentHashMap<Integer, VariableAssignment>> activeAssignmentsByFurniId;
private volatile boolean permanentAssignmentsLoaded; private volatile boolean permanentAssignmentsLoaded;
private final java.util.concurrent.atomic.AtomicBoolean broadcastRequested = new java.util.concurrent.atomic.AtomicBoolean(false);
public RoomFurniVariableManager(Room room) { public RoomFurniVariableManager(Room room) {
this.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())); 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() { public void broadcastSnapshot() {
this.requestBroadcast();
}
public void broadcastSnapshotRaw() {
RoomUserVariableManager.Snapshot userSnapshot = this.room.getUserVariableManager().createSnapshot(); RoomUserVariableManager.Snapshot userSnapshot = this.room.getUserVariableManager().createSnapshot();
Snapshot furniSnapshot = this.createSnapshot(); Snapshot furniSnapshot = this.createSnapshot();
RoomVariableManager.Snapshot roomSnapshot = this.room.getRoomVariableManager().createSnapshot(); RoomVariableManager.Snapshot roomSnapshot = this.room.getRoomVariableManager().createSnapshot();
@@ -148,55 +148,8 @@ public class RoomItemManager {
item = this.roomItems.get(id); item = this.roomItems.get(id);
} }
// Check special types if not found in main storage
RoomSpecialTypes specialTypes = this.room.getRoomSpecialTypes();
if (item == null) { if (item == null) {
item = specialTypes.getBanzaiTeleporter(id); item = this.room.getRoomSpecialTypes().getSpecialItem(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);
} }
return item; return item;
@@ -726,7 +679,7 @@ public class RoomItemManager {
item instanceof WiredBlob || item instanceof WiredBlob ||
item instanceof InteractionTent || item instanceof InteractionTent ||
item instanceof InteractionSnowboardSlope || item instanceof InteractionSnowboardSlope ||
item instanceof InteractionFireworks) { item instanceof InteractionFireworks || item instanceof InteractionVoteCounter) {
specialTypes.addUndefined(item); specialTypes.addUndefined(item);
} }
} }
@@ -899,7 +852,7 @@ public class RoomItemManager {
item instanceof InteractionStickyPole || item instanceof InteractionStickyPole ||
item instanceof WiredBlob || item instanceof WiredBlob ||
item instanceof InteractionTent || item instanceof InteractionTent ||
item instanceof InteractionSnowboardSlope) { item instanceof InteractionSnowboardSlope || item instanceof InteractionVoteCounter) {
specialTypes.removeUndefined(item); specialTypes.removeUndefined(item);
} }
@@ -1020,6 +1020,10 @@ public class RoomManager {
room.getYoutubeWatchers()).compose()); room.getYoutubeWatchers()).compose());
} }
habbo.getClient().sendResponse(new com.eu.habbo.messages.outgoing.soundboard.SoundboardSettingsComposer(
room.isSoundboardEnabled(),
Emulator.getGameEnvironment().getSoundboardManager().getSounds()).compose());
WiredManager.triggerUserEntersRoom(room, habbo.getRoomUnit()); WiredManager.triggerUserEntersRoom(room, habbo.getRoomUnit());
room.habboEntered(habbo); room.habboEntered(habbo);
@@ -71,6 +71,7 @@ public class RoomSpecialTypes {
private final THashMap<Integer, InteractionFreezeExitTile> freezeExitTile; private final THashMap<Integer, InteractionFreezeExitTile> freezeExitTile;
private final THashMap<Integer, HabboItem> undefined; private final THashMap<Integer, HabboItem> undefined;
private final Set<ICycleable> cycleTasks; private final Set<ICycleable> cycleTasks;
private final ConcurrentHashMap<Integer, HabboItem> specialItemsById = new ConcurrentHashMap<>();
public RoomSpecialTypes() { public RoomSpecialTypes() {
this.banzaiTeleporters = new THashMap<>(0); this.banzaiTeleporters = new THashMap<>(0);
@@ -115,11 +116,11 @@ public class RoomSpecialTypes {
} }
public void addBanzaiTeleporter(InteractionBattleBanzaiTeleporter item) { 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) { public void removeBanzaiTeleporter(InteractionBattleBanzaiTeleporter item) {
this.banzaiTeleporters.remove(item.getId()); this.banzaiTeleporters.remove(item.getId()); this.specialItemsById.remove(item.getId());
} }
public THashSet<InteractionBattleBanzaiTeleporter> getBanzaiTeleporters() { public THashSet<InteractionBattleBanzaiTeleporter> getBanzaiTeleporters() {
@@ -155,11 +156,11 @@ public class RoomSpecialTypes {
} }
public void addNest(InteractionNest item) { 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) { public void removeNest(InteractionNest item) {
this.nests.remove(item.getId()); this.nests.remove(item.getId()); this.specialItemsById.remove(item.getId());
} }
public THashSet<InteractionNest> getNests() { public THashSet<InteractionNest> getNests() {
@@ -177,11 +178,11 @@ public class RoomSpecialTypes {
} }
public void addPetDrink(InteractionPetDrink item) { 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) { public void removePetDrink(InteractionPetDrink item) {
this.petDrinks.remove(item.getId()); this.petDrinks.remove(item.getId()); this.specialItemsById.remove(item.getId());
} }
public THashSet<InteractionPetDrink> getPetDrinks() { public THashSet<InteractionPetDrink> getPetDrinks() {
@@ -199,11 +200,11 @@ public class RoomSpecialTypes {
} }
public void addPetFood(InteractionPetFood item) { 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) { public void removePetFood(InteractionPetFood petFood) {
this.petFoods.remove(petFood.getId()); this.petFoods.remove(petFood.getId()); this.specialItemsById.remove(petFood.getId());
} }
public THashSet<InteractionPetFood> getPetFoods() { public THashSet<InteractionPetFood> getPetFoods() {
@@ -221,11 +222,11 @@ public class RoomSpecialTypes {
} }
public void addPetToy(InteractionPetToy item) { 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) { public void removePetToy(InteractionPetToy petToy) {
this.petToys.remove(petToy.getId()); this.petToys.remove(petToy.getId()); this.specialItemsById.remove(petToy.getId());
} }
public THashSet<InteractionPetToy> getPetToys() { public THashSet<InteractionPetToy> getPetToys() {
@@ -243,11 +244,11 @@ public class RoomSpecialTypes {
} }
public void addPetTree(InteractionPetTree item) { 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) { public void removePetTree(InteractionPetTree petTree) {
this.petTrees.remove(petTree.getId()); this.petTrees.remove(petTree.getId()); this.specialItemsById.remove(petTree.getId());
} }
public THashSet<InteractionPetTree> getPetTrees() { public THashSet<InteractionPetTree> getPetTrees() {
@@ -270,12 +271,14 @@ public class RoomSpecialTypes {
synchronized (this.rollers) { synchronized (this.rollers) {
this.rollers.put(item.getId(), item); this.rollers.put(item.getId(), item);
} }
this.specialItemsById.put(item.getId(), item);
} }
public void removeRoller(InteractionRoller roller) { public void removeRoller(InteractionRoller roller) {
synchronized (this.rollers) { synchronized (this.rollers) {
this.rollers.remove(roller.getId()); this.rollers.remove(roller.getId());
} }
this.specialItemsById.remove(roller.getId());
} }
public THashMap<Integer, InteractionRoller> getRollers() { public THashMap<Integer, InteractionRoller> getRollers() {
@@ -469,11 +472,11 @@ public class RoomSpecialTypes {
// Add to type-based index // Add to type-based index
this.wiredTriggers.computeIfAbsent(trigger.getType(), k -> ConcurrentHashMap.newKeySet()) this.wiredTriggers.computeIfAbsent(trigger.getType(), k -> ConcurrentHashMap.newKeySet())
.add(trigger); .add(trigger);
// Add to spatial index // Add to spatial index
long key = coordinateKey(trigger.getX(), trigger.getY()); long key = coordinateKey(trigger.getX(), trigger.getY());
this.wiredTriggersByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet()) this.wiredTriggersByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
.add(trigger); .add(trigger);
this.specialItemsById.put(trigger.getId(), trigger);
} }
/** /**
@@ -489,7 +492,6 @@ public class RoomSpecialTypes {
this.wiredTriggers.remove(trigger.getType()); this.wiredTriggers.remove(trigger.getType());
} }
} }
// Remove from spatial index // Remove from spatial index
long key = coordinateKey(trigger.getX(), trigger.getY()); long key = coordinateKey(trigger.getX(), trigger.getY());
Set<InteractionWiredTrigger> locationTriggers = this.wiredTriggersByLocation.get(key); Set<InteractionWiredTrigger> locationTriggers = this.wiredTriggersByLocation.get(key);
@@ -499,6 +501,7 @@ public class RoomSpecialTypes {
this.wiredTriggersByLocation.remove(key); this.wiredTriggersByLocation.remove(key);
} }
} }
this.specialItemsById.remove(trigger.getId());
} }
/** /**
@@ -589,11 +592,11 @@ public class RoomSpecialTypes {
// Add to type-based index // Add to type-based index
this.wiredEffects.computeIfAbsent(effect.getType(), k -> ConcurrentHashMap.newKeySet()) this.wiredEffects.computeIfAbsent(effect.getType(), k -> ConcurrentHashMap.newKeySet())
.add(effect); .add(effect);
// Add to spatial index // Add to spatial index
long key = coordinateKey(effect.getX(), effect.getY()); long key = coordinateKey(effect.getX(), effect.getY());
this.wiredEffectsByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet()) this.wiredEffectsByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
.add(effect); .add(effect);
this.specialItemsById.put(effect.getId(), effect);
} }
/** /**
@@ -609,7 +612,6 @@ public class RoomSpecialTypes {
this.wiredEffects.remove(effect.getType()); this.wiredEffects.remove(effect.getType());
} }
} }
// Remove from spatial index // Remove from spatial index
long key = coordinateKey(effect.getX(), effect.getY()); long key = coordinateKey(effect.getX(), effect.getY());
Set<InteractionWiredEffect> locationEffects = this.wiredEffectsByLocation.get(key); Set<InteractionWiredEffect> locationEffects = this.wiredEffectsByLocation.get(key);
@@ -619,6 +621,7 @@ public class RoomSpecialTypes {
this.wiredEffectsByLocation.remove(key); this.wiredEffectsByLocation.remove(key);
} }
} }
this.specialItemsById.remove(effect.getId());
} }
/** /**
@@ -709,11 +712,11 @@ public class RoomSpecialTypes {
// Add to type-based index // Add to type-based index
this.wiredConditions.computeIfAbsent(condition.getType(), k -> ConcurrentHashMap.newKeySet()) this.wiredConditions.computeIfAbsent(condition.getType(), k -> ConcurrentHashMap.newKeySet())
.add(condition); .add(condition);
// Add to spatial index // Add to spatial index
long key = coordinateKey(condition.getX(), condition.getY()); long key = coordinateKey(condition.getX(), condition.getY());
this.wiredConditionsByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet()) this.wiredConditionsByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
.add(condition); .add(condition);
this.specialItemsById.put(condition.getId(), condition);
} }
/** /**
@@ -729,7 +732,6 @@ public class RoomSpecialTypes {
this.wiredConditions.remove(condition.getType()); this.wiredConditions.remove(condition.getType());
} }
} }
// Remove from spatial index // Remove from spatial index
long key = coordinateKey(condition.getX(), condition.getY()); long key = coordinateKey(condition.getX(), condition.getY());
Set<InteractionWiredCondition> locationConditions = this.wiredConditionsByLocation.get(key); Set<InteractionWiredCondition> locationConditions = this.wiredConditionsByLocation.get(key);
@@ -739,6 +741,7 @@ public class RoomSpecialTypes {
this.wiredConditionsByLocation.remove(key); this.wiredConditionsByLocation.remove(key);
} }
} }
this.specialItemsById.remove(condition.getId());
} }
/** /**
@@ -805,11 +808,11 @@ public class RoomSpecialTypes {
*/ */
public void addExtra(InteractionWiredExtra extra) { public void addExtra(InteractionWiredExtra extra) {
this.wiredExtras.put(extra.getId(), extra); this.wiredExtras.put(extra.getId(), extra);
// Add to spatial index // Add to spatial index
long key = coordinateKey(extra.getX(), extra.getY()); long key = coordinateKey(extra.getX(), extra.getY());
this.wiredExtrasByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet()) this.wiredExtrasByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
.add(extra); .add(extra);
this.specialItemsById.put(extra.getId(), extra);
} }
/** /**
@@ -818,7 +821,6 @@ public class RoomSpecialTypes {
*/ */
public void removeExtra(InteractionWiredExtra extra) { public void removeExtra(InteractionWiredExtra extra) {
this.wiredExtras.remove(extra.getId()); this.wiredExtras.remove(extra.getId());
// Remove from spatial index // Remove from spatial index
long key = coordinateKey(extra.getX(), extra.getY()); long key = coordinateKey(extra.getX(), extra.getY());
Set<InteractionWiredExtra> locationExtras = this.wiredExtrasByLocation.get(key); Set<InteractionWiredExtra> locationExtras = this.wiredExtrasByLocation.get(key);
@@ -828,6 +830,7 @@ public class RoomSpecialTypes {
this.wiredExtrasByLocation.remove(key); this.wiredExtrasByLocation.remove(key);
} }
} }
this.specialItemsById.remove(extra.getId());
} }
/** /**
@@ -880,11 +883,11 @@ public class RoomSpecialTypes {
} }
public void addGameScoreboard(InteractionGameScoreboard scoreboard) { 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) { 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() { public THashMap<Integer, InteractionFreezeScoreboard> getFreezeScoreboards() {
@@ -980,11 +983,11 @@ public class RoomSpecialTypes {
} }
public void addGameGate(InteractionGameGate gameGate) { 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) { 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() { public THashMap<Integer, InteractionFreezeGate> getFreezeGates() {
@@ -1021,11 +1024,11 @@ public class RoomSpecialTypes {
} }
public void addGameTimer(InteractionGameTimer gameTimer) { 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) { 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() { public THashMap<Integer, InteractionGameTimer> getGameTimers() {
@@ -1043,7 +1046,7 @@ public class RoomSpecialTypes {
} }
public void addFreezeExitTile(InteractionFreezeExitTile freezeExitTile) { 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() { public THashMap<Integer, InteractionFreezeExitTile> getFreezeExitTiles() {
@@ -1051,7 +1054,7 @@ public class RoomSpecialTypes {
} }
public void removeFreezeExitTile(InteractionFreezeExitTile freezeExitTile) { public void removeFreezeExitTile(InteractionFreezeExitTile freezeExitTile) {
this.freezeExitTile.remove(freezeExitTile.getId()); this.freezeExitTile.remove(freezeExitTile.getId()); this.specialItemsById.remove(freezeExitTile.getId());
} }
public boolean hasFreezeExitTile() { public boolean hasFreezeExitTile() {
@@ -1062,12 +1065,14 @@ public class RoomSpecialTypes {
synchronized (this.undefined) { synchronized (this.undefined) {
this.undefined.put(item.getId(), item); this.undefined.put(item.getId(), item);
} }
this.specialItemsById.put(item.getId(), item);
} }
public void removeUndefined(HabboItem item) { public void removeUndefined(HabboItem item) {
synchronized (this.undefined) { synchronized (this.undefined) {
this.undefined.remove(item.getId()); this.undefined.remove(item.getId());
} }
this.specialItemsById.remove(item.getId());
} }
public THashSet<HabboItem> getItemsOfType(Class<? extends HabboItem> type) { public THashSet<HabboItem> getItemsOfType(Class<? extends HabboItem> type) {
@@ -1130,6 +1135,10 @@ public class RoomSpecialTypes {
this.cycleTasks.remove(task); this.cycleTasks.remove(task);
} }
public HabboItem getSpecialItem(int itemId) {
return this.specialItemsById.get(itemId);
}
public synchronized void dispose() { public synchronized void dispose() {
this.banzaiTeleporters.clear(); this.banzaiTeleporters.clear();
this.nests.clear(); this.nests.clear();
@@ -1142,6 +1151,7 @@ public class RoomSpecialTypes {
this.wiredTriggers.clear(); this.wiredTriggers.clear();
this.wiredEffects.clear(); this.wiredEffects.clear();
this.wiredConditions.clear(); this.wiredConditions.clear();
this.wiredExtras.clear();
this.gameScoreboards.clear(); this.gameScoreboards.clear();
this.gameGates.clear(); this.gameGates.clear();
@@ -1150,6 +1160,7 @@ public class RoomSpecialTypes {
this.freezeExitTile.clear(); this.freezeExitTile.clear();
this.undefined.clear(); this.undefined.clear();
this.cycleTasks.clear(); this.cycleTasks.clear();
this.specialItemsById.clear();
} }
public Rectangle tentAt(RoomTile location) { public Rectangle tentAt(RoomTile location) {
@@ -29,7 +29,6 @@ public class RoomTileManager {
*/ */
public void updateTile(RoomTile tile) { public void updateTile(RoomTile tile) {
if (tile != null) { if (tile != null) {
this.room.tileCache.remove(tile);
this.room.getItemManager().tileCache.remove(tile); this.room.getItemManager().tileCache.remove(tile);
tile.setStackHeight(this.getStackHeight(tile.x, tile.y, false)); tile.setStackHeight(this.getStackHeight(tile.x, tile.y, false));
tile.setState(this.calculateTileState(tile)); tile.setState(this.calculateTileState(tile));
@@ -41,7 +40,6 @@ public class RoomTileManager {
*/ */
public void updateTiles(THashSet<RoomTile> tiles) { public void updateTiles(THashSet<RoomTile> tiles) {
for (RoomTile tile : tiles) { for (RoomTile tile : tiles) {
this.room.tileCache.remove(tile);
this.room.getItemManager().tileCache.remove(tile); this.room.getItemManager().tileCache.remove(tile);
tile.setStackHeight(this.getStackHeight(tile.x, tile.y, false)); tile.setStackHeight(this.getStackHeight(tile.x, tile.y, false));
tile.setState(this.calculateTileState(tile)); tile.setState(this.calculateTileState(tile));
@@ -71,6 +71,24 @@ public class RoomUnitManager {
*/ */
public void clear() { public void clear() {
synchronized (this.room.roomUnitLock) { 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.unitCounter = 0;
this.currentHabbos.clear(); this.currentHabbos.clear();
this.currentPets.clear(); this.currentPets.clear();
@@ -222,6 +240,8 @@ public class RoomUnitManager {
} }
if (habbo.getRoomUnit() != null) { if (habbo.getRoomUnit() != null) {
WiredMoveCarryHelper.cleanupRoomUnit(habbo.getRoomUnit());
WiredUserMovementHelper.cleanupRoomUnit(habbo.getRoomUnit());
WiredManager.triggerUserLeavesRoom(this.room, habbo.getRoomUnit()); WiredManager.triggerUserLeavesRoom(this.room, habbo.getRoomUnit());
if (WiredFreezeUtil.isFrozen(habbo.getRoomUnit())) { if (WiredFreezeUtil.isFrozen(habbo.getRoomUnit())) {
WiredFreezeUtil.unfreeze(this.room, habbo.getRoomUnit()); WiredFreezeUtil.unfreeze(this.room, habbo.getRoomUnit());
@@ -646,14 +666,22 @@ public class RoomUnitManager {
public boolean removeBot(Bot bot) { public boolean removeBot(Bot bot) {
synchronized (this.currentBots) { synchronized (this.currentBots) {
if (this.currentBots.containsKey(bot.getId())) { if (this.currentBots.containsKey(bot.getId())) {
if (bot.getRoomUnit() != null && bot.getRoomUnit().getCurrentLocation() != null) { if (bot.getRoomUnit() != null) {
bot.getRoomUnit().getCurrentLocation().removeUnit(bot.getRoomUnit()); WiredMoveCarryHelper.cleanupRoomUnit(bot.getRoomUnit());
WiredUserMovementHelper.cleanupRoomUnit(bot.getRoomUnit());
if (bot.getRoomUnit().getCurrentLocation() != null) {
bot.getRoomUnit().getCurrentLocation().removeUnit(bot.getRoomUnit());
}
} }
this.currentBots.remove(bot.getId()); this.currentBots.remove(bot.getId());
bot.getRoomUnit().setInRoom(false); if (bot.getRoomUnit() != null) {
bot.getRoomUnit().setInRoom(false);
}
bot.setRoom(null); 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); bot.setRoomUnit(null);
return true; return true;
} }
@@ -876,7 +904,12 @@ public class RoomUnitManager {
* Removes a Pet from the room. * Removes a Pet from the room.
*/ */
public Pet removePet(int petId) { 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() { 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.currentHabbos.clear();
this.currentBots.clear(); this.currentBots.clear();
this.currentPets.clear(); this.currentPets.clear();
@@ -35,6 +35,7 @@ public class RoomUserVariableManager {
private final Room room; private final Room room;
private final ConcurrentHashMap<Integer, ConcurrentHashMap<Integer, VariableAssignment>> activeAssignmentsByUserId; 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) { public RoomUserVariableManager(Room room) {
this.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())); 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() { public void broadcastSnapshot() {
this.requestBroadcast();
}
public void broadcastSnapshotRaw() {
Snapshot userSnapshot = this.createSnapshot(); Snapshot userSnapshot = this.createSnapshot();
RoomFurniVariableManager.Snapshot furniSnapshot = this.room.getFurniVariableManager().createSnapshot(); RoomFurniVariableManager.Snapshot furniSnapshot = this.room.getFurniVariableManager().createSnapshot();
RoomVariableManager.Snapshot roomSnapshot = this.room.getRoomVariableManager().createSnapshot(); RoomVariableManager.Snapshot roomSnapshot = this.room.getRoomVariableManager().createSnapshot();
@@ -35,6 +35,7 @@ public class RoomVariableManager {
private final Room room; private final Room room;
private final ConcurrentHashMap<Integer, VariableAssignment> activeAssignmentsByDefinitionId; private final ConcurrentHashMap<Integer, VariableAssignment> activeAssignmentsByDefinitionId;
private volatile boolean persistentValuesLoaded; private volatile boolean persistentValuesLoaded;
private final java.util.concurrent.atomic.AtomicBoolean broadcastRequested = new java.util.concurrent.atomic.AtomicBoolean(false);
public RoomVariableManager(Room room) { public RoomVariableManager(Room room) {
this.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())); 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() { public void broadcastSnapshot() {
this.requestBroadcast();
}
public void broadcastSnapshotRaw() {
RoomUserVariableManager.Snapshot userSnapshot = this.room.getUserVariableManager().createSnapshot(); RoomUserVariableManager.Snapshot userSnapshot = this.room.getUserVariableManager().createSnapshot();
RoomFurniVariableManager.Snapshot furniSnapshot = this.room.getFurniVariableManager().createSnapshot(); RoomFurniVariableManager.Snapshot furniSnapshot = this.room.getFurniVariableManager().createSnapshot();
Snapshot roomSnapshot = this.createSnapshot(); Snapshot roomSnapshot = this.createSnapshot();
@@ -0,0 +1,78 @@
package com.eu.habbo.habbohotel.soundboard;
import com.eu.habbo.Emulator;
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.Statement;
import java.util.ArrayList;
import java.util.List;
public class SoundboardManager {
private static final Logger LOGGER = LoggerFactory.getLogger(SoundboardManager.class);
private final List<SoundboardSound> sounds = new ArrayList<>();
public SoundboardManager() {
long millis = System.currentTimeMillis();
this.bootstrap();
this.reload();
LOGGER.info("Soundboard Manager -> Loaded! ({} MS, {} sounds)", System.currentTimeMillis() - millis, this.sounds.size());
}
// Self-bootstrap: room flag column + sounds table, so the feature works even
// before the manual migration is applied.
private void bootstrap() {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
Statement statement = connection.createStatement()) {
statement.execute("ALTER TABLE `rooms` ADD COLUMN IF NOT EXISTS `soundboard_enabled` TINYINT(1) NOT NULL DEFAULT 0");
statement.execute("CREATE TABLE IF NOT EXISTS `soundboard_sounds` (" +
"`id` INT(11) NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL DEFAULT '', " +
"`url` VARCHAR(255) NOT NULL DEFAULT '', `enabled` TINYINT(1) NOT NULL DEFAULT 1, " +
"`sort_order` INT(11) NOT NULL DEFAULT 0, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
} catch (SQLException e) {
LOGGER.error("Failed to bootstrap soundboard schema", e);
}
}
public void reload() {
this.sounds.clear();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT id, name, url FROM soundboard_sounds WHERE enabled = 1 ORDER BY sort_order ASC, id ASC");
ResultSet set = statement.executeQuery()) {
while (set.next()) {
this.sounds.add(new SoundboardSound(set));
}
} catch (SQLException e) {
LOGGER.error("Failed to load soundboard sounds", e);
}
}
public List<SoundboardSound> getSounds() {
return this.sounds;
}
public SoundboardSound getSound(int id) {
for (SoundboardSound sound : this.sounds) {
if (sound.id == id) return sound;
}
return null;
}
// Owner toggle persists the room flag with a dedicated UPDATE (kept out of
// the big room-settings save to avoid touching that statement).
public void setRoomEnabled(int roomId, boolean enabled) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("UPDATE rooms SET soundboard_enabled = ? WHERE id = ? LIMIT 1")) {
statement.setString(1, enabled ? "1" : "0");
statement.setInt(2, roomId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to set soundboard_enabled for room {}", roomId, e);
}
}
}
@@ -0,0 +1,17 @@
package com.eu.habbo.habbohotel.soundboard;
import java.sql.ResultSet;
import java.sql.SQLException;
// One soundboard pad: a named audio clip served from a URL (uploaded via the CMS).
public class SoundboardSound {
public final int id;
public final String name;
public final String url;
public SoundboardSound(ResultSet set) throws SQLException {
this.id = set.getInt("id");
this.name = set.getString("name");
this.url = set.getString("url");
}
}
@@ -46,6 +46,7 @@ public class HabboInfo implements Runnable {
private int InfostandStand; private int InfostandStand;
private int InfostandOverlay; private int InfostandOverlay;
private int InfostandCardBg; private int InfostandCardBg;
private int InfostandBorder;
private int loadingRoom; private int loadingRoom;
private Room currentRoom; private Room currentRoom;
private String roomEntryMethod = "door"; private String roomEntryMethod = "door";
@@ -93,6 +94,11 @@ public class HabboInfo implements Runnable {
this.InfostandStand = set.getInt("background_stand_id"); this.InfostandStand = set.getInt("background_stand_id");
this.InfostandOverlay = set.getInt("background_overlay_id"); this.InfostandOverlay = set.getInt("background_overlay_id");
this.InfostandCardBg = set.getInt("background_card_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; this.currentRoom = null;
} catch (SQLException e) { } catch (SQLException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
@@ -300,6 +306,15 @@ public class HabboInfo implements Runnable {
public void setInfostandCardBg(int infostandCardBg) { public void setInfostandCardBg(int infostandCardBg) {
InfostandCardBg = infostandCardBg; InfostandCardBg = infostandCardBg;
} }
public int getInfostandBorder() {
return InfostandBorder;
}
public void setInfostandBorder(int infostandBorder) {
InfostandBorder = infostandBorder;
}
public Rank getRank() { public Rank getRank() {
return this.rank; return this.rank;
} }
@@ -587,7 +602,7 @@ public class HabboInfo implements Runnable {
try { try {
SqlQueries.update( 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.motto,
this.online ? "1" : "0", this.online ? "1" : "0",
this.look, this.look,
@@ -604,6 +619,7 @@ public class HabboInfo implements Runnable {
this.InfostandStand, this.InfostandStand,
this.InfostandOverlay, this.InfostandOverlay,
this.InfostandCardBg, this.InfostandCardBg,
this.InfostandBorder,
this.id); this.id);
} catch (SqlQueries.DataAccessException e) { } catch (SqlQueries.DataAccessException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
@@ -38,6 +38,7 @@ public class HabboManager {
private final ConcurrentHashMap<Integer, Habbo> onlineHabbos; private final ConcurrentHashMap<Integer, Habbo> onlineHabbos;
private final ConcurrentHashMap<String, Habbo> onlineHabbosByName; private final ConcurrentHashMap<String, Habbo> onlineHabbosByName;
private final ConcurrentHashMap<Integer, String> usernameCache = new ConcurrentHashMap<>();
public HabboManager() { public HabboManager() {
long millis = System.currentTimeMillis(); long millis = System.currentTimeMillis();
@@ -131,15 +132,12 @@ public class HabboManager {
Emulator.getPluginManager().fireEvent(new UserRegisteredEvent(habbo)); Emulator.getPluginManager().fireEvent(new UserRegisteredEvent(habbo));
} }
if (!Emulator.debugging) { // NB: il ticket SSO NON viene svuotato qui di proposito. Dietro
try (PreparedStatement stmt = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) { // Cloudflare il WebSocket viene droppato e il client ritenta più
stmt.setString(1, ""); // volte con lo STESSO ticket: se lo consumassimo al primo uso, i
stmt.setInt(2, habbo.getHabboInfo().getId()); // retry (e l'hard-refresh) fallirebbero con "non-existing SSO token".
stmt.execute(); // Il ticket resta valido fino alla scadenza (auth_ticket_expires_at,
} catch (SQLException e) { // TTL gestito dal CMS) o finché il CMS non ne scrive uno nuovo / logout.
LOGGER.error("Caught SQL exception", e);
}
}
} }
} }
} catch (SQLException e) { } catch (SQLException e) {
@@ -158,6 +156,26 @@ public class HabboManager {
return this.getHabbo(id).getHabboInfo(); 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() { public int getOnlineCount() {
return this.onlineHabbos.size(); return this.onlineHabbos.size();
} }
@@ -89,10 +89,13 @@ public class HabboStats implements Runnable {
public long lastTradeTimestamp = Emulator.getIntUnixTimestamp(); public long lastTradeTimestamp = Emulator.getIntUnixTimestamp();
public long lastGiftTimestamp = Emulator.getIntUnixTimestamp(); public long lastGiftTimestamp = Emulator.getIntUnixTimestamp();
public long lastPurchaseTimestamp = Emulator.getIntUnixTimestamp(); public long lastPurchaseTimestamp = Emulator.getIntUnixTimestamp();
public long lastFloorplanSaveTimestamp = 0;
public int uiFlags; public int uiFlags;
public boolean hasGottenDefaultSavedSearches; public boolean hasGottenDefaultSavedSearches;
private HabboInfo habboInfo; private HabboInfo habboInfo;
private boolean allowTrade; private boolean allowTrade;
private boolean mentionsEnabled;
private boolean massMentionsEnabled;
private int clubExpireTimestamp; private int clubExpireTimestamp;
private int muteEndTime; private int muteEndTime;
public int maxFriends; public int maxFriends;
@@ -130,6 +133,8 @@ public class HabboStats implements Runnable {
this.guilds = new ArrayList<>(); this.guilds = new ArrayList<>();
this.tags = set.getString("tags").split(";"); this.tags = set.getString("tags").split(";");
this.allowTrade = set.getString("can_trade").equals("1"); this.allowTrade = set.getString("can_trade").equals("1");
this.mentionsEnabled = "1".equals(safeColumnString(set, "mentions_enabled", "1"));
this.massMentionsEnabled = "1".equals(safeColumnString(set, "mass_mentions_enabled", "1"));
this.votedRooms = new TIntArrayStack(); this.votedRooms = new TIntArrayStack();
this.clubExpireTimestamp = set.getInt("club_expire_timestamp"); this.clubExpireTimestamp = set.getInt("club_expire_timestamp");
this.loginStreak = set.getInt("login_streak"); this.loginStreak = set.getInt("login_streak");
@@ -748,13 +753,6 @@ public class HabboStats implements Runnable {
return 0; return 0;
} }
/**
* Ignore an user.
*
* @param gameClient The client to which this HabboStats instance belongs.
* @param userId The user to ignore.
* @return true if successfully ignored, false otherwise.
*/
public boolean ignoreUser(GameClient gameClient, int userId) { public boolean ignoreUser(GameClient gameClient, int userId) {
final Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); final Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
@@ -804,6 +802,44 @@ public class HabboStats implements Runnable {
else return this.allowTrade; else return this.allowTrade;
} }
public boolean mentionsEnabled() {
return this.mentionsEnabled;
}
public boolean massMentionsEnabled() {
return this.massMentionsEnabled;
}
public void setMentionsEnabled(boolean enabled) {
this.mentionsEnabled = enabled;
persistFlag("mentions_enabled", enabled);
}
public void setMassMentionsEnabled(boolean enabled) {
this.massMentionsEnabled = enabled;
persistFlag("mass_mentions_enabled", enabled);
}
private void persistFlag(String column, boolean enabled) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET `" + column + "` = ? WHERE user_id = ? LIMIT 1")) {
statement.setString(1, enabled ? "1" : "0");
statement.setInt(2, this.habboInfo.getId());
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to persist users_settings.{} for user {}", column, this.habboInfo.getId(), e);
}
}
private static String safeColumnString(ResultSet set, String column, String defaultValue) {
try {
String value = set.getString(column);
return value == null ? defaultValue : value;
} catch (SQLException e) {
return defaultValue;
}
}
public void setAllowTrade(boolean allowTrade) { public void setAllowTrade(boolean allowTrade) {
this.allowTrade = allowTrade; this.allowTrade = allowTrade;
} }
@@ -24,7 +24,8 @@ public class InfostandBackgroundManager {
BACKGROUND("background"), BACKGROUND("background"),
STAND("stand"), STAND("stand"),
OVERLAY("overlay"), OVERLAY("overlay"),
CARD("card"); CARD("card"),
BORDER("border");
public final String dbValue; public final String dbValue;
@@ -89,11 +90,12 @@ public class InfostandBackgroundManager {
this.enforce = loaded > 0; this.enforce = loaded > 0;
if (this.enforce) { 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.BACKGROUND).size(),
this.entries.get(Category.STAND).size(), this.entries.get(Category.STAND).size(),
this.entries.get(Category.OVERLAY).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 { } else {
LOGGER.info("InfostandBackgroundManager -> infostand_backgrounds is empty, server-side validation disabled (only range clamp will apply)."); LOGGER.info("InfostandBackgroundManager -> infostand_backgrounds is empty, server-side validation disabled (only range clamp will apply).");
} }
@@ -0,0 +1,430 @@
package com.eu.habbo.habbohotel.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboItem;
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.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
public class WheelManager {
private static final Logger LOGGER = LoggerFactory.getLogger(WheelManager.class);
private static final int RECENT_KEEP = 50;
private static final int SECONDS_PER_DAY = 86400;
public static final Set<String> VALID_PRIZE_TYPES = Set.of(
"credits", "points", "spin", "item", "badge", "nothing");
public static final int MAX_PRIZES_PER_SAVE = 64;
public static final int MAX_STRING_LEN = 64;
public static final int MAX_PRIZE_AMOUNT = 1_000_000;
public static final int MAX_ITEM_QUANTITY = 100;
public static final int MAX_WEIGHT = 1_000_000;
public static final int MAX_EXTRA_SPINS = 10_000;
private static final long MIN_SPIN_INTERVAL_MS = 1500L;
private final List<WheelPrize> prizes = new ArrayList<>();
private int totalWeight = 0;
private int freeSpinsPerDay = 1;
private int spinCost = 50;
private int spinCostType = 5;
private final ConcurrentHashMap<Integer, Long> lastSpinAt = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, WheelUserState> userStateCache = new ConcurrentHashMap<>();
private final java.util.concurrent.CopyOnWriteArrayList<WheelRecentWin> recentWinsCache = new java.util.concurrent.CopyOnWriteArrayList<>();
public WheelManager() {
long millis = System.currentTimeMillis();
this.reload();
LOGGER.info("Wheel Manager -> Loaded! ({} MS)", System.currentTimeMillis() - millis);
}
public void reload() {
this.loadSettings();
this.loadPrizes();
this.loadRecentWins();
}
private void loadSettings() {
this.freeSpinsPerDay = Emulator.getConfig().getInt("wheel.free_spins_per_day", 1);
this.spinCost = Emulator.getConfig().getInt("wheel.spin_cost", 50);
this.spinCostType = Emulator.getConfig().getInt("wheel.spin_cost_type", 5);
}
private void loadPrizes() {
this.prizes.clear();
this.totalWeight = 0;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT * FROM wheel_prizes WHERE enabled = 1 ORDER BY sort_order ASC, id ASC");
ResultSet set = statement.executeQuery()) {
while (set.next()) {
WheelPrize prize = new WheelPrize(set);
this.prizes.add(prize);
this.totalWeight += prize.weight;
}
} catch (SQLException e) {
LOGGER.error("Failed to load fortune wheel prizes", e);
}
}
public List<WheelPrize> getPrizes() {
return this.prizes;
}
public int getSpinCost() {
return this.spinCost;
}
public int getSpinCostType() {
return this.spinCostType;
}
private int today() {
return Emulator.getIntUnixTimestamp() / SECONDS_PER_DAY;
}
public synchronized WheelUserState getUserState(int userId) {
int today = this.today();
WheelUserState cached = this.userStateCache.get(userId);
if (cached != null) {
if (cached.lastReset != today) {
cached.freeSpins = this.freeSpinsPerDay;
cached.lastReset = today;
this.persistUserState(userId, cached);
}
return cached;
}
WheelUserState state = new WheelUserState();
boolean exists = false;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT free_spins, extra_spins, last_reset FROM wheel_user_state WHERE user_id = ?")) {
statement.setInt(1, userId);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) {
state.freeSpins = set.getInt("free_spins");
state.extraSpins = set.getInt("extra_spins");
state.lastReset = set.getInt("last_reset");
exists = true;
}
}
} catch (SQLException e) {
LOGGER.error("Failed to read wheel state for user {}", userId, e);
}
if (!exists) {
state.freeSpins = this.freeSpinsPerDay;
state.extraSpins = 0;
state.lastReset = today;
this.persistUserState(userId, state);
} else if (state.lastReset != today) {
state.freeSpins = this.freeSpinsPerDay;
state.lastReset = today;
this.persistUserState(userId, state);
}
this.userStateCache.put(userId, state);
return state;
}
private void persistUserState(int userId, WheelUserState state) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO wheel_user_state (user_id, free_spins, extra_spins, last_reset) VALUES (?, ?, ?, ?) " +
"ON DUPLICATE KEY UPDATE free_spins = VALUES(free_spins), extra_spins = VALUES(extra_spins), last_reset = VALUES(last_reset)")) {
statement.setInt(1, userId);
statement.setInt(2, state.freeSpins);
statement.setInt(3, state.extraSpins);
statement.setInt(4, state.lastReset);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to persist wheel state for user {}", userId, e);
}
}
public synchronized WheelPrize spin(Habbo habbo) {
int userId = habbo.getHabboInfo().getId();
long now = System.currentTimeMillis();
Long last = this.lastSpinAt.get(userId);
if (last != null && (now - last) < MIN_SPIN_INTERVAL_MS) return null;
this.lastSpinAt.put(userId, now);
WheelUserState state = this.getUserState(userId);
boolean usedFree;
if (state.freeSpins > 0) {
state.freeSpins--;
usedFree = true;
} else if (state.extraSpins > 0) {
state.extraSpins--;
usedFree = false;
} else {
return null;
}
WheelPrize prize = this.pickWeighted();
if (prize == null) {
if (usedFree) state.freeSpins++; else state.extraSpins++;
return null;
}
this.giveReward(habbo, prize, state);
this.persistUserState(userId, state);
this.recordWin(habbo, prize);
return prize;
}
private WheelPrize pickWeighted() {
if (this.prizes.isEmpty() || this.totalWeight <= 0) return null;
int roll = ThreadLocalRandom.current().nextInt(this.totalWeight);
int acc = 0;
for (WheelPrize prize : this.prizes) {
acc += prize.weight;
if (roll < acc) return prize;
}
return this.prizes.get(this.prizes.size() - 1);
}
private void giveReward(Habbo habbo, WheelPrize prize, WheelUserState state) {
int amount = Math.max(0, Math.min(prize.amount, MAX_PRIZE_AMOUNT));
switch (prize.type) {
case "credits":
if (amount > 0) habbo.giveCredits(amount);
break;
case "points":
if (amount > 0) habbo.givePoints(prize.pointsType, amount);
break;
case "spin":
int room = Math.max(0, MAX_EXTRA_SPINS - state.extraSpins);
state.extraSpins += Math.min(amount, room);
break;
case "item":
this.giveItem(habbo, prize, Math.min(amount, MAX_ITEM_QUANTITY));
break;
case "badge":
if (prize.value != null && !prize.value.isEmpty()) {
habbo.addBadge(prize.value, "Fortune Wheel");
}
break;
case "nothing":
default:
break;
}
}
private void giveItem(Habbo habbo, WheelPrize prize, int quantity) {
if (quantity <= 0 || prize.value == null) return;
int baseId;
try {
baseId = Integer.parseInt(prize.value.trim());
} catch (NumberFormatException e) {
return;
}
Item base = Emulator.getGameEnvironment().getItemManager().getItem(baseId);
if (base == null) return;
THashSet<HabboItem> items = new THashSet<>();
for (int i = 0; i < quantity; i++) {
HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(habbo.getHabboInfo().getId(), base, 0, 0, "");
if (item != null) items.add(item);
}
if (!items.isEmpty()) {
habbo.addFurniture(items);
}
}
private void recordWin(Habbo habbo, WheelPrize prize) {
WheelRecentWin win = new WheelRecentWin(
habbo.getHabboInfo().getUsername(),
habbo.getHabboInfo().getLook(),
prize.label);
this.recentWinsCache.add(0, win);
while (this.recentWinsCache.size() > RECENT_KEEP) {
this.recentWinsCache.remove(this.recentWinsCache.size() - 1);
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(
"INSERT INTO wheel_recent_wins (user_id, username, look, prize_label, won_at) VALUES (?, ?, ?, ?, ?)")) {
statement.setInt(1, habbo.getHabboInfo().getId());
statement.setString(2, habbo.getHabboInfo().getUsername());
statement.setString(3, habbo.getHabboInfo().getLook());
statement.setString(4, prize.label);
statement.setInt(5, Emulator.getIntUnixTimestamp());
statement.executeUpdate();
}
try (PreparedStatement trim = connection.prepareStatement(
"DELETE FROM wheel_recent_wins WHERE id < (SELECT id FROM (SELECT id FROM wheel_recent_wins ORDER BY id DESC LIMIT 1 OFFSET ?) t)")) {
trim.setInt(1, RECENT_KEEP - 1);
trim.executeUpdate();
}
} catch (SQLException e) {
LOGGER.error("Failed to record wheel win", e);
}
}
public List<WheelRecentWin> getRecentWins(int limit) {
if (limit <= 0) return new ArrayList<>();
int size = this.recentWinsCache.size();
if (size == 0) return new ArrayList<>();
int take = Math.min(limit, size);
return new ArrayList<>(this.recentWinsCache.subList(0, take));
}
private void loadRecentWins() {
this.recentWinsCache.clear();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT username, look, prize_label FROM wheel_recent_wins ORDER BY id DESC LIMIT ?")) {
statement.setInt(1, RECENT_KEEP);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
this.recentWinsCache.add(new WheelRecentWin(
set.getString("username"),
set.getString("look"),
set.getString("prize_label")));
}
}
} catch (SQLException e) {
LOGGER.error("Failed to load wheel recent wins", e);
}
}
public synchronized boolean buySpin(Habbo habbo) {
if (this.spinCost <= 0) return false;
int userId = habbo.getHabboInfo().getId();
WheelUserState state = this.getUserState(userId);
if (state.extraSpins >= MAX_EXTRA_SPINS) return false;
if (this.spinCostType == -1) {
if (habbo.getHabboInfo().getCredits() < this.spinCost) return false;
habbo.giveCredits(-this.spinCost);
} else {
if (habbo.getHabboInfo().getCurrencyAmount(this.spinCostType) < this.spinCost) return false;
habbo.givePoints(this.spinCostType, -this.spinCost);
}
state.extraSpins++;
this.persistUserState(userId, state);
return true;
}
/**
* Persists a single prize. An {@code id <= 0} inserts a brand-new prize and
* returns its generated id; a positive id updates the existing row (and
* re-enables it, so a previously soft-deleted prize can be brought back).
* {@code sortOrder} reflects the prize's position in the editor so the
* wheel layout matches what the admin sees. Returns the effective row id,
* or {@code 0} if the write failed.
*/
public int savePrize(int id, String type, String value, int amount, int pointsType, int weight, String label, int sortOrder) {
String safeType = (type != null && VALID_PRIZE_TYPES.contains(type)) ? type : "nothing";
String safeValue = truncate(value, MAX_STRING_LEN);
String safeLabel = truncate(label, MAX_STRING_LEN);
int safeAmount = clamp(amount, 0, MAX_PRIZE_AMOUNT);
int safeWeight = clamp(weight, 0, MAX_WEIGHT);
int safeSort = clamp(sortOrder, 0, MAX_PRIZES_PER_SAVE);
if (id > 0) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE wheel_prizes SET type = ?, value = ?, amount = ?, points_type = ?, weight = ?, label = ?, sort_order = ?, enabled = 1 WHERE id = ?")) {
statement.setString(1, safeType);
statement.setString(2, safeValue);
statement.setInt(3, safeAmount);
statement.setInt(4, pointsType);
statement.setInt(5, safeWeight);
statement.setString(6, safeLabel);
statement.setInt(7, safeSort);
statement.setInt(8, id);
statement.executeUpdate();
return id;
} catch (SQLException e) {
LOGGER.error("Failed to save wheel prize {}", id, e);
return 0;
}
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO wheel_prizes (type, value, amount, points_type, weight, label, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?, 1, ?)",
Statement.RETURN_GENERATED_KEYS)) {
statement.setString(1, safeType);
statement.setString(2, safeValue);
statement.setInt(3, safeAmount);
statement.setInt(4, pointsType);
statement.setInt(5, safeWeight);
statement.setString(6, safeLabel);
statement.setInt(7, safeSort);
statement.executeUpdate();
try (ResultSet keys = statement.getGeneratedKeys()) {
if (keys.next()) return keys.getInt(1);
}
} catch (SQLException e) {
LOGGER.error("Failed to insert wheel prize", e);
}
return 0;
}
/**
* Soft-deletes every enabled prize whose id is not in {@code keptIds} by
* setting {@code enabled = 0}. This is intentionally non-destructive: rows
* stay in the table (so historical references and re-enabling remain
* possible) but {@link #loadPrizes()} only ever loads {@code enabled = 1}.
* An empty set disables all prizes.
*/
public void disablePrizesNotIn(Set<Integer> keptIds) {
if (keptIds == null) return;
StringBuilder sql = new StringBuilder("UPDATE wheel_prizes SET enabled = 0 WHERE enabled = 1");
if (!keptIds.isEmpty()) {
StringJoiner ids = new StringJoiner(",", " AND id NOT IN (", ")");
for (Integer keptId : keptIds) {
ids.add(Integer.toString(keptId));
}
sql.append(ids);
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(sql.toString())) {
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to disable removed wheel prizes", e);
}
}
private static String truncate(String s, int max) {
if (s == null) return "";
return s.length() <= max ? s : s.substring(0, max);
}
private static int clamp(int value, int min, int max) {
if (value < min) return min;
if (value > max) return max;
return value;
}
}
@@ -0,0 +1,44 @@
package com.eu.habbo.habbohotel.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.items.Item;
import java.sql.ResultSet;
import java.sql.SQLException;
// One slice of the wheel. type = item | badge | credits | points | spin | nothing.
public class WheelPrize {
public final int id;
public final String type;
public final String value; // item: base item id ; badge: badge code ; others: unused
public final int amount; // item qty / credits / points / extra spins
public final int pointsType; // for type=points
public final int weight;
public final String label;
public final int spriteId; // resolved for item prizes so the client can render the furni icon
public WheelPrize(ResultSet set) throws SQLException {
this.id = set.getInt("id");
this.type = set.getString("type");
this.value = set.getString("value");
this.amount = set.getInt("amount");
this.pointsType = set.getInt("points_type");
this.weight = Math.max(0, set.getInt("weight"));
this.label = set.getString("label");
this.spriteId = resolveSpriteId(this.type, this.value);
}
private static int resolveSpriteId(String type, String value) {
if (!"item".equals(type) || value == null) return 0;
try {
Item item = Emulator.getGameEnvironment().getItemManager().getItem(Integer.parseInt(value.trim()));
return item != null ? item.getSpriteId() : 0;
} catch (NumberFormatException e) {
return 0;
}
}
public String badgeCode() {
return "badge".equals(this.type) && this.value != null ? this.value : "";
}
}
@@ -0,0 +1,14 @@
package com.eu.habbo.habbohotel.wheel;
// A row in the "latest winners" panel. Denormalized (username/look stored at win time).
public class WheelRecentWin {
public final String username;
public final String look;
public final String prizeLabel;
public WheelRecentWin(String username, String look, String prizeLabel) {
this.username = username != null ? username : "";
this.look = look != null ? look : "";
this.prizeLabel = prizeLabel != null ? prizeLabel : "";
}
}
@@ -0,0 +1,12 @@
package com.eu.habbo.habbohotel.wheel;
// Per-user spin balance. freeSpins resets daily (lazy, on access); extraSpins persist.
public class WheelUserState {
public int freeSpins;
public int extraSpins;
public int lastReset; // day index (unix / 86400) of the last daily reset
public int totalSpins() {
return this.freeSpins + this.extraSpins;
}
}
@@ -43,6 +43,7 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -89,11 +90,11 @@ public class WiredHandler {
long millis = System.currentTimeMillis(); long millis = System.currentTimeMillis();
List<LegacyExecutionPlan> executionPlans = new ArrayList<>(); List<LegacyExecutionPlan> executionPlans = new ArrayList<>();
List<RoomTile> triggeredTiles = new ArrayList<>(); LinkedHashSet<Long> triggeredTiles = new LinkedHashSet<>();
for (InteractionWiredTrigger trigger : triggers) { 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; continue;
LegacyExecutionPlan executionPlan = new LegacyExecutionPlan(); LegacyExecutionPlan executionPlan = new LegacyExecutionPlan();
@@ -103,8 +104,6 @@ public class WiredHandler {
if (triggerType.equals(WiredTriggerType.SAY_SOMETHING)) if (triggerType.equals(WiredTriggerType.SAY_SOMETHING))
talked = true; talked = true;
triggeredTiles.add(tile);
} }
} }
@@ -139,20 +138,19 @@ public class WiredHandler {
long millis = System.currentTimeMillis(); long millis = System.currentTimeMillis();
List<LegacyExecutionPlan> executionPlans = new ArrayList<>(); List<LegacyExecutionPlan> executionPlans = new ArrayList<>();
List<RoomTile> triggeredTiles = new ArrayList<>(); LinkedHashSet<Long> triggeredTiles = new LinkedHashSet<>();
for (InteractionWiredTrigger trigger : triggers) { for (InteractionWiredTrigger trigger : triggers) {
if (trigger.getClass() != triggerType) continue; 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; continue;
LegacyExecutionPlan executionPlan = new LegacyExecutionPlan(); LegacyExecutionPlan executionPlan = new LegacyExecutionPlan();
if (handle(trigger, roomUnit, room, stuff, executionPlan)) { if (handle(trigger, roomUnit, room, stuff, executionPlan)) {
executionPlans.add(executionPlan); executionPlans.add(executionPlan);
triggeredTiles.add(tile);
} }
} }
@@ -187,6 +185,11 @@ public class WiredHandler {
WiredExtraExecutionLimit executionLimitExtra = null; WiredExtraExecutionLimit executionLimitExtra = null;
WiredExtraRandom randomExtra = null; WiredExtraRandom randomExtra = null;
int conditionEvaluationMode = WiredExtraOrEval.MODE_ALL;
int conditionEvaluationValue = 1;
boolean hasExtraUnseen = false;
boolean hasExtraExecuteInOrder = false;
for (InteractionWiredExtra extra : extras) { for (InteractionWiredExtra extra : extras) {
if (executionLimitExtra == null && extra instanceof WiredExtraExecutionLimit) { if (executionLimitExtra == null && extra instanceof WiredExtraExecutionLimit) {
executionLimitExtra = (WiredExtraExecutionLimit) extra; executionLimitExtra = (WiredExtraExecutionLimit) extra;
@@ -195,18 +198,22 @@ public class WiredHandler {
if (randomExtra == null && extra instanceof WiredExtraRandom) { if (randomExtra == null && extra instanceof WiredExtraRandom) {
randomExtra = (WiredExtraRandom) extra; 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()) { 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)) { if (!evaluateConditions(conditions, roomUnit, room, stuff, conditionEvaluationMode, conditionEvaluationValue)) {
for (InteractionWiredCondition condition : conditions) { for (InteractionWiredCondition condition : conditions) {
@@ -230,9 +237,6 @@ public class WiredHandler {
trigger.setCooldown(millis); 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) { for (InteractionWiredExtra extra : extras) {
extra.activateBox(room, roomUnit, millis); extra.activateBox(room, roomUnit, millis);
} }
@@ -244,7 +248,7 @@ public class WiredHandler {
executionPlan.executeInOrder = hasExtraExecuteInOrder; executionPlan.executeInOrder = hasExtraExecuteInOrder;
if (hasExtraUnseen) { if (hasExtraUnseen) {
for (InteractionWiredExtra extra : room.getRoomSpecialTypes().getExtras(trigger.getX(), trigger.getY())) { for (InteractionWiredExtra extra : extras) {
if (extra instanceof WiredExtraUnseen) { if (extra instanceof WiredExtraUnseen) {
extra.setExtradata(extra.getExtradata().equals("1") ? "0" : "1"); extra.setExtradata(extra.getExtradata().equals("1") ? "0" : "1");
InteractionWiredEffect effect = ((WiredExtraUnseen) extra).getUnseenEffect(effectList); 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) { for (InteractionWiredEffect effect : queueableEffects) {
delays.add(effect.getDelay()); delayBatches.computeIfAbsent(effect.getDelay(), ignored -> new ArrayList<>()).add(effect);
} }
for (Integer delay : delays) { for (Map.Entry<Integer, List<InteractionWiredEffect>> entry : delayBatches.entrySet()) {
List<InteractionWiredEffect> delayBatch = new ArrayList<>(); Integer delay = entry.getKey();
List<InteractionWiredEffect> delayBatch = entry.getValue();
for (InteractionWiredEffect effect : queueableEffects) {
if (effect.getDelay() == delay) {
delayBatch.add(effect);
}
}
if (delayBatch.isEmpty()) { if (delayBatch.isEmpty()) {
continue; continue;
} }
@@ -424,11 +422,19 @@ public class WiredHandler {
public static GsonBuilder getGsonBuilder() { public static GsonBuilder getGsonBuilder() {
if(gsonBuilder == null) { if(gsonBuilder == null) {
gsonBuilder = new GsonBuilder(); synchronized (WiredHandler.class) {
if (gsonBuilder == null) {
gsonBuilder = new GsonBuilder();
}
}
} }
return 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) { public static boolean executeEffectsAtTiles(THashSet<RoomTile> tiles, final RoomUnit roomUnit, final Room room, final Object[] stuff) {
for (RoomTile tile : tiles) { for (RoomTile tile : tiles) {
if (room != null) { if (room != null) {
@@ -470,7 +476,7 @@ public class WiredHandler {
private static void completeReward(Habbo habbo, WiredEffectGiveReward wiredBox, WiredGiveRewardItem reward, int successCode) { private static void completeReward(Habbo habbo, WiredEffectGiveReward wiredBox, WiredGiveRewardItem reward, int successCode) {
if (wiredBox.limit > 0) if (wiredBox.limit > 0)
wiredBox.given++; wiredBox.incrementGiven();
persistReward(wiredBox.getId(), habbo.getHabboInfo().getId(), reward.id, Emulator.getIntUnixTimestamp()); persistReward(wiredBox.getId(), habbo.getHabboInfo().getId(), reward.id, Emulator.getIntUnixTimestamp());
habbo.getClient().sendResponse(new WiredRewardAlertComposer(successCode)); habbo.getClient().sendResponse(new WiredRewardAlertComposer(successCode));
@@ -569,93 +575,124 @@ public class WiredHandler {
public static boolean getReward(Habbo habbo, WiredEffectGiveReward wiredBox) { public static boolean getReward(Habbo habbo, WiredEffectGiveReward wiredBox) {
if (wiredBox.limit > 0) { 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)); habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.LIMITED_NO_MORE_AVAILABLE));
return false; 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(1, habbo.getHabboInfo().getId());
statement.setInt(2, wiredBox.getId()); statement.setInt(2, wiredBox.getId());
statement.setInt(3, wiredBox.rewardItems.size()); statement.setInt(3, wiredBox.rewardItems.size());
try (ResultSet set = statement.executeQuery()) { try (ResultSet set = statement.executeQuery()) {
if (set.first()) { 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) { if (wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_ONCE) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED)); failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED;
return false;
} }
} }
set.beforeFirst(); if (failureCode == -1) {
if (set.next()) {
if (wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_N_MINUTES) { if (wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_N_MINUTES) {
if (Emulator.getIntUnixTimestamp() - set.getInt("timestamp") <= 60) { if (Emulator.getIntUnixTimestamp() - set.getInt("timestamp") <= 60) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_MINUTE)); failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_MINUTE;
return false;
} }
} }
if (wiredBox.uniqueRewards) { if (failureCode == -1 && wiredBox.uniqueRewards) {
if (set.getInt("row_count") == wiredBox.rewardItems.size()) { if (rowCount == wiredBox.rewardItems.size()) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALL_COLLECTED)); failureCode = WiredRewardAlertComposer.REWARD_ALL_COLLECTED;
return false;
} }
} }
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))) { if (!(Emulator.getIntUnixTimestamp() - set.getInt("timestamp") >= (3600 * wiredBox.limitationInterval))) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_HOUR)); failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_HOUR;
return false;
} }
} }
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))) { if (!(Emulator.getIntUnixTimestamp() - set.getInt("timestamp") >= (86400 * wiredBox.limitationInterval))) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_TODAY)); failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_TODAY;
return false;
} }
} }
} }
if (wiredBox.uniqueRewards) { if (failureCode == -1) {
for (WiredGiveRewardItem item : wiredBox.rewardItems) { if (wiredBox.uniqueRewards) {
set.beforeFirst(); for (WiredGiveRewardItem item : wiredBox.rewardItems) {
boolean found = false; set.beforeFirst();
boolean found = false;
while (set.next()) { while (set.next()) {
if (set.getInt("reward_id") == item.id) if (set.getInt("reward_id") == item.id)
found = true; found = true;
}
if (!found) {
rewardToGive = item;
break;
}
} }
if (!found) { if (rewardToGive == null) {
return giveReward(habbo, wiredBox, item); 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) { } catch (SQLException e) {
LOGGER.error("Caught SQL exception", 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; return false;
@@ -71,27 +71,12 @@ public final class RoomWiredStackIndex implements WiredStackIndex {
return Collections.emptyList(); return Collections.emptyList();
} }
// Check cache first
if (useCache) { if (useCache) {
Map<WiredEvent.Type, List<WiredStack>> roomCache = cache.get(room.getId()); return cache.computeIfAbsent(room.getId(), k -> new ConcurrentHashMap<>())
if (roomCache != null) { .computeIfAbsent(type, t -> buildStacks(room, t));
List<WiredStack> cached = roomCache.get(type); } else {
if (cached != null) { return buildStacks(room, type);
return cached;
}
}
} }
// 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 @Override
@@ -206,16 +191,27 @@ public final class RoomWiredStackIndex implements WiredStackIndex {
THashSet<InteractionWiredExtra> extras = specialTypes.getExtras(x, y); THashSet<InteractionWiredExtra> extras = specialTypes.getExtras(x, y);
int conditionEvaluationMode = WiredExtraOrEval.MODE_ALL; int conditionEvaluationMode = WiredExtraOrEval.MODE_ALL;
int conditionEvaluationValue = 1; int conditionEvaluationValue = 1;
boolean useRandom = specialTypes.hasExtraType(x, y, WiredExtraRandom.class); boolean useRandom = false;
boolean useUnseen = specialTypes.hasExtraType(x, y, WiredExtraUnseen.class); boolean useUnseen = false;
boolean executeInOrder = specialTypes.hasExtraType(x, y, WiredExtraExecuteInOrder.class); boolean executeInOrder = false;
if (extras != null) { if (extras != null) {
for (InteractionWiredExtra extra : extras) { 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) { if (extra instanceof WiredExtraOrEval) {
conditionEvaluationMode = ((WiredExtraOrEval) extra).getEvaluationMode(); conditionEvaluationMode = ((WiredExtraOrEval) extra).getEvaluationMode();
conditionEvaluationValue = ((WiredExtraOrEval) extra).getCompareValue(); conditionEvaluationValue = ((WiredExtraOrEval) extra).getCompareValue();
break;
} }
} }
} }
@@ -86,10 +86,10 @@ public final class WiredEngine {
public static int MONITOR_USAGE_WINDOW_MS = 1000; public static int MONITOR_USAGE_WINDOW_MS = 1000;
/** Monitor execution cap per room window */ /** 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 */ /** 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 */ /** Average execution threshold that marks overload */
public static int MONITOR_OVERLOAD_AVERAGE_MS = 50; public static int MONITOR_OVERLOAD_AVERAGE_MS = 50;
@@ -180,14 +180,19 @@ public final class WiredEngine {
int roomId = room.getId(); int roomId = room.getId();
if (this.isRoomBanned(roomId)) {
return false;
}
// Soft rate limiting to prevent rapid-fire event spam without banning whole rooms // Soft rate limiting to prevent rapid-fire event spam without banning whole rooms
if (isRateLimited(roomId, room, event.getType())) { if (isRateLimited(roomId, room, event.getType())) {
return false; return false;
} }
// Check and increment recursion depth to prevent infinite loops // Check and increment recursion depth to prevent infinite loops
int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0); int currentDepth = roomRecursionDepth.merge(roomId, 1, Integer::sum);
if (currentDepth >= MAX_RECURSION_DEPTH) { if (currentDepth > MAX_RECURSION_DEPTH) {
roomRecursionDepth.merge(roomId, -1, Integer::sum);
getDiagnostics(roomId).recordRecursionTimeout( getDiagnostics(roomId).recordRecursionTimeout(
System.currentTimeMillis(), System.currentTimeMillis(),
String.format("Recursion depth %d/%d while handling %s", currentDepth, MAX_RECURSION_DEPTH, event.getType().name()), 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"); debug(room, "RECURSION LIMIT REACHED - aborting to prevent crash");
return false; return false;
} }
roomRecursionDepth.put(roomId, currentDepth + 1);
try { try {
return handleEventInternal(event, room, negateConditions); return handleEventInternal(event, room, negateConditions);
} finally { } finally {
// Decrement recursion depth // Decrement recursion depth
int newDepth = roomRecursionDepth.getOrDefault(roomId, 1) - 1; roomRecursionDepth.compute(roomId, (k, v) -> (v == null || v <= 1) ? null : v - 1);
if (newDepth <= 0) {
roomRecursionDepth.remove(roomId);
} else {
roomRecursionDepth.put(roomId, newDepth);
}
} }
} }
@@ -234,28 +233,27 @@ public final class WiredEngine {
int roomId = room.getId(); int roomId = room.getId();
if (this.isRoomBanned(roomId)) {
return false;
}
if (isRateLimited(roomId, room, event.getType())) { if (isRateLimited(roomId, room, event.getType())) {
return false; return false;
} }
int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0); int currentDepth = roomRecursionDepth.merge(roomId, 1, Integer::sum);
if (currentDepth >= MAX_RECURSION_DEPTH) { if (currentDepth > MAX_RECURSION_DEPTH) {
roomRecursionDepth.merge(roomId, -1, Integer::sum);
LOGGER.warn("Wired recursion limit reached in room {} (depth: {}). " + LOGGER.warn("Wired recursion limit reached in room {} (depth: {}). " +
"Possible infinite loop detected (source item execution). Aborting.", roomId, currentDepth); "Possible infinite loop detected (source item execution). Aborting.", roomId, currentDepth);
debug(room, "RECURSION LIMIT REACHED - aborting source-item execution"); debug(room, "RECURSION LIMIT REACHED - aborting source-item execution");
return false; return false;
} }
roomRecursionDepth.put(roomId, currentDepth + 1);
try { try {
return handleEventForSourceItemInternal(event, room, sourceItemId); return handleEventForSourceItemInternal(event, room, sourceItemId);
} finally { } finally {
int newDepth = roomRecursionDepth.getOrDefault(roomId, 1) - 1; roomRecursionDepth.compute(roomId, (k, v) -> (v == null || v <= 1) ? null : v - 1);
if (newDepth <= 0) {
roomRecursionDepth.remove(roomId);
} else {
roomRecursionDepth.put(roomId, newDepth);
}
} }
} }
@@ -1094,7 +1092,13 @@ public final class WiredEngine {
} }
private void animateFilteredSelectorBox(Room room, InteractionWiredEffect wiredEffect) { 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; return;
} }
@@ -1364,11 +1368,10 @@ public final class WiredEngine {
? String.valueOf(stack.triggerItem().getId()) ? String.valueOf(stack.triggerItem().getId())
: "default"; : "default";
int current = unseenIndices.getOrDefault(key, -1); return unseenIndices.compute(key, (k, current) -> {
int next = (current + 1) % effectCount; if (current == null) current = -1;
unseenIndices.put(key, next); return (current + 1) % effectCount;
});
return next;
} }
/** /**
@@ -1622,6 +1625,8 @@ public final class WiredEngine {
clearRoomRecursionDepth(roomId); clearRoomRecursionDepth(roomId);
clearRoomRateLimiters(roomId); clearRoomRateLimiters(roomId);
clearRoomSourceStackCache(roomId); clearRoomSourceStackCache(roomId);
clearRoomDiagnostics(roomId);
clearRoomBan(roomId);
} }
/** /**
@@ -1684,38 +1689,46 @@ public final class WiredEngine {
* @param room the room object * @param room the room object
*/ */
private void banRoom(int roomId, Room room, WiredEvent.Type eventType, int eventCount) { 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( getDiagnostics(roomId).recordKilled(
System.currentTimeMillis(), System.currentTimeMillis(),
String.format("Rate limit exceeded for %s with %d event(s) in %dms", eventType.name(), eventCount, RATE_LIMIT_WINDOW_MS), String.format("Rate limit exceeded for %s with %d event(s) in %dms", eventType.name(), eventCount, RATE_LIMIT_WINDOW_MS),
eventType.name(), eventType.name(),
0 0
); );
long banMinutes = WIRED_BAN_DURATION_MS / 60000; // Only actually ban the room if ban duration is configured (> 0)
if (WIRED_BAN_DURATION_MS > 0) {
// Send alert to all users in the room long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS;
String roomAlertMessage = Emulator.getTexts().getValue("wired.abuse.room.alert") bannedRooms.put(roomId, banExpiry);
.replace("%minutes%", String.valueOf(banMinutes));
room.sendComposer(new GenericAlertComposer(roomAlertMessage).compose()); long banMinutes = WIRED_BAN_DURATION_MS / 60000;
// Send scripter bubble alert to staff with room link // Send alert to all users in the room
THashMap<String, String> keys = new THashMap<>(); String roomAlertMessage = Emulator.getTexts().getValue("wired.abuse.room.alert")
keys.put("title", Emulator.getTexts().getValue("wired.abuse.staff.title")); .replace("%minutes%", String.valueOf(banMinutes));
keys.put("message", Emulator.getTexts().getValue("wired.abuse.staff.message") room.sendComposer(new GenericAlertComposer(roomAlertMessage).compose());
.replace("%roomname%", room.getName())
.replace("%owner%", room.getOwnerName()) // Send scripter bubble alert to staff with room link
.replace("%minutes%", String.valueOf(banMinutes))); THashMap<String, String> keys = new THashMap<>();
keys.put("linkUrl", "event:navigator/goto/" + roomId); keys.put("title", Emulator.getTexts().getValue("wired.abuse.staff.title"));
keys.put("linkTitle", Emulator.getTexts().getValue("wired.abuse.staff.link")); keys.put("message", Emulator.getTexts().getValue("wired.abuse.staff.message")
Emulator.getGameEnvironment().getHabboManager().sendPacketToHabbosWithPermission( .replace("%roomname%", room.getName())
new BubbleAlertComposer("admin.staffalert", keys).compose(), .replace("%owner%", room.getOwnerName())
"acc_modtool_room_info" .replace("%minutes%", String.valueOf(banMinutes)));
); keys.put("linkUrl", "event:navigator/goto/" + roomId);
keys.put("linkTitle", Emulator.getTexts().getValue("wired.abuse.staff.link"));
LOGGER.warn("Wired abuse detected in room {} ({}). Owner: {}. Wired banned for {} minutes.", Emulator.getGameEnvironment().getHabboManager().sendPacketToHabbosWithPermission(
roomId, room.getName(), room.getOwnerName(), banMinutes); 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) { if (room != null) {
room.getFurniVariableManager().clearTransientAssignments(); room.getFurniVariableManager().clearTransientAssignments();
room.getRoomVariableManager().clearTransientAssignments(); room.getRoomVariableManager().clearTransientAssignments();
} invalidateRoom(room);
if (engine != null && room != null) {
engine.clearRoomExecutionCaches(room.getId());
} }
} }
@@ -1112,18 +1109,16 @@ public final class WiredManager {
} }
private static void persistReward(int wiredId, int habboId, int rewardId, int timestamp) { private static void persistReward(int wiredId, int habboId, int rewardId, int timestamp) {
Emulator.getThreading().run(() -> { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO wired_rewards_given (wired_item, user_id, reward_id, timestamp) VALUES (?, ?, ?, ?)")) {
PreparedStatement statement = connection.prepareStatement("INSERT INTO wired_rewards_given (wired_item, user_id, reward_id, timestamp) VALUES ( ?, ?, ?, ?)")) { statement.setInt(1, wiredId);
statement.setInt(1, wiredId); statement.setInt(2, habboId);
statement.setInt(2, habboId); statement.setInt(3, rewardId);
statement.setInt(3, rewardId); statement.setInt(4, timestamp);
statement.setInt(4, timestamp); statement.execute();
statement.execute(); } catch (SQLException e) {
} catch (SQLException e) { LOGGER.error("Caught SQL exception", e);
LOGGER.error("Caught SQL exception", e); }
}
});
} }
private static void completeReward(Habbo habbo, WiredEffectGiveReward wiredBox, WiredGiveRewardItem reward, int successCode) { 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) { public static boolean getReward(Habbo habbo, WiredEffectGiveReward wiredBox) {
if (wiredBox.getLimit() > 0) { synchronized (wiredBox) {
if (wiredBox.getLimit() - wiredBox.getGiven() == 0) { if (wiredBox.getLimit() > 0) {
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.LIMITED_NO_MORE_AVAILABLE)); 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; 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)) { if (failureCode != -1) {
statement.setInt(1, habbo.getHabboInfo().getId()); habbo.getClient().sendResponse(new WiredRewardAlertComposer(failureCode));
statement.setInt(2, wiredBox.getId()); return false;
statement.setInt(3, wiredBox.getRewardItems().size()); }
try (ResultSet set = statement.executeQuery()) { if (rewardToGive == null) {
if (set.first()) { if (wiredBox.isUniqueRewards()) {
if (set.getInt("row_count") >= 1) { if (!wiredBox.getRewardItems().isEmpty()) {
if (wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_ONCE) { rewardToGive = wiredBox.getRewardItems().get(0);
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;
} else { } else {
int randomNumber = Emulator.getRandom().nextInt(101); failureCode = WiredRewardAlertComposer.REWARD_ALL_COLLECTED;
}
int count = 0; } else {
for (WiredGiveRewardItem item : wiredBox.getRewardItems()) { int randomNumber = Emulator.getRandom().nextInt(101);
if (randomNumber >= count && randomNumber <= (count + item.probability)) { int count = 0;
return giveReward(habbo, wiredBox, item); for (WiredGiveRewardItem item : wiredBox.getRewardItems()) {
} if (randomNumber >= count && randomNumber <= (count + item.probability)) {
rewardToGive = item;
count += item.probability; break;
} }
count += item.probability;
}
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.UNLUCKY_NO_REWARD)); if (rewardToGive == null) {
return false; 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; 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 ArrayDeque<HistoryEntry> history;
private final int maxHistoryEntries; private final int maxHistoryEntries;
private long windowStartedAt; private final java.util.concurrent.atomic.AtomicLong windowStartedAt = new java.util.concurrent.atomic.AtomicLong();
private int usageCurrentWindow; private final java.util.concurrent.atomic.AtomicInteger usageCurrentWindow = new java.util.concurrent.atomic.AtomicInteger();
private int delayedEventsPending; private final java.util.concurrent.atomic.AtomicInteger delayedEventsPending = new java.util.concurrent.atomic.AtomicInteger();
private long totalExecutionMsCurrentWindow; private final java.util.concurrent.atomic.AtomicLong totalExecutionMsCurrentWindow = new java.util.concurrent.atomic.AtomicLong();
private int executionSamplesCurrentWindow; private final java.util.concurrent.atomic.AtomicInteger executionSamplesCurrentWindow = new java.util.concurrent.atomic.AtomicInteger();
private int averageExecutionMs; private volatile int averageExecutionMs;
private int peakExecutionMs; private volatile int peakExecutionMs;
private int consecutiveHeavyWindows; private volatile int consecutiveHeavyWindows;
private int consecutiveOverloadWindows; private volatile int consecutiveOverloadWindows;
private boolean heavy; private volatile boolean heavy;
private String peakExecutionSourceLabel; private volatile String peakExecutionSourceLabel;
private int peakExecutionSourceId; private volatile int peakExecutionSourceId;
private String peakExecutionReason; private volatile String peakExecutionReason;
public WiredRoomDiagnostics(int usageWindowMs, int usageLimitPerWindow, int delayedEventsLimit, public WiredRoomDiagnostics(int usageWindowMs, int usageLimitPerWindow, int delayedEventsLimit,
int overloadAverageThresholdMs, int overloadPeakThresholdMs, 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) { public boolean tryConsumeExecutionBudget(int estimatedCost, long now, String sourceLabel, int sourceId, String reason) {
rollWindow(now); rollWindowIfNeeded(now);
int normalizedCost = Math.max(0, estimatedCost); 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, record(Type.EXECUTION_CAP, now,
buildExecutionCapReason(normalizedCost, reason), buildExecutionCapReason(normalizedCost, reason, currentUsage),
sourceLabel, sourceLabel,
sourceId); sourceId);
return false; return false;
} }
this.usageCurrentWindow += normalizedCost;
return true; return true;
} }
public synchronized boolean tryScheduleDelayedEvent(long now, String sourceLabel, int sourceId, String reason) { public boolean tryScheduleDelayedEvent(long now, String sourceLabel, int sourceId, String reason) {
rollWindow(now); rollWindowIfNeeded(now);
if ((this.delayedEventsPending + 1) > this.delayedEventsLimit) { int currentPending = this.delayedEventsPending.incrementAndGet();
if (currentPending > this.delayedEventsLimit) {
record(Type.DELAYED_EVENTS_CAP, now, record(Type.DELAYED_EVENTS_CAP, now,
buildDelayedCapReason(reason), buildDelayedCapReason(reason, currentPending),
sourceLabel, sourceLabel,
sourceId); sourceId);
return false; return false;
} }
this.delayedEventsPending++;
return true; return true;
} }
public synchronized void completeDelayedEvent() { public void completeDelayedEvent() {
if (this.delayedEventsPending > 0) { this.delayedEventsPending.updateAndGet(v -> v > 0 ? v - 1 : 0);
this.delayedEventsPending--;
}
} }
public synchronized void recordExecution(long elapsedMs, long now, String sourceLabel, int sourceId, String reason) { public void recordExecution(long elapsedMs, long now, String sourceLabel, int sourceId, String reason) {
rollWindow(now); rollWindowIfNeeded(now);
int normalizedElapsed = (int) Math.max(0L, elapsedMs); int normalizedElapsed = (int) Math.max(0L, elapsedMs);
this.totalExecutionMsCurrentWindow += normalizedElapsed; long total = this.totalExecutionMsCurrentWindow.addAndGet(normalizedElapsed);
this.executionSamplesCurrentWindow++; int samples = this.executionSamplesCurrentWindow.incrementAndGet();
this.averageExecutionMs = (int) Math.round(this.totalExecutionMsCurrentWindow / (double) this.executionSamplesCurrentWindow); this.averageExecutionMs = (int) Math.round(total / (double) samples);
if (normalizedElapsed >= this.peakExecutionMs) { if (normalizedElapsed >= this.peakExecutionMs) {
this.peakExecutionMs = normalizedElapsed; synchronized (this) {
this.peakExecutionSourceLabel = sanitizeSourceLabel(sourceLabel); if (normalizedElapsed >= this.peakExecutionMs) {
this.peakExecutionSourceId = Math.max(0, sourceId); this.peakExecutionMs = normalizedElapsed;
this.peakExecutionReason = sanitizeReason(reason); 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) { public void recordKilled(long now, String reason, String sourceLabel, int sourceId) {
rollWindow(now); rollWindowIfNeeded(now);
record(Type.KILLED, now, reason, sourceLabel, sourceId); record(Type.KILLED, now, reason, sourceLabel, sourceId);
} }
public synchronized void recordRecursionTimeout(long now, String reason, String sourceLabel, int sourceId) { public void recordRecursionTimeout(long now, String reason, String sourceLabel, int sourceId) {
rollWindow(now); rollWindowIfNeeded(now);
record(Type.RECURSION_TIMEOUT, now, reason, sourceLabel, sourceId); 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) { 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<LogEntry> logEntries = new ArrayList<>(Type.values().length);
List<HistoryEntry> historyEntries = new ArrayList<>(this.history.size()); List<HistoryEntry> historyEntries = new ArrayList<>(this.history.size());
@@ -422,10 +424,10 @@ public final class WiredRoomDiagnostics {
} }
return new Snapshot( return new Snapshot(
this.usageCurrentWindow, this.usageCurrentWindow.get(),
this.usageLimitPerWindow, this.usageLimitPerWindow,
this.heavy, this.heavy,
this.delayedEventsPending, this.delayedEventsPending.get(),
this.delayedEventsLimit, this.delayedEventsLimit,
this.averageExecutionMs, this.averageExecutionMs,
this.peakExecutionMs, this.peakExecutionMs,
@@ -444,30 +446,40 @@ public final class WiredRoomDiagnostics {
); );
} }
private void rollWindow(long now) { private void rollWindowIfNeeded(long now) {
if (this.windowStartedAt <= 0L) { long startedAt = this.windowStartedAt.get();
this.windowStartedAt = now; if (startedAt <= 0L) {
this.windowStartedAt.compareAndSet(startedAt, now);
return; return;
} }
while ((now - this.windowStartedAt) >= this.usageWindowMs) { if ((now - startedAt) >= this.usageWindowMs) {
evaluateWindow(this.windowStartedAt + this.usageWindowMs); synchronized (this) {
this.windowStartedAt += this.usageWindowMs; startedAt = this.windowStartedAt.get();
this.usageCurrentWindow = 0; if ((now - startedAt) >= this.usageWindowMs) {
this.totalExecutionMsCurrentWindow = 0L; while ((now - startedAt) >= this.usageWindowMs) {
this.executionSamplesCurrentWindow = 0; evaluateWindow(startedAt + this.usageWindowMs);
this.averageExecutionMs = 0; startedAt += this.usageWindowMs;
this.peakExecutionMs = 0;
this.peakExecutionSourceLabel = null; this.usageCurrentWindow.set(0);
this.peakExecutionSourceId = 0; this.totalExecutionMsCurrentWindow.set(0L);
this.peakExecutionReason = null; 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) { private void evaluateWindow(long now) {
int usagePercent = (int) Math.round((this.usageCurrentWindow * 100D) / this.usageLimitPerWindow); int usagePercent = (int) Math.round((this.usageCurrentWindow.get() * 100D) / this.usageLimitPerWindow);
int delayedPercent = (int) Math.round((this.delayedEventsPending * 100D) / this.delayedEventsLimit); int delayedPercent = (int) Math.round((this.delayedEventsPending.get() * 100D) / this.delayedEventsLimit);
boolean overloadWindow = (this.executionSamplesCurrentWindow > 0) boolean overloadWindow = (this.executionSamplesCurrentWindow.get() > 0)
&& ((this.averageExecutionMs >= this.overloadAverageThresholdMs) || (this.peakExecutionMs >= this.overloadPeakThresholdMs)); && ((this.averageExecutionMs >= this.overloadAverageThresholdMs) || (this.peakExecutionMs >= this.overloadPeakThresholdMs));
boolean heavyWindow = (usagePercent >= this.heavyUsageThresholdPercent) boolean heavyWindow = (usagePercent >= this.heavyUsageThresholdPercent)
|| (delayedPercent >= this.heavyDelayedThresholdPercent) || (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( return joinReason(
reason, reason,
String.format("Estimated stack cost %d would exceed usage budget %d/%d in %dms window", String.format("Estimated stack cost %d would exceed usage budget %d/%d in %dms window",
normalizedCost, normalizedCost,
this.usageCurrentWindow, currentUsage,
this.usageLimitPerWindow, this.usageLimitPerWindow,
this.usageWindowMs) this.usageWindowMs)
); );
} }
private String buildDelayedCapReason(String reason) { private String buildDelayedCapReason(String reason, int currentPending) {
return joinReason( return joinReason(
reason, reason,
String.format("Pending delayed events would exceed queue %d/%d", String.format("Pending delayed events would exceed queue %d/%d",
this.delayedEventsPending, currentPending,
this.delayedEventsLimit) this.delayedEventsLimit)
); );
} }
@@ -544,7 +556,7 @@ public final class WiredRoomDiagnostics {
this.overloadAverageThresholdMs, this.overloadAverageThresholdMs,
this.peakExecutionMs, this.peakExecutionMs,
this.overloadPeakThresholdMs, this.overloadPeakThresholdMs,
this.executionSamplesCurrentWindow, this.executionSamplesCurrentWindow.get(),
this.usageWindowMs) this.usageWindowMs)
); );
} }
@@ -33,6 +33,8 @@ import java.util.Locale;
public final class WiredTextPlaceholderUtil { public final class WiredTextPlaceholderUtil {
private static final char PRESERVED_SPACE = '\u00A0'; 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() { private WiredTextPlaceholderUtil() {
} }
@@ -56,13 +58,20 @@ public final class WiredTextPlaceholderUtil {
String resolvedText = text; String resolvedText = text;
int replacementCount = 0;
for (InteractionWiredExtra extra : WiredExecutionOrderUtil.sort(extras)) { for (InteractionWiredExtra extra : WiredExecutionOrderUtil.sort(extras)) {
if (extra instanceof WiredExtraTextOutputUsername) { if (extra instanceof WiredExtraTextOutputUsername) {
WiredExtraTextOutputUsername usernameExtra = (WiredExtraTextOutputUsername) extra; WiredExtraTextOutputUsername usernameExtra = (WiredExtraTextOutputUsername) extra;
String placeholderToken = usernameExtra.getPlaceholderToken(); String placeholderToken = usernameExtra.getPlaceholderToken();
if (!placeholderToken.isEmpty() && resolvedText.contains(placeholderToken)) { 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; continue;
@@ -73,7 +82,12 @@ public final class WiredTextPlaceholderUtil {
String placeholderToken = furniExtra.getPlaceholderToken(); String placeholderToken = furniExtra.getPlaceholderToken();
if (!placeholderToken.isEmpty() && resolvedText.contains(placeholderToken)) { 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; continue;
@@ -84,7 +98,12 @@ public final class WiredTextPlaceholderUtil {
String placeholderToken = variableExtra.getPlaceholderToken(); String placeholderToken = variableExtra.getPlaceholderToken();
if (!placeholderToken.isEmpty() && resolvedText.contains(placeholderToken)) { 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); 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) { private static String preserveRepeatedSpaces(String text) {
if (text == null || text.length() < 2) { if (text == null || text.length() < 2) {
return text; 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.TemporalAdjusters;
import java.time.temporal.WeekFields; import java.time.temporal.WeekFields;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -23,7 +24,7 @@ import java.util.stream.Stream;
public class WiredHighscoreManager { public class WiredHighscoreManager {
private static final Logger LOGGER = LoggerFactory.getLogger(WiredHighscoreManager.class); 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 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"); 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() { private void loadHighscoreData() {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM items_highscore_data")) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM items_highscore_data")) {
statement.setFetchSize(1000);
try (ResultSet set = statement.executeQuery()) { try (ResultSet set = statement.executeQuery()) {
while (set.next()) { while (set.next()) {
WiredHighscoreDataEntry entry = new WiredHighscoreDataEntry(set); WiredHighscoreDataEntry entry = new WiredHighscoreDataEntry(set);
if (!this.data.containsKey(entry.getItemId())) { this.data.computeIfAbsent(entry.getItemId(), k -> Collections.synchronizedList(new ArrayList<>())).add(entry);
this.data.put(entry.getItemId(), new ArrayList<>());
}
this.data.get(entry.getItemId()).add(entry);
} }
} }
} catch (SQLException e) { } catch (SQLException e) {
@@ -77,33 +75,39 @@ public class WiredHighscoreManager {
} }
public void addHighscoreData(WiredHighscoreDataEntry entry) { public void addHighscoreData(WiredHighscoreDataEntry entry) {
if (!this.data.containsKey(entry.getItemId())) { this.data.computeIfAbsent(entry.getItemId(), k -> Collections.synchronizedList(new ArrayList<>())).add(entry);
this.data.put(entry.getItemId(), new ArrayList<>());
}
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.execute();
statement.setInt(1, entry.getItemId()); } catch (SQLException e) {
statement.setString(2, String.join(",", entry.getUserIds().stream().map(Object::toString).collect(Collectors.toList()))); LOGGER.error("Caught SQL exception", e);
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);
}
} }
public List<WiredHighscoreRow> getHighscoreRowsForItem(int itemId, WiredHighscoreClearType clearType, WiredHighscoreScoreType scoreType) { public List<WiredHighscoreRow> getHighscoreRowsForItem(int itemId, WiredHighscoreClearType clearType, WiredHighscoreScoreType scoreType) {
if (!this.data.containsKey(itemId)) return null; 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())) .filter(entry -> this.timeMatchesEntry(entry, clearType) && (scoreType != WiredHighscoreScoreType.MOSTWIN || entry.isWin()))
.map(entry -> new WiredHighscoreRow( .map(entry -> new WiredHighscoreRow(
entry.getUserIds().stream() entry.getUserIds().stream()
.map(id -> Emulator.getGameEnvironment().getHabboManager().getHabboInfo(id).getUsername()) .map(id -> Emulator.getGameEnvironment().getHabboManager().getCachedUsername(id))
.collect(Collectors.toList()), .collect(Collectors.toList()),
entry.getScore() entry.getScore()
)); ));
@@ -167,7 +171,7 @@ public class WiredHighscoreManager {
return false; return false;
} }
public HashMap<Integer, List<WiredHighscoreDataEntry>> getData() { public Map<Integer, List<WiredHighscoreDataEntry>> getData() {
return this.data; return this.data;
} }
@@ -176,7 +180,7 @@ public class WiredHighscoreManager {
} }
public void setEntriesForItemId(int itemId, List<WiredHighscoreDataEntry> entries) { public void setEntriesForItemId(int itemId, List<WiredHighscoreDataEntry> entries) {
this.data.put(itemId, entries); this.data.put(itemId, Collections.synchronizedList(entries));
} }
private long getTodayStartTimestamp() { private long getTodayStartTimestamp() {
@@ -60,11 +60,15 @@ public final class WiredTickService {
/** Whether a shard worker loop is currently scheduled/running. */ /** Whether a shard worker loop is currently scheduled/running. */
private AtomicBoolean[] shardScheduled; private AtomicBoolean[] shardScheduled;
private final ConcurrentHashMap<Integer, Set<WiredTickable>> roomTickables; private final ConcurrentHashMap<Integer, Set<WiredTickable>>[] shardRoomTickables;
private final AtomicBoolean running; private final AtomicBoolean running;
@SuppressWarnings("unchecked")
private WiredTickService() { 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); this.running = new AtomicBoolean(false);
} }
@@ -232,7 +236,9 @@ public final class WiredTickService {
shardProcessedTicks = null; shardProcessedTicks = null;
shardScheduled = null; shardScheduled = null;
roomTickables.clear(); for (int i = 0; i < MAX_WORKER_COUNT; i++) {
shardRoomTickables[i].clear();
}
LOGGER.info("WiredTickService stopped"); LOGGER.info("WiredTickService stopped");
} }
@@ -246,7 +252,8 @@ public final class WiredTickService {
} }
int roomId = room.getId(); 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)) { if (tickables.add(tickable)) {
tickable.onRegistered(room, System.currentTimeMillis()); tickable.onRegistered(room, System.currentTimeMillis());
@@ -259,7 +266,8 @@ public final class WiredTickService {
} }
int roomId = room.getId(); 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 != null) {
if (tickables.remove(tickable)) { if (tickables.remove(tickable)) {
@@ -267,13 +275,14 @@ public final class WiredTickService {
} }
if (tickables.isEmpty()) { if (tickables.isEmpty()) {
roomTickables.remove(roomId); shardRoomTickables[shardIndex].remove(roomId);
} }
} }
} }
public void unregister(int roomId, int tickableId) { 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) { if (tickables != null) {
tickables.removeIf(t -> { tickables.removeIf(t -> {
@@ -288,7 +297,7 @@ public final class WiredTickService {
}); });
if (tickables.isEmpty()) { if (tickables.isEmpty()) {
roomTickables.remove(roomId); shardRoomTickables[shardIndex].remove(roomId);
} }
} }
} }
@@ -298,11 +307,12 @@ public final class WiredTickService {
return; 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) { if (tickables != null) {
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]); for (WiredTickable tickable : tickables) {
for (WiredTickable tickable : snapshot) {
try { try {
if (tickable != null) { if (tickable != null) {
tickable.onUnregistered(room); 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; 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) { if (tickables != null) {
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]); for (WiredTickable tickable : tickables) {
for (WiredTickable tickable : snapshot) {
try { try {
if (tickable != null) { if (tickable != null) {
tickable.resetTimer(); tickable.resetTimer();
@@ -347,16 +358,25 @@ public final class WiredTickService {
} }
public int getTickableCount(int roomId) { 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; return tickables != null ? tickables.size() : 0;
} }
public int getTotalTickableCount() { 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() { 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() { public long getTickCount() {
@@ -396,6 +416,12 @@ public final class WiredTickService {
break; 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); processShardTick(shardIndex, nextTick);
shardProcessedTicks[shardIndex].set(nextTick); shardProcessedTicks[shardIndex].set(nextTick);
} }
@@ -414,12 +440,8 @@ public final class WiredTickService {
int processedTickables = 0; int processedTickables = 0;
int processedRooms = 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(); int roomId = entry.getKey();
if (getShardIndex(roomId) != shardIndex) {
continue;
}
Set<WiredTickable> tickables = entry.getValue(); Set<WiredTickable> tickables = entry.getValue();
if (tickables == null || tickables.isEmpty()) { if (tickables == null || tickables.isEmpty()) {
continue; continue;
@@ -435,14 +457,9 @@ public final class WiredTickService {
} }
long roomStart = System.currentTimeMillis(); long roomStart = System.currentTimeMillis();
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
if (snapshot.length == 0) {
continue;
}
processedRooms++; processedRooms++;
for (WiredTickable tickable : snapshot) { for (WiredTickable tickable : tickables) {
long tickableStart = System.currentTimeMillis(); long tickableStart = System.currentTimeMillis();
if (tickable == null) { if (tickable == null) {
@@ -489,7 +506,7 @@ public final class WiredTickService {
shardIndex, shardIndex,
roomId, roomId,
currentTick, currentTick,
snapshot.length, tickables.size(),
roomDuration roomDuration
); );
} }
@@ -2,6 +2,7 @@ package com.eu.habbo.messages;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient; 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.Incoming;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.incoming.achievements.RequestAchievementConfigurationEvent; import com.eu.habbo.messages.incoming.achievements.RequestAchievementConfigurationEvent;
@@ -38,6 +39,7 @@ import com.eu.habbo.messages.incoming.hotelview.*;
import com.eu.habbo.messages.incoming.inventory.*; import com.eu.habbo.messages.incoming.inventory.*;
import com.eu.habbo.messages.incoming.inventory.nickicons.*; import com.eu.habbo.messages.incoming.inventory.nickicons.*;
import com.eu.habbo.messages.incoming.inventory.prefixes.*; import com.eu.habbo.messages.incoming.inventory.prefixes.*;
import com.eu.habbo.messages.incoming.mentions.*;
import com.eu.habbo.messages.incoming.modtool.*; import com.eu.habbo.messages.incoming.modtool.*;
import com.eu.habbo.messages.incoming.navigator.*; import com.eu.habbo.messages.incoming.navigator.*;
import com.eu.habbo.messages.incoming.polls.AnswerPollEvent; import com.eu.habbo.messages.incoming.polls.AnswerPollEvent;
@@ -180,6 +182,8 @@ public class PacketManager {
return; return;
try { try {
EmulatorNetworkStats.recordIncoming(packet.bytesAvailable() + 6);
if (this.isRegistered(packet.getMessageId())) { if (this.isRegistered(packet.getMessageId())) {
Class<? extends MessageHandler> handlerClass = this.incoming.get(packet.getMessageId()); Class<? extends MessageHandler> handlerClass = this.incoming.get(packet.getMessageId());
@@ -423,6 +427,9 @@ public class PacketManager {
} }
void registerRooms() throws Exception { void registerRooms() throws Exception {
this.registerHandler(Incoming.RequestMentionsEvent, RequestMentionsEvent.class);
this.registerHandler(Incoming.MarkMentionsReadEvent, MarkMentionsReadEvent.class);
this.registerHandler(Incoming.DeleteMentionEvent, DeleteMentionEvent.class);
this.registerHandler(Incoming.RequestRoomLoadEvent, RequestRoomLoadEvent.class); this.registerHandler(Incoming.RequestRoomLoadEvent, RequestRoomLoadEvent.class);
this.registerHandler(Incoming.RequestHeightmapEvent, RequestRoomHeightmapEvent.class); this.registerHandler(Incoming.RequestHeightmapEvent, RequestRoomHeightmapEvent.class);
this.registerHandler(Incoming.RequestRoomHeightmapEvent, RequestRoomHeightmapEvent.class); this.registerHandler(Incoming.RequestRoomHeightmapEvent, RequestRoomHeightmapEvent.class);
@@ -716,5 +723,42 @@ public class PacketManager {
this.registerHandler(Incoming.YouTubeRoomPlayEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomPlayEvent.class); this.registerHandler(Incoming.YouTubeRoomPlayEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomPlayEvent.class);
this.registerHandler(Incoming.YouTubeRoomWatchingEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomWatchingEvent.class); this.registerHandler(Incoming.YouTubeRoomWatchingEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomWatchingEvent.class);
this.registerHandler(Incoming.YouTubeRoomSettingsEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomSettingsEvent.class); this.registerHandler(Incoming.YouTubeRoomSettingsEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomSettingsEvent.class);
// Housekeeping (in-client admin panel)
this.registerHandler(Incoming.HousekeepingFindUserByNameEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindUserByNameEvent.class);
this.registerHandler(Incoming.HousekeepingFindUserByIdEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindUserByIdEvent.class);
this.registerHandler(Incoming.HousekeepingBanUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingBanUserEvent.class);
this.registerHandler(Incoming.HousekeepingUnbanUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingUnbanUserEvent.class);
this.registerHandler(Incoming.HousekeepingMuteUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingMuteUserEvent.class);
this.registerHandler(Incoming.HousekeepingKickUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingKickUserEvent.class);
this.registerHandler(Incoming.HousekeepingForceDisconnectUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingForceDisconnectUserEvent.class);
this.registerHandler(Incoming.HousekeepingSetUserRankEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSetUserRankEvent.class);
this.registerHandler(Incoming.HousekeepingTradeLockUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingTradeLockUserEvent.class);
this.registerHandler(Incoming.HousekeepingResetUserPasswordEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingResetUserPasswordEvent.class);
this.registerHandler(Incoming.HousekeepingFindRoomByIdEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindRoomByIdEvent.class);
this.registerHandler(Incoming.HousekeepingSearchRoomsEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSearchRoomsEvent.class);
this.registerHandler(Incoming.HousekeepingRoomStateEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingRoomStateEvent.class);
this.registerHandler(Incoming.HousekeepingMuteRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingMuteRoomEvent.class);
this.registerHandler(Incoming.HousekeepingKickAllFromRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingKickAllFromRoomEvent.class);
this.registerHandler(Incoming.HousekeepingTransferRoomOwnershipEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingTransferRoomOwnershipEvent.class);
this.registerHandler(Incoming.HousekeepingDeleteRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingDeleteRoomEvent.class);
this.registerHandler(Incoming.HousekeepingGiveCreditsEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGiveCreditsEvent.class);
this.registerHandler(Incoming.HousekeepingGiveCurrencyEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGiveCurrencyEvent.class);
this.registerHandler(Incoming.HousekeepingGrantItemEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGrantItemEvent.class);
this.registerHandler(Incoming.HousekeepingSetHcSubscriptionEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSetHcSubscriptionEvent.class);
this.registerHandler(Incoming.HousekeepingSendHotelAlertEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSendHotelAlertEvent.class);
this.registerHandler(Incoming.HousekeepingGetDashboardEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGetDashboardEvent.class);
this.registerHandler(Incoming.HousekeepingListActionLogEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingListActionLogEvent.class);
this.registerHandler(Incoming.RequestRareValuesEvent, com.eu.habbo.messages.incoming.rarevalues.RequestRareValuesEvent.class);
this.registerHandler(Incoming.WheelOpenEvent, com.eu.habbo.messages.incoming.wheel.WheelOpenEvent.class);
this.registerHandler(Incoming.WheelSpinEvent, com.eu.habbo.messages.incoming.wheel.WheelSpinEvent.class);
this.registerHandler(Incoming.WheelBuySpinEvent, com.eu.habbo.messages.incoming.wheel.WheelBuySpinEvent.class);
this.registerHandler(Incoming.WheelAdminGetPrizesEvent, com.eu.habbo.messages.incoming.wheel.WheelAdminGetPrizesEvent.class);
this.registerHandler(Incoming.WheelAdminSavePrizesEvent, com.eu.habbo.messages.incoming.wheel.WheelAdminSavePrizesEvent.class);
this.registerHandler(Incoming.SoundboardPlayEvent, com.eu.habbo.messages.incoming.soundboard.SoundboardPlayEvent.class);
this.registerHandler(Incoming.SoundboardSetEnabledEvent, com.eu.habbo.messages.incoming.soundboard.SoundboardSetEnabledEvent.class);
} }
} }
@@ -460,4 +460,43 @@ public class Incoming {
public static final int YouTubeRoomPlayEvent = 8001; public static final int YouTubeRoomPlayEvent = 8001;
public static final int YouTubeRoomWatchingEvent = 8002; public static final int YouTubeRoomWatchingEvent = 8002;
public static final int YouTubeRoomSettingsEvent = 8003; public static final int YouTubeRoomSettingsEvent = 8003;
// Housekeeping (in-client admin panel) IDs 9100..9199 reserved
public static final int HousekeepingFindUserByNameEvent = 9100;
public static final int HousekeepingFindUserByIdEvent = 9101;
public static final int HousekeepingBanUserEvent = 9102;
public static final int HousekeepingUnbanUserEvent = 9103;
public static final int HousekeepingMuteUserEvent = 9104;
public static final int HousekeepingKickUserEvent = 9105;
public static final int HousekeepingForceDisconnectUserEvent = 9106;
public static final int HousekeepingSetUserRankEvent = 9107;
public static final int HousekeepingTradeLockUserEvent = 9108;
public static final int HousekeepingResetUserPasswordEvent = 9109;
public static final int HousekeepingFindRoomByIdEvent = 9110;
public static final int HousekeepingSearchRoomsEvent = 9111;
public static final int HousekeepingRoomStateEvent = 9112;
public static final int HousekeepingMuteRoomEvent = 9113;
public static final int HousekeepingKickAllFromRoomEvent = 9114;
public static final int HousekeepingTransferRoomOwnershipEvent = 9115;
public static final int HousekeepingDeleteRoomEvent = 9116;
public static final int HousekeepingGiveCreditsEvent = 9117;
public static final int HousekeepingGiveCurrencyEvent = 9118;
public static final int HousekeepingGrantItemEvent = 9119;
public static final int HousekeepingSetHcSubscriptionEvent = 9120;
public static final int HousekeepingSendHotelAlertEvent = 9121;
public static final int HousekeepingGetDashboardEvent = 9122;
public static final int HousekeepingListActionLogEvent = 9123;
// Custom features IDs 9300+ reserved
public static final int RequestRareValuesEvent = 9300;
public static final int WheelOpenEvent = 9301;
public static final int WheelSpinEvent = 9302;
public static final int WheelBuySpinEvent = 9303;
public static final int WheelAdminGetPrizesEvent = 9304;
public static final int WheelAdminSavePrizesEvent = 9305;
public static final int SoundboardPlayEvent = 9306;
public static final int SoundboardSetEnabledEvent = 9307;
public static final int RequestMentionsEvent = 4803;
public static final int MarkMentionsReadEvent = 4804;
public static final int DeleteMentionEvent = 4805;
} }
@@ -2,10 +2,8 @@ package com.eu.habbo.messages.incoming.catalog;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.achievements.AchievementManager; import com.eu.habbo.habbohotel.achievements.AchievementManager;
import com.eu.habbo.habbohotel.catalog.CatalogItem; import com.eu.habbo.habbohotel.catalog.*;
import com.eu.habbo.habbohotel.catalog.CatalogLimitedConfiguration; import com.eu.habbo.habbohotel.catalog.layouts.*;
import com.eu.habbo.habbohotel.catalog.CatalogManager;
import com.eu.habbo.habbohotel.catalog.CatalogPage;
import com.eu.habbo.habbohotel.items.FurnitureType; import com.eu.habbo.habbohotel.items.FurnitureType;
import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.*; 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.Habbo;
import com.eu.habbo.habbohotel.users.HabboBadge; import com.eu.habbo.habbohotel.users.HabboBadge;
import com.eu.habbo.habbohotel.users.HabboItem; 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.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.catalog.*; import com.eu.habbo.messages.outgoing.catalog.*;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; 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.generic.alerts.HotelWillCloseInMinutesComposer;
import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer; import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer;
import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer; import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer;
import com.eu.habbo.messages.outgoing.users.UserClubComposer;
import com.eu.habbo.threading.runnables.ShutdownEmulator; import com.eu.habbo.threading.runnables.ShutdownEmulator;
import gnu.trove.map.hash.THashMap; import gnu.trove.map.hash.THashMap;
import gnu.trove.set.hash.THashSet; import gnu.trove.set.hash.THashSet;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.sql.Connection; import java.sql.*;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Calendar; import java.util.Calendar;
public class CatalogBuyItemAsGiftEvent extends MessageHandler { public class CatalogBuyItemAsGiftEvent extends MessageHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CatalogBuyItemAsGiftEvent.class); private static final Logger LOGGER = LoggerFactory.getLogger(CatalogBuyItemAsGiftEvent.class);
private static final int USERNAME_MAX = 32;
private static final int EXTRADATA_MAX = 256;
@Override @Override
public int getRatelimit() { public int getRatelimit() {
return 500; return 500;
@@ -44,61 +44,66 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
@Override @Override
public void handle() throws Exception { public void handle() throws Exception {
LOGGER.error("DEBUG GIFT: entered CatalogBuyItemAsGiftEvent.handle()");
if (Emulator.getIntUnixTimestamp() - this.client.getHabbo().getHabboStats().lastGiftTimestamp >= CatalogManager.PURCHASE_COOLDOWN) { if (Emulator.getIntUnixTimestamp() - this.client.getHabbo().getHabboStats().lastGiftTimestamp >= CatalogManager.PURCHASE_COOLDOWN) {
this.client.getHabbo().getHabboStats().lastGiftTimestamp = Emulator.getIntUnixTimestamp();
if (ShutdownEmulator.timestamp > 0) { 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 HotelWillCloseInMinutesComposer((ShutdownEmulator.timestamp - Emulator.getIntUnixTimestamp()) / 60));
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return; return;
} }
if (this.client.getHabbo().getHabboStats().isPurchasingFurniture) { synchronized (this.client.getHabbo().getHabboStats()) {
LOGGER.error("DEBUG GIFT: isPurchasingFurniture already true"); if (this.client.getHabbo().getHabboStats().isPurchasingFurniture) {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); LOGGER.debug("isPurchasingFurniture already true");
return; this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
} else { return;
}
this.client.getHabbo().getHabboStats().isPurchasingFurniture = true; this.client.getHabbo().getHabboStats().isPurchasingFurniture = true;
} }
int paidCredits = 0;
int paidPoints = 0;
int paidPointsType = 0;
try { try {
int pageId = this.packet.readInt(); int pageId = this.packet.readInt();
int itemId = this.packet.readInt(); int itemId = this.packet.readInt();
String extraData = this.packet.readString(); String extraData = this.packet.readString();
if (extraData.length() > EXTRADATA_MAX) extraData = extraData.substring(0, EXTRADATA_MAX);
String username = this.packet.readString(); 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 spriteId = this.packet.readInt();
int color = this.packet.readInt(); int color = this.packet.readInt();
int ribbonId = this.packet.readInt(); int ribbonId = this.packet.readInt();
boolean showName = this.packet.readBoolean(); boolean showName = this.packet.readBoolean();
LOGGER.error( LOGGER.debug("Gift request: pageId={}, itemId={}, spriteId={}, color={}, ribbonId={}", pageId, itemId, spriteId, color, ribbonId);
"DEBUG GIFT: pageId={}, itemId={}, extraData={}, username={}, spriteId={}, color={}, ribbonId={}, showName={}, message={}",
pageId, itemId, extraData, username, spriteId, color, ribbonId, showName, message
);
int userId = 0; 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) if (!Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId)
&& !Emulator.getGameEnvironment().getCatalogManager().giftFurnis.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()); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return; return;
} }
if (!GiftConfigurationComposer.BOX_TYPES.contains(color) || !GiftConfigurationComposer.RIBBON_TYPES.contains(ribbonId)) { 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()); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return; 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); Integer iItemId = Emulator.getGameEnvironment().getCatalogManager().giftWrappers.get(spriteId);
if (iItemId == null) { if (iItemId == null) {
@@ -106,7 +111,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
} }
if (iItemId == null) { 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()); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return; return;
} }
@@ -114,7 +119,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
Item giftItem = Emulator.getGameEnvironment().getItemManager().getItem(iItemId); Item giftItem = Emulator.getGameEnvironment().getItemManager().getItem(iItemId);
if (giftItem == null) { 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( giftItem = Emulator.getGameEnvironment().getItemManager().getItem(
(Integer) Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray()[ (Integer) Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray()[
Emulator.getRandom().nextInt(Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size()) Emulator.getRandom().nextInt(Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size())
@@ -122,7 +127,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
); );
if (giftItem == null) { 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()); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return; return;
} }
@@ -132,7 +137,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username); Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
if (habbo == null) { 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 = ?")) { try (PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE username = ?")) {
statement.setString(1, username); statement.setString(1, username);
@@ -149,7 +154,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
} }
if (userId == 0) { if (userId == 0) {
LOGGER.error("DEBUG GIFT: receiver not found -> {}", username); LOGGER.debug("receiver not found -> {}", username);
this.client.sendResponse(new GiftReceiverNotFoundComposer()); this.client.sendResponse(new GiftReceiverNotFoundComposer());
return; return;
} }
@@ -157,38 +162,47 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId); CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId);
if (page == null) { if (page == null) {
LOGGER.error("DEBUG GIFT: page null -> {}", pageId); LOGGER.debug("page null -> {}", pageId);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return; return;
} }
if (page.getRank() > this.client.getHabbo().getHabboInfo().getRank().getId() || !page.isEnabled() || !page.isVisible()) { if (page.getRank() > this.client.getHabbo().getHabboInfo().getRank().getId() || !page.isEnabled() || !page.isVisible()) {
LOGGER.error("DEBUG GIFT: page access denied. pageRank={}, userRank={}, enabled={}, visible={}", LOGGER.debug("page access denied. pageRank={}, userRank={}, enabled={}, visible={}", page.getRank(), this.client.getHabbo().getHabboInfo().getRank().getId(), page.isEnabled(), page.isVisible());
page.getRank(),
this.client.getHabbo().getHabboInfo().getRank().getId(),
page.isEnabled(),
page.isVisible());
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL)); this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return; return;
} }
CatalogItem item = page.getCatalogItem(itemId); CatalogItem item = page.getCatalogItem(itemId);
// Search-results gift sends the catalog offer_id as
// itemId, not catalog_items.id - see the same fix in
// CatalogBuyItemEvent. Fall back to scanning the
// page for the matching offer_id.
if (item == null) { if (item == null) {
LOGGER.error("DEBUG GIFT: catalog item null -> {}", itemId); for (CatalogItem candidate : page.getCatalogItems().valueCollection()) {
if (candidate != null && candidate.getOfferId() == itemId) {
item = candidate;
break;
}
}
}
if (item == null) {
LOGGER.debug("catalog item null -> {}", itemId);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return; return;
} }
if (item.isClubOnly() && !this.client.getHabbo().getHabboStats().hasActiveClub()) { 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)); this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.REQUIRES_CLUB));
return; return;
} }
for (Item baseItem : item.getBaseItems()) { for (Item baseItem : item.getBaseItems()) {
if (!baseItem.allowGift()) { 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)); this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return; return;
} }
@@ -196,7 +210,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
if (item.isLimited()) { if (item.isLimited()) {
if (item.getLimitedStack() == item.getLimitedSells()) { 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()); this.client.sendResponse(new AlertLimitedSoldOutComposer());
return; return;
} }
@@ -205,14 +219,14 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
int totalCredits = item.getCredits(); int totalCredits = item.getCredits();
int totalPoints = item.getPoints(); 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() if (totalCredits > this.client.getHabbo().getHabboInfo().getCredits()
|| totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType())) { || totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType())) {
LOGGER.error("DEBUG GIFT: not enough currency. creditsNeeded={}, creditsHave={}, pointsNeeded={}, pointsHave={}, pointsType={}", LOGGER.debug("not enough currency. creditsNeeded={}, pointsNeeded={}, pointsType={}", totalCredits, totalPoints, item.getPointsType());
totalCredits,
this.client.getHabbo().getHabboInfo().getCredits(),
totalPoints,
this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType()),
item.getPointsType());
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL)); this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return; return;
} }
@@ -223,7 +237,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
if (item.isLimited()) { if (item.isLimited()) {
if (Emulator.getGameEnvironment().getCatalogManager().getLimitedConfig(item).available() == 0) { 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()); this.client.sendResponse(new AlertLimitedSoldOutComposer());
return; return;
} }
@@ -231,7 +245,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) { if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) {
int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total"); int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total");
if (this.client.getHabbo().getHabboStats().totalLtds() >= ltdLimit) { 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( this.client.getHabbo().alert(
Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total") Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total")
.replace("%itemname%", item.getBaseItems().iterator().next().getFullName()) .replace("%itemname%", item.getBaseItems().iterator().next().getFullName())
@@ -242,7 +256,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item"); ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item");
if (this.client.getHabbo().getHabboStats().totalLtds(item.getId()) >= ltdLimit) { 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( this.client.getHabbo().alert(
Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item") Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item")
.replace("%itemname%", item.getBaseItems().iterator().next().getFullName()) .replace("%itemname%", item.getBaseItems().iterator().next().getFullName())
@@ -293,20 +307,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
} }
if (badgeFound) { 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)); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.ALREADY_HAVE_BADGE));
return; return;
} }
if (item.getAmount() > 1 || item.getBaseItems().size() > 1) { 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()); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return; return;
} }
for (Item baseItem : item.getBaseItems()) { for (Item baseItem : item.getBaseItems()) {
if (item.getItemAmount(baseItem.getId()) > 1) { 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()); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return; return;
} }
@@ -330,11 +344,11 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
badgeFound = true; badgeFound = true;
} }
} else if (item.getName().startsWith("rentable_bot_")) { } 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()); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return; return;
} else if (Item.isPet(baseItem)) { } 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()); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return; return;
} else { } else {
@@ -370,8 +384,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
HabboItem teleportTwo = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData); HabboItem teleportTwo = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
if (teleportOne == null || teleportTwo == null) { if (teleportOne == null || teleportTwo == null) {
LOGGER.error("DEBUG GIFT: teleport creation failed. baseItemId={}, teleportOneNull={}, teleportTwoNull={}", LOGGER.debug("teleport creation failed. baseItemId={}, teleportOneNull={}, teleportTwoNull={}", baseItem.getId(), teleportOne == null, teleportTwo == null);
baseItem.getId(), teleportOne == null, teleportTwo == null);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return; return;
} }
@@ -384,7 +397,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedNumber, limitedNumber, extraData); HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedNumber, limitedNumber, extraData);
if (habboItem == null) { 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)); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return; return;
} }
@@ -397,13 +410,13 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
HabboItem createdItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData); HabboItem createdItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
if (createdItem == null) { 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)); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return; return;
} }
if (!(createdItem instanceof InteractionGuildFurni)) { 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)); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return; return;
} }
@@ -428,7 +441,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData); HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
if (habboItem == null) { 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)); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return; return;
} }
@@ -437,7 +450,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
} }
} }
} else { } 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 AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
this.client.sendResponse(new GenericAlertComposer(Emulator.getTexts().getValue("error.catalog.buy.not_yet"))); this.client.sendResponse(new GenericAlertComposer(Emulator.getTexts().getValue("error.catalog.buy.not_yet")));
return; return;
@@ -446,7 +459,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
} }
if (itemsList.isEmpty()) { 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)); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return; return;
} }
@@ -455,7 +468,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
for (HabboItem i : itemsList) { for (HabboItem i : itemsList) {
if (i == null) { 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)); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return; return;
} }
@@ -475,10 +488,37 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
.append("\t") .append("\t")
.append(this.client.getHabbo().getHabboInfo().getLook()); .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); HabboItem gift = Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, giftData.toString(), 0, 0);
if (gift == null) { 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)); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return; return;
} }
@@ -486,9 +526,8 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
if (limitedConfiguration != null) { if (limitedConfiguration != null) {
for (HabboItem itm : itemsList) { for (HabboItem itm : itemsList) {
if (itm == null) { if (itm == null) {
LOGGER.error("DEBUG GIFT: null item before limitedSold()"); // Trip the catch path so the deduction is refunded.
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); throw new IllegalStateException("null item before limitedSold()");
return;
} }
limitedConfiguration.limitedSold(item.getId(), this.client.getHabbo(), itm); limitedConfiguration.limitedSold(item.getId(), this.client.getHabbo(), itm);
} }
@@ -526,32 +565,189 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
); );
} }
if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) { // Gift fully delivered; commit cooldown and clear refund tracking so the catch block can't double-refund.
if (totalCredits > 0) { this.client.getHabbo().getHabboStats().lastGiftTimestamp = Emulator.getIntUnixTimestamp();
this.client.getHabbo().giveCredits(-totalCredits); 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)); this.client.sendResponse(new PurchaseOKComposer(item));
} }
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Exception caught", e); LOGGER.error("Exception caught", e);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); 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 { } finally {
this.client.getHabbo().getHabboStats().isPurchasingFurniture = false; this.client.getHabbo().getHabboStats().isPurchasingFurniture = false;
} }
} else { } else {
LOGGER.error("DEBUG GIFT: cooldown blocked purchase"); LOGGER.debug("cooldown blocked purchase");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); 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;
}
}
} }
@@ -5,6 +5,7 @@ import com.eu.habbo.habbohotel.bots.BotManager;
import com.eu.habbo.habbohotel.catalog.*; import com.eu.habbo.habbohotel.catalog.*;
import com.eu.habbo.habbohotel.catalog.layouts.*; import com.eu.habbo.habbohotel.catalog.layouts.*;
import com.eu.habbo.habbohotel.items.FurnitureType; import com.eu.habbo.habbohotel.items.FurnitureType;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.pets.PetManager; import com.eu.habbo.habbohotel.pets.PetManager;
import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport;
@@ -201,15 +202,48 @@ public class CatalogBuyItemEvent extends MessageHandler {
else else
item = page.getCatalogItem(itemId); item = page.getCatalogItem(itemId);
// temp patch, can a dev with better knowledge than me look into this asap pls.
if (page instanceof BotsLayout) { // Search-results buy sends the catalog offer_id as itemId
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_BOTS) && this.client.getHabbo().getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) { // (FurnitureOffer.offerId is derived from furnidata's
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + "")); // purchaseOfferId, which matches `catalog_items.offer_id`),
return; // not the `catalog_items.id` primary key that getCatalogItem
// expects. Fall back to scanning the page for the matching
// offer_id so the search buy flow works.
if (item == null && !(page instanceof RecentPurchasesLayout)) {
for (CatalogItem candidate : page.getCatalogItems().valueCollection()) {
if (candidate != null && candidate.getOfferId() == itemId) {
item = candidate;
break;
}
} }
} }
if (page instanceof PetsLayout) { // Inventory cap check based on the actual base items the
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_PETS) && this.client.getHabbo().getInventory().getPetsComponent().getPets().size() >= PetManager.MAXIMUM_PET_INVENTORY_SIZE) { // purchase will create, not the page layout - bots/pets
// can legitimately live on bundle pages, search results,
// recent-purchases, etc., and the layout-instanceof check
// missed all those paths. Mirrors the bot/pet branches
// inside CatalogManager.purchaseItem (Item.isBot / isPet
// and the same prefix check) so detection stays in sync.
boolean itemHasBot = false;
boolean itemHasPet = false;
if (item != null) {
for (Item baseItem : item.getBaseItems()) {
if (baseItem == null) continue;
if (Item.isBot(baseItem)) itemHasBot = true;
if (Item.isPet(baseItem)) itemHasPet = true;
}
}
if (itemHasBot && !this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_BOTS)
&& this.client.getHabbo().getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
return;
}
if (itemHasPet) {
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_PETS)
&& this.client.getHabbo().getInventory().getPetsComponent().getPets().size() >= PetManager.MAXIMUM_PET_INVENTORY_SIZE) {
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.pets.max.inventory").replace("%amount%", PetManager.MAXIMUM_PET_INVENTORY_SIZE + "")); this.client.getHabbo().alert(Emulator.getTexts().getValue("error.pets.max.inventory").replace("%amount%", PetManager.MAXIMUM_PET_INVENTORY_SIZE + ""));
return; return;
} }
@@ -36,8 +36,21 @@ public class CatalogAdminCreatePageEvent extends MessageHandler {
pageLayout = CatalogPageLayouts.default_3x3; 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( 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) { if (page == null) {
@@ -1,6 +1,7 @@
package com.eu.habbo.messages.incoming.catalog.catalogadmin; package com.eu.habbo.messages.incoming.catalog.catalogadmin;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.catalog.CatalogPage;
import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.catalog.CatalogPageType;
import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
@@ -11,6 +12,9 @@ import java.sql.PreparedStatement;
public class CatalogAdminMovePageEvent extends MessageHandler { public class CatalogAdminMovePageEvent extends MessageHandler {
private static final int MAX_PARENT_WALK = 64;
private static final int ROOT_PARENT_ID = -1;
@Override @Override
public void handle() throws Exception { public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
@@ -24,12 +28,10 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
String tableName = (pageType == CatalogPageType.BUILDER) ? "catalog_pages_bc" : "catalog_pages"; String tableName = (pageType == CatalogPageType.BUILDER) ? "catalog_pages_bc" : "catalog_pages";
// Special values: -1 = toggle enabled, -2 = toggle visible
if (newParentId == -1) { if (newParentId == -1) {
// Toggle enabled
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement( 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.setInt(1, pageId);
statement.execute(); statement.execute();
} }
@@ -38,21 +40,43 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
} }
if (newParentId == -2) { if (newParentId == -2) {
// Toggle visible
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement( 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.setInt(1, pageId);
statement.execute(); statement.execute();
} }
this.client.sendResponse(new CatalogAdminResultComposer(true, "Visibility toggled")); this.client.sendResponse(new CatalogAdminResultComposer(true, "Visibility toggled"));
return; 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(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement( 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(1, newParentId);
statement.setInt(2, newIndex); statement.setInt(2, newIndex);
statement.setInt(3, pageId); statement.setInt(3, pageId);
@@ -61,4 +85,16 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
this.client.sendResponse(new CatalogAdminResultComposer(true, "Page moved")); 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.Emulator;
import com.eu.habbo.habbohotel.catalog.CatalogPage; 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.catalog.CatalogPageType;
import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; 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.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
public class CatalogAdminSavePageEvent extends MessageHandler { 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 @Override
public void handle() throws Exception { public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
@@ -34,7 +53,7 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
String textDetails = this.packet.readString(); String textDetails = this.packet.readString();
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
CatalogPageType catalogMode = 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); CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType);
if (page == null) { if (page == null) {
@@ -42,9 +61,55 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
return; 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) 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_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 = ?, catalog_mode = ? 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(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(query)) { PreparedStatement statement = connection.prepareStatement(query)) {
@@ -60,7 +125,8 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
statement.setString(8, headline); statement.setString(8, headline);
statement.setString(9, teaser); statement.setString(9, teaser);
statement.setString(10, textDetails); statement.setString(10, textDetails);
statement.setInt(11, pageId); statement.setString(11, text1);
statement.setInt(12, pageId);
} else { } else {
statement.setString(2, caption2); statement.setString(2, caption2);
statement.setString(3, layout); statement.setString(3, layout);
@@ -73,8 +139,9 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
statement.setString(10, headline); statement.setString(10, headline);
statement.setString(11, teaser); statement.setString(11, teaser);
statement.setString(12, textDetails); statement.setString(12, textDetails);
statement.setString(13, catalogMode.name()); statement.setString(13, text1);
statement.setInt(14, pageId); statement.setString(14, catalogMode.name());
statement.setInt(15, pageId);
} }
statement.execute(); statement.execute();
@@ -82,4 +149,28 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
this.client.sendResponse(new CatalogAdminResultComposer(true, "Page saved")); 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);
}
} }
@@ -4,23 +4,36 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.*; import com.eu.habbo.habbohotel.rooms.*;
import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer;
import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer;
import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer;
import com.eu.habbo.messages.outgoing.rooms.ForwardToRoomComposer; import com.eu.habbo.messages.outgoing.rooms.ForwardToRoomComposer;
import gnu.trove.set.hash.THashSet; import gnu.trove.set.hash.THashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner; import java.util.StringJoiner;
import java.util.regex.Pattern;
public class FloorPlanEditorSaveEvent extends MessageHandler { public class FloorPlanEditorSaveEvent extends MessageHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(FloorPlanEditorSaveEvent.class);
public static int MAXIMUM_FLOORPLAN_WIDTH_LENGTH = 64; public static int MAXIMUM_FLOORPLAN_WIDTH_LENGTH = 64;
public static int MAXIMUM_FLOORPLAN_SIZE = 64 * 64; public static int MAXIMUM_FLOORPLAN_SIZE = 64 * 64;
private static final int SAVE_COOLDOWN_SECONDS = 3;
private static final int MAX_AUTO_PICKUP_ITEMS = 500;
private static final Pattern ALLOWED_MAP_CHARS = Pattern.compile("[a-zA-Z0-9\r]+");
@Override @Override
public int getRatelimit() { public int getRatelimit() {
return 500; return 500;
@@ -38,153 +51,244 @@ public class FloorPlanEditorSaveEvent extends MessageHandler {
if (room == null) if (room == null)
return; return;
if (room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) { if (!(room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER))) {
StringJoiner errors = new StringJoiner("<br />"); return;
String map = this.packet.readString(); }
map = map.replace("X", "x");
String[] mapRows = map.split("\r"); long now = Emulator.getIntUnixTimestamp();
if (now - this.client.getHabbo().getHabboStats().lastFloorplanSaveTimestamp < SAVE_COOLDOWN_SECONDS) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, "Please wait a few seconds before saving again."));
return;
}
int firstRowSize = mapRows[0].length(); StringJoiner errors = new StringJoiner("<br />");
String map = this.packet.readString();
if (Emulator.getConfig().getBoolean("hotel.room.floorplan.check.enabled")) { if (map == null || map.length() > MAXIMUM_FLOORPLAN_SIZE) {
if (!map.matches("[a-zA-Z0-9\r]+")) errors.add("${notification.floorplan_editor.error.title}"); LOGGER.warn("Floorplan save rejected (oversize): user={} room={} mapLen={}",
this.client.getHabbo().getHabboInfo().getId(), room.getId(), map == null ? 0 : map.length());
return;
}
Arrays.stream(mapRows) if (!ALLOWED_MAP_CHARS.matcher(map).matches()) {
.filter(line -> line.length() != firstRowSize) LOGGER.warn("Floorplan save rejected (illegal chars): user={} room={}",
.findAny() this.client.getHabbo().getHabboInfo().getId(), room.getId());
.ifPresent(s -> errors.add("(General): Line " + (Arrays.asList(mapRows).indexOf(s) + 1) + " is of different length than line 1")); return;
}
if (map.isEmpty() || map.replace("x", "").replace("\r", "").isEmpty()) { map = map.replace("X", "x");
errors.add("${notification.floorplan_editor.error.message.effective_height_is_0}");
String[] mapRows = map.split("\r");
if (mapRows.length == 0 || mapRows.length > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) {
return;
}
int firstRowSize = mapRows[0].length();
if (firstRowSize == 0 || firstRowSize > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) {
return;
}
for (String row : mapRows) {
if (row.length() != firstRowSize) {
return;
}
}
if (Emulator.getConfig().getBoolean("hotel.room.floorplan.check.enabled")) {
if (map.replace("x", "").replace("\r", "").isEmpty()) {
errors.add("${notification.floorplan_editor.error.message.effective_height_is_0}");
}
}
int doorX = this.packet.readInt();
int doorY = this.packet.readInt();
if (doorX < 0 || doorX >= firstRowSize || doorY < 0 || doorY >= mapRows.length) {
errors.add("${notification.floorplan_editor.error.message.entry_tile_outside_map}");
} else if (mapRows[doorY].charAt(doorX) == 'x') {
errors.add("${notification.floorplan_editor.error.message.entry_not_on_tile}");
}
int doorRotation = this.packet.readInt();
if (doorRotation < 0 || doorRotation > 7) {
errors.add("${notification.floorplan_editor.error.message.invalid_entry_tile_direction}");
}
int wallSize = this.packet.readInt();
if (wallSize < -2 || wallSize > 1) {
errors.add("${notification.floorplan_editor.error.message.invalid_wall_thickness}");
}
int floorSize = this.packet.readInt();
if (floorSize < -2 || floorSize > 1) {
errors.add("${notification.floorplan_editor.error.message.invalid_floor_thickness}");
}
int wallHeight = -1;
if (this.packet.bytesAvailable() >= 4)
wallHeight = this.packet.readInt();
if (wallHeight < -1 || wallHeight > 15) {
errors.add("${notification.floorplan_editor.error.message.invalid_walls_fixed_height}");
}
boolean autoPickup = false;
if (this.packet.bytesAvailable() >= 1) {
autoPickup = this.packet.readBoolean();
}
if (errors.length() > 0) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString()));
return;
}
THashSet<RoomTile> locked_tileList = room.getLockedTiles();
THashSet<RoomTile> new_tileList = new THashSet<>();
THashSet<HabboItem> itemsToPickup = new THashSet<>();
int blockedX = -1;
int blockedY = -1;
blockingRoomItemScan:
for (int y = 0; y < mapRows.length; y++) {
for (int x = 0; x < firstRowSize; x++) {
RoomTile tile = room.getLayout().getTile((short) x, (short) y);
new_tileList.add(tile);
String square = String.valueOf(mapRows[y].charAt(x));
short height;
if (square.equalsIgnoreCase("x") && room.getTopItemAt(x, y) != null) {
if (autoPickup) {
THashSet<HabboItem> here = room.getItemsAt(x, y);
if (here != null) itemsToPickup.addAll(here);
continue;
}
blockedX = x;
blockedY = y;
break blockingRoomItemScan;
} }
if (map.length() > MAXIMUM_FLOORPLAN_SIZE) { try {
errors.add("${notification.floorplan_editor.error.message.too_large_area}"); if (square.isEmpty()) {
} height = 0;
} else if (Emulator.isNumeric(square)) {
if (mapRows.length > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) errors.add("${notification.floorplan_editor.error.message.too_large_height}"); height = Short.parseShort(square);
else if (Arrays.stream(mapRows).anyMatch(l -> l.length() > MAXIMUM_FLOORPLAN_WIDTH_LENGTH || l.isEmpty())) errors.add("${notification.floorplan_editor.error.message.too_large_width}"); } else {
int idx = "abcdefghijklmnopqrstuvwxyz".indexOf(square.toLowerCase());
if (errors.length() > 0) { if (idx < 0) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString())); return;
}
height = (short) Math.min(26, 10 + idx);
}
} catch (NumberFormatException e) {
return; return;
} }
}
int doorX = this.packet.readInt(); if (tile != null && tile.state != RoomTileState.INVALID && height != tile.z && room.getTopItemAt(x, y) != null) {
int doorY = this.packet.readInt(); if (autoPickup) {
THashSet<HabboItem> here = room.getItemsAt(x, y);
if (doorX < 0 || doorX > firstRowSize || doorY < 0 || doorY >= mapRows.length) { if (here != null) itemsToPickup.addAll(here);
errors.add("${notification.floorplan_editor.error.message.entry_tile_outside_map}"); continue;
}
if (doorY < mapRows.length && doorX < mapRows[doorY].length() && mapRows[doorY].charAt(doorX) == 'x') {
errors.add("${notification.floorplan_editor.error.message.entry_not_on_tile}");
}
int doorRotation = this.packet.readInt();
if (doorRotation < 0 || doorRotation > 7) {
errors.add("${notification.floorplan_editor.error.message.invalid_entry_tile_direction}");
}
int wallSize = this.packet.readInt();
if (wallSize < -2 || wallSize > 1) {
errors.add("${notification.floorplan_editor.error.message.invalid_wall_thickness}");
}
int floorSize = this.packet.readInt();
if (floorSize < -2 || floorSize > 1) {
errors.add("${notification.floorplan_editor.error.message.invalid_floor_thickness}");
}
int wallHeight = -1;
if (this.packet.bytesAvailable() >= 4)
wallHeight = this.packet.readInt();
if (wallHeight < -1 || wallHeight > 15) {
errors.add("${notification.floorplan_editor.error.message.invalid_walls_fixed_height}");
}
THashSet<RoomTile> locked_tileList = room.getLockedTiles();
THashSet<RoomTile> new_tileList = new THashSet<>();
blockingRoomItemScan:
for (int y = 0; y < mapRows.length; y++) {
for (int x = 0; x < firstRowSize; x++) {
RoomTile tile = room.getLayout().getTile((short) x, (short) y);
new_tileList.add(tile);
String square = String.valueOf(mapRows[y].charAt(x));
short height;
if (square.equalsIgnoreCase("x") && room.getTopItemAt(x, y) != null) {
errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
break blockingRoomItemScan;
} else {
if (square.isEmpty()) {
height = 0;
} else if (Emulator.isNumeric(square)) {
height = Short.parseShort(square);
} else {
height = (short) (10 + "ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(square.toUpperCase()));
}
}
if (tile != null && tile.state != RoomTileState.INVALID && height != tile.z && room.getTopItemAt(x, y) != null) {
errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
break blockingRoomItemScan;
} }
blockedX = x;
blockedY = y;
break blockingRoomItemScan;
} }
} }
}
if (blockedX < 0) {
locked_tileList.removeAll(new_tileList); locked_tileList.removeAll(new_tileList);
if (!locked_tileList.isEmpty()) { if (!locked_tileList.isEmpty()) {
errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}"); if (autoPickup) {
for (RoomTile lt : locked_tileList) {
THashSet<HabboItem> here = room.getItemsAt(lt.x, lt.y);
if (here != null) itemsToPickup.addAll(here);
}
} else {
RoomTile first = locked_tileList.iterator().next();
blockedX = first.x;
blockedY = first.y;
}
} }
}
if (blockedX >= 0) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key,
"${notification.floorplan_editor.error.message.change_blocked_by_room_item} (" + blockedX + ", " + blockedY + ")"));
return;
}
if (autoPickup && !itemsToPickup.isEmpty()) {
if (errors.length() > 0) { if (itemsToPickup.size() > MAX_AUTO_PICKUP_ITEMS) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString())); LOGGER.warn("Floorplan auto-pickup rejected (over cap): user={} room={} itemCount={} cap={}",
this.client.getHabbo().getHabboInfo().getId(), room.getId(), itemsToPickup.size(), MAX_AUTO_PICKUP_ITEMS);
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key,
"Too many items would be picked up (" + itemsToPickup.size() + " > " + MAX_AUTO_PICKUP_ITEMS + "). Remove some furniture manually and save again."));
return; return;
} }
RoomLayout layout = room.getLayout(); Map<Integer, ArrayList<HabboItem>> byOwner = new HashMap<>();
for (HabboItem itm : itemsToPickup) {
if (layout instanceof CustomRoomLayout) { if (itm == null) continue;
layout.setDoorX((short) doorX); byOwner.computeIfAbsent(itm.getUserId(), k -> new ArrayList<>()).add(itm);
layout.setDoorY((short) doorY); room.pickUpItem(itm, null);
layout.setDoorDirection(doorRotation);
layout.setHeightmap(map);
layout.parse();
if (layout.getDoorTile() == null) {
this.client.getHabbo().alert("Error");
((CustomRoomLayout) layout).needsUpdate(false);
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
return;
}
((CustomRoomLayout) layout).needsUpdate(true);
Emulator.getThreading().run((CustomRoomLayout) layout);
} else {
layout = Emulator.getGameEnvironment().getRoomManager().insertCustomLayout(room, map, doorX, doorY, doorRotation);
} }
if (layout != null) { for (Map.Entry<Integer, ArrayList<HabboItem>> entry : byOwner.entrySet()) {
room.setHasCustomLayout(true); Habbo owner = Emulator.getGameEnvironment().getHabboManager().getHabbo(entry.getKey());
room.setNeedsUpdate(true); if (owner == null) continue;
room.setLayout(layout); for (HabboItem itm : entry.getValue()) {
room.setWallSize(wallSize); owner.getClient().sendResponse(new AddHabboItemComposer(itm));
room.setFloorSize(floorSize);
room.setWallHeight(wallHeight);
room.save();
Collection<Habbo> habbos = new ArrayList<>(room.getUserCount());
habbos.addAll(room.getHabbos());
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
room = Emulator.getGameEnvironment().getRoomManager().loadRoom(room.getId());
ServerMessage message = new ForwardToRoomComposer(room.getId()).compose();
for (Habbo habbo : habbos) {
habbo.getClient().sendResponse(message);
} }
owner.getClient().sendResponse(new InventoryRefreshComposer());
}
LOGGER.info("Floorplan auto-pickup: user={} room={} itemCount={} owners={}",
this.client.getHabbo().getHabboInfo().getId(), room.getId(), itemsToPickup.size(), byOwner.size());
}
RoomLayout layout = room.getLayout();
if (layout instanceof CustomRoomLayout) {
layout.setDoorX((short) doorX);
layout.setDoorY((short) doorY);
layout.setDoorDirection(doorRotation);
layout.setHeightmap(map);
layout.parse();
if (layout.getDoorTile() == null) {
this.client.getHabbo().alert("Error");
((CustomRoomLayout) layout).needsUpdate(false);
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
return;
}
((CustomRoomLayout) layout).needsUpdate(true);
Emulator.getThreading().run((CustomRoomLayout) layout);
} else {
layout = Emulator.getGameEnvironment().getRoomManager().insertCustomLayout(room, map, doorX, doorY, doorRotation);
}
if (layout != null) {
room.setHasCustomLayout(true);
room.setNeedsUpdate(true);
room.setLayout(layout);
room.setWallSize(wallSize);
room.setFloorSize(floorSize);
room.setWallHeight(wallHeight);
room.save();
this.client.getHabbo().getHabboStats().lastFloorplanSaveTimestamp = now;
LOGGER.info("Floorplan saved: user={} room={} mapLen={} rows={} cols={}",
this.client.getHabbo().getHabboInfo().getId(), room.getId(), map.length(), mapRows.length, firstRowSize);
Collection<Habbo> habbos = new ArrayList<>(room.getUserCount());
habbos.addAll(room.getHabbos());
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
room = Emulator.getGameEnvironment().getRoomManager().loadRoom(room.getId());
ServerMessage message = new ForwardToRoomComposer(room.getId()).compose();
for (Habbo habbo : habbos) {
habbo.getClient().sendResponse(message);
} }
} }
} }
@@ -25,45 +25,55 @@ public class GuildAcceptMembershipEvent extends MessageHandler {
int userId = this.packet.readInt(); int userId = this.packet.readInt();
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId); Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
if (guild == null) {
return;
}
GuildMember actorMember = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo());
boolean canAccept = guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId()
|| this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)
|| (actorMember != null && (actorMember.getRank().equals(GuildRank.ADMIN) || actorMember.getRank().equals(GuildRank.OWNER)));
if (!canAccept) {
return;
}
GuildMember targetMember = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId);
if (targetMember == null) {
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.NO_LONGER_MEMBER));
return;
}
if (targetMember.getRank().type != GuildRank.REQUESTED.type) {
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.ALREADY_ACCEPTED));
return;
}
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
if (guild != null) { GuildAcceptedMembershipEvent event = new GuildAcceptedMembershipEvent(guild, userId, habbo);
GuildMember groupMember = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo()); Emulator.getPluginManager().fireEvent(event);
if (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId()
|| this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN) if (event.isCancelled()) {
|| (groupMember != null && (groupMember.getRank().equals(GuildRank.ADMIN) || groupMember.getRank().equals(GuildRank.OWNER)))) { return;
if (habbo != null) { }
if (habbo.getHabboStats().hasGuild(guild.getId())) {
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.ALREADY_ACCEPTED)); if (habbo != null) {
return; habbo.getHabboStats().addGuild(guild.getId());
} else { }
//Check the user has requested
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, habbo); Emulator.getGameEnvironment().getGuildManager().joinGuild(guild, this.client, userId, true);
if (member == null || member.getRank().type != GuildRank.REQUESTED.type) { guild.decreaseRequestCount();
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.NO_LONGER_MEMBER)); guild.increaseMemberCount();
return; this.client.sendResponse(new GuildRefreshMembersListComposer(guild));
} else {
GuildAcceptedMembershipEvent event = new GuildAcceptedMembershipEvent(guild, userId, habbo); if (habbo != null) {
Emulator.getPluginManager().fireEvent(event); Room room = habbo.getHabboInfo().getCurrentRoom();
if (!event.isCancelled()) { if (room != null && room.getGuildId() == guildId) {
habbo.getHabboStats().addGuild(guild.getId()); habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId)));
Emulator.getGameEnvironment().getGuildManager().joinGuild(guild, this.client, habbo.getHabboInfo().getId(), true); room.refreshRightsForHabbo(habbo);
guild.decreaseRequestCount();
guild.increaseMemberCount();
this.client.sendResponse(new GuildRefreshMembersListComposer(guild));
Room room = habbo.getHabboInfo().getCurrentRoom();
if (room != null) {
if (room.getGuildId() == guildId) {
habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId)));
room.refreshRightsForHabbo(habbo);
}
}
}
}
}
} else {
Emulator.getGameEnvironment().getGuildManager().joinGuild(guild, this.client, userId, true);
}
} }
} }
} }
@@ -29,6 +29,11 @@ public class GuildDeclineMembershipEvent extends MessageHandler {
if (guild != null) { if (guild != null) {
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo()); GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo());
if (userId == this.client.getHabbo().getHabboInfo().getId() || guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || (member != null && (member.getRank().equals(GuildRank.ADMIN) || member.getRank().equals(GuildRank.OWNER))) || this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)) { if (userId == this.client.getHabbo().getHabboInfo().getId() || guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || (member != null && (member.getRank().equals(GuildRank.ADMIN) || member.getRank().equals(GuildRank.OWNER))) || this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)) {
GuildMember target = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId);
if (target == null || target.getRank().type != GuildRank.REQUESTED.type) {
return;
}
guild.decreaseRequestCount(); guild.decreaseRequestCount();
Emulator.getGameEnvironment().getGuildManager().removeMember(guild, userId); Emulator.getGameEnvironment().getGuildManager().removeMember(guild, userId);
this.client.sendResponse(new GuildMembersComposer(guild, Emulator.getGameEnvironment().getGuildManager().getGuildMembers(guild, 0, 0, ""), this.client.getHabbo(), 0, 0, "", true, Emulator.getGameEnvironment().getGuildManager().getGuildMembersCount(guild, 0, 0, ""))); this.client.sendResponse(new GuildMembersComposer(guild, Emulator.getGameEnvironment().getGuildManager().getGuildMembers(guild, 0, 0, ""), this.client.getHabbo(), 0, 0, "", true, Emulator.getGameEnvironment().getGuildManager().getGuildMembersCount(guild, 0, 0, "")));
@@ -30,20 +30,69 @@ public class RequestGuildBuyEvent extends MessageHandler {
final String name = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo()); final String name = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo());
final String description = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo()); final String description = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo());
if(name.length() > 29){ if (name.length() == 0 || name.length() > 29) {
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.INVALID_GUILD_NAME)); this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.INVALID_GUILD_NAME));
return; return;
} }
if(description.length() > 254){ if (description.length() > 254) {
return; return;
} }
if (Emulator.getConfig().getBoolean("catalog.guild.hc_required", true) && !this.client.getHabbo().getHabboStats().hasActiveClub()) { if (Emulator.getConfig().getBoolean("catalog.guild.hc_required", true) && !this.client.getHabbo().getHabboStats().hasActiveClub()) {
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.HC_REQUIRED)); this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.HC_REQUIRED));
return; return;
} }
int roomId = this.packet.readInt();
Room r = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
if (r == null) {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
if (r.hasGuild() || r.getGuildId() != 0) {
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.ROOM_ALREADY_IN_USE));
return;
}
if (r.getOwnerId() != this.client.getHabbo().getHabboInfo().getId()) {
String message = Emulator.getTexts().getValue("scripter.warning.guild.buy.owner").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%roomname%", r.getName().replace("%owner%", r.getOwnerName()));
ScripterManager.scripterDetected(this.client, message);
LOGGER.info(message);
return;
}
int colorOne = this.packet.readInt();
int colorTwo = this.packet.readInt();
int count = this.packet.readInt();
StringBuilder badge = new StringBuilder();
byte base = 1;
while (base < count) {
int id = this.packet.readInt();
int color = this.packet.readInt();
int pos = this.packet.readInt();
if (base == 1) {
badge.append("b");
} else {
badge.append("s");
}
badge.append(id < 100 ? "0" : "").append(id < 10 ? "0" : "").append(id).append(color < 10 ? "0" : "").append(color).append(pos);
base += 3;
}
// Only charge the player once every step has been validated. Previously the
// credits were deducted before the room was checked, so a purchase that
// failed afterwards (missing room, room already used by a guild, not the
// owner) still took the credits without ever creating the group.
if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) { if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) {
int guildPrice = Emulator.getConfig().getInt("catalog.guild.price"); int guildPrice = Emulator.getConfig().getInt("catalog.guild.price");
if (this.client.getHabbo().getHabboInfo().getCredits() >= guildPrice) { if (this.client.getHabbo().getHabboInfo().getCredits() >= guildPrice) {
@@ -54,78 +103,34 @@ public class RequestGuildBuyEvent extends MessageHandler {
} }
} }
int roomId = this.packet.readInt(); Guild guild = Emulator.getGameEnvironment().getGuildManager().createGuild(this.client.getHabbo(), roomId, r.getName(), name, description, badge.toString(), colorOne, colorTwo);
Room r = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId); r.setGuild(guild.getId());
r.removeAllRights();
r.setNeedsUpdate(true);
if (r != null) { Emulator.getGameEnvironment().getGuildManager().addGuild(guild);
if (r.hasGuild()) {
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.ROOM_ALREADY_IN_USE)); if (Emulator.getConfig().getBoolean("imager.internal.enabled")) {
return; Emulator.getBadgeImager().generate(guild);
}
this.client.sendResponse(new PurchaseOKComposer());
this.client.sendResponse(new GuildBoughtComposer(guild));
r.refreshGuild(guild);
for (Habbo habbo : r.getHabbos()) {
if (habbo.getClient() == null) {
continue;
} }
if (r.getOwnerId() == this.client.getHabbo().getHabboInfo().getId()) { habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, null));
if (r.getGuildId() == 0) {
int colorOne = this.packet.readInt();
int colorTwo = this.packet.readInt();
int count = this.packet.readInt(); if (habbo.getHabboInfo().getId() != this.client.getHabbo().getHabboInfo().getId()) {
habbo.getClient().sendResponse(new RoomDataComposer(r, habbo, true, false));
StringBuilder badge = new StringBuilder();
byte base = 1;
while (base < count) {
int id = this.packet.readInt();
int color = this.packet.readInt();
int pos = this.packet.readInt();
if (base == 1) {
badge.append("b");
} else {
badge.append("s");
}
badge.append(id < 100 ? "0" : "").append(id < 10 ? "0" : "").append(id).append(color < 10 ? "0" : "").append(color).append(pos);
base += 3;
}
Guild guild = Emulator.getGameEnvironment().getGuildManager().createGuild(this.client.getHabbo(), roomId, r.getName(), name, description, badge.toString(), colorOne, colorTwo);
r.setGuild(guild.getId());
r.removeAllRights();
r.setNeedsUpdate(true);
Emulator.getGameEnvironment().getGuildManager().addGuild(guild);
if (Emulator.getConfig().getBoolean("imager.internal.enabled")) {
Emulator.getBadgeImager().generate(guild);
}
this.client.sendResponse(new PurchaseOKComposer());
this.client.sendResponse(new GuildBoughtComposer(guild));
r.refreshGuild(guild);
for (Habbo habbo : r.getHabbos()) {
if (habbo.getClient() == null) {
continue;
}
habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, null));
if (habbo.getHabboInfo().getId() != this.client.getHabbo().getHabboInfo().getId()) {
habbo.getClient().sendResponse(new RoomDataComposer(r, habbo, true, false));
}
}
Emulator.getPluginManager().fireEvent(new GuildPurchasedEvent(guild, this.client.getHabbo()));
}
} else {
String message = Emulator.getTexts().getValue("scripter.warning.guild.buy.owner").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%roomname%", r.getName().replace("%owner%", r.getOwnerName()));
ScripterManager.scripterDetected(this.client, message);
LOGGER.info(message);
} }
} }
Emulator.getPluginManager().fireEvent(new GuildPurchasedEvent(guild, this.client.getHabbo()));
} }
} }
@@ -2,7 +2,11 @@ package com.eu.habbo.messages.incoming.guilds.forums;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.guilds.Guild; import com.eu.habbo.habbohotel.guilds.Guild;
import com.eu.habbo.habbohotel.guilds.GuildMember;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer; import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer;
public class GuildForumDataEvent extends MessageHandler { public class GuildForumDataEvent extends MessageHandler {
@@ -20,10 +24,18 @@ public class GuildForumDataEvent extends MessageHandler {
if (guild == null) return; if (guild == null) return;
if (!guild.hasForum()) return; if (!guild.hasForum()) return;
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId());
boolean staff = this.client.getHabbo().hasPermission(Permission.ACC_MODTOOL_TICKET_Q);
if (!guild.canHabboReadForum(this.client.getHabbo().getHabboInfo().getId(), member, staff)) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FORUMS_ACCESS_DENIED.key).compose());
return;
}
this.client.sendResponse(new GuildForumDataComposer(guild, this.client.getHabbo())); this.client.sendResponse(new GuildForumDataComposer(guild, this.client.getHabbo()));
if (!Emulator.getGameEnvironment().getGuildManager().hasViewedForum(this.client.getHabbo().getHabboInfo().getId(), guildId)) { if (!Emulator.getGameEnvironment().getGuildManager().hasViewedForum(this.client.getHabbo().getHabboInfo().getId(), guildId)) {
Emulator.getGameEnvironment().getGuildManager().addView(this.client.getHabbo().getHabboInfo().getId(), guildId); Emulator.getGameEnvironment().getGuildManager().addView(this.client.getHabbo().getHabboInfo().getId(), guildId);
} }
} }
} }
@@ -2,7 +2,11 @@ package com.eu.habbo.messages.incoming.guilds.forums;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.guilds.Guild; import com.eu.habbo.habbohotel.guilds.Guild;
import com.eu.habbo.habbohotel.guilds.GuildMember;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer; import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer;
import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumThreadsComposer; import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumThreadsComposer;
import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer; import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer;
@@ -24,8 +28,15 @@ public class GuildForumThreadsEvent extends MessageHandler {
this.client.sendResponse(new ConnectionErrorComposer(404)); this.client.sendResponse(new ConnectionErrorComposer(404));
return; return;
} }
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId());
boolean staff = this.client.getHabbo().hasPermission(Permission.ACC_MODTOOL_TICKET_Q);
if (!guild.canHabboReadForum(this.client.getHabbo().getHabboInfo().getId(), member, staff)) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FORUMS_ACCESS_DENIED.key).compose());
return;
}
this.client.sendResponse(new GuildForumDataComposer(guild, this.client.getHabbo())); this.client.sendResponse(new GuildForumDataComposer(guild, this.client.getHabbo()));
this.client.sendResponse(new GuildForumThreadsComposer(guild, index)); this.client.sendResponse(new GuildForumThreadsComposer(guild, index));
} }
} }
@@ -38,7 +38,6 @@ public class GuildForumThreadsMessagesEvent extends MessageHandler {
return; return;
} }
// Verify thread belongs to the requested guild
if (thread.getGuildId() != guildId) { if (thread.getGuildId() != guildId) {
this.client.sendResponse(new ConnectionErrorComposer(403)); this.client.sendResponse(new ConnectionErrorComposer(403));
return; return;
@@ -47,6 +46,11 @@ public class GuildForumThreadsMessagesEvent extends MessageHandler {
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId()); GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId());
boolean isGuildAdministrator = (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || (member != null && member.getRank().equals(GuildRank.ADMIN))); boolean isGuildAdministrator = (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || (member != null && member.getRank().equals(GuildRank.ADMIN)));
if (!guild.canHabboReadForum(this.client.getHabbo().getHabboInfo().getId(), member, hasStaffPermissions)) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FORUMS_ACCESS_DENIED.key).compose());
return;
}
if (thread.getState() != ForumThreadState.HIDDEN_BY_GUILD_ADMIN || hasStaffPermissions || isGuildAdministrator) { if (thread.getState() != ForumThreadState.HIDDEN_BY_GUILD_ADMIN || hasStaffPermissions || isGuildAdministrator) {
this.client.sendResponse(new GuildForumCommentsComposer(guildId, threadId, index, thread.getComments(limit, index))); this.client.sendResponse(new GuildForumCommentsComposer(guildId, threadId, index, thread.getComments(limit, index)));
this.client.sendResponse(new GuildForumDataComposer(guild, this.client.getHabbo())); this.client.sendResponse(new GuildForumDataComposer(guild, this.client.getHabbo()));
@@ -1,5 +1,6 @@
package com.eu.habbo.messages.incoming.handshake; package com.eu.habbo.messages.incoming.handshake;
import com.eu.habbo.Emulator;
import com.eu.habbo.messages.NoAuthMessage; import com.eu.habbo.messages.NoAuthMessage;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -24,6 +25,15 @@ public class MachineIDEvent extends MessageHandler {
this.client.setMachineId(storedMachineId); this.client.setMachineId(storedMachineId);
// Persist the machine fingerprint onto the user so machine/super bans can
// target it (createOfflineUserBan copies users.machine_id). The Nitro client
// sends this UniqueID packet right after the SSO ticket, so the Habbo is
// normally already loaded by the time we get here.
if (!storedMachineId.isEmpty() && this.client.getHabbo() != null && this.client.getHabbo().getHabboInfo() != null) {
this.client.getHabbo().getHabboInfo().setMachineID(storedMachineId);
Emulator.getThreading().run(this.client.getHabbo());
}
LOGGER.debug("Setting client MachineId to {}", storedMachineId); LOGGER.debug("Setting client MachineId to {}", storedMachineId);
} }
} }
@@ -133,17 +133,10 @@ public class SecureLoginEvent extends MessageHandler {
this.client.setHabbo(habbo); this.client.setHabbo(habbo);
this.client.setMachineId(habbo.getHabboInfo().getMachineID()); this.client.setMachineId(habbo.getHabboInfo().getMachineID());
// Clear the SSO ticket now that session is resumed (prevent reuse) // NB: NON svuotiamo il ticket SSO qui (vedi HabboManager.loadHabbo):
if (!Emulator.debugging) { // dietro Cloudflare il client ritenta la connessione con lo stesso
try (java.sql.Connection conn = Emulator.getDatabase().getDataSource().getConnection(); // ticket, quindi deve restare valido fino alla scadenza TTL. Consumarlo
java.sql.PreparedStatement stmt = conn.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) { // farebbe fallire i retry / l'hard-refresh con "non-existing SSO token".
stmt.setString(1, "");
stmt.setInt(2, habbo.getHabboInfo().getId());
stmt.execute();
} catch (Exception e) {
LOGGER.error("Failed to clear SSO ticket after session resume", e);
}
}
} else { } else {
// Normal login load from database // Normal login load from database
habbo = Emulator.getGameEnvironment().getHabboManager().loadHabbo(sso); habbo = Emulator.getGameEnvironment().getHabboManager().loadHabbo(sso);
@@ -168,6 +161,12 @@ public class SecureLoginEvent extends MessageHandler {
throw new NullPointerException(habbo.getHabboInfo().getUsername() + " has a NON EXISTING RANK!"); throw new NullPointerException(habbo.getHabboInfo().getUsername() + " has a NON EXISTING RANK!");
} }
// If the machine fingerprint already arrived (UniqueID before login),
// persist it so machine/super bans can target this user.
if (this.client.getMachineId() != null && !this.client.getMachineId().isEmpty()) {
this.client.getHabbo().getHabboInfo().setMachineID(this.client.getMachineId());
}
Emulator.getThreading().run(habbo); Emulator.getThreading().run(habbo);
Emulator.getGameEnvironment().getHabboManager().addHabbo(habbo); Emulator.getGameEnvironment().getHabboManager().addHabbo(habbo);
} catch (Exception e) { } catch (Exception e) {
@@ -0,0 +1,61 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.modtool.ModToolBan;
import com.eu.habbo.habbohotel.modtool.ModToolBanType;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
import java.util.List;
/**
* Apply an arbitrary-duration account ban. Duration is taken in hours
* from the wire and converted to seconds for ModToolManager.ban
* unlike ModToolSanctionBanEvent which only accepts the four fixed
* Habbo-protocol banType buckets.
*/
public class HousekeepingBanUserEvent extends MessageHandler {
private static final String ACTION_KEY = "user.ban";
private static final int SECONDS_IN_HOUR = 3600;
// 100-year ceiling, matches ModToolSanctionBanEvent's permanent ban.
private static final int MAX_DURATION_SECONDS = 100 * 365 * 24 * 3600;
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int userId = this.packet.readInt();
String reason = this.packet.readString();
int hours = this.packet.readInt();
if (userId <= 0 || hours <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
long durationLong = (long) hours * SECONDS_IN_HOUR;
int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong;
List<ModToolBan> bans = Emulator.getGameEnvironment().getModToolManager()
.ban(userId, this.client.getHabbo(), reason != null ? reason : "", duration, ModToolBanType.ACCOUNT, 0);
if (bans == null || bans.isEmpty()) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.ban_failed"));
return;
}
// ModToolBan doesn't expose the `bans` table autoinc id on the
// object, so we return the target user id as the actionId it's
// the only stable handle the client can use until a dedicated
// housekeeping_log row id supersedes it.
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -0,0 +1,68 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* Permanently delete a room. Mirrors the minimum-viable subset of
* RequestDeleteRoomEvent: eject all users from the live room, dispose
* + uncache, then DELETE FROM rooms. Pets/guild/custom-layout cleanup
* is intentionally skipped on this slice leftover rows in those
* tables become orphans but don't crash the emulator; a follow-up
* pass can cascade once we have a HK audit-log row to attach the
* orphan-cleanup to.
*/
public class HousekeepingDeleteRoomEvent extends MessageHandler {
private static final String ACTION_KEY = "room.delete";
@Override
public int getRatelimit() {
return 2000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int roomId = this.packet.readInt();
if (roomId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
if (room != null) {
room.ejectAll();
room.preventUnloading = false;
room.dispose();
Emulator.getGameEnvironment().getRoomManager().uncacheRoom(room);
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM rooms WHERE id = ? LIMIT 1")) {
statement.setInt(1, roomId);
int rows = statement.executeUpdate();
if (rows == 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
return;
}
} catch (SQLException e) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
return;
}
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
}
}

Some files were not shown because too many files have changed in this diff Show More