Compare commits

...

184 Commits

Author SHA1 Message Date
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
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
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
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
github-actions[bot] dca405ffb5 🆙 Bump version to 4.2.9 [skip ci] 2026-05-19 08:07:32 +00:00
DuckieTM 4190fa96d4 Merge pull request #106 from duckietm/dev
Dev
2026-05-19 10:06:40 +02:00
duckietm 033faaeab6 🆙 Update Database 2026-05-19 10:04:59 +02:00
DuckieTM 98326e11af Merge pull request #104 from duckietm/main
Main to DEV
2026-05-19 10:03:12 +02:00
github-actions[bot] 0f2666916f 🆙 Bump version to 4.2.8 [skip ci] 2026-05-19 07:58:26 +00:00
DuckieTM 46041eedfe Merge pull request #103 from medievalshell/Dev
feat(furnieditor): split-aware FurniDataManager + JSON5 tolerance
2026-05-19 09:57:34 +02:00
medievalshell e334a3e0ac feat(auth): backward-compatible TTL check on SSO auth_ticket
Pairs with the CMS-side change introducing auth_ticket_expires_at (60s
expiry written on every ticket issuance). Without an emulator-side
verification the column was advisory only — this commit gates every
SELECT that resolves a user by auth_ticket on

    auth_ticket = ?
    AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW())

The NULL branch preserves backward-compatibility: CMS deployments that
do not yet populate the column keep working exactly like before
(every ticket passes the WHERE clause as soon as auth_ticket matches),
and the TTL takes effect automatically the moment a CMS starts writing
the expiry value.

Five SELECTs touched:
- SessionEndpoints.java (cms-issued SSO + remember-token flow)
- HabboManager.loadHabbo (game client login by ticket)
- SecureLoginEvent (legacy handshake path)

DB schema delivered both ways:
- Database Updates/Own_Database_RunFirst/020_auth_ticket_ttl.sql:
  idempotent ALTER, skips if column already present (information_schema
  guard so re-running the bundle is safe).
- Default Database/FullDatabase.sql: column added to the `users` table
  definition for fresh installs.

Bumps the emulator version to 4.2.7.
2026-05-19 00:46:58 +02:00
medievalshell 53b7dba185 feat(furnieditor): split-aware FurniDataManager + JSON5 tolerance
Aligns the :furnidata in-game admin command with the split-aware gamedata
layout shipped by the Nitro V3 client. FurniDataManager now resolves the
furnidata source through three accepted shapes:

- legacy single-file path (filesystem or http URL ending in .json/.json5)
- split-mode directory (URL ending with '/') — walks core/custom/seasonal
  tiers via manifest.json5 files and merges by item id, with later tiers
  overriding earlier ones (same semantics as the client-side loader)
- fallback to furni.editor.asset.base.path when the renderer config is
  missing or contains an unresolved placeholder

Adds a small JSON5 sanitiser (stripJson5) that removes line and block
comments and trailing commas before handing the content to Gson, so both
the renderer config and the split-mode files can be JSON or JSON5
without pulling in a JSON5 dependency. String contents are preserved
verbatim — comment-looking substrings inside strings (e.g. URLs) are
not touched.

Bumps the emulator version to 4.2.6.
2026-05-18 22:00:16 +02:00
github-actions[bot] efb4997bdb 🆙 Bump version to 4.1.16 [skip ci] 2026-05-18 10:57:52 +00:00
DuckieTM 7617f8483e Merge pull request #102 from duckietm/dev
Dev
2026-05-18 12:56:50 +02:00
duckietm 4f9fa9fc93 🆙 Database updated to TuT instalation 2026-05-18 12:56:28 +02:00
DuckieTM d1d8d14bec 🆙 Update AboutCommand 2026-05-16 10:47:06 +02:00
duckietm 1909f6d3c1 🆙 Update DB Updates 2026-05-13 11:39:47 +02:00
github-actions[bot] 8709a72b6e 🆙 Bump version to 4.1.15 [skip ci] 2026-05-12 08:55:48 +00:00
DuckieTM c331da9fbe Merge pull request #101 from duckietm/dev
Dev
2026-05-12 10:54:51 +02:00
duckietm f9a079da02 🆙 comibe SQLs 2026-05-12 09:18:22 +02:00
duckietm 89eb989c26 🆙 Refactor AuthHttpHandler for the API and Websocket 2026-05-12 09:11:43 +02:00
duckietm 47be392d8e 🆕 Added Reset password / Email and chenge username in user settings 2026-05-11 18:06:34 +02:00
duckietm d9465a0a65 🆙 Update Some security updates for guilds 2026-05-08 15:38:14 +02:00
duckietm 90314d00fe 🆙 Fix Guilds removal 2026-05-08 15:19:00 +02:00
duckietm 56c73b9d98 🆙 Small fix for the websocket, some CF users have problems with the max frame size 2026-05-08 08:03:51 +02:00
github-actions[bot] e6093f959f 🆙 Bump version to 4.1.14 [skip ci] 2026-05-06 10:51:57 +00:00
DuckieTM c854770561 Merge pull request #100 from duckietm/dev
Dev
2026-05-06 12:51:02 +02:00
DuckieTM a0b59134ee Merge pull request #99 from Lorenzune/merge-duckie-main-2026-05-06
Merge live secure runtime updates into dev
2026-05-06 07:08:37 +02:00
Lorenzune 67924289ac Complete secure config example 2026-05-06 06:27:49 +02:00
Lorenzune 26326bcc0e Merge remote-tracking branch 'duckie/main' into merge-duckie-main-2026-05-06
# Conflicts:
#	Database Updates/016_custom_prefixes_setup.sql
#	Database Updates/custom_nick_icons_setup.sql
#	Database Updates/remember_login_tokens.sql
#	Database Updates/wired_message_length_512.sql
#	Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java
#	Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java
#	Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java
#	Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java
#	Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java
2026-05-06 04:23:14 +02:00
duckietm ee0613a480 🆙 Update 2026-05-05 12:50:28 +02:00
DuckieTM 37d7885663 🆙 update 2026-05-05 12:09:05 +02:00
github-actions[bot] fdf0e5d806 🆙 Bump version to 4.1.13 [skip ci] 2026-05-04 13:38:38 +00:00
DuckieTM c64d3b7b8d Merge pull request #98 from duckietm/dev
Dev
2026-05-04 15:37:38 +02:00
duckietm c2b85c0c8c 🆙 Redone Background profiles 2026-05-04 15:15:41 +02:00
duckietm f8a651b059 🆙 Security update Info stand background 2026-05-04 13:18:06 +02:00
github-actions[bot] 00f9feab14 🆙 Bump version to 4.1.12 [skip ci] 2026-05-04 08:54:02 +00:00
DuckieTM 0b37705b65 Merge pull request #97 from duckietm/dev
Dev
2026-05-04 10:53:08 +02:00
duckietm 9b77ca1016 🆙 Cleanup 2026-05-04 10:52:48 +02:00
duckietm 39941cd496 🆕 Added extra packet for the pets 2026-05-04 10:08:37 +02:00
duckietm 7095dfad43 🆙 Fix Pickall 2026-05-04 08:20:58 +02:00
DuckieTM 750b172304 Merge pull request #96 from simoleo89/feat/full-box-background
Feat/full box background
2026-05-04 08:03:55 +02:00
simoleo89 5afa1f274c feat(profile): add background_card_id for full-box card backgrounds
Introduces a 4th profile-style id (cardBg) alongside the existing
background/stand/overlay triplet. The new id is meant to render a
background that fills the entire user info card on the client.

- HabboInfo: new InfostandCardBg field, loaded/saved with the
  existing background ids; users.background_card_id column added
  via sqlupdates/add_users_background_card_id.sql.
- ChangeInfostandBgEvent: reads a 4th int with bytesAvailable
  guard to remain compatible with older clients.
- RoomUserDataComposer, RoomUsersComposer, UserProfileComposer:
  append the cardBg int after the existing trio. Bot sections in
  RoomUsersComposer pad an extra zero to keep field count consistent.
2026-05-03 22:09:53 +02:00
duckietm 8f59eb652f 🆙 As NAcho wants it, add effect on disconnected user & small security update 2026-05-01 16:59:34 +02:00
duckietm 8a8cd1121e 🆕 Create Custom Bage & Security update 2026-05-01 15:58:48 +02:00
github-actions[bot] 60e5ba3a6a 🆙 Bump version to 4.1.11 [skip ci] 2026-05-01 05:49:08 +00:00
DuckieTM 9fa3fad70c Merge pull request #95 from duckietm/dev
🆕 News API
2026-05-01 07:48:08 +02:00
duckietm 860f61f765 🆕 News API 2026-04-30 17:21:33 +02:00
github-actions[bot] c5137bf3dc 🆙 Bump version to 4.1.10 [skip ci] 2026-04-29 15:11:05 +00:00
DuckieTM 5150418796 Merge pull request #94 from duckietm/dev
Dev
2026-04-29 17:10:02 +02:00
duckietm 5c71b318fb 🆙 Latest compiled version 2026-04-29 17:09:43 +02:00
duckietm 1cac407c45 🆕 Effect selection in user dropdown 2026-04-29 13:20:53 +02:00
github-actions[bot] d85eecd624 🆙 Bump version to 4.1.9 [skip ci] 2026-04-28 11:52:58 +00:00
DuckieTM c50098a945 Merge pull request #93 from duckietm/dev
🆕 Added Staffchat to the Emu
2026-04-28 13:52:02 +02:00
duckietm 0224f3f416 🆕 Added Staffchat to the Emu
!!! Do not run the Staffchat plugin anymore !!!!

- execute the sql:

INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`)
VALUES ( '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.' )
ON DUPLICATE KEY UPDATE `max_value` = VALUES(`max_value`), `comment`   = VALUES(`comment`);
2026-04-28 13:51:04 +02:00
github-actions[bot] 03d37650a0 🆙 Bump version to 4.1.8 [skip ci] 2026-04-28 09:32:12 +00:00
DuckieTM f4e5449443 Merge pull request #92 from duckietm/dev
🆙 Added Ban to the API
2026-04-28 11:31:15 +02:00
duckietm 1ebc8314a8 🆙 Added Ban to the API 2026-04-28 11:30:54 +02:00
Lorenzune 59ce829fe0 Merge duckie main into live merge branch 2026-04-25 13:52:04 +02:00
Lorenzune 9bad1eb3f6 Update secure configuration example paths 2026-04-25 13:30:00 +02:00
github-actions[bot] 85a60cf591 🆙 Bump version to 4.1.7 [skip ci] 2026-04-24 20:10:07 +00:00
DuckieTM 41d7420251 Merge pull request #91 from duckietm/dev
🆙 Added some btter logging and fix pre-existing leak in GameByteDecoder
2026-04-24 22:09:13 +02:00
DuckieTM 5dd602ebab 🆙 Added some btter logging and fix pre-existing leak in GameByteDecoder 2026-04-24 22:08:27 +02:00
Lorenzune f51617d092 Add secure mode config toggles 2026-04-24 15:55:39 +02:00
Lorenzune 585af846c4 Add secure assets and remember login support 2026-04-23 16:27:01 +02:00
Lorenzune dde2c4143c checkpoint: secure config gdm and api baseline 2026-04-23 07:01:09 +02:00
Lorenzune 26999c254b Merge remote-tracking branch 'duckie/main' into duckie-live-merge-2026-04-21 2026-04-22 09:43:43 +02:00
Lorenzune dd96523496 Merge latest duckie main with UI login 2026-04-21 11:44:19 +02:00
Lorenzune 02f3ded44e Merge remote-tracking branch 'duckie-temp/main' into duckie-merge-2026-04-21
# Conflicts:
#	Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java
#	Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java
#	Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/ItemsComponent.java
#	Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java
2026-04-21 11:20:06 +02:00
Lorenzune 8bbe8640b0 WIP preserve local changes before duckie merge 2026-04-21 11:13:32 +02:00
Lorenzune 078fb3db60 Fix wired text capture and showmessage behavior 2026-04-21 08:54:02 +02:00
275 changed files with 48763 additions and 34391 deletions
+46
View File
@@ -0,0 +1,46 @@
-- ============================================================================
-- 020_auth_ticket_ttl.sql
--
-- Adds an explicit expiry timestamp to the SSO auth_ticket on `users`.
--
-- The CMS issuing the ticket is expected to populate auth_ticket_expires_at
-- (e.g. NOW() + INTERVAL 60 SECOND) on every login redirect. The emulator-
-- side SELECT queries that look up a user by auth_ticket have been changed to
--
-- WHERE auth_ticket = ?
-- AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW())
--
-- The NULL branch keeps backward-compatibility with CMS deployments that do
-- not populate the column yet: existing rows continue to authenticate the
-- same way they always did, and the TTL kicks in only once the CMS starts
-- writing the expiry value.
--
-- Idempotent: skips the ALTER if the column already exists.
-- ============================================================================
SET @col_exists = (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'users'
AND COLUMN_NAME = 'auth_ticket_expires_at'
);
SET @ddl = IF(@col_exists = 0,
'ALTER TABLE `users` ADD COLUMN `auth_ticket_expires_at` TIMESTAMP NULL DEFAULT NULL AFTER `auth_ticket`',
'SELECT ''auth_ticket_expires_at already present, skipping'' AS info'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
UPDATE emulator_settings SET `key`='ws.whitelist' WHERE `key`='websockets.whitelist';
UPDATE emulator_settings SET `key`='ws.host' WHERE `key`='ws.nitro.host';
UPDATE emulator_settings SET `key`='ws.port' WHERE `key`='ws.nitro.port';
INSERT IGNORE INTO emulator_settings (`key`, `value`)
VALUES ('ws.ip.header', 'X-Forwarded-For');
INSERT IGNORE INTO emulator_settings (`key`, `value`)
VALUES ('ws.enabled', 'true');
@@ -0,0 +1,33 @@
ALTER TABLE users
ADD COLUMN `background_border_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_id`;
ALTER TABLE infostand_backgrounds
CHANGE COLUMN `category` `category` ENUM('background', 'stand', 'overlay', 'card', 'border') NOT NULL ;
INSERT IGNORE INTO `infostand_backgrounds` (`id`, `category`, `min_rank`, `is_hc_only`, `is_ambassador_only`) VALUES
(1, 'border', 1, 0, 0),
(2, 'border', 1, 0, 0),
(3, 'border', 1, 0, 0),
(4, 'border', 1, 0, 0),
(5, 'border', 1, 0, 0),
(6, 'border', 1, 0, 0),
(7, 'border', 1, 0, 0),
(8, 'border', 1, 0, 0),
(9, 'border', 1, 0, 0),
(10, 'border', 1, 0, 0),
(11, 'border', 1, 0, 0),
(12, 'border', 1, 0, 0),
(13, 'border', 1, 0, 0),
(14, 'border', 1, 0, 0),
(15, 'border', 1, 0, 0),
(16, 'border', 1, 0, 0),
(17, 'border', 1, 0, 0),
(18, 'border', 1, 0, 0),
(19, 'border', 1, 0, 0),
(20, 'border', 1, 0, 0),
(21, 'border', 1, 0, 0),
(22, 'border', 1, 0, 0),
(23, 'border', 1, 0, 0),
(24, 'border', 1, 0, 0),
(25, 'border', 1, 0, 0);
@@ -0,0 +1,481 @@
-- ============================================================
-- Live required schema
-- ============================================================
-- Consolidated schema for the currently used Nitro/Arcturus live
-- additions. This file intentionally excludes old/unused migration
-- artifacts and dump-only data.
--
-- Scope:
-- - tables/columns currently referenced by Java code
-- - runtime settings required by secure assets/API, login, wired, and UI
-- - safe CREATE IF NOT EXISTS / ADD COLUMN IF NOT EXISTS statements
--
-- Assumes the base Arcturus database already exists.
-- Tested for MariaDB-style syntax used by this project.
-- ============================================================
SET NAMES utf8mb4;
-- ------------------------------------------------------------
-- Core settings support
-- ------------------------------------------------------------
ALTER TABLE `emulator_settings`
ADD COLUMN IF NOT EXISTS `comment` TEXT NULL DEFAULT '' AFTER `value`;
ALTER TABLE catalog_pages
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL', 'BUILDER', 'BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `includes`;
CREATE TABLE IF NOT EXISTS `wired_emulator_settings` (
`key` VARCHAR(255) NOT NULL,
`value` TEXT NOT NULL,
`comment` TEXT NULL DEFAULT '',
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
('crypto.ws.enabled', '0'),
('crypto.ws.signing.enabled', '0'),
('crypto.ws.signing.public_key', ''),
('crypto.ws.signing.private_key', ''),
('login.access.jwt.secret', ''),
('login.remember.duration.days', '30'),
('login.remember.rotate.interval.minutes', '15'),
('login.remember.jwt.secret', ''),
('login.turnstile.enabled', '0'),
('login.turnstile.sitekey', ''),
('login.turnstile.secretkey', ''),
('login.ratelimit.enabled', '1'),
('login.ratelimit.max_attempts', '5'),
('login.ratelimit.window_sec', '60'),
('login.ratelimit.lockout_sec', '120'),
('login.register.enabled', '1'),
('register.max_per_ip', '5'),
('register.default.look', 'hr-100-7.hd-180-1.ch-210-66.lg-270-82.sh-290-80'),
('register.default.motto', 'I love Habbo!'),
('password.reset.url', 'http://localhost/reset-password'),
('smtp.provider', 'own'),
('smtp.host', 'localhost'),
('smtp.port', '587'),
('smtp.username', ''),
('smtp.password', ''),
('smtp.from_address', 'no-reply@example.com'),
('smtp.from_name', 'Habbo Hotel'),
('smtp.use_tls', '1'),
('smtp.use_ssl', '0'),
('new_user_credits', '0'),
('new_user_duckets', '0'),
('new_user_diamonds', '0')
ON DUPLICATE KEY UPDATE `value` = `value`;
INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`) VALUES
('wired.engine.enabled', '1', 'Compatibility flag. The runtime uses the new wired engine.'),
('wired.engine.exclusive', '1', 'Compatibility flag. The runtime uses exclusive wired engine execution.'),
('wired.engine.maxStepsPerStack', '100', 'Maximum internal processing steps allowed for a single wired stack execution.'),
('wired.engine.debug', '0', 'Enable verbose debug logging for the wired engine.'),
('wired.custom.enabled', '0', 'Enable custom legacy wired compatibility behavior.'),
('hotel.wired.furni.selection.count', '5', 'Maximum number of furni that a wired box can store or select.'),
('hotel.wired.max_delay', '20', 'Maximum delay value accepted by wired effects that support delayed execution.'),
('hotel.wired.message.max_length', '512', 'Maximum length of wired message text fields.'),
('wired.effect.teleport.delay', '500', 'Delay in milliseconds used by wired teleport movement.'),
('wired.place.under', '0', 'Allow placing wired furniture underneath other items when room rules permit it.'),
('wired.tick.interval.ms', '50', 'Global wired tick interval in milliseconds.'),
('wired.tick.resolution', '100', 'Legacy wired tick resolution value.'),
('wired.tick.debug', '0', 'Enable verbose logging for the wired tick service.'),
('wired.tick.thread.priority', '6', 'Java thread priority used by the wired tick service.'),
('wired.highscores.displaycount', '25', 'Maximum number of wired highscore entries shown to users.'),
('wired.abuse.max.recursion.depth', '10', 'Maximum recursive wired depth before execution is stopped.'),
('wired.abuse.max.events.per.window', '100', 'Maximum identical wired events allowed inside the abuse rate-limit window.'),
('wired.abuse.rate.limit.window.ms', '10000', 'Wired abuse rate-limit window in milliseconds.'),
('wired.abuse.ban.duration.ms', '600000', 'Temporary wired ban duration after abuse detection.'),
('wired.monitor.usage.window.ms', '1000', 'Rolling window size for wired usage monitoring.'),
('wired.monitor.usage.limit', '1000', 'Maximum wired usage budget in one monitor window.'),
('wired.monitor.delayed.events.limit', '100', 'Maximum delayed wired events queued in one room.'),
('wired.monitor.overload.average.ms', '50', 'Average execution time threshold for overload tracking.'),
('wired.monitor.overload.peak.ms', '150', 'Peak execution time threshold for overload tracking.'),
('wired.monitor.overload.consecutive.windows', '2', 'Consecutive overloaded windows required before logging overload.'),
('wired.monitor.heavy.usage.percent', '70', 'Usage percentage threshold for heavy-room tracking.'),
('wired.monitor.heavy.consecutive.windows', '5', 'Consecutive windows above heavy usage threshold.'),
('wired.monitor.heavy.delayed.percent', '60', 'Delayed queue percentage threshold for heavy-room tracking.')
ON DUPLICATE KEY UPDATE
`value` = VALUES(`value`),
`comment` = VALUES(`comment`);
-- ------------------------------------------------------------
-- Login API, room templates, remember-me, and news
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `password_resets` (
`user_id` INT(11) NOT NULL,
`token` VARCHAR(128) NOT NULL,
`expires_at` TIMESTAMP NOT NULL,
`created_ip` VARCHAR(64) NOT NULL DEFAULT '',
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_password_resets_token` (`token`),
CONSTRAINT `fk_password_resets_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `users_remember_families` (
`family_id` CHAR(36) NOT NULL,
`user_id` INT(11) NOT NULL,
`current_version` INT(11) NOT NULL DEFAULT 1,
`created_at` INT(11) NOT NULL,
`expires_at` INT(11) NOT NULL,
`revoked` TINYINT(1) NOT NULL DEFAULT 0,
`last_ip` VARCHAR(45) NOT NULL DEFAULT '',
PRIMARY KEY (`family_id`),
KEY `idx_users_remember_families_user_id` (`user_id`),
KEY `idx_users_remember_families_expires_at` (`expires_at`),
CONSTRAINT `fk_users_remember_families_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `room_templates` (
`template_id` INT(11) NOT NULL AUTO_INCREMENT,
`title` VARCHAR(128) NOT NULL DEFAULT '',
`description` VARCHAR(256) NOT NULL DEFAULT '',
`thumbnail` VARCHAR(512) NOT NULL DEFAULT '',
`sort_order` INT(11) NOT NULL DEFAULT 0,
`enabled` ENUM('0','1') NOT NULL DEFAULT '1',
`name` VARCHAR(50) NOT NULL DEFAULT '',
`room_description` VARCHAR(250) NOT NULL DEFAULT '',
`model` VARCHAR(100) NOT NULL,
`password` VARCHAR(50) NOT NULL DEFAULT '',
`state` ENUM('open','locked','password','invisible') NOT NULL DEFAULT 'open',
`users_max` INT(11) NOT NULL DEFAULT 25,
`category` INT(11) NOT NULL DEFAULT 0,
`paper_floor` VARCHAR(50) NOT NULL DEFAULT '0.0',
`paper_wall` VARCHAR(50) NOT NULL DEFAULT '0.0',
`paper_landscape` VARCHAR(50) NOT NULL DEFAULT '0.0',
`thickness_wall` INT(11) NOT NULL DEFAULT 0,
`thickness_floor` INT(11) NOT NULL DEFAULT 0,
`moodlight_data` VARCHAR(2048) NOT NULL DEFAULT '',
`override_model` ENUM('0','1') NOT NULL DEFAULT '0',
`trade_mode` INT(2) NOT NULL DEFAULT 2,
`heightmap` MEDIUMTEXT NOT NULL,
`door_x` INT(11) NOT NULL DEFAULT 0,
`door_y` INT(11) NOT NULL DEFAULT 0,
`door_dir` INT(4) NOT NULL DEFAULT 2,
PRIMARY KEY (`template_id`),
KEY `idx_room_templates_enabled_sort` (`enabled`, `sort_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `room_templates_items` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`template_id` INT(11) NOT NULL,
`item_id` INT(11) UNSIGNED NOT NULL,
`wall_pos` VARCHAR(20) NOT NULL DEFAULT '',
`x` INT(11) NOT NULL DEFAULT 0,
`y` INT(11) NOT NULL DEFAULT 0,
`z` DOUBLE(10,6) NOT NULL DEFAULT 0.000000,
`rot` INT(11) NOT NULL DEFAULT 0,
`extra_data` VARCHAR(2096) NOT NULL DEFAULT '',
`wired_data` VARCHAR(4096) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_room_templates_items_template_id` (`template_id`),
KEY `idx_room_templates_items_item_id` (`item_id`),
CONSTRAINT `fk_room_templates_items_template`
FOREIGN KEY (`template_id`) REFERENCES `room_templates` (`template_id`) ON DELETE CASCADE,
CONSTRAINT `fk_room_templates_items_item_base`
FOREIGN KEY (`item_id`) REFERENCES `items_base` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `ui_news` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`title` VARCHAR(150) NOT NULL,
`body` TEXT NOT NULL,
`image` MEDIUMTEXT DEFAULT NULL,
`link_text` VARCHAR(80) NOT NULL DEFAULT '',
`link_url` VARCHAR(255) NOT NULL DEFAULT '',
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_ui_news_enabled_sort` (`enabled`, `sort_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
INSERT INTO `ui_news` (`title`, `body`, `image`, `link_text`, `link_url`, `enabled`, `sort_order`)
SELECT 'Welcome to the Hotel!', 'Catch up on the latest events, updates and competitions happening right now in the hotel.', '', '', '', 1, 0
WHERE NOT EXISTS (SELECT 1 FROM `ui_news`);
-- ------------------------------------------------------------
-- Wired runtime data
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `room_wired_settings` (
`room_id` INT(11) NOT NULL,
`inspect_mask` INT(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can open and inspect Wired in the room. 1=everyone, 2=users with rights, 4=group members, 8=group admins.',
`modify_mask` INT(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can modify Wired in the room. 2=users with rights, 4=group members, 8=group admins.',
PRIMARY KEY (`room_id`),
CONSTRAINT `fk_room_wired_settings_room`
FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `room_wired_variables` (
`room_id` INT(11) NOT NULL,
`variable_item_id` INT(11) NOT NULL,
`value` INT(11) DEFAULT NULL,
`created_at` INT(11) NOT NULL DEFAULT 0,
`updated_at` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`room_id`, `variable_item_id`),
KEY `idx_room_wired_variables_room_item` (`room_id`, `variable_item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `room_user_wired_variables` (
`room_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`variable_item_id` INT(11) NOT NULL,
`value` INT(11) DEFAULT NULL,
`created_at` INT(11) NOT NULL DEFAULT 0,
`updated_at` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`room_id`, `user_id`, `variable_item_id`),
KEY `idx_room_user_wired_variables_room_item` (`room_id`, `variable_item_id`),
KEY `idx_room_user_wired_variables_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `room_furni_wired_variables` (
`room_id` INT(11) NOT NULL,
`furni_id` INT(11) NOT NULL,
`variable_item_id` INT(11) NOT NULL,
`value` INT(11) DEFAULT NULL,
`created_at` INT(11) NOT NULL DEFAULT 0,
`updated_at` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`room_id`, `furni_id`, `variable_item_id`),
KEY `idx_room_furni_wired_variables_room_item` (`room_id`, `variable_item_id`),
KEY `idx_room_furni_wired_variables_furni` (`furni_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ------------------------------------------------------------
-- User customization: prefixes, nick icons, profile backgrounds
-- ------------------------------------------------------------
ALTER TABLE `users`
ADD COLUMN IF NOT EXISTS `background_id` INT(11) NOT NULL DEFAULT 0 AFTER `machine_id`,
ADD COLUMN IF NOT EXISTS `background_stand_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_id`,
ADD COLUMN IF NOT EXISTS `background_overlay_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_stand_id`,
ADD COLUMN IF NOT EXISTS `background_card_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_overlay_id`;
CREATE TABLE IF NOT EXISTS `infostand_backgrounds` (
`id` INT(11) NOT NULL,
`category` ENUM('background','stand','overlay','card') NOT NULL,
`min_rank` INT(11) NOT NULL DEFAULT 0,
`is_hc_only` TINYINT(1) NOT NULL DEFAULT 0,
`is_ambassador_only` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`, `category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO `infostand_backgrounds` (`id`, `category`, `min_rank`, `is_hc_only`, `is_ambassador_only`) VALUES
(0, 'background', 0, 0, 0),
(0, 'stand', 0, 0, 0),
(0, 'overlay', 0, 0, 0),
(0, 'card', 0, 0, 0);
CREATE TABLE IF NOT EXISTS `user_prefixes` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`text` VARCHAR(50) NOT NULL,
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
`icon` VARCHAR(50) NOT NULL DEFAULT '',
`effect` VARCHAR(50) NOT NULL DEFAULT '',
`font` VARCHAR(50) NOT NULL DEFAULT '',
`catalog_prefix_id` INT(11) NOT NULL DEFAULT 0,
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
`points` INT(11) NOT NULL DEFAULT 0,
`points_type` INT(11) NOT NULL DEFAULT 0,
`is_custom` TINYINT(1) NOT NULL DEFAULT 1,
`active` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_user_prefixes_user_id` (`user_id`),
KEY `idx_user_prefixes_user_active` (`user_id`, `active`),
CONSTRAINT `fk_user_prefixes_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `custom_prefixes_catalog` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
`text` VARCHAR(50) NOT NULL,
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
`icon` VARCHAR(50) NOT NULL DEFAULT '',
`effect` VARCHAR(50) NOT NULL DEFAULT '',
`font` VARCHAR(50) NOT NULL DEFAULT '',
`points` INT(11) NOT NULL DEFAULT 0,
`points_type` INT(11) NOT NULL DEFAULT 0,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `user_visual_settings` (
`user_id` INT(11) NOT NULL,
`display_order` VARCHAR(50) NOT NULL DEFAULT 'icon-prefix-name',
PRIMARY KEY (`user_id`),
CONSTRAINT `fk_user_visual_settings_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
`key_name` VARCHAR(100) NOT NULL,
`value` VARCHAR(255) NOT NULL,
PRIMARY KEY (`key_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`word` VARCHAR(100) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_custom_prefix_blacklist_word` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES
('max_length', '15'),
('min_rank_to_buy', '1'),
('price_credits', '5'),
('price_points', '0'),
('points_type', '0'),
('font_price_credits', '10'),
('font_price_points', '0'),
('font_points_type', '0');
INSERT IGNORE INTO `custom_prefixes_catalog`
(`id`, `display_name`, `text`, `color`, `icon`, `effect`, `font`, `points`, `points_type`, `enabled`, `sort_order`)
VALUES
(1, 'VIP', 'VIP', '#FFD700', '', 'glow', '', 10, 0, 1, 1),
(2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2),
(3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3);
CREATE TABLE IF NOT EXISTS `custom_nick_icons_catalog` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`icon_key` VARCHAR(50) NOT NULL,
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
`points` INT(11) NOT NULL DEFAULT 0,
`points_type` INT(11) NOT NULL DEFAULT 0,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_custom_nick_icons_catalog_icon_key` (`icon_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `user_nick_icons` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`icon_key` VARCHAR(50) NOT NULL,
`active` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_nick_icons_user_icon` (`user_id`, `icon_key`),
KEY `idx_user_nick_icons_user_id` (`user_id`),
KEY `idx_user_nick_icons_user_active` (`user_id`, `active`),
CONSTRAINT `fk_user_nick_icons_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO `custom_nick_icons_catalog` (`icon_key`, `display_name`, `points`, `points_type`, `enabled`, `sort_order`) VALUES
('1', 'Icon 1', 10, 0, 1, 1),
('2', 'Icon 2', 10, 0, 1, 2),
('3', 'Icon 3', 10, 0, 1, 3),
('4', 'Icon 4', 10, 0, 1, 4),
('5', 'Icon 5', 10, 0, 1, 5),
('6', 'Icon 6', 10, 0, 1, 6);
-- ------------------------------------------------------------
-- Custom badge maker
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `users_custom_badge_settings` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`badge_path` VARCHAR(255) NOT NULL DEFAULT '/var/www/gamedata/c_images/album1584',
`badge_url` VARCHAR(255) NOT NULL DEFAULT '/gamedata/c_images/album1584',
`price_badge` INT(11) NOT NULL DEFAULT 0,
`currency_type` INT(11) NOT NULL DEFAULT -1,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
INSERT INTO `users_custom_badge_settings` (`id`, `badge_path`, `badge_url`, `price_badge`, `currency_type`)
SELECT 1, '/var/www/gamedata/c_images/album1584', '/gamedata/c_images/album1584', 50, 5
WHERE NOT EXISTS (SELECT 1 FROM `users_custom_badge_settings` WHERE `id` = 1);
CREATE TABLE IF NOT EXISTS `user_custom_badge` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`badge_id` VARCHAR(64) NOT NULL,
`badge_name` VARCHAR(64) NOT NULL DEFAULT '',
`badge_description` VARCHAR(255) NOT NULL DEFAULT '',
`date_created` INT(11) NOT NULL DEFAULT 0,
`date_edit` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_custom_badge_badge_id` (`badge_id`),
KEY `idx_user_custom_badge_user_id` (`user_id`),
CONSTRAINT `fk_user_custom_badge_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- ------------------------------------------------------------
-- UI/catalog compatibility values used by the current client
-- ------------------------------------------------------------
INSERT INTO `chat_bubbles` (`type`, `name`, `permission`, `overridable`, `triggers_talking_furniture`) VALUES
(200, 'SHOW_MESSAGE_RED', '', 1, 0),
(201, 'SHOW_MESSAGE_GREEN', '', 1, 0),
(202, 'SHOW_MESSAGE_BLUE', '', 1, 0),
(210, 'SHOW_MESSAGE_ALERT', '', 1, 0),
(211, 'SHOW_MESSAGE_INFO', '', 1, 0),
(212, 'SHOW_MESSAGE_WARNING', '', 1, 0),
(220, 'SHOW_MESSAGE_WRONG', '', 1, 0),
(221, 'SHOW_MESSAGE_WRONG_CIRCLED', '', 1, 0),
(222, 'SHOW_MESSAGE_CORRECT', '', 1, 0),
(223, 'SHOW_MESSAGE_CORRECT_CIRCLED', '', 1, 0),
(224, 'SHOW_MESSAGE_QUESTION', '', 1, 0),
(225, 'SHOW_MESSAGE_QUESTION_CIRCLED', '', 1, 0),
(226, 'SHOW_MESSAGE_ARROW_UP', '', 1, 0),
(227, 'SHOW_MESSAGE_ARROW_UP_CIRCLED', '', 1, 0),
(228, 'SHOW_MESSAGE_ARROW_DOWN', '', 1, 0),
(229, 'SHOW_MESSAGE_ARROW_DOWN_CIRCLED', '', 1, 0),
(250, 'SHOW_MESSAGE_SKULL', '', 1, 0),
(251, 'SHOW_MESSAGE_SKULL_ALT', '', 1, 0),
(252, 'SHOW_MESSAGE_MAGNIFIER', '', 1, 0)
ON DUPLICATE KEY UPDATE
`name` = VALUES(`name`),
`permission` = VALUES(`permission`),
`overridable` = VALUES(`overridable`),
`triggers_talking_furniture` = VALUES(`triggers_talking_furniture`);
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
('commands.keys.cmd_setroom_template', 'setroom_template;set_room_template'),
('commands.succes.cmd_setroom_template.verify', 'Copy the current room "%roomname%" to room_templates? Type :setroom_template %generic.yes% to confirm.'),
('commands.succes.cmd_setroom_template', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'),
('commands.error.cmd_setroom_template', 'Could not save room as template. Check the server log for details.'),
('commands.error.cmd_setroom_template.no_room', 'You must be inside a room to use this command.'),
('commands.keys.cmd_give_prefix', 'giveprefix'),
('commands.keys.cmd_list_prefixes', 'listprefixes'),
('commands.keys.cmd_remove_prefix', 'removeprefix'),
('commands.keys.cmd_prefix_blacklist', 'prefixblacklist'),
('wiredfurni.badgereceived.body', 'You have just received a new Badge! Check your Inventory!'),
('wiredfurni.badgereceived.title', 'Badge received!');
-- Optional permission metadata for normalized permission schemas.
-- Actual rank values still belong in the permissions/permission_ranks setup.
CREATE TABLE IF NOT EXISTS `permission_definitions` (
`permission_key` VARCHAR(64) NOT NULL,
`max_value` TINYINT(3) UNSIGNED NOT NULL DEFAULT 1,
`comment` TEXT NOT NULL,
PRIMARY KEY (`permission_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`) VALUES
('cmd_setroom_template', 1, 'Allows using :setroom_template to copy a room into the login room-template table.'),
('cmd_give_prefix', 1, 'Allows granting custom prefixes to users.'),
('cmd_list_prefixes', 1, 'Allows listing custom prefixes assigned to users.'),
('cmd_remove_prefix', 1, 'Allows removing custom prefixes from users.'),
('cmd_prefix_blacklist', 1, 'Allows managing the custom prefix blacklist.')
ON DUPLICATE KEY UPDATE
`max_value` = VALUES(`max_value`),
`comment` = VALUES(`comment`);
-- ------------------------------------------------------------
-- Explicitly obsolete table from older remember-me attempts.
-- The current Java uses users_remember_families only.
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `users_remember_tokens`;
@@ -0,0 +1,31 @@
-- ============================================================
-- Fix: acc_supporttool wrongly granted to VIP / wrongly denied to Super Mod
-- ============================================================
-- The default permission_definitions seed shipped acc_supporttool
-- with rank pattern (0, 1, 1, 1, 1, 0, 1) — i.e. rank_2 (VIP) and
-- rank_3 (X, junior helper) had ALLOWED, while rank_6 (Super Mod)
-- did NOT. That's two bugs:
--
-- * VIP users see the ModTools button on the toolbar and can
-- open Room/User info windows. The actual sanction endpoints
-- still gate on ACC_SUPPORTTOOL server-side so they can't
-- actually moderate, but the UI exposure is wrong and lets a
-- VIP request user info / room info / chatlogs they have no
-- business reading.
-- * Super Mod is denied the tool entirely, which is obviously
-- unintended given the rank name.
--
-- Intended pattern: only Support (4) and up — (0, 0, 0, 1, 1, 1, 1).
--
-- Run on existing deployments to align with the corrected default
-- seed in `Default Database/FullDatabase.sql`. Idempotent.
UPDATE `permission_definitions`
SET `rank_1` = 0,
`rank_2` = 0,
`rank_3` = 0,
`rank_4` = 1,
`rank_5` = 1,
`rank_6` = 1,
`rank_7` = 1
WHERE `permission_key` = 'acc_supporttool';
+6
View File
@@ -0,0 +1,6 @@
ALTER TABLE catalog_club_offers
ADD COLUMN IF NOT EXISTS giftable ENUM('0','1') NOT NULL DEFAULT '0';
INSERT INTO emulator_texts (`key`, `value`)
VALUES ('prereg.reward.you.received', 'You have recived:'),
('generic.days', 'days');
@@ -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;
-990
View File
@@ -1,990 +0,0 @@
UPDATE emulator_settings SET `value` = '1' WHERE (`key` = 'wired.engine.enabled');
UPDATE emulator_settings SET `value` = '1' WHERE (`key` = 'wired.engine.exclusive');
ALTER TABLE emulator_settings
ADD COLUMN IF NOT EXISTS `comment` VARCHAR(255) NOT NULL AFTER `value`;
CREATE TABLE IF NOT EXISTS `catalog_items_bc` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`item_ids` varchar(666) NOT NULL,
`page_id` int(11) NOT NULL,
`catalog_name` varchar(100) NOT NULL,
`order_number` int(11) NOT NULL DEFAULT 1,
`extradata` varchar(500) NOT NULL DEFAULT '',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `catalog_pages_bc` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`parent_id` int(11) NOT NULL DEFAULT -1,
`caption` varchar(128) NOT NULL,
`page_layout` enum(
'default_3x3','club_buy','club_gift','frontpage','spaces','recycler',
'recycler_info','recycler_prizes','trophies','plasto','marketplace',
'marketplace_own_items','spaces_new','soundmachine','guilds','guild_furni',
'info_duckets','info_rentables','info_pets','roomads','single_bundle',
'sold_ltd_items','badge_display','bots','pets','pets2','pets3',
'productpage1','room_bundle','recent_purchases',
'default_3x3_color_grouping','guild_forum','vip_buy','info_loyalty',
'loyalty_vip_buy','collectibles','petcustomization','frontpage_featured'
) NOT NULL DEFAULT 'default_3x3',
`icon_color` int(11) NOT NULL DEFAULT 1,
`icon_image` int(11) NOT NULL DEFAULT 1,
`order_num` int(11) NOT NULL DEFAULT 1,
`visible` enum('0','1') NOT NULL DEFAULT '1',
`enabled` enum('0','1') NOT NULL DEFAULT '1',
`page_headline` varchar(1024) NOT NULL DEFAULT '',
`page_teaser` varchar(64) NOT NULL DEFAULT '',
`page_special` varchar(2048) DEFAULT '' COMMENT 'Gold Bubble: catalog_special_txtbg1 // Speech Bubble: catalog_special_txtbg2 // Place normal text in page_text_teaser',
`page_text1` text DEFAULT NULL,
`page_text2` text DEFAULT NULL,
`page_text_details` text DEFAULT NULL,
`page_text_teaser` text DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci ROW_FORMAT=DYNAMIC;
ALTER TABLE `catalog_club_offers`
MODIFY COLUMN `type` ENUM('HC','VIP','BUILDERS_CLUB','BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC';
ALTER TABLE `catalog_pages`
MODIFY COLUMN `page_layout` ENUM(
'default_3x3',
'club_buy',
'club_gift',
'frontpage',
'spaces',
'recycler',
'recycler_info',
'recycler_prizes',
'trophies',
'plasto',
'marketplace',
'marketplace_own_items',
'spaces_new',
'soundmachine',
'guilds',
'guild_furni',
'info_duckets',
'info_rentables',
'info_pets',
'roomads',
'single_bundle',
'sold_ltd_items',
'badge_display',
'bots',
'pets',
'pets2',
'pets3',
'productpage1',
'room_bundle',
'recent_purchases',
'default_3x3_color_grouping',
'guild_forum',
'vip_buy',
'info_loyalty',
'loyalty_vip_buy',
'collectibles',
'petcustomization',
'frontpage_featured',
'builders_club_frontpage',
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
ALTER TABLE `catalog_pages`
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL','BUILDER','BOTH') NOT NULL DEFAULT 'NORMAL'
AFTER `club_only`;
ALTER TABLE `catalog_pages_bc`
MODIFY COLUMN `page_layout` ENUM(
'default_3x3',
'club_buy',
'club_gift',
'frontpage',
'spaces',
'recycler',
'recycler_info',
'recycler_prizes',
'trophies',
'plasto',
'marketplace',
'marketplace_own_items',
'spaces_new',
'soundmachine',
'guilds',
'guild_furni',
'info_duckets',
'info_rentables',
'info_pets',
'roomads',
'single_bundle',
'sold_ltd_items',
'badge_display',
'bots',
'pets',
'pets2',
'pets3',
'productpage1',
'room_bundle',
'recent_purchases',
'default_3x3_color_grouping',
'guild_forum',
'vip_buy',
'info_loyalty',
'loyalty_vip_buy',
'collectibles',
'petcustomization',
'frontpage_featured',
'builders_club_frontpage',
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
SET @col_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'users_settings'
AND COLUMN_NAME = 'builders_club_bonus_furni'
);
SET @sql := IF(@col_exists = 0,
'ALTER TABLE `users_settings` ADD COLUMN `builders_club_bonus_furni` INT NOT NULL DEFAULT 0;',
'SELECT "exists";'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS `wired_emulator_settings` (
`key` varchar(191) NOT NULL,
`value` text NOT NULL,
`comment` text NOT NULL,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`)
SELECT 'wired.engine.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.enabled' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.'
UNION ALL
SELECT 'wired.engine.exclusive', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.exclusive' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.'
UNION ALL
SELECT 'wired.engine.maxStepsPerStack', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.maxStepsPerStack' LIMIT 1), '100'), 'Maximum amount of internal processing steps allowed for a single wired stack execution.'
UNION ALL
SELECT 'wired.engine.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.debug' LIMIT 1), '0'), 'Enable verbose debug logging for the new wired engine.'
UNION ALL
SELECT 'wired.custom.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.custom.enabled' LIMIT 1), '0'), 'Enable custom legacy wired behaviour such as user-based cooldown exceptions and compatibility logic.'
UNION ALL
SELECT 'hotel.wired.furni.selection.count', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.furni.selection.count' LIMIT 1), '5'), 'Maximum number of furni that a wired box can store or select.'
UNION ALL
SELECT 'hotel.wired.max_delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.max_delay' LIMIT 1), '20'), 'Maximum delay value accepted by wired effects that support delayed execution.'
UNION ALL
SELECT 'hotel.wired.message.max_length', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.message.max_length' LIMIT 1), '100'), 'Maximum length of text fields used by wired messages and bot text effects.'
UNION ALL
SELECT 'wired.effect.teleport.delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.effect.teleport.delay' LIMIT 1), '500'), 'Delay in milliseconds used by wired teleport movement.'
UNION ALL
SELECT 'wired.place.under', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.place.under' LIMIT 1), '0'), 'Allow placing wired furniture underneath other items when room rules permit it.'
UNION ALL
SELECT 'wired.tick.interval.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.interval.ms' LIMIT 1), '50'), 'Global wired tick interval in milliseconds used by repeaters and other tick-driven wired items.'
UNION ALL
SELECT 'wired.tick.resolution', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.resolution' LIMIT 1), '100'), 'Legacy wired tick resolution value kept for compatibility with older wired timing setups.'
UNION ALL
SELECT 'wired.tick.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.debug' LIMIT 1), '0'), 'Enable verbose logging for the wired tick service.'
UNION ALL
SELECT 'wired.tick.thread.priority', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.thread.priority' LIMIT 1), '6'), 'Java thread priority used by the wired tick service.'
UNION ALL
SELECT 'wired.highscores.displaycount', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.highscores.displaycount' LIMIT 1), '25'), 'Maximum number of wired highscore entries shown to users when a highscore is displayed.'
UNION ALL
SELECT 'wired.abuse.max.recursion.depth', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.recursion.depth' LIMIT 1), '10'), 'Maximum recursive wired depth allowed before execution is stopped.'
UNION ALL
SELECT 'wired.abuse.max.events.per.window', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.events.per.window' LIMIT 1), '100'), 'Maximum amount of identical wired events allowed inside the abuse rate-limit window before a room ban is applied.'
UNION ALL
SELECT 'wired.abuse.rate.limit.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.rate.limit.window.ms' LIMIT 1), '10000'), 'Time window in milliseconds used by the wired abuse rate limiter.'
UNION ALL
SELECT 'wired.abuse.ban.duration.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.ban.duration.ms' LIMIT 1), '600000'), 'Duration in milliseconds of the temporary wired ban after abuse detection.'
UNION ALL
SELECT 'wired.monitor.usage.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.window.ms' LIMIT 1), '1000'), 'Rolling window size in milliseconds used to calculate wired usage in the :wired monitor.'
UNION ALL
SELECT 'wired.monitor.usage.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.limit' LIMIT 1), '1000'), 'Maximum wired usage budget allowed in one monitor window before EXECUTION_CAP is raised.'
UNION ALL
SELECT 'wired.monitor.delayed.events.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.delayed.events.limit' LIMIT 1), '100'), 'Maximum number of delayed wired events that can be queued in one room at the same time.'
UNION ALL
SELECT 'wired.monitor.overload.average.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.average.ms' LIMIT 1), '50'), 'Average execution time threshold in milliseconds that starts overload tracking.'
UNION ALL
SELECT 'wired.monitor.overload.peak.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.peak.ms' LIMIT 1), '150'), 'Peak single execution time threshold in milliseconds that starts overload tracking.'
UNION ALL
SELECT 'wired.monitor.overload.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.consecutive.windows' LIMIT 1), '2'), 'Number of consecutive overloaded monitor windows required before logging EXECUTOR_OVERLOAD.'
UNION ALL
SELECT 'wired.monitor.heavy.usage.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.usage.percent' LIMIT 1), '70'), 'Usage percentage threshold that contributes to marking a room as heavy in the :wired monitor.'
UNION ALL
SELECT 'wired.monitor.heavy.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.consecutive.windows' LIMIT 1), '5'), 'Number of consecutive windows above the heavy usage threshold required before the room is marked as heavy.'
UNION ALL
SELECT 'wired.monitor.heavy.delayed.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.delayed.percent' LIMIT 1), '60'), 'Delayed queue percentage threshold that also contributes to the heavy-room calculation.'
ON DUPLICATE KEY UPDATE
`value` = VALUES(`value`),
`comment` = VALUES(`comment`);
DELETE FROM `emulator_settings`
WHERE `key` IN (
'wired.engine.enabled',
'wired.engine.exclusive',
'wired.engine.maxStepsPerStack',
'wired.engine.debug',
'wired.custom.enabled',
'hotel.wired.furni.selection.count',
'hotel.wired.max_delay',
'hotel.wired.message.max_length',
'wired.effect.teleport.delay',
'wired.place.under',
'wired.tick.interval.ms',
'wired.tick.resolution',
'wired.tick.debug',
'wired.tick.thread.priority',
'wired.highscores.displaycount',
'wired.abuse.max.recursion.depth',
'wired.abuse.max.events.per.window',
'wired.abuse.rate.limit.window.ms',
'wired.abuse.ban.duration.ms',
'wired.monitor.usage.window.ms',
'wired.monitor.usage.limit',
'wired.monitor.delayed.events.limit',
'wired.monitor.overload.average.ms',
'wired.monitor.overload.peak.ms',
'wired.monitor.overload.consecutive.windows',
'wired.monitor.heavy.usage.percent',
'wired.monitor.heavy.consecutive.windows',
'wired.monitor.heavy.delayed.percent'
);
UPDATE `emulator_settings` SET `comment` = 'Allow whispering while a user stands inside a mute area.' WHERE `key` = 'room.chat.mutearea.allow_whisper';
UPDATE `emulator_settings` SET `comment` = 'HTML or text format used for room chat prefixes.' WHERE `key` = 'room.chat.prefix.format';
UPDATE `emulator_settings` SET `comment` = 'Badge code displayed on promoted rooms.' WHERE `key` = 'room.promotion.badge';
UPDATE `emulator_settings` SET `comment` = 'Image used by Rosie bubble notifications.' WHERE `key` = 'rosie.bubble.image.url';
UPDATE `emulator_settings` SET `comment` = 'Currency type used by Rosie when buying a room or room package.' WHERE `key` = 'rosie.buyroom.currency.type';
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `runtime.threads`.' WHERE `key` = 'runtime.threads';
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.private.chats`.' WHERE `key` = 'save.private.chats';
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.room.chats`.' WHERE `key` = 'save.room.chats';
UPDATE `emulator_settings` SET `comment` = 'Expose moderation tickets to the scripter or automation tooling.' WHERE `key` = 'scripter.modtool.tickets';
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for diamonds.' WHERE `key` = 'seasonal.currency.diamond';
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for duckets.' WHERE `key` = 'seasonal.currency.ducket';
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated display names for seasonal currency types.' WHERE `key` = 'seasonal.currency.names';
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for pixels.' WHERE `key` = 'seasonal.currency.pixel';
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for shells.' WHERE `key` = 'seasonal.currency.shell';
UPDATE `emulator_settings` SET `comment` = 'Primary seasonal currency type ID.' WHERE `key` = 'seasonal.primary.type';
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated list of currency type IDs treated as seasonal currencies.' WHERE `key` = 'seasonal.types';
UPDATE `emulator_settings` SET `comment` = 'Achievement code granted for the HC subscription tier.' WHERE `key` = 'subscriptions.hc.achievement';
UPDATE `emulator_settings` SET `comment` = 'Number of days before expiry when HC discount offers become available.' WHERE `key` = 'subscriptions.hc.discount.days_before_end';
UPDATE `emulator_settings` SET `comment` = 'Enable discounted HC renewal offers.' WHERE `key` = 'subscriptions.hc.discount.enabled';
UPDATE `emulator_settings` SET `comment` = 'Reset tracked credits spent when the HC subscription expires.' WHERE `key` = 'subscriptions.hc.payday.creditsspent_reset_on_expire';
UPDATE `emulator_settings` SET `comment` = 'Currency rewarded by the HC payday system.' WHERE `key` = 'subscriptions.hc.payday.currency';
UPDATE `emulator_settings` SET `comment` = 'Enable the HC payday reward system.' WHERE `key` = 'subscriptions.hc.payday.enabled';
UPDATE `emulator_settings` SET `comment` = 'Date interval used between HC payday reward runs.' WHERE `key` = 'subscriptions.hc.payday.interval';
UPDATE `emulator_settings` SET `comment` = 'Next scheduled execution date for HC payday rewards.' WHERE `key` = 'subscriptions.hc.payday.next_date';
UPDATE `emulator_settings` SET `comment` = 'Percentage of eligible spending returned by HC payday.' WHERE `key` = 'subscriptions.hc.payday.percentage';
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated streak thresholds and rewards for HC payday.' WHERE `key` = 'subscriptions.hc.payday.streak';
UPDATE `emulator_settings` SET `comment` = 'Enable the subscription background scheduler.' WHERE `key` = 'subscriptions.scheduler.enabled';
UPDATE `emulator_settings` SET `comment` = 'Interval in minutes between subscription scheduler runs.' WHERE `key` = 'subscriptions.scheduler.interval';
UPDATE `emulator_settings` SET `comment` = 'Compatibility marker used by the custom team wired implementation. Do not remove.' WHERE `key` = 'team.wired.update.rc-1';
UPDATE `emulator_settings` SET `comment` = 'API key used by the YouTube integration.' WHERE `key` = 'youtube.apikey';
DROP VIEW IF EXISTS `permissions_matrix_view`;
DROP PROCEDURE IF EXISTS `refresh_permissions_matrix_view`;
DROP TABLE IF EXISTS `permission_rank_values`;
DROP TABLE IF EXISTS `permission_nodes`;
CREATE TABLE IF NOT EXISTS `permission_ranks` (
`id` int(11) NOT NULL,
`rank_name` varchar(25) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
`hidden_rank` tinyint(1) NOT NULL DEFAULT 0,
`badge` varchar(12) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '',
`job_description` varchar(255) NOT NULL DEFAULT 'Here to help',
`staff_color` varchar(8) NOT NULL DEFAULT '#327fa8',
`staff_background` varchar(255) NOT NULL DEFAULT 'staff-bg.png',
`level` int(11) NOT NULL DEFAULT 1,
`room_effect` int(11) NOT NULL DEFAULT 0,
`log_commands` enum('0','1') CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '0',
`prefix` varchar(5) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '',
`prefix_color` varchar(7) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '',
`auto_credits_amount` int(11) DEFAULT 0,
`auto_pixels_amount` int(11) DEFAULT 0,
`auto_gotw_amount` int(11) DEFAULT 0,
`auto_points_amount` int(11) DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `permission_definitions` (
`permission_key` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
`max_value` tinyint(3) unsigned NOT NULL DEFAULT 1,
`comment` text NOT NULL,
PRIMARY KEY (`permission_key`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci ROW_FORMAT=DYNAMIC;
ALTER TABLE `permission_definitions`
DROP COLUMN IF EXISTS `category`,
DROP COLUMN IF EXISTS `value_type`,
DROP COLUMN IF EXISTS `sort_order`;
INSERT INTO `permission_ranks` (
`id`,
`rank_name`,
`hidden_rank`,
`badge`,
`job_description`,
`staff_color`,
`staff_background`,
`level`,
`room_effect`,
`log_commands`,
`prefix`,
`prefix_color`,
`auto_credits_amount`,
`auto_pixels_amount`,
`auto_gotw_amount`,
`auto_points_amount`
)
SELECT
`id`,
`rank_name`,
`hidden_rank`,
`badge`,
`job_description`,
`staff_color`,
`staff_background`,
`level`,
`room_effect`,
`log_commands`,
`prefix`,
`prefix_color`,
`auto_credits_amount`,
`auto_pixels_amount`,
`auto_gotw_amount`,
`auto_points_amount`
FROM `permissions`
ON DUPLICATE KEY UPDATE
`rank_name` = VALUES(`rank_name`),
`hidden_rank` = VALUES(`hidden_rank`),
`badge` = VALUES(`badge`),
`job_description` = VALUES(`job_description`),
`staff_color` = VALUES(`staff_color`),
`staff_background` = VALUES(`staff_background`),
`level` = VALUES(`level`),
`room_effect` = VALUES(`room_effect`),
`log_commands` = VALUES(`log_commands`),
`prefix` = VALUES(`prefix`),
`prefix_color` = VALUES(`prefix_color`),
`auto_credits_amount` = VALUES(`auto_credits_amount`),
`auto_pixels_amount` = VALUES(`auto_pixels_amount`),
`auto_gotw_amount` = VALUES(`auto_gotw_amount`),
`auto_points_amount` = VALUES(`auto_points_amount`);
DROP PROCEDURE IF EXISTS `refresh_permission_definition_rank_columns`;
DELIMITER $$
CREATE PROCEDURE `refresh_permission_definition_rank_columns`()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE current_rank_id INT;
DECLARE current_column_name VARCHAR(32);
DECLARE column_exists INT DEFAULT 0;
DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN rank_cursor;
rank_loop: LOOP
FETCH rank_cursor INTO current_rank_id;
IF done = 1 THEN
LEAVE rank_loop;
END IF;
SET current_column_name = CONCAT('rank_', current_rank_id);
SELECT COUNT(*)
INTO column_exists
FROM `information_schema`.`columns`
WHERE `table_schema` = DATABASE()
AND `table_name` = 'permission_definitions'
AND `column_name` = current_column_name;
IF column_exists = 0 THEN
SET @alter_permissions_column_sql = CONCAT(
'ALTER TABLE `permission_definitions` ADD COLUMN `',
current_column_name,
'` tinyint(3) unsigned NOT NULL DEFAULT 0'
);
PREPARE alter_permissions_column_stmt FROM @alter_permissions_column_sql;
EXECUTE alter_permissions_column_stmt;
DEALLOCATE PREPARE alter_permissions_column_stmt;
END IF;
END LOOP;
CLOSE rank_cursor;
END$$
DELIMITER ;
CALL `refresh_permission_definition_rank_columns`();
INSERT INTO `permission_definitions` (
`permission_key`,
`max_value`,
`comment`
)
SELECT
`column_name` AS `permission_key`,
CASE
WHEN `column_type` LIKE '%''2''%' THEN 2
ELSE 1
END AS `max_value`,
CASE
WHEN COALESCE(`column_comment`, '') <> '' THEN `column_comment`
WHEN `column_name` LIKE 'cmd\_%' AND `column_type` LIKE '%''2''%' THEN CONCAT(
'Controls access to the :',
REPLACE(SUBSTRING(`column_name`, 5), '_', ' '),
' command. Values: 0 = disabled, 1 = allowed, 2 = allowed only when room-owner rights may be used.'
)
WHEN `column_name` LIKE 'cmd\_%' THEN CONCAT(
'Controls access to the :',
REPLACE(SUBSTRING(`column_name`, 5), '_', ' '),
' command. Values: 0 = disabled, 1 = allowed.'
)
WHEN `column_name` LIKE 'acc\_%' AND `column_type` LIKE '%''2''%' THEN CONCAT(
'Controls the ',
REPLACE(SUBSTRING(`column_name`, 5), '_', ' '),
' capability for this rank. Values: 0 = disabled, 1 = enabled, 2 = enabled only when room-owner rights may be used.'
)
WHEN `column_name` LIKE 'acc\_%' THEN CONCAT(
'Controls the ',
REPLACE(SUBSTRING(`column_name`, 5), '_', ' '),
' capability for this rank. Values: 0 = disabled, 1 = enabled.'
)
ELSE CONCAT(
'Legacy permission-related value migrated from the old permissions table for ',
`column_name`,
'.'
)
END AS `comment`
FROM `information_schema`.`columns`
WHERE `table_schema` = DATABASE()
AND `table_name` = 'permissions'
AND `column_name` NOT IN (
'id',
'rank_name',
'hidden_rank',
'badge',
'job_description',
'staff_color',
'staff_background',
'level',
'room_effect',
'log_commands',
'prefix',
'prefix_color',
'auto_credits_amount',
'auto_pixels_amount',
'auto_gotw_amount',
'auto_points_amount'
)
ON DUPLICATE KEY UPDATE
`max_value` = VALUES(`max_value`),
`comment` = VALUES(`comment`);
DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`;
CREATE TEMPORARY TABLE `tmp_permission_comments` (
`permission_key` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
`comment` text NOT NULL,
PRIMARY KEY (`permission_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci;
INSERT INTO `tmp_permission_comments` (`permission_key`, `comment`) VALUES
('cmd_about', 'Allows using :about to display emulator, revision, or hotel information exposed by the command.'),
('cmd_alert', 'Allows using :alert to send a hotel alert popup to a specific user.'),
('cmd_allow_trading', 'Allows using the trading-toggle command to enable or disable trading for a target user.'),
('cmd_badge', 'Allows granting a badge code to a target user through a command.'),
('cmd_ban', 'Allows banning users from the hotel.'),
('cmd_blockalert', 'Allows sending the block-alert style moderation message.'),
('cmd_bots', 'Allows using :bots to list the bots currently placed in the room.'),
('cmd_bundle', 'Allows using :bundle / :roombundle to create a catalog room-bundle offer for the current room.'),
('cmd_calendar', 'Allows using the hotel calendar command and any calendar actions wired to that command entry.'),
('cmd_changename', 'Allows forcing a user-name change through the change-name command flow.'),
('cmd_chatcolor', 'Allows changing the active chat bubble color through the chat-color command.'),
('cmd_commands', 'Allows using :commands to list the command keys available to the current user.'),
('cmd_connect_camera', 'Allows using the command that links the in-room camera feature to the current room session.'),
('cmd_control', 'Allows using :control to take over another in-room user and stop controlling them later.'),
('cmd_coords', 'Allows using :coords to inspect room coordinates for tiles, users, or furniture.'),
('cmd_credits', 'Allows giving or removing credits from a user through the staff currency command.'),
('cmd_subscription', 'Allows granting or editing subscription time through the subscription command.'),
('cmd_danceall', 'Allows forcing every Habbo currently in the room to dance.'),
('cmd_diagonal', 'Allows toggling diagonal walking for the current room.'),
('cmd_disconnect', 'Allows disconnecting a user from the hotel immediately.'),
('cmd_duckets', 'Allows giving or removing duckets from a user through the staff currency command.'),
('cmd_ejectall', 'Allows ejecting all users from the current room.'),
('cmd_empty', 'Allows clearing the current user furniture inventory through the empty-inventory command.'),
('cmd_empty_bots', 'Allows clearing the current user bot inventory through the empty-bots command.'),
('cmd_empty_pets', 'Allows clearing the current user pet inventory through the empty-pets command.'),
('cmd_enable', 'Allows applying an avatar effect to yourself, or to another user when acc_enable_others is also granted.'),
('cmd_event', 'Allows marking the current room as an event room through the event command.'),
('cmd_faceless', 'Allows toggling the faceless avatar visual state on the executing room unit.'),
('cmd_fastwalk', 'Allows toggling fast-walk mode for yourself or another in-room user.'),
('cmd_filterword', 'Allows adding or removing entries from the configured word filter through command usage.'),
('cmd_freeze', 'Allows freezing a target user in place.'),
('cmd_freeze_bots', 'Allows freezing bots that are placed in the room.'),
('cmd_gift', 'Allows sending a gift to a target user through the gift command.'),
('cmd_give_rank', 'Allows setting another user rank through the give-rank command.'),
('cmd_ha', 'Allows sending a hotel-wide alert.'),
('acc_can_stalk', 'Allows following users even when they have disabled stalking.'),
('cmd_hal', 'Allows sending a hotel-wide alert with a clickable link or extended content.'),
('cmd_invisible', 'Allows toggling invisible staff mode.'),
('cmd_ip_ban', 'Allows banning a user by IP address.'),
('cmd_machine_ban', 'Allows banning a user by machine identifier.'),
('cmd_hand_item', 'Allows spawning or changing the hand item currently held by a user.'),
('cmd_happyhour', 'Allows starting or stopping the happy-hour event flow exposed by the happyhour command.'),
('cmd_hidewired', 'Allows toggling whether wired furniture is visually hidden in the current room.'),
('cmd_kickall', 'Allows kicking every user from the current room.'),
('cmd_softkick', 'Allows soft-kicking a user back to the hotel view without a full sanction.'),
('cmd_massbadge', 'Allows giving the same badge to many users at once.'),
('cmd_roombadge', 'Allows setting or overriding the room badge shown to users.'),
('cmd_masscredits', 'Allows giving credits to many users at once through the mass-credits command.'),
('cmd_massduckets', 'Allows giving duckets to many users at once through the mass-duckets command.'),
('cmd_massgift', 'Allows sending the same gift to many users at once.'),
('cmd_masspoints', 'Allows giving activity points to many users at once through the mass-points command.'),
('cmd_moonwalk', 'Allows toggling the moonwalk avatar effect for yourself while you are inside a room.'),
('cmd_mimic', 'Allows copying another user appearance or presence state through the mimic command.'),
('cmd_multi', 'Allows executing multiple chat commands from the special sticky/post-it scripting payload.'),
('cmd_mute', 'Allows muting a target user.'),
('cmd_pet_info', 'Allows opening the detailed pet-information view for a pet.'),
('cmd_pickall', 'Allows picking up every furniture item from the current room.'),
('cmd_plugins', 'Legacy key for the :plugins command, which currently lists loaded plugins without enforcing this dedicated permission node in code.'),
('cmd_points', 'Allows giving or removing activity points from a user through the points command.'),
('cmd_promote_offer', 'Allows using :promoteoffer to list active target offers or switch the globally promoted target offer.'),
('cmd_pull', 'Allows pulling a nearby user onto the tile directly in front of you.'),
('cmd_push', 'Allows pushing the user standing in front of you one tile farther in the direction you are facing.'),
('cmd_redeem', 'Allows redeeming redeemable inventory items through the redeem command flow.'),
('cmd_reload_room', 'Allows unloading and reloading the current room, then forwarding the occupants back into the fresh room instance.'),
('cmd_roomalert', 'Allows sending the same alert message to everyone in the current room.'),
('cmd_roomcredits', 'Allows giving credits to every Habbo currently in the room.'),
('cmd_roomeffect', 'Allows applying the same avatar effect id to every Habbo currently in the room.'),
('cmd_roomgift', 'Allows sending the same gift to every Habbo currently in the room.'),
('cmd_roomitem', 'Allows setting the same hand-item id for every Habbo in the room; using 0 clears the hand item.'),
('cmd_roommute', 'Allows muting every Habbo currently in the room.'),
('cmd_roompixels', 'Allows giving duckets or pixels to every Habbo currently in the room.'),
('cmd_roompoints', 'Allows giving activity points to every Habbo currently in the room.'),
('cmd_say', 'Allows forcing another online user to say a custom message in their current room.'),
('cmd_say_all', 'Allows making everyone in the room say a message.'),
('cmd_setmax', 'Allows using :setmax to change the maximum user capacity of the current room.'),
('cmd_set_poll', 'Allows using :setpoll to attach or remove a poll on the current room.'),
('cmd_setpublic', 'Allows using :setpublic to change the room public/private visibility state.'),
('cmd_setspeed', 'Allows using :setspeed to change the room walking speed setting.'),
('cmd_shout', 'Allows forcing another online user to shout a custom message in their current room.'),
('cmd_shout_all', 'Allows making everyone in the room shout a message.'),
('cmd_shutdown', 'Allows using the shutdown command to stop the emulator process.'),
('cmd_sitdown', 'Allows forcing users to sit down through the sitdown command.'),
('cmd_staffalert', 'Allows sending an alert that is visible only to staff members.'),
('cmd_staffonline', 'Allows viewing the current list of online staff members.'),
('cmd_summon', 'Allows summoning a target user into the room where the staff member currently is.'),
('cmd_summonrank', 'Allows summoning all online users of a given rank into the current room.'),
('cmd_super_ban', 'Allows issuing the strongest ban command variant exposed by the super-ban command.'),
('cmd_stalk', 'Allows following another user to their room.'),
('cmd_superpull', 'Allows pulling a user to the tile in front of you without the short-range reach check used by :pull.'),
('cmd_take_badge', 'Allows removing a badge code from a target user.'),
('cmd_talk', 'Allows using the legacy :talk command to make another user speak a command-provided message.'),
('cmd_teleport', 'Allows toggling the room-unit teleport mode used by the :teleport command.'),
('cmd_trash', 'Allows deleting or trashing furniture/items through the trash command flow.'),
('cmd_transform', 'Allows transforming your room unit into a chosen pet type, race, and color.'),
('cmd_unban', 'Allows removing active bans.'),
('cmd_unload', 'Allows disposing the current room instance immediately through :unload / :crash.'),
('cmd_unmute', 'Allows removing an active mute from a target user.'),
('cmd_update_achievements', 'Allows using :update_achievements to reload achievements configuration.'),
('cmd_update_bots', 'Allows using :update_bots to reload bot data and bot configuration.'),
('cmd_update_catalogue', 'Allows using :update_catalogue to reload catalogue pages and offers.'),
('cmd_update_config', 'Allows using :update_config to reload emulator configuration settings.'),
('cmd_update_guildparts', 'Allows using :update_guildparts to reload guild badge parts and guild configuration.'),
('cmd_update_hotel_view', 'Allows using :update_hotel_view to reload hotel-view assets or settings.'),
('cmd_update_items', 'Allows using :update_items to reload item data and furniture definitions.'),
('cmd_update_navigator', 'Allows using :update_navigator to reload navigator configuration and listings.'),
('cmd_update_permissions', 'Allows using :update_permissions to reload ranks and permissions from the database.'),
('cmd_update_pet_data', 'Allows using :update_pet_data to reload pet types and pet races.'),
('cmd_update_plugins', 'Allows using :update_plugins to reload plugin data or plugin metadata.'),
('cmd_update_polls', 'Allows using :update_polls to reload poll and questionnaire data.'),
('cmd_update_texts', 'Allows using :update_texts to reload external texts and localizations.'),
('cmd_update_wordfilter', 'Allows using :update_wordfilter to reload the word-filter list.'),
('cmd_userinfo', 'Allows opening the detailed user-information view used by staff tools.'),
('cmd_word_quiz', 'Allows starting a room word-quiz event with a custom question and optional duration.'),
('cmd_warp', 'Allows instantly warping your room unit to a target tile.'),
('acc_anychatcolor', 'Allows selecting any chat bubble color, including normally restricted colors.'),
('acc_anyroomowner', 'Treats the rank as room owner for owner-only checks such as room settings, wired saving, rights management, floorplan editing, and similar room-owner gates.'),
('acc_empty_others', 'Allows :empty, :empty_bots, and :empty_pets to target another user inventory instead of only your own.'),
('acc_enable_others', 'Allows :enable to apply avatar effects to another user instead of only to yourself.'),
('acc_see_whispers', 'Allows seeing whispers sent between other users in the room.'),
('acc_see_tentchat', 'Allows seeing tent chat or similar hidden chat channels that are normally not visible to everyone.'),
('acc_superwired', 'Allows saving advanced wired data without the normal wordfilter and reward payload restrictions applied to regular users.'),
('acc_supporttool', 'Allows opening and using the support/moderation tool interface.'),
('acc_unkickable', 'Prevents the user from being kicked by normal moderation or room commands.'),
('acc_guildgate', 'Allows bypassing guild gate access restrictions.'),
('acc_moverotate', 'Allows moving, rotating, and saving wired furniture without the usual room-owner restriction checks.'),
('acc_placefurni', 'Allows placing furniture, opening :wired, and passing room-right checks that normally require owner or controller rights.'),
('acc_unlimited_bots', 'Removes both the bot inventory cap and the per-room bot placement cap for this rank.'),
('acc_unlimited_pets', 'Removes both the pet inventory cap and the per-room pet placement cap for this rank.'),
('acc_hide_ip', 'Hides the user IP address in staff tools and other staff-facing views.'),
('acc_hide_mail', 'Hides the user email address in moderation tools and staff views.'),
('acc_not_mimiced', 'Prevents other users from mimicking this account.'),
('acc_chat_no_flood', 'Exempts the user from flood protection limits.'),
('acc_staff_chat', 'Allows accessing staff-only chat channels and staff broadcasts.'),
('acc_staff_pick', 'Allows using staff item pick-up actions that bypass normal room ownership restrictions.'),
('acc_enteranyroom', 'Allows entering rooms regardless of door mode, bans, or normal access restrictions.'),
('acc_fullrooms', 'Allows entering rooms even when they are at maximum user capacity.'),
('acc_infinite_credits', 'Prevents credits from being consumed when a command or purchase checks credit balance.'),
('acc_infinite_pixels', 'Prevents duckets or pixels from being consumed when the balance is checked.'),
('acc_infinite_points', 'Prevents activity points from being consumed when the balance is checked.'),
('acc_ambassador', 'Marks the rank as an ambassador for ambassador-only tools and visuals.'),
('acc_debug', 'Allows using debug-only features, commands, or internal tooling.'),
('acc_chat_no_limit', 'Lets the user hear and be heard regardless of room hearing distance limits.'),
('acc_chat_no_filter', 'Bypasses the word filter for chat and staff-generated messages.'),
('acc_nomute', 'Prevents the user from being muted by normal mute checks.'),
('acc_guild_admin', 'Allows bypassing guild admin restrictions when managing guilds.'),
('acc_catalog_ids', 'Allows seeing internal catalogue page ids, offer ids, or related technical catalogue identifiers.'),
('acc_modtool_ticket_q', 'Allows seeing and handling the moderation ticket queue.'),
('acc_modtool_user_logs', 'Allows reading user chat logs in the moderation tool.'),
('acc_modtool_user_alert', 'Allows sending moderation alerts or cautions to users.'),
('acc_modtool_user_kick', 'Allows kicking users from the moderation tool.'),
('acc_modtool_user_ban', 'Allows banning users from the moderation tool.'),
('acc_modtool_room_info', 'Allows viewing room information in the moderation tool.'),
('acc_modtool_room_logs', 'Allows viewing room chat logs in the moderation tool.'),
('acc_trade_anywhere', 'Allows starting trades outside the normal trade-enabled areas.'),
('acc_update_notifications', 'Allows receiving update notifications emitted by the emulator.'),
('acc_helper_use_guide_tool', 'Allows opening the helper guide tool.'),
('acc_helper_give_guide_tours', 'Allows accepting and handling guide tour requests.'),
('acc_helper_judge_chat_reviews', 'Allows reviewing helper or chat review tickets.'),
('acc_floorplan_editor', 'Allows opening and saving the floorplan editor.'),
('acc_camera', 'Allows using the in-room camera feature and related camera UI actions.'),
('acc_ads_background', 'Allows editing room advertisement backgrounds.'),
('cmd_wordquiz', 'Legacy alias of cmd_word_quiz for starting a room word-quiz event.'),
('acc_room_staff_tags', 'Shows staff tags or markers above the user while inside rooms.'),
('acc_infinite_friends', 'Removes the normal friend-list size limit.'),
('acc_mimic_unredeemed', 'Allows mimicking looks even when they contain unreleased or restricted clothing.'),
('cmd_update_youtube_playlists', 'Allows reloading YouTube playlist configuration for furniture integrations.'),
('cmd_add_youtube_playlist', 'Allows adding a new YouTube playlist entry.'),
('acc_mention', 'Allows using mention-related chat features beyond the normal rank restriction.'),
('cmd_setstate', 'Legacy room-editor permission for :setstate / :ss, used to change the selected furni state or extradata value.'),
('cmd_buildheight', 'Legacy room-editor permission for :buildheight / :bh, used to change the room build-height override.'),
('cmd_setrotation', 'Legacy room-editor permission for :setrotation / :rot, used to change the rotation of the selected furni.'),
('cmd_sellroom', 'Allows putting the current room up for sale through the sell-room command.'),
('cmd_buyroom', 'Allows purchasing a room that is marked as for sale through the buy-room command.'),
('cmd_pay', 'Allows transferring currency to another user through the pay command.'),
('cmd_kill', 'Allows using the kill command effect exposed by the current command set.'),
('cmd_hoverboard', 'Allows toggling the hoverboard effect or hoverboard movement mode.'),
('cmd_kiss', 'Allows using the kiss interaction command on another user.'),
('cmd_hug', 'Allows using the hug interaction command on another user.'),
('cmd_welcome', 'Allows triggering the welcome command behavior defined by the current command set.'),
('cmd_disable_effects', 'Allows disabling active avatar effects through the disable-effects command.'),
('cmd_brb', 'Allows toggling the be-right-back status command.'),
('cmd_nuke', 'Allows using the nuke command exposed by the current command set.'),
('cmd_slime', 'Allows applying the slime command/effect exposed by the current command set.'),
('cmd_explain', 'Allows using the explain command to send the predefined explanation/help flow to users.'),
('cmd_closedice', 'Legacy essentials permission for :closedice, used to close dice items in the room or all dice at once.'),
('acc_closedice_room', 'Legacy companion permission used by older closed-dice room checks.'),
('cmd_set', 'Legacy essentials permission for :set / :changefurni, the generic furni editing command documented by :set info.'),
('cmd_furnidata', 'Allows viewing technical furnidata information in-game for selected furniture.'),
('kiss_cmd', 'Legacy alias used for the kiss command permission.'),
('acc_calendar_force', 'Allows claiming calendar rewards even when the normal day-difference timing check would block the claim.'),
('cmd_update_calendar', 'Allows using :update_calendar to reload calendar definitions and rewards.'),
('cmd_update_all', 'Allows using :update_all to reload all supported runtime data sets in one command.'),
('cms_dance', 'Legacy CMS-side permission kept for website integrations; no direct in-emulator command handler was found in the current tree.'),
('acc_catalogfurni', 'Allows using catalogue administration features related to furniture pages and offers.'),
('acc_unignorable', 'Prevents the account from being ignored by other users through the ignore system.'),
('cmd_update_chat_bubbles', 'Allows using :update_chat_bubbles to reload chat-bubble definitions and assets.'),
('cmd_calendar_staff', 'Allows the staff-only actions exposed by the calendar command flow.');
UPDATE `permission_definitions` pd
INNER JOIN `tmp_permission_comments` tc ON tc.`permission_key` = pd.`permission_key`
SET pd.`comment` = tc.`comment`;
DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`;
DROP PROCEDURE IF EXISTS `refresh_permission_definition_values`;
DELIMITER $$
CREATE PROCEDURE `refresh_permission_definition_values`()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE current_rank_id INT;
DECLARE current_column_name VARCHAR(32);
DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN rank_cursor;
rank_loop: LOOP
FETCH rank_cursor INTO current_rank_id;
IF done = 1 THEN
LEAVE rank_loop;
END IF;
SET current_column_name = CONCAT('rank_', current_rank_id);
SELECT GROUP_CONCAT(
CONCAT(
'SELECT ''',
REPLACE(`column_name`, '''', ''''''),
''' AS permission_key, CAST(COALESCE(`',
REPLACE(`column_name`, '`', '``'),
'`, ''0'') AS UNSIGNED) AS permission_value FROM `permissions` WHERE `id` = ',
current_rank_id
)
ORDER BY `ordinal_position`
SEPARATOR ' UNION ALL '
) INTO @permission_rank_source_sql
FROM `information_schema`.`columns`
WHERE `table_schema` = DATABASE()
AND `table_name` = 'permissions'
AND `column_name` NOT IN (
'id',
'rank_name',
'hidden_rank',
'badge',
'job_description',
'staff_color',
'staff_background',
'level',
'room_effect',
'log_commands',
'prefix',
'prefix_color',
'auto_credits_amount',
'auto_pixels_amount',
'auto_gotw_amount',
'auto_points_amount'
);
SET @permission_rank_update_sql = CONCAT(
'UPDATE `permission_definitions` pd ',
'INNER JOIN (',
@permission_rank_source_sql,
') src ON src.permission_key = pd.permission_key ',
'SET pd.`',
current_column_name,
'` = src.permission_value'
);
PREPARE permission_rank_update_stmt FROM @permission_rank_update_sql;
EXECUTE permission_rank_update_stmt;
DEALLOCATE PREPARE permission_rank_update_stmt;
END LOOP;
CLOSE rank_cursor;
END$$
DELIMITER ;
CALL `refresh_permission_definition_values`();
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_id` 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_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;
CREATE TABLE IF NOT EXISTS `room_wired_variables` (
`room_id` int(11) NOT NULL,
`variable_item_id` int(11) NOT NULL,
`value` int(11) NOT NULL DEFAULT 0,
`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;
ALTER TABLE `room_user_wired_variables`
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
ALTER TABLE `room_user_wired_variables`
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
UPDATE `room_user_wired_variables`
SET
`created_at` = IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()),
`updated_at` = IF(`updated_at` > 0, `updated_at`, IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()));
ALTER TABLE `room_furni_wired_variables`
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
ALTER TABLE `room_furni_wired_variables`
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
UPDATE `room_furni_wired_variables`
SET
`created_at` = IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()),
`updated_at` = IF(`updated_at` > 0, `updated_at`, IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()));
ALTER TABLE `room_wired_variables`
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
ALTER TABLE `room_wired_variables`
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
UPDATE `room_wired_variables`
SET
`created_at` = 0,
`updated_at` = IF(`updated_at` > 0, `updated_at`, UNIX_TIMESTAMP());
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`);
ALTER TABLE `catalog_club_offers`
MODIFY COLUMN `type` ENUM('HC', 'VIP', 'BUILDERS_CLUB', 'BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC';
ALTER TABLE `catalog_pages`
MODIFY COLUMN `page_layout` ENUM(
'default_3x3',
'club_buy',
'club_gift',
'frontpage',
'spaces',
'recycler',
'recycler_info',
'recycler_prizes',
'trophies',
'plasto',
'marketplace',
'marketplace_own_items',
'spaces_new',
'soundmachine',
'guilds',
'guild_furni',
'info_duckets',
'info_rentables',
'info_pets',
'roomads',
'single_bundle',
'sold_ltd_items',
'badge_display',
'bots',
'pets',
'pets2',
'pets3',
'productpage1',
'room_bundle',
'recent_purchases',
'default_3x3_color_grouping',
'guild_forum',
'vip_buy',
'info_loyalty',
'loyalty_vip_buy',
'collectibles',
'petcustomization',
'frontpage_featured',
'builders_club_frontpage',
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
ALTER TABLE `catalog_pages`
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL', 'BUILDER', 'BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `club_only`;
ALTER TABLE `rooms`
ADD COLUMN IF NOT EXISTS `builders_club_trial_locked` TINYINT(1) NOT NULL DEFAULT 0 AFTER `allow_underpass`,
ADD COLUMN IF NOT EXISTS `builders_club_original_state` VARCHAR(16) NOT NULL DEFAULT 'open' AFTER `builders_club_trial_locked`;
CREATE TABLE IF NOT EXISTS `builders_club_items` (
`item_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`room_id` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`item_id`),
KEY `idx_builders_club_items_user_id` (`user_id`),
KEY `idx_builders_club_items_room_id` (`room_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
ALTER TABLE `catalog_pages_bc`
MODIFY COLUMN `page_layout` ENUM(
'default_3x3',
'club_buy',
'club_gift',
'frontpage',
'spaces',
'recycler',
'recycler_info',
'recycler_prizes',
'trophies',
'plasto',
'marketplace',
'marketplace_own_items',
'spaces_new',
'soundmachine',
'guilds',
'guild_furni',
'info_duckets',
'info_rentables',
'info_pets',
'roomads',
'single_bundle',
'sold_ltd_items',
'badge_display',
'bots',
'pets',
'pets2',
'pets3',
'productpage1',
'room_bundle',
'recent_purchases',
'default_3x3_color_grouping',
'guild_forum',
'vip_buy',
'info_loyalty',
'loyalty_vip_buy',
'collectibles',
'petcustomization',
'frontpage_featured',
'builders_club_frontpage',
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
@@ -1,37 +1,3 @@
-- =============================================================================
-- Consolidated Database Updates - All-in-One
-- =============================================================================
-- This file combines ALL individual update scripts from SQL/Database Updates/
-- into a single idempotent migration. Every statement is safe to re-run:
-- - ALTER TABLE ADD COLUMN IF NOT EXISTS (MariaDB 10.0+)
-- - ALTER TABLE CHANGE/MODIFY COLUMN IF EXISTS
-- - CREATE TABLE IF NOT EXISTS
-- - INSERT IGNORE / ON DUPLICATE KEY UPDATE for settings
-- - TRUNCATE + re-insert for reference data (breeding)
--
-- Run order: This file FIRST, then 001_optimize_gameserver.sql
--
-- Source files (in applied order):
-- 1. UpdateDatabase_Allow_diagonale.sql
-- 2. UpdateDatabase_BOT.sql
-- 3. UpdateDatabase_Banners.sql
-- 4. UpdateDatabase_DanceCMD.sql
-- 5. UpdateDatabase_Happiness.sql
-- 6. UpdateDatabase_Websocket.sql
-- 7. UpdateDatabase_unignorable.sql
-- 8. Default_Camera.sql
-- 9. 07012026_UpdateDatabase_to_4-0-1.sql
-- 10. 09012026_UpdateDatabase_to_4-0-2.sql
-- 11. 12012026_Battle Banzai.sql (same as #10, deduplicated)
-- 12. 12012026_Breeding Fixes.sql
-- 13. 12012026_ChatBubbles.sql
-- 14. 16032026_updateall_command.sql
-- 15. 17032026_allow_underpass.sql
-- 16. 19032026_hotel_timezone.sql
-- 17. 21022026_user_prefixes.sql
-- 18. 06042026_builders_club_catalog_offers.sql
-- =============================================================================
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
SET @OLD_SQL_MODE = @@SQL_MODE;
@@ -155,6 +121,12 @@ INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
('camera.extradata', '{"t":"%timestamp%","u":"%id%","m":"","s":"%room_id%","w":"%url%"}');
INSERT INTO emulator_settings (`key`, `value`) VALUES
('session.reconnect.grace.seconds', '5');
INSERT INTO emulator_settings (`key`, `value`) VALUES
('session.reconnect.effect.id', '188');
-- Camera emulator texts
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
('camera.permission', 'You do not have permission to use the camera.');
@@ -512,8 +484,13 @@ ALTER TABLE `users_settings`
ADD COLUMN IF NOT EXISTS `builders_club_bonus_furni` INT(11) NOT NULL DEFAULT 0 AFTER `hc_gifts_claimed`;
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`)
VALUES ( '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.' )
ON DUPLICATE KEY UPDATE `max_value` = VALUES(`max_value`), `comment` = VALUES(`comment`);
-- =============================================================================
-- Done
-- Done.
-- =============================================================================
SET FOREIGN_KEY_CHECKS = 1;
SET SQL_MODE = @OLD_SQL_MODE;
@@ -1,6 +1,6 @@
INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
('furni.editor.renderer.config.path', '/var/www/Gamedata/config/renderer-config.json'),
('furni.editor.asset.base.path', '/var/www/Gamedata/furniture/nitro-assets/');
('furni.editor.renderer.config.path', '/var/www/Nitro-V3/dist/configuration/renderer-config.json'),
('furni.editor.asset.base.path', '/var/www/gamedata/furniture/');
ALTER TABLE permissions
ADD COLUMN `acc_catalogfurni` ENUM('0','1') NOT NULL DEFAULT '0' AFTER `acc_catalog_ids`;
@@ -1,15 +1,31 @@
-- Normalizes the legacy `permissions` table into:
-- 1. `permission_ranks` -> one row per rank with rank metadata.
-- 2. `permission_definitions` -> one row per permission key with comments and one `rank_<id>` column per rank.
-- 2. `permission_definitions` -> one row per permission key with comments
-- and one `rank_<id>` column per rank.
--
-- This migration keeps the old `permissions` table untouched so the emulator can safely fall back to it.
-- It also cleans up the older experimental normalized objects if they were already created.
-- This version uses NO stored procedures and NO DELIMITER directives, so it
-- works in any MySQL/MariaDB client (HeidiSQL, DBeaver, mysql CLI, phpMyAdmin)
-- regardless of how that client handles delimiters.
--
-- It builds two large dynamic SQL strings via GROUP_CONCAT and executes each
-- with PREPARE / EXECUTE. That replaces both stored procedures from the
-- original migration.
SET SESSION group_concat_max_len = 1048576;
-- --------------------------------------------------------------------------
-- Clean up older experimental objects from previous attempts.
-- --------------------------------------------------------------------------
DROP VIEW IF EXISTS `permissions_matrix_view`;
DROP PROCEDURE IF EXISTS `refresh_permissions_matrix_view`;
DROP PROCEDURE IF EXISTS `refresh_permission_definition_rank_columns`;
DROP PROCEDURE IF EXISTS `refresh_permission_definition_values`;
DROP TABLE IF EXISTS `permission_rank_values`;
DROP TABLE IF EXISTS `permission_nodes`;
-- --------------------------------------------------------------------------
-- Target tables.
-- --------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `permission_ranks` (
`id` int(11) NOT NULL,
`rank_name` varchar(25) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
@@ -42,41 +58,30 @@ ALTER TABLE `permission_definitions`
DROP COLUMN IF EXISTS `value_type`,
DROP COLUMN IF EXISTS `sort_order`;
-- --------------------------------------------------------------------------
-- Make sure the legacy `permissions` table has the rank-metadata columns
-- the migration reads from it.
-- --------------------------------------------------------------------------
ALTER TABLE `permissions`
ADD COLUMN IF NOT EXISTS `hidden_rank` tinyint(1) NOT NULL DEFAULT 0 AFTER `rank_name`,
ADD COLUMN IF NOT EXISTS `job_description` varchar(255) NOT NULL DEFAULT 'Here to help' AFTER `badge`,
ADD COLUMN IF NOT EXISTS `staff_color` varchar(8) NOT NULL DEFAULT '#327fa8' AFTER `job_description`,
ADD COLUMN IF NOT EXISTS `staff_background` varchar(255) NOT NULL DEFAULT 'staff-bg.png' AFTER `staff_color`;
-- --------------------------------------------------------------------------
-- Copy rank metadata into `permission_ranks`.
-- --------------------------------------------------------------------------
INSERT INTO `permission_ranks` (
`id`,
`rank_name`,
`hidden_rank`,
`badge`,
`job_description`,
`staff_color`,
`staff_background`,
`level`,
`room_effect`,
`log_commands`,
`prefix`,
`prefix_color`,
`auto_credits_amount`,
`auto_pixels_amount`,
`auto_gotw_amount`,
`auto_points_amount`
`id`, `rank_name`, `hidden_rank`, `badge`, `job_description`,
`staff_color`, `staff_background`, `level`, `room_effect`, `log_commands`,
`prefix`, `prefix_color`,
`auto_credits_amount`, `auto_pixels_amount`, `auto_gotw_amount`, `auto_points_amount`
)
SELECT
`id`,
`rank_name`,
`hidden_rank`,
`badge`,
`job_description`,
`staff_color`,
`staff_background`,
`level`,
`room_effect`,
`log_commands`,
`prefix`,
`prefix_color`,
`auto_credits_amount`,
`auto_pixels_amount`,
`auto_gotw_amount`,
`auto_points_amount`
`id`, `rank_name`, `hidden_rank`, `badge`, `job_description`,
`staff_color`, `staff_background`, `level`, `room_effect`, `log_commands`,
`prefix`, `prefix_color`,
`auto_credits_amount`, `auto_pixels_amount`, `auto_gotw_amount`, `auto_points_amount`
FROM `permissions`
ON DUPLICATE KEY UPDATE
`rank_name` = VALUES(`rank_name`),
@@ -95,55 +100,30 @@ ON DUPLICATE KEY UPDATE
`auto_gotw_amount` = VALUES(`auto_gotw_amount`),
`auto_points_amount` = VALUES(`auto_points_amount`);
DROP PROCEDURE IF EXISTS `refresh_permission_definition_rank_columns`;
-- --------------------------------------------------------------------------
-- Add a `rank_<id>` column to `permission_definitions` for every rank,
-- in one dynamic ALTER TABLE statement.
-- (Replaces the refresh_permission_definition_rank_columns procedure.)
-- --------------------------------------------------------------------------
SET @add_rank_columns_sql = NULL;
DELIMITER $$
CREATE PROCEDURE `refresh_permission_definition_rank_columns`()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE current_rank_id INT;
DECLARE current_column_name VARCHAR(32);
DECLARE column_exists INT DEFAULT 0;
DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
SELECT GROUP_CONCAT(
CONCAT('ADD COLUMN IF NOT EXISTS `rank_', `id`, '` tinyint(3) unsigned NOT NULL DEFAULT 0')
ORDER BY `id` ASC
SEPARATOR ', '
)
INTO @add_rank_columns_sql
FROM `permission_ranks`;
OPEN rank_cursor;
SET @add_rank_columns_sql = CONCAT('ALTER TABLE `permission_definitions` ', @add_rank_columns_sql);
rank_loop: LOOP
FETCH rank_cursor INTO current_rank_id;
IF done = 1 THEN
LEAVE rank_loop;
END IF;
SET current_column_name = CONCAT('rank_', current_rank_id);
SELECT COUNT(*)
INTO column_exists
FROM `information_schema`.`columns`
WHERE `table_schema` = DATABASE()
AND `table_name` = 'permission_definitions'
AND `column_name` = current_column_name;
IF column_exists = 0 THEN
SET @alter_permissions_column_sql = CONCAT(
'ALTER TABLE `permission_definitions` ADD COLUMN `',
current_column_name,
'` tinyint(3) unsigned NOT NULL DEFAULT 0'
);
PREPARE alter_permissions_column_stmt FROM @alter_permissions_column_sql;
EXECUTE alter_permissions_column_stmt;
DEALLOCATE PREPARE alter_permissions_column_stmt;
END IF;
END LOOP;
CLOSE rank_cursor;
END$$
DELIMITER ;
CALL `refresh_permission_definition_rank_columns`();
PREPARE add_rank_columns_stmt FROM @add_rank_columns_sql;
EXECUTE add_rank_columns_stmt;
DEALLOCATE PREPARE add_rank_columns_stmt;
-- --------------------------------------------------------------------------
-- Seed `permission_definitions` from the columns of the legacy table.
-- --------------------------------------------------------------------------
INSERT INTO `permission_definitions` (
`permission_key`,
`max_value`,
@@ -187,27 +167,18 @@ FROM `information_schema`.`columns`
WHERE `table_schema` = DATABASE()
AND `table_name` = 'permissions'
AND `column_name` NOT IN (
'id',
'rank_name',
'hidden_rank',
'badge',
'job_description',
'staff_color',
'staff_background',
'level',
'room_effect',
'log_commands',
'prefix',
'prefix_color',
'auto_credits_amount',
'auto_pixels_amount',
'auto_gotw_amount',
'auto_points_amount'
'id', 'rank_name', 'hidden_rank', 'badge', 'job_description',
'staff_color', 'staff_background', 'level', 'room_effect', 'log_commands',
'prefix', 'prefix_color',
'auto_credits_amount', 'auto_pixels_amount', 'auto_gotw_amount', 'auto_points_amount'
)
ON DUPLICATE KEY UPDATE
`max_value` = VALUES(`max_value`),
`comment` = VALUES(`comment`);
`comment` = VALUES(`comment`);
-- --------------------------------------------------------------------------
-- Override generated comments with curated text where we have it.
-- --------------------------------------------------------------------------
DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`;
CREATE TEMPORARY TABLE `tmp_permission_comments` (
@@ -421,79 +392,107 @@ SET pd.`comment` = tc.`comment`;
DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`;
DROP PROCEDURE IF EXISTS `refresh_permission_definition_values`;
-- --------------------------------------------------------------------------
-- Copy values from the wide `permissions` table into each `rank_<id>` column
-- of `permission_definitions`, one rank at a time, via dynamic SQL.
-- (Replaces the refresh_permission_definition_values procedure.)
--
-- Strategy: build a single UPDATE per rank that joins `permission_definitions`
-- against a derived table whose rows are (permission_key, value) for that
-- rank — one row per permission column in `permissions`.
-- --------------------------------------------------------------------------
DELIMITER $$
CREATE PROCEDURE `refresh_permission_definition_values`()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE current_rank_id INT;
DECLARE current_column_name VARCHAR(32);
DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
-- We need to loop over rank ids without a stored procedure. We do that by
-- selecting all rank ids into a comma-separated string and then iterating
-- with substring math using a CTE-driven counter. Simpler: build one giant
-- UPDATE per rank by hand using GROUP_CONCAT and then EXECUTE each in turn.
--
-- To avoid a procedural loop entirely, we instead emit a *single* UPDATE
-- that uses CASE expressions to set every `rank_<id>` column from a single
-- derived table containing all ranks' values. This is one PREPARE / EXECUTE.
OPEN rank_cursor;
SET @permission_columns_sql = NULL;
rank_loop: LOOP
FETCH rank_cursor INTO current_rank_id;
-- All the permission columns from the legacy table, comma separated and quoted.
SELECT GROUP_CONCAT(
CONCAT('`', REPLACE(`column_name`, '`', '``'), '`')
ORDER BY `ordinal_position`
SEPARATOR ', '
)
INTO @permission_columns_sql
FROM `information_schema`.`columns`
WHERE `table_schema` = DATABASE()
AND `table_name` = 'permissions'
AND `column_name` NOT IN (
'id', 'rank_name', 'hidden_rank', 'badge', 'job_description',
'staff_color', 'staff_background', 'level', 'room_effect', 'log_commands',
'prefix', 'prefix_color',
'auto_credits_amount', 'auto_pixels_amount', 'auto_gotw_amount', 'auto_points_amount'
);
IF done = 1 THEN
LEAVE rank_loop;
END IF;
-- Build the UNPIVOT body: one "SELECT id, 'col' AS k, `col` AS v FROM permissions UNION ALL ..." per column.
SET @unpivot_sql = NULL;
SET current_column_name = CONCAT('rank_', current_rank_id);
SELECT GROUP_CONCAT(
CONCAT(
'SELECT `id` AS rank_id, ''',
REPLACE(`column_name`, '''', ''''''),
''' AS permission_key, CAST(COALESCE(`',
REPLACE(`column_name`, '`', '``'),
'`, ''0'') AS UNSIGNED) AS permission_value FROM `permissions`'
)
ORDER BY `ordinal_position`
SEPARATOR ' UNION ALL '
)
INTO @unpivot_sql
FROM `information_schema`.`columns`
WHERE `table_schema` = DATABASE()
AND `table_name` = 'permissions'
AND `column_name` NOT IN (
'id', 'rank_name', 'hidden_rank', 'badge', 'job_description',
'staff_color', 'staff_background', 'level', 'room_effect', 'log_commands',
'prefix', 'prefix_color',
'auto_credits_amount', 'auto_pixels_amount', 'auto_gotw_amount', 'auto_points_amount'
);
SELECT GROUP_CONCAT(
CONCAT(
'SELECT ''',
REPLACE(`column_name`, '''', ''''''),
''' AS permission_key, CAST(COALESCE(`',
REPLACE(`column_name`, '`', '``'),
'`, ''0'') AS UNSIGNED) AS permission_value FROM `permissions` WHERE `id` = ',
current_rank_id
)
ORDER BY `ordinal_position`
SEPARATOR ' UNION ALL '
) INTO @permission_rank_source_sql
FROM `information_schema`.`columns`
WHERE `table_schema` = DATABASE()
AND `table_name` = 'permissions'
AND `column_name` NOT IN (
'id',
'rank_name',
'hidden_rank',
'badge',
'job_description',
'staff_color',
'staff_background',
'level',
'room_effect',
'log_commands',
'prefix',
'prefix_color',
'auto_credits_amount',
'auto_pixels_amount',
'auto_gotw_amount',
'auto_points_amount'
);
-- Build the SET clause: `rank_<id>` = MAX(CASE WHEN rank_id = <id> THEN permission_value END) for each rank.
SET @set_clause_sql = NULL;
SET @permission_rank_update_sql = CONCAT(
'UPDATE `permission_definitions` pd ',
'INNER JOIN (',
@permission_rank_source_sql,
') src ON src.permission_key = pd.permission_key ',
'SET pd.`',
current_column_name,
'` = src.permission_value'
);
SELECT GROUP_CONCAT(
CONCAT(
'pd.`rank_', `id`,
'` = COALESCE(src.`rank_', `id`, '`, pd.`rank_', `id`, '`)'
)
ORDER BY `id` ASC
SEPARATOR ', '
)
INTO @set_clause_sql
FROM `permission_ranks`;
PREPARE permission_rank_update_stmt FROM @permission_rank_update_sql;
EXECUTE permission_rank_update_stmt;
DEALLOCATE PREPARE permission_rank_update_stmt;
END LOOP;
-- Pivot subquery: one row per permission_key, one column per rank_<id>.
SET @pivot_sql = NULL;
CLOSE rank_cursor;
END$$
DELIMITER ;
SELECT GROUP_CONCAT(
CONCAT(
'MAX(CASE WHEN rank_id = ', `id`,
' THEN permission_value END) AS `rank_', `id`, '`'
)
ORDER BY `id` ASC
SEPARATOR ', '
)
INTO @pivot_sql
FROM `permission_ranks`;
CALL `refresh_permission_definition_values`();
SET @final_update_sql = CONCAT(
'UPDATE `permission_definitions` pd ',
'INNER JOIN ( ',
'SELECT permission_key, ', @pivot_sql, ' ',
'FROM ( ', @unpivot_sql, ' ) u ',
'GROUP BY permission_key ',
') src ON src.permission_key = pd.`permission_key` ',
'SET ', @set_clause_sql
);
PREPARE final_update_stmt FROM @final_update_sql;
EXECUTE final_update_stmt;
DEALLOCATE PREPARE final_update_stmt;
@@ -0,0 +1,500 @@
UPDATE emulator_settings SET `value` = '1' WHERE (`key` = 'wired.engine.enabled');
UPDATE emulator_settings SET `value` = '1' WHERE (`key` = 'wired.engine.exclusive');
ALTER TABLE emulator_settings
ADD COLUMN IF NOT EXISTS `comment` VARCHAR(255) NOT NULL AFTER `value`;
CREATE TABLE IF NOT EXISTS `catalog_items_bc` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`item_ids` varchar(666) NOT NULL,
`page_id` int(11) NOT NULL,
`catalog_name` varchar(100) NOT NULL,
`order_number` int(11) NOT NULL DEFAULT 1,
`extradata` varchar(500) NOT NULL DEFAULT '',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `catalog_pages_bc` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`parent_id` int(11) NOT NULL DEFAULT -1,
`caption` varchar(128) NOT NULL,
`page_layout` enum(
'default_3x3','club_buy','club_gift','frontpage','spaces','recycler',
'recycler_info','recycler_prizes','trophies','plasto','marketplace',
'marketplace_own_items','spaces_new','soundmachine','guilds','guild_furni',
'info_duckets','info_rentables','info_pets','roomads','single_bundle',
'sold_ltd_items','badge_display','bots','pets','pets2','pets3',
'productpage1','room_bundle','recent_purchases',
'default_3x3_color_grouping','guild_forum','vip_buy','info_loyalty',
'loyalty_vip_buy','collectibles','petcustomization','frontpage_featured'
) NOT NULL DEFAULT 'default_3x3',
`icon_color` int(11) NOT NULL DEFAULT 1,
`icon_image` int(11) NOT NULL DEFAULT 1,
`order_num` int(11) NOT NULL DEFAULT 1,
`visible` enum('0','1') NOT NULL DEFAULT '1',
`enabled` enum('0','1') NOT NULL DEFAULT '1',
`page_headline` varchar(1024) NOT NULL DEFAULT '',
`page_teaser` varchar(64) NOT NULL DEFAULT '',
`page_special` varchar(2048) DEFAULT '' COMMENT 'Gold Bubble: catalog_special_txtbg1 // Speech Bubble: catalog_special_txtbg2 // Place normal text in page_text_teaser',
`page_text1` text DEFAULT NULL,
`page_text2` text DEFAULT NULL,
`page_text_details` text DEFAULT NULL,
`page_text_teaser` text DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci ROW_FORMAT=DYNAMIC;
ALTER TABLE `catalog_club_offers`
MODIFY COLUMN `type` ENUM('HC','VIP','BUILDERS_CLUB','BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC';
ALTER TABLE `catalog_pages`
MODIFY COLUMN `page_layout` ENUM(
'default_3x3',
'club_buy',
'club_gift',
'frontpage',
'spaces',
'recycler',
'recycler_info',
'recycler_prizes',
'trophies',
'plasto',
'marketplace',
'marketplace_own_items',
'spaces_new',
'soundmachine',
'guilds',
'guild_furni',
'info_duckets',
'info_rentables',
'info_pets',
'roomads',
'single_bundle',
'sold_ltd_items',
'badge_display',
'bots',
'pets',
'pets2',
'pets3',
'productpage1',
'room_bundle',
'recent_purchases',
'default_3x3_color_grouping',
'guild_forum',
'vip_buy',
'info_loyalty',
'loyalty_vip_buy',
'collectibles',
'petcustomization',
'frontpage_featured',
'builders_club_frontpage',
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
ALTER TABLE `catalog_pages`
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL','BUILDER','BOTH') NOT NULL DEFAULT 'NORMAL'
AFTER `club_only`;
ALTER TABLE `catalog_pages_bc`
MODIFY COLUMN `page_layout` ENUM(
'default_3x3',
'club_buy',
'club_gift',
'frontpage',
'spaces',
'recycler',
'recycler_info',
'recycler_prizes',
'trophies',
'plasto',
'marketplace',
'marketplace_own_items',
'spaces_new',
'soundmachine',
'guilds',
'guild_furni',
'info_duckets',
'info_rentables',
'info_pets',
'roomads',
'single_bundle',
'sold_ltd_items',
'badge_display',
'bots',
'pets',
'pets2',
'pets3',
'productpage1',
'room_bundle',
'recent_purchases',
'default_3x3_color_grouping',
'guild_forum',
'vip_buy',
'info_loyalty',
'loyalty_vip_buy',
'collectibles',
'petcustomization',
'frontpage_featured',
'builders_club_frontpage',
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
SET @col_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'users_settings'
AND COLUMN_NAME = 'builders_club_bonus_furni'
);
SET @sql := IF(@col_exists = 0,
'ALTER TABLE `users_settings` ADD COLUMN `builders_club_bonus_furni` INT NOT NULL DEFAULT 0;',
'SELECT "exists";'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS `wired_emulator_settings` (
`key` varchar(191) NOT NULL,
`value` text NOT NULL,
`comment` text NOT NULL,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`)
SELECT 'wired.engine.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.enabled' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.'
UNION ALL
SELECT 'wired.engine.exclusive', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.exclusive' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.'
UNION ALL
SELECT 'wired.engine.maxStepsPerStack', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.maxStepsPerStack' LIMIT 1), '100'), 'Maximum amount of internal processing steps allowed for a single wired stack execution.'
UNION ALL
SELECT 'wired.engine.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.debug' LIMIT 1), '0'), 'Enable verbose debug logging for the new wired engine.'
UNION ALL
SELECT 'wired.custom.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.custom.enabled' LIMIT 1), '0'), 'Enable custom legacy wired behaviour such as user-based cooldown exceptions and compatibility logic.'
UNION ALL
SELECT 'hotel.wired.furni.selection.count', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.furni.selection.count' LIMIT 1), '5'), 'Maximum number of furni that a wired box can store or select.'
UNION ALL
SELECT 'hotel.wired.max_delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.max_delay' LIMIT 1), '20'), 'Maximum delay value accepted by wired effects that support delayed execution.'
UNION ALL
SELECT 'hotel.wired.message.max_length', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.message.max_length' LIMIT 1), '100'), 'Maximum length of text fields used by wired messages and bot text effects.'
UNION ALL
SELECT 'wired.effect.teleport.delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.effect.teleport.delay' LIMIT 1), '500'), 'Delay in milliseconds used by wired teleport movement.'
UNION ALL
SELECT 'wired.place.under', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.place.under' LIMIT 1), '0'), 'Allow placing wired furniture underneath other items when room rules permit it.'
UNION ALL
SELECT 'wired.tick.interval.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.interval.ms' LIMIT 1), '50'), 'Global wired tick interval in milliseconds used by repeaters and other tick-driven wired items.'
UNION ALL
SELECT 'wired.tick.resolution', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.resolution' LIMIT 1), '100'), 'Legacy wired tick resolution value kept for compatibility with older wired timing setups.'
UNION ALL
SELECT 'wired.tick.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.debug' LIMIT 1), '0'), 'Enable verbose logging for the wired tick service.'
UNION ALL
SELECT 'wired.tick.thread.priority', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.thread.priority' LIMIT 1), '6'), 'Java thread priority used by the wired tick service.'
UNION ALL
SELECT 'wired.highscores.displaycount', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.highscores.displaycount' LIMIT 1), '25'), 'Maximum number of wired highscore entries shown to users when a highscore is displayed.'
UNION ALL
SELECT 'wired.abuse.max.recursion.depth', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.recursion.depth' LIMIT 1), '10'), 'Maximum recursive wired depth allowed before execution is stopped.'
UNION ALL
SELECT 'wired.abuse.max.events.per.window', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.events.per.window' LIMIT 1), '100'), 'Maximum amount of identical wired events allowed inside the abuse rate-limit window before a room ban is applied.'
UNION ALL
SELECT 'wired.abuse.rate.limit.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.rate.limit.window.ms' LIMIT 1), '10000'), 'Time window in milliseconds used by the wired abuse rate limiter.'
UNION ALL
SELECT 'wired.abuse.ban.duration.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.ban.duration.ms' LIMIT 1), '600000'), 'Duration in milliseconds of the temporary wired ban after abuse detection.'
UNION ALL
SELECT 'wired.monitor.usage.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.window.ms' LIMIT 1), '1000'), 'Rolling window size in milliseconds used to calculate wired usage in the :wired monitor.'
UNION ALL
SELECT 'wired.monitor.usage.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.limit' LIMIT 1), '1000'), 'Maximum wired usage budget allowed in one monitor window before EXECUTION_CAP is raised.'
UNION ALL
SELECT 'wired.monitor.delayed.events.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.delayed.events.limit' LIMIT 1), '100'), 'Maximum number of delayed wired events that can be queued in one room at the same time.'
UNION ALL
SELECT 'wired.monitor.overload.average.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.average.ms' LIMIT 1), '50'), 'Average execution time threshold in milliseconds that starts overload tracking.'
UNION ALL
SELECT 'wired.monitor.overload.peak.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.peak.ms' LIMIT 1), '150'), 'Peak single execution time threshold in milliseconds that starts overload tracking.'
UNION ALL
SELECT 'wired.monitor.overload.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.consecutive.windows' LIMIT 1), '2'), 'Number of consecutive overloaded monitor windows required before logging EXECUTOR_OVERLOAD.'
UNION ALL
SELECT 'wired.monitor.heavy.usage.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.usage.percent' LIMIT 1), '70'), 'Usage percentage threshold that contributes to marking a room as heavy in the :wired monitor.'
UNION ALL
SELECT 'wired.monitor.heavy.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.consecutive.windows' LIMIT 1), '5'), 'Number of consecutive windows above the heavy usage threshold required before the room is marked as heavy.'
UNION ALL
SELECT 'wired.monitor.heavy.delayed.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.delayed.percent' LIMIT 1), '60'), 'Delayed queue percentage threshold that also contributes to the heavy-room calculation.'
ON DUPLICATE KEY UPDATE
`value` = VALUES(`value`),
`comment` = VALUES(`comment`);
DELETE FROM `emulator_settings`
WHERE `key` IN (
'wired.engine.enabled',
'wired.engine.exclusive',
'wired.engine.maxStepsPerStack',
'wired.engine.debug',
'wired.custom.enabled',
'hotel.wired.furni.selection.count',
'hotel.wired.max_delay',
'hotel.wired.message.max_length',
'wired.effect.teleport.delay',
'wired.place.under',
'wired.tick.interval.ms',
'wired.tick.resolution',
'wired.tick.debug',
'wired.tick.thread.priority',
'wired.highscores.displaycount',
'wired.abuse.max.recursion.depth',
'wired.abuse.max.events.per.window',
'wired.abuse.rate.limit.window.ms',
'wired.abuse.ban.duration.ms',
'wired.monitor.usage.window.ms',
'wired.monitor.usage.limit',
'wired.monitor.delayed.events.limit',
'wired.monitor.overload.average.ms',
'wired.monitor.overload.peak.ms',
'wired.monitor.overload.consecutive.windows',
'wired.monitor.heavy.usage.percent',
'wired.monitor.heavy.consecutive.windows',
'wired.monitor.heavy.delayed.percent'
);
UPDATE `emulator_settings` SET `comment` = 'Allow whispering while a user stands inside a mute area.' WHERE `key` = 'room.chat.mutearea.allow_whisper';
UPDATE `emulator_settings` SET `comment` = 'HTML or text format used for room chat prefixes.' WHERE `key` = 'room.chat.prefix.format';
UPDATE `emulator_settings` SET `comment` = 'Badge code displayed on promoted rooms.' WHERE `key` = 'room.promotion.badge';
UPDATE `emulator_settings` SET `comment` = 'Image used by Rosie bubble notifications.' WHERE `key` = 'rosie.bubble.image.url';
UPDATE `emulator_settings` SET `comment` = 'Currency type used by Rosie when buying a room or room package.' WHERE `key` = 'rosie.buyroom.currency.type';
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `runtime.threads`.' WHERE `key` = 'runtime.threads';
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.private.chats`.' WHERE `key` = 'save.private.chats';
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.room.chats`.' WHERE `key` = 'save.room.chats';
UPDATE `emulator_settings` SET `comment` = 'Expose moderation tickets to the scripter or automation tooling.' WHERE `key` = 'scripter.modtool.tickets';
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for diamonds.' WHERE `key` = 'seasonal.currency.diamond';
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for duckets.' WHERE `key` = 'seasonal.currency.ducket';
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated display names for seasonal currency types.' WHERE `key` = 'seasonal.currency.names';
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for pixels.' WHERE `key` = 'seasonal.currency.pixel';
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for shells.' WHERE `key` = 'seasonal.currency.shell';
UPDATE `emulator_settings` SET `comment` = 'Primary seasonal currency type ID.' WHERE `key` = 'seasonal.primary.type';
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated list of currency type IDs treated as seasonal currencies.' WHERE `key` = 'seasonal.types';
UPDATE `emulator_settings` SET `comment` = 'Achievement code granted for the HC subscription tier.' WHERE `key` = 'subscriptions.hc.achievement';
UPDATE `emulator_settings` SET `comment` = 'Number of days before expiry when HC discount offers become available.' WHERE `key` = 'subscriptions.hc.discount.days_before_end';
UPDATE `emulator_settings` SET `comment` = 'Enable discounted HC renewal offers.' WHERE `key` = 'subscriptions.hc.discount.enabled';
UPDATE `emulator_settings` SET `comment` = 'Reset tracked credits spent when the HC subscription expires.' WHERE `key` = 'subscriptions.hc.payday.creditsspent_reset_on_expire';
UPDATE `emulator_settings` SET `comment` = 'Currency rewarded by the HC payday system.' WHERE `key` = 'subscriptions.hc.payday.currency';
UPDATE `emulator_settings` SET `comment` = 'Enable the HC payday reward system.' WHERE `key` = 'subscriptions.hc.payday.enabled';
UPDATE `emulator_settings` SET `comment` = 'Date interval used between HC payday reward runs.' WHERE `key` = 'subscriptions.hc.payday.interval';
UPDATE `emulator_settings` SET `comment` = 'Next scheduled execution date for HC payday rewards.' WHERE `key` = 'subscriptions.hc.payday.next_date';
UPDATE `emulator_settings` SET `comment` = 'Percentage of eligible spending returned by HC payday.' WHERE `key` = 'subscriptions.hc.payday.percentage';
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated streak thresholds and rewards for HC payday.' WHERE `key` = 'subscriptions.hc.payday.streak';
UPDATE `emulator_settings` SET `comment` = 'Enable the subscription background scheduler.' WHERE `key` = 'subscriptions.scheduler.enabled';
UPDATE `emulator_settings` SET `comment` = 'Interval in minutes between subscription scheduler runs.' WHERE `key` = 'subscriptions.scheduler.interval';
UPDATE `emulator_settings` SET `comment` = 'Compatibility marker used by the custom team wired implementation. Do not remove.' WHERE `key` = 'team.wired.update.rc-1';
UPDATE `emulator_settings` SET `comment` = 'API key used by the YouTube integration.' WHERE `key` = 'youtube.apikey';
-- =============================================================
-- Permissions normalization is handled by 005_normalize_permissions_schema.sql
-- (Removed from this file to avoid DELIMITER issues in HeidiSQL.)
-- =============================================================
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_id` 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_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;
CREATE TABLE IF NOT EXISTS `room_wired_variables` (
`room_id` int(11) NOT NULL,
`variable_item_id` int(11) NOT NULL,
`value` int(11) NOT NULL DEFAULT 0,
`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;
ALTER TABLE `room_user_wired_variables`
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
ALTER TABLE `room_user_wired_variables`
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
UPDATE `room_user_wired_variables`
SET
`created_at` = IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()),
`updated_at` = IF(`updated_at` > 0, `updated_at`, IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()));
ALTER TABLE `room_furni_wired_variables`
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
ALTER TABLE `room_furni_wired_variables`
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
UPDATE `room_furni_wired_variables`
SET
`created_at` = IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()),
`updated_at` = IF(`updated_at` > 0, `updated_at`, IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()));
ALTER TABLE `room_wired_variables`
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
ALTER TABLE `room_wired_variables`
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
UPDATE `room_wired_variables`
SET
`created_at` = 0,
`updated_at` = IF(`updated_at` > 0, `updated_at`, UNIX_TIMESTAMP());
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`);
ALTER TABLE `catalog_club_offers`
MODIFY COLUMN `type` ENUM('HC', 'VIP', 'BUILDERS_CLUB', 'BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC';
ALTER TABLE `catalog_pages`
MODIFY COLUMN `page_layout` ENUM(
'default_3x3',
'club_buy',
'club_gift',
'frontpage',
'spaces',
'recycler',
'recycler_info',
'recycler_prizes',
'trophies',
'plasto',
'marketplace',
'marketplace_own_items',
'spaces_new',
'soundmachine',
'guilds',
'guild_furni',
'info_duckets',
'info_rentables',
'info_pets',
'roomads',
'single_bundle',
'sold_ltd_items',
'badge_display',
'bots',
'pets',
'pets2',
'pets3',
'productpage1',
'room_bundle',
'recent_purchases',
'default_3x3_color_grouping',
'guild_forum',
'vip_buy',
'info_loyalty',
'loyalty_vip_buy',
'collectibles',
'petcustomization',
'frontpage_featured',
'builders_club_frontpage',
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
ALTER TABLE `catalog_pages`
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL', 'BUILDER', 'BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `club_only`;
ALTER TABLE `rooms`
ADD COLUMN IF NOT EXISTS `builders_club_trial_locked` TINYINT(1) NOT NULL DEFAULT 0 AFTER `allow_underpass`,
ADD COLUMN IF NOT EXISTS `builders_club_original_state` VARCHAR(16) NOT NULL DEFAULT 'open' AFTER `builders_club_trial_locked`;
CREATE TABLE IF NOT EXISTS `builders_club_items` (
`item_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`room_id` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`item_id`),
KEY `idx_builders_club_items_user_id` (`user_id`),
KEY `idx_builders_club_items_room_id` (`room_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
ALTER TABLE `catalog_pages_bc`
MODIFY COLUMN `page_layout` ENUM(
'default_3x3',
'club_buy',
'club_gift',
'frontpage',
'spaces',
'recycler',
'recycler_info',
'recycler_prizes',
'trophies',
'plasto',
'marketplace',
'marketplace_own_items',
'spaces_new',
'soundmachine',
'guilds',
'guild_furni',
'info_duckets',
'info_rentables',
'info_pets',
'roomads',
'single_bundle',
'sold_ltd_items',
'badge_display',
'bots',
'pets',
'pets2',
'pets3',
'productpage1',
'room_bundle',
'recent_purchases',
'default_3x3_color_grouping',
'guild_forum',
'vip_buy',
'info_loyalty',
'loyalty_vip_buy',
'collectibles',
'petcustomization',
'frontpage_featured',
'builders_club_frontpage',
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
File diff suppressed because one or more lines are too long
@@ -0,0 +1,28 @@
-- Make sure that the emulator has write access to the badge_path folder !!!!!
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 `badge_id` (`badge_id`),
KEY `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;
@@ -0,0 +1 @@
ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `background_card_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_overlay_id`;
@@ -0,0 +1,356 @@
-- ============================================================
-- Custom Prefix System - Complete Setup (safe upgrade version)
-- ============================================================
-- Questo script è pensato per essere rieseguito senza errori
-- anche se le tabelle esistono già con una struttura parziale.
-- ------------------------------------------------------------
-- 1. Main user prefixes table
-- ------------------------------------------------------------
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`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_user_active` (`user_id`, `active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ------------------------------------------------------------
-- 2. Catalog table
-- ------------------------------------------------------------
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;
-- ------------------------------------------------------------
-- 3. User visual settings
-- ------------------------------------------------------------
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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ------------------------------------------------------------
-- 4. Prefix settings table
-- ------------------------------------------------------------
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;
-- ------------------------------------------------------------
-- 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
-- ============================================================
-- ------------------------------------------------------------
-- user_prefixes: add missing columns safely
-- ------------------------------------------------------------
SET @dbname = DATABASE();
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'font'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `font` VARCHAR(50) NOT NULL DEFAULT '''' AFTER `effect`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'catalog_prefix_id'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `catalog_prefix_id` INT(11) NOT NULL DEFAULT 0 AFTER `font`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'display_name'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `display_name` VARCHAR(100) NOT NULL DEFAULT '''' AFTER `catalog_prefix_id`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'points'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `points` INT(11) NOT NULL DEFAULT 0 AFTER `display_name`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'points_type'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `points_type` INT(11) NOT NULL DEFAULT 0 AFTER `points`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'is_custom'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `is_custom` TINYINT(1) NOT NULL DEFAULT 1 AFTER `points_type`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- ------------------------------------------------------------
-- custom_prefixes_catalog: add missing columns safely
-- ------------------------------------------------------------
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'font'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `font` VARCHAR(50) NOT NULL DEFAULT '''' AFTER `effect`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'points'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `points` INT(11) NOT NULL DEFAULT 0 AFTER `font`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'points_type'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `points_type` INT(11) NOT NULL DEFAULT 0 AFTER `points`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'enabled'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `enabled` TINYINT(1) NOT NULL DEFAULT 1 AFTER `points_type`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'sort_order'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `sort_order` INT(11) NOT NULL DEFAULT 0 AFTER `enabled`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- ============================================================
-- Default settings
-- ============================================================
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');
-- ============================================================
-- Default catalog entries
-- ============================================================
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);
-- ============================================================
-- Example blacklist entries
-- ============================================================
INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES
('admin'),
('staff'),
('mod'),
('owner');
-- ============================================================
-- Notes
-- ============================================================
-- Preset prefixes for `:customize` are loaded directly by
-- UserNickIconsComposer and displayed inside the `:customize` panel.
--
-- This setup does not require rows in `catalog_pages`.
--
-- Command texts / permission inserts are intentionally omitted
-- for compatibility with both legacy and normalized permission schemas.
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
-- GivePrefix command
('commands.keys.cmd_give_prefix', 'giveprefix'),
('commands.error.cmd_give_prefix.usage', 'Usage: :giveprefix <username> <text> <color> [icon] [effect]'),
('commands.error.cmd_give_prefix.invalid_color', 'Invalid color format. Use hex format (#FF0000).'),
('commands.error.cmd_give_prefix.too_long', 'Prefix text is too long (max 15 characters).'),
('commands.error.cmd_give_prefix.user_not_found', 'User not found or not online.'),
('commands.succes.cmd_give_prefix', 'Prefix {%prefix%} successfully given to %user%!'),
-- ListPrefixes command
('commands.keys.cmd_list_prefixes', 'listprefixes'),
('commands.error.cmd_list_prefixes.usage', 'Usage: :listprefixes <username>'),
('commands.error.cmd_list_prefixes.user_not_found', 'User not found or not online.'),
('commands.succes.cmd_list_prefixes.header', 'Prefixes of %user%:'),
('commands.succes.cmd_list_prefixes.empty', '%user% has no prefixes.'),
-- RemovePrefix command
('commands.keys.cmd_remove_prefix', 'removeprefix'),
('commands.error.cmd_remove_prefix.usage', 'Usage: :removeprefix <username> <id|all>'),
('commands.error.cmd_remove_prefix.user_not_found', 'User not found or not online.'),
('commands.error.cmd_remove_prefix.invalid_id', 'Invalid prefix ID. Must be a number or "all".'),
('commands.error.cmd_remove_prefix.not_found', 'Prefix not found for this user.'),
('commands.succes.cmd_remove_prefix', 'Prefix #%id% removed from %user%.'),
('commands.succes.cmd_remove_prefix.all', 'All prefixes removed from %user%.'),
-- PrefixBlacklist command
('commands.keys.cmd_prefix_blacklist', 'prefixblacklist'),
('commands.error.cmd_prefix_blacklist.usage', 'Usage: :prefixblacklist <add|remove|list> [word]'),
('commands.error.cmd_prefix_blacklist.empty_word', 'Word cannot be empty.'),
('commands.succes.cmd_prefix_blacklist.header', 'Blacklisted prefix words:'),
('commands.succes.cmd_prefix_blacklist.empty', 'No blacklisted words.'),
('commands.succes.cmd_prefix_blacklist.added', 'Word "%word%" added to prefix blacklist.'),
('commands.succes.cmd_prefix_blacklist.removed', 'Word "%word%" removed from prefix blacklist.');
INSERT IGNORE INTO permission_definitions
(permission_key, max_value, rank_1, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7)
VALUES
('cmd_give_prefix', '1', '0', '0', '0', '0', '0', '0', '1'),
('cmd_list_prefixes', '1', '0', '0', '0', '0', '0', '0', '1'),
('cmd_remove_prefix', '1', '0', '0', '0', '0', '0', '0', '1'),
('cmd_prefix_blacklist', '1', '0', '0', '0', '0', '0', '0', '1');
@@ -0,0 +1,244 @@
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=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT INTO `infostand_backgrounds` (`id`, `category`, `min_rank`, `is_hc_only`, `is_ambassador_only`) VALUES
(0, 'background', 0, 0, 0),
(1, 'background', 0, 0, 0),
(2, 'background', 0, 0, 0),
(3, 'background', 0, 0, 0),
(4, 'background', 0, 0, 0),
(5, 'background', 0, 0, 0),
(6, 'background', 0, 0, 0),
(7, 'background', 0, 0, 0),
(8, 'background', 0, 0, 0),
(9, 'background', 0, 0, 0),
(10, 'background', 0, 0, 0),
(11, 'background', 0, 0, 0),
(12, 'background', 0, 0, 0),
(13, 'background', 0, 0, 0),
(14, 'background', 0, 0, 0),
(15, 'background', 0, 0, 0),
(16, 'background', 0, 0, 0),
(17, 'background', 0, 0, 0),
(18, 'background', 0, 0, 0),
(19, 'background', 0, 0, 0),
(20, 'background', 0, 0, 0),
(21, 'background', 0, 0, 0),
(22, 'background', 0, 0, 0),
(23, 'background', 0, 0, 0),
(24, 'background', 0, 0, 0),
(25, 'background', 0, 0, 0),
(26, 'background', 0, 0, 0),
(27, 'background', 0, 0, 0),
(28, 'background', 0, 0, 0),
(29, 'background', 0, 0, 0),
(30, 'background', 0, 0, 0),
(31, 'background', 0, 0, 0),
(32, 'background', 0, 0, 0),
(33, 'background', 0, 0, 0),
(34, 'background', 0, 0, 0),
(35, 'background', 0, 0, 0),
(36, 'background', 0, 0, 0),
(37, 'background', 0, 0, 0),
(38, 'background', 0, 0, 0),
(39, 'background', 0, 0, 0),
(40, 'background', 0, 0, 0),
(41, 'background', 0, 0, 0),
(42, 'background', 0, 1, 0),
(43, 'background', 0, 1, 0),
(44, 'background', 0, 1, 0),
(45, 'background', 0, 1, 0),
(46, 'background', 0, 1, 0),
(47, 'background', 0, 1, 0),
(48, 'background', 0, 1, 0),
(49, 'background', 0, 1, 0),
(50, 'background', 0, 1, 0),
(51, 'background', 0, 1, 0),
(52, 'background', 0, 1, 0),
(53, 'background', 0, 1, 0),
(54, 'background', 0, 1, 0),
(55, 'background', 0, 1, 0),
(56, 'background', 0, 1, 0),
(57, 'background', 0, 1, 0),
(58, 'background', 0, 1, 0),
(59, 'background', 0, 1, 0),
(60, 'background', 0, 1, 0),
(61, 'background', 0, 1, 0),
(62, 'background', 0, 1, 0),
(63, 'background', 0, 1, 0),
(64, 'background', 0, 1, 0),
(65, 'background', 0, 1, 0),
(66, 'background', 0, 1, 0),
(67, 'background', 0, 1, 0),
(68, 'background', 0, 1, 0),
(69, 'background', 0, 1, 0),
(70, 'background', 0, 1, 0),
(71, 'background', 0, 1, 0),
(72, 'background', 0, 1, 0),
(73, 'background', 0, 1, 0),
(74, 'background', 0, 1, 0),
(75, 'background', 0, 1, 0),
(76, 'background', 0, 1, 0),
(77, 'background', 0, 1, 0),
(78, 'background', 0, 1, 0),
(79, 'background', 0, 1, 0),
(80, 'background', 0, 1, 0),
(81, 'background', 0, 1, 0),
(82, 'background', 0, 1, 0),
(83, 'background', 0, 1, 0),
(84, 'background', 0, 1, 0),
(85, 'background', 0, 1, 0),
(86, 'background', 0, 1, 0),
(87, 'background', 0, 1, 0),
(88, 'background', 0, 1, 0),
(89, 'background', 0, 1, 0),
(90, 'background', 0, 1, 0),
(91, 'background', 0, 1, 0),
(92, 'background', 0, 1, 0),
(93, 'background', 0, 1, 0),
(94, 'background', 0, 1, 0),
(95, 'background', 0, 1, 0),
(96, 'background', 0, 1, 0),
(97, 'background', 0, 1, 0),
(98, 'background', 0, 1, 0),
(99, 'background', 0, 1, 0),
(100, 'background', 0, 1, 0),
(101, 'background', 2, 0, 0),
(102, 'background', 0, 1, 0),
(103, 'background', 0, 1, 0),
(104, 'background', 0, 1, 0),
(105, 'background', 0, 1, 0),
(106, 'background', 0, 1, 0),
(107, 'background', 0, 1, 0),
(108, 'background', 0, 1, 0),
(109, 'background', 0, 1, 0),
(110, 'background', 0, 1, 0),
(111, 'background', 0, 1, 0),
(112, 'background', 0, 1, 0),
(113, 'background', 0, 1, 0),
(114, 'background', 0, 1, 0),
(115, 'background', 0, 1, 0),
(116, 'background', 0, 1, 0),
(117, 'background', 0, 1, 0),
(118, 'background', 0, 1, 0),
(119, 'background', 0, 1, 0),
(120, 'background', 0, 1, 0),
(121, 'background', 0, 1, 0),
(122, 'background', 0, 1, 0),
(123, 'background', 0, 1, 0),
(124, 'background', 0, 1, 0),
(125, 'background', 0, 1, 0),
(126, 'background', 0, 1, 0),
(127, 'background', 0, 1, 0),
(128, 'background', 0, 1, 0),
(129, 'background', 0, 1, 0),
(130, 'background', 0, 1, 0),
(131, 'background', 0, 1, 0),
(132, 'background', 0, 1, 0),
(133, 'background', 0, 1, 0),
(134, 'background', 0, 1, 0),
(135, 'background', 0, 1, 0),
(136, 'background', 0, 1, 0),
(137, 'background', 0, 1, 0),
(138, 'background', 0, 1, 0),
(139, 'background', 0, 1, 0),
(140, 'background', 0, 1, 0),
(141, 'background', 0, 1, 0),
(142, 'background', 0, 1, 0),
(143, 'background', 0, 1, 0),
(144, 'background', 0, 1, 0),
(145, 'background', 0, 1, 0),
(146, 'background', 0, 1, 0),
(147, 'background', 0, 1, 0),
(148, 'background', 0, 1, 0),
(149, 'background', 0, 1, 0),
(150, 'background', 0, 1, 0),
(151, 'background', 0, 1, 0),
(152, 'background', 0, 1, 0),
(153, 'background', 0, 1, 0),
(154, 'background', 0, 1, 0),
(155, 'background', 0, 1, 0),
(156, 'background', 0, 1, 0),
(157, 'background', 0, 1, 0),
(158, 'background', 0, 1, 0),
(159, 'background', 0, 1, 0),
(160, 'background', 0, 1, 0),
(161, 'background', 0, 1, 0),
(162, 'background', 0, 1, 0),
(163, 'background', 0, 1, 0),
(164, 'background', 0, 1, 0),
(165, 'background', 0, 1, 0),
(166, 'background', 0, 1, 0),
(167, 'background', 0, 1, 0),
(168, 'background', 0, 1, 0),
(169, 'background', 0, 1, 0),
(170, 'background', 0, 1, 0),
(171, 'background', 0, 1, 0),
(172, 'background', 0, 1, 0),
(173, 'background', 0, 1, 0),
(174, 'background', 0, 1, 0),
(175, 'background', 0, 1, 0),
(176, 'background', 0, 1, 0),
(177, 'background', 0, 1, 0),
(178, 'background', 0, 1, 0),
(179, 'background', 0, 1, 0),
(180, 'background', 0, 1, 0),
(181, 'background', 0, 1, 0),
(182, 'background', 0, 1, 0),
(183, 'background', 0, 1, 0),
(184, 'background', 0, 1, 0),
(185, 'background', 0, 1, 0),
(186, 'background', 0, 1, 0),
(187, 'background', 0, 1, 0),
(0, 'stand', 0, 0, 0),
(1, 'stand', 0, 0, 0),
(2, 'stand', 0, 0, 0),
(3, 'stand', 0, 0, 0),
(4, 'stand', 0, 0, 0),
(5, 'stand', 0, 0, 0),
(6, 'stand', 0, 0, 0),
(7, 'stand', 0, 0, 0),
(8, 'stand', 0, 0, 0),
(9, 'stand', 0, 0, 0),
(10, 'stand', 0, 0, 0),
(11, 'stand', 0, 0, 0),
(12, 'stand', 0, 0, 0),
(13, 'stand', 0, 0, 0),
(14, 'stand', 0, 0, 0),
(15, 'stand', 0, 0, 0),
(16, 'stand', 0, 1, 0),
(17, 'stand', 0, 1, 0),
(18, 'stand', 0, 1, 0),
(19, 'stand', 0, 1, 0),
(20, 'stand', 0, 1, 0),
(21, 'stand', 0, 1, 0),
(0, 'overlay', 0, 0, 0),
(1, 'overlay', 0, 0, 0),
(2, 'overlay', 0, 1, 0),
(3, 'overlay', 0, 1, 0),
(4, 'overlay', 0, 1, 0),
(5, 'overlay', 0, 1, 0),
(6, 'overlay', 0, 1, 0),
(7, 'overlay', 0, 1, 0),
(8, 'overlay', 0, 1, 0),
(1, 'card', 0, 0, 0),
(2, 'card', 0, 0, 0),
(3, 'card', 0, 0, 0),
(4, 'card', 0, 0, 0),
(5, 'card', 0, 0, 0),
(6, 'card', 0, 0, 0),
(7, 'card', 0, 0, 0),
(8, 'card', 0, 0, 0),
(9, 'card', 0, 0, 0),
(10, 'card', 0, 0, 0),
(11, 'card', 0, 0, 0),
(12, 'card', 0, 0, 0),
(13, 'card', 0, 0, 0),
(14, 'card', 0, 0, 0),
(15, 'card', 0, 0, 0);
@@ -0,0 +1,6 @@
ALTER TABLE users
ADD COLUMN `last_username_change` INT(11) NOT NULL DEFAULT 0;
INSERT INTO emulator_settings (`key`, `value`, `comment`)
VALUES ('rename.cooldown_days', '30', 'Days between username changes');
@@ -0,0 +1,51 @@
-- ============================================================
-- Nick Icon Customization Setup
-- ============================================================
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_icon_key` (`icon_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
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_icon` (`user_id`, `icon_key`),
KEY `idx_user_id` (`user_id`),
KEY `idx_user_active` (`user_id`, `active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
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);
ALTER TABLE `custom_nick_icons_catalog`
ADD COLUMN IF NOT EXISTS `display_name` VARCHAR(100) NOT NULL DEFAULT '' AFTER `icon_key`;
ALTER TABLE `users`
ADD COLUMN IF NOT EXISTS `remember_token_hash` VARCHAR(64) NOT NULL DEFAULT '' AFTER `auth_ticket`;
ALTER TABLE `users`
ADD COLUMN IF NOT EXISTS `remember_token_expires_at` INT(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `remember_token_hash`;
ALTER TABLE `users`
ADD INDEX IF NOT EXISTS `idx_users_remember_token_hash` (`remember_token_hash`);
INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`)
VALUES ('hotel.wired.message.max_length', '512', 'Maximum length of text fields used by wired messages and bot text effects.')
ON DUPLICATE KEY UPDATE `value` = '512';
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4 -3
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.1.6</version>
<version>4.2.29</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -38,6 +38,7 @@
<archive>
<manifest>
<mainClass>com.eu.habbo.Emulator</mainClass>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
@@ -163,7 +164,7 @@
<version>2.13.0</version>
</dependency>
<!-- jBCrypt used by the built-in /api/auth/* HTTP login handler
<!-- jBCrypt used by the built-in /api/auth/* HTTP login handler
to verify Laravel-style $2y$ BCrypt hashes from users.password -->
<dependency>
<groupId>org.mindrot</groupId>
@@ -171,7 +172,7 @@
<version>0.4</version>
</dependency>
<!-- Jakarta Mail used by the built-in forgot-password endpoint
<!-- Jakarta Mail used by the built-in forgot-password endpoint
when smtp.* keys are configured in emulator_settings -->
<dependency>
<groupId>org.eclipse.angus</groupId>
@@ -1,17 +0,0 @@
-- ============================================================
-- Catalog & Furni Admin Permission
-- Adds acc_catalogfurni permission to the permissions table
-- Required by: CatalogAdmin packet handlers (10050-10059)
-- ============================================================
-- 1. Add the column to the permissions table
ALTER TABLE `permissions`
ADD COLUMN `acc_catalogfurni` ENUM('0','1') NOT NULL DEFAULT '0'
AFTER `acc_catalog_ids`;
-- 2. Enable for Administrator (rank 7) by default
UPDATE `permissions` SET `acc_catalogfurni` = '1' WHERE `id` = 7;
-- Optional: enable for other ranks as needed
-- UPDATE `permissions` SET `acc_catalogfurni` = '1' WHERE `id` = 6; -- Super Mod
-- UPDATE `permissions` SET `acc_catalogfurni` = '1' WHERE `id` = 5; -- Moderator
@@ -1,115 +0,0 @@
-- ============================================================
-- Custom Prefix System - Complete Setup
-- ============================================================
-- 1. Main user prefixes table
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 '',
`active` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_user_active` (`user_id`, `active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. Prefix settings table
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;
-- Default settings
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');
-- 3. Blacklisted words 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;
-- Example blacklist entries (customize as needed)
INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES
('admin'),
('staff'),
('mod'),
('owner');
-- 4. Add effect column (if table already exists without it)
-- ALTER TABLE `user_prefixes` ADD COLUMN IF NOT EXISTS `effect` VARCHAR(50) NOT NULL DEFAULT '' AFTER `icon`;
-- ============================================================
-- Catalog page for custom prefixes
-- ============================================================
-- NOTE: Adjust parent_id to match your catalog parent category ID.
-- Example: parent_id = -1 for root, or the ID of your "Extra" / "Specials" category
INSERT INTO `catalog_pages` (
`parent_id`, `caption`, `caption_save`, `icon_image`, `visible`, `enabled`,
`min_rank`, `page_layout`, `page_strings_1`, `page_strings_2`
) VALUES (
-1,
'Custom Prefix',
'custom_prefix',
1,
1,
1,
1,
'custom_prefix',
'Create your own custom prefix!\rChoose text, colors, icon and effects to stand out in chat.',
''
);
-- ============================================================
-- Command texts (insert into emulator_texts if not present)
-- ============================================================
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
-- GivePrefix command
('commands.keys.cmd_give_prefix', 'giveprefix'),
('commands.error.cmd_give_prefix.usage', 'Usage: :giveprefix <username> <text> <color> [icon] [effect]'),
('commands.error.cmd_give_prefix.invalid_color', 'Invalid color format. Use hex format (#FF0000).'),
('commands.error.cmd_give_prefix.too_long', 'Prefix text is too long (max 15 characters).'),
('commands.error.cmd_give_prefix.user_not_found', 'User not found or not online.'),
('commands.succes.cmd_give_prefix', 'Prefix {%prefix%} successfully given to %user%!'),
-- ListPrefixes command
('commands.keys.cmd_list_prefixes', 'listprefixes'),
('commands.error.cmd_list_prefixes.usage', 'Usage: :listprefixes <username>'),
('commands.error.cmd_list_prefixes.user_not_found', 'User not found or not online.'),
('commands.succes.cmd_list_prefixes.header', 'Prefixes of %user%:'),
('commands.succes.cmd_list_prefixes.empty', '%user% has no prefixes.'),
-- RemovePrefix command
('commands.keys.cmd_remove_prefix', 'removeprefix'),
('commands.error.cmd_remove_prefix.usage', 'Usage: :removeprefix <username> <id|all>'),
('commands.error.cmd_remove_prefix.user_not_found', 'User not found or not online.'),
('commands.error.cmd_remove_prefix.invalid_id', 'Invalid prefix ID. Must be a number or "all".'),
('commands.error.cmd_remove_prefix.not_found', 'Prefix not found for this user.'),
('commands.succes.cmd_remove_prefix', 'Prefix #%id% removed from %user%.'),
('commands.succes.cmd_remove_prefix.all', 'All prefixes removed from %user%.'),
-- PrefixBlacklist command
('commands.keys.cmd_prefix_blacklist', 'prefixblacklist'),
('commands.error.cmd_prefix_blacklist.usage', 'Usage: :prefixblacklist <add|remove|list> [word]'),
('commands.error.cmd_prefix_blacklist.empty_word', 'Word cannot be empty.'),
('commands.succes.cmd_prefix_blacklist.header', 'Blacklisted prefix words:'),
('commands.succes.cmd_prefix_blacklist.empty', 'No blacklisted words.'),
('commands.succes.cmd_prefix_blacklist.added', 'Word "%word%" added to prefix blacklist.'),
('commands.succes.cmd_prefix_blacklist.removed', 'Word "%word%" removed from prefix blacklist.');
-- ============================================================
-- Permissions for prefix commands (add to permissions table)
-- ============================================================
INSERT IGNORE INTO `permissions` (`id`, `rank_id`, `permission_name`, `setting_type`) VALUES
(NULL, 7, 'cmd_give_prefix', '1'),
(NULL, 7, 'cmd_list_prefixes', '1'),
(NULL, 7, 'cmd_remove_prefix', '1'),
(NULL, 7, 'cmd_prefix_blacklist', '1');
@@ -6,6 +6,7 @@ import ch.qos.logback.core.ConsoleAppender;
import com.eu.habbo.core.*;
import com.eu.habbo.core.consolecommands.ConsoleCommand;
import com.eu.habbo.database.Database;
import com.eu.habbo.gui.EmulatorDashboard;
import com.eu.habbo.habbohotel.GameEnvironment;
import com.eu.habbo.habbohotel.gameclients.SessionResumeManager;
import com.eu.habbo.networking.gameserver.GameServer;
@@ -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 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 MINOR = 1;
public final static int BUILD = 0;
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 =
"\n" +
"███╗ ███╗ ██████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗████████╗ █████╗ ██████╗ \n" +
@@ -186,6 +198,10 @@ public final class Emulator {
Emulator.isReady = true;
Emulator.timeStarted = getIntUnixTimestamp();
if (Emulator.getConfig().getBoolean("gui.enabled", true)) {
EmulatorDashboard.launch();
}
if (Emulator.getConfig().getInt("runtime.threads") < (Runtime.getRuntime().availableProcessors() * 2)) {
LOGGER.warn("Emulator settings runtime.threads ({}) can be increased to ({}) to possibly increase performance.",
Emulator.getConfig().getInt("runtime.threads"),
@@ -28,6 +28,7 @@ public class RoomUserPetComposer extends MessageComposer {
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendString(this.petType + " " + this.race + " " + this.color + " 2 2 -1 0 3 -1 0");
this.response.appendInt(this.habbo.getRoomUnit().getId());
this.response.appendInt(this.habbo.getRoomUnit().getX());
@@ -49,6 +50,7 @@ public class RoomUserPetComposer extends MessageComposer {
this.response.appendString("");
this.response.appendString("unknown");
this.response.appendInt(0);
this.response.appendInt(0);
return this.response;
}
@@ -0,0 +1,631 @@
package com.eu.habbo.gui;
import com.eu.habbo.Emulator;
import com.eu.habbo.monitoring.EmulatorStatsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.MatteBorder;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Path2D;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class EmulatorDashboard extends JFrame {
private static final Logger LOGGER = LoggerFactory.getLogger(EmulatorDashboard.class);
// Modern Dark Theme Colors
private static final Color COLOR_BG = new Color(18, 18, 18);
private static final Color COLOR_SURFACE = new Color(30, 30, 30);
private static final Color COLOR_SURFACE_HOVER = new Color(45, 45, 45);
private static final Color COLOR_PRIMARY = new Color(99, 102, 241);
private static final Color COLOR_PRIMARY_SOFT = new Color(99, 102, 241, 45);
private static final Color COLOR_SUCCESS = new Color(34, 197, 94);
private static final Color COLOR_WARNING = new Color(245, 158, 11);
private static final Color COLOR_TEXT = new Color(240, 240, 240);
private static final Color COLOR_TEXT_MUTED = new Color(150, 150, 150);
private static final Color COLOR_TEXT_SUBTLE = new Color(110, 110, 110);
private static final Color COLOR_BORDER = new Color(50, 50, 50);
private static final Font FONT_TITLE = new Font("Segoe UI", Font.BOLD, 26);
private static final Font FONT_SECTION = new Font("Segoe UI", Font.BOLD, 16);
private static final Font FONT_SMALL = new Font("Segoe UI", Font.PLAIN, 12);
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
private static EmulatorDashboard instance;
// Overview Tab
private final JLabel memLabel = createMetricLabel();
private final JLabel cpuLabel = createMetricLabel();
private final JLabel threadLabel = createMetricLabel();
private final JLabel usersLabel = createMetricLabel();
private final JLabel roomsLabel = createMetricLabel();
private final JLabel wiredLabel = createMetricLabel();
private final JLabel uptimeLabel = createStatusValueLabel();
private final JLabel lastUpdatedLabel = createStatusValueLabel();
private final JLabel footerStatusLabel = createStatusValueLabel();
private final MemoryGraphPanel memoryGraph = new MemoryGraphPanel();
// Tables
private final DefaultTableModel usersTableModel;
private final DefaultTableModel roomsTableModel;
private final DefaultTableModel wiredTableModel;
private final JTable usersTable;
private final JTable roomsTable;
private final JTable wiredTable;
private final JLabel usersCountLabel = createCountLabel();
private final JLabel roomsCountLabel = createCountLabel();
private final JLabel wiredCountLabel = createCountLabel();
// UI Components
private final JPanel cardsPanel;
private final CardLayout cardLayout;
private final Map<String, JPanel> navButtons = new HashMap<>();
private String selectedCardName = "Overview";
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "Dashboard-Updater");
t.setDaemon(true);
return t;
});
private EmulatorDashboard() {
setTitle("Arcturus Morningstar - System Dashboard");
setSize(1100, 700);
setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
setLocationRelativeTo(null);
getContentPane().setBackground(COLOR_BG);
setLayout(new BorderLayout());
// Setup custom Look & Feel basics to remove weird Swing borders
UIManager.put("ScrollBar.background", COLOR_BG);
UIManager.put("ScrollBar.thumb", COLOR_SURFACE_HOVER);
// Sidebar
JPanel sidebar = new JPanel();
sidebar.setLayout(new BoxLayout(sidebar, BoxLayout.Y_AXIS));
sidebar.setBackground(COLOR_SURFACE);
sidebar.setPreferredSize(new Dimension(220, 0));
sidebar.setBorder(new MatteBorder(0, 0, 0, 1, COLOR_BORDER));
// Sidebar Header
JPanel brandPanel = new JPanel(new BorderLayout());
brandPanel.setBackground(COLOR_SURFACE);
brandPanel.setBorder(new EmptyBorder(20, 20, 30, 20));
JLabel brandTitle = new JLabel("Arcturus");
brandTitle.setFont(new Font("Segoe UI", Font.BOLD, 22));
brandTitle.setForeground(COLOR_TEXT);
JLabel brandSub = new JLabel("v" + Emulator.version);
brandSub.setFont(new Font("Segoe UI", Font.PLAIN, 12));
brandSub.setForeground(COLOR_PRIMARY);
brandPanel.add(brandTitle, BorderLayout.NORTH);
brandPanel.add(brandSub, BorderLayout.SOUTH);
sidebar.add(brandPanel);
// Main Cards
cardLayout = new CardLayout();
cardsPanel = new JPanel(cardLayout);
cardsPanel.setBackground(COLOR_BG);
// Setup Tabs
usersTableModel = createTableModel(new String[]{"ID", "Username", "Rank", "Credits", "Room ID"});
roomsTableModel = createTableModel(new String[]{"Room ID", "Name", "Players", "Items", "Tickables", "CPU (ms)", "Est. RAM (KB)", "Thread"});
wiredTableModel = createTableModel(new String[]{"Room ID", "Avg Tick", "Peak Tick", "Usage %", "Delayed", "Overloaded?", "Heavy?"});
usersTable = createDashboardTable(usersTableModel);
roomsTable = createDashboardTable(roomsTableModel);
wiredTable = createDashboardTable(wiredTableModel);
cardsPanel.add(createOverviewTab(), "Overview");
cardsPanel.add(createTableTab("Online Users", "Players currently connected to the emulator.", usersTable, usersCountLabel), "Online Users");
cardsPanel.add(createTableTab("Active Rooms", "Loaded rooms with lightweight performance indicators.", roomsTable, roomsCountLabel), "Active Rooms");
cardsPanel.add(createTableTab("Wired Diagnostics", "Rooms currently using wired timing, delay and execution budget.", wiredTable, wiredCountLabel), "Wired Diagnostics");
// Sidebar Navigation
sidebar.add(createNavButton("Overview", "Overview"));
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
sidebar.add(createNavButton("Online Users", "Online Users"));
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
sidebar.add(createNavButton("Active Rooms", "Active Rooms"));
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
sidebar.add(createNavButton("Wired Diagnostics", "Wired Diagnostics"));
sidebar.add(Box.createVerticalGlue());
add(sidebar, BorderLayout.WEST);
add(cardsPanel, BorderLayout.CENTER);
add(createStatusBar(), BorderLayout.SOUTH);
addComponentListener(new ComponentAdapter() {
@Override
public void componentShown(ComponentEvent e) {
setActiveCard("Overview");
}
});
// Start updates
scheduler.scheduleAtFixedRate(this::updateMetrics, 0, 1, TimeUnit.SECONDS);
}
private DefaultTableModel createTableModel(String[] cols) {
return new DefaultTableModel(cols, 0) {
@Override public boolean isCellEditable(int row, int column) { return false; }
};
}
private JPanel createNavButton(String text, String cardName) {
JPanel btn = new JPanel(new BorderLayout());
btn.setBackground(COLOR_SURFACE);
btn.setMaximumSize(new Dimension(220, 45));
btn.setBorder(new EmptyBorder(0, 18, 0, 0));
btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
navButtons.put(cardName, btn);
JLabel lbl = new JLabel(text);
lbl.setFont(new Font("Segoe UI", Font.BOLD, 14));
lbl.setForeground(COLOR_TEXT_MUTED);
btn.add(lbl, BorderLayout.CENTER);
btn.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
btn.setBackground(COLOR_SURFACE_HOVER);
lbl.setForeground(COLOR_TEXT);
}
@Override
public void mouseExited(MouseEvent e) {
updateNavButtonStyle(cardName, btn, lbl);
}
@Override
public void mouseClicked(MouseEvent e) {
setActiveCard(cardName);
}
});
updateNavButtonStyle(cardName, btn, lbl);
return btn;
}
private JPanel createOverviewTab() {
JPanel wrapper = new JPanel(new BorderLayout());
wrapper.setBackground(COLOR_BG);
wrapper.setBorder(new EmptyBorder(30, 30, 30, 30));
JPanel header = new JPanel(new BorderLayout(0, 14));
header.setOpaque(false);
JLabel title = new JLabel("Dashboard Overview");
title.setFont(FONT_TITLE);
title.setForeground(COLOR_TEXT);
JLabel subtitle = new JLabel("Operational view of emulator health, activity and wired performance.");
subtitle.setFont(new Font("Segoe UI", Font.PLAIN, 13));
subtitle.setForeground(COLOR_TEXT_MUTED);
JPanel titleBlock = new JPanel();
titleBlock.setOpaque(false);
titleBlock.setLayout(new BoxLayout(titleBlock, BoxLayout.Y_AXIS));
titleBlock.add(title);
titleBlock.add(Box.createRigidArea(new Dimension(0, 4)));
titleBlock.add(subtitle);
header.add(titleBlock, BorderLayout.NORTH);
header.add(createOverviewMetaPanel(), BorderLayout.SOUTH);
wrapper.add(header, BorderLayout.NORTH);
JPanel content = new JPanel(new GridLayout(1, 2, 20, 20));
content.setOpaque(false);
content.setBorder(new EmptyBorder(20, 0, 0, 0));
// Left Stats
JPanel statsPanel = new JPanel(new GridLayout(3, 2, 12, 12));
statsPanel.setOpaque(false);
statsPanel.add(createMetricCard("Memory Allocation", memLabel));
statsPanel.add(createMetricCard("CPU Load", cpuLabel));
statsPanel.add(createMetricCard("Active OS Threads", threadLabel));
statsPanel.add(createMetricCard("Connected Players", usersLabel));
statsPanel.add(createMetricCard("Loaded Rooms", roomsLabel));
statsPanel.add(createMetricCard("Wired Tickables", wiredLabel));
content.add(statsPanel);
// Right Graph
JPanel graphContainer = new JPanel(new BorderLayout());
graphContainer.setBackground(COLOR_SURFACE);
graphContainer.setBorder(BorderFactory.createCompoundBorder(
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
new EmptyBorder(15, 15, 15, 15)
));
JLabel gTitle = new JLabel("Realtime Memory Usage");
gTitle.setFont(FONT_SECTION);
gTitle.setForeground(COLOR_TEXT_MUTED);
gTitle.setBorder(new EmptyBorder(0, 0, 15, 0));
graphContainer.add(gTitle, BorderLayout.NORTH);
graphContainer.add(memoryGraph, BorderLayout.CENTER);
content.add(graphContainer);
wrapper.add(content, BorderLayout.CENTER);
return wrapper;
}
private JPanel createMetricCard(String title, JLabel valueLabel) {
JPanel card = new JPanel(new BorderLayout());
card.setBackground(COLOR_SURFACE);
card.setBorder(BorderFactory.createCompoundBorder(
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
new EmptyBorder(15, 20, 15, 20)
));
JLabel tLabel = new JLabel(title);
tLabel.setFont(new Font("Segoe UI", Font.BOLD, 13));
tLabel.setForeground(COLOR_TEXT_MUTED);
card.add(tLabel, BorderLayout.NORTH);
card.add(valueLabel, BorderLayout.SOUTH);
return card;
}
private JLabel createMetricLabel() {
JLabel label = new JLabel("-");
label.setFont(new Font("Segoe UI", Font.BOLD, 28));
label.setForeground(COLOR_TEXT);
return label;
}
private JPanel createOverviewMetaPanel() {
JPanel panel = new JPanel(new GridLayout(1, 3, 12, 12));
panel.setOpaque(false);
panel.add(createStatusCard("Uptime", uptimeLabel, COLOR_PRIMARY));
panel.add(createStatusCard("Last Refresh", lastUpdatedLabel, COLOR_SUCCESS));
panel.add(createStatusCard("GUI Status", footerStatusLabel, COLOR_WARNING));
return panel;
}
private JPanel createStatusCard(String title, JLabel valueLabel, Color accent) {
JPanel panel = new JPanel(new BorderLayout());
panel.setBackground(COLOR_SURFACE);
panel.setBorder(BorderFactory.createCompoundBorder(
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
new EmptyBorder(12, 14, 12, 14)
));
JPanel accentBar = new JPanel();
accentBar.setBackground(accent);
accentBar.setPreferredSize(new Dimension(6, 0));
JPanel content = new JPanel();
content.setOpaque(false);
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
JLabel label = new JLabel(title);
label.setFont(FONT_SMALL);
label.setForeground(COLOR_TEXT_MUTED);
content.add(label);
content.add(Box.createRigidArea(new Dimension(0, 6)));
content.add(valueLabel);
panel.add(accentBar, BorderLayout.WEST);
panel.add(content, BorderLayout.CENTER);
return panel;
}
private JLabel createStatusValueLabel() {
JLabel label = new JLabel("-");
label.setFont(new Font("Segoe UI", Font.BOLD, 16));
label.setForeground(COLOR_TEXT);
return label;
}
private JLabel createCountLabel() {
JLabel label = new JLabel("0 rows");
label.setFont(FONT_SMALL);
label.setForeground(COLOR_TEXT_MUTED);
return label;
}
private JTable createDashboardTable(DefaultTableModel model) {
JTable table = new JTable(model);
table.setBackground(COLOR_SURFACE);
table.setForeground(COLOR_TEXT);
table.setGridColor(COLOR_BORDER);
table.setRowHeight(34);
table.setFont(new Font("Segoe UI", Font.PLAIN, 13));
table.setFillsViewportHeight(true);
table.setSelectionBackground(COLOR_PRIMARY);
table.setSelectionForeground(Color.WHITE);
table.setShowVerticalLines(false);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setAutoCreateRowSorter(true);
JTableHeader header = table.getTableHeader();
header.setBackground(new Color(22, 22, 22));
header.setForeground(COLOR_TEXT_MUTED);
header.setFont(new Font("Segoe UI", Font.BOLD, 13));
header.setPreferredSize(new Dimension(0, 38));
header.setBorder(BorderFactory.createMatteBorder(1, 0, 1, 0, COLOR_BORDER));
((DefaultTableCellRenderer) header.getDefaultRenderer()).setHorizontalAlignment(JLabel.LEFT);
((DefaultTableCellRenderer) header.getDefaultRenderer()).setBorder(new EmptyBorder(0, 10, 0, 0));
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
label.setBorder(new EmptyBorder(0, 10, 0, 10));
label.setForeground(isSelected ? Color.WHITE : COLOR_TEXT);
label.setBackground(isSelected ? COLOR_PRIMARY : ((row % 2 == 0) ? COLOR_SURFACE : new Color(35, 35, 35)));
return label;
}
});
return table;
}
private JPanel createTableTab(String title, String subtitle, JTable table, JLabel countLabel) {
JPanel wrapper = new JPanel(new BorderLayout());
wrapper.setBackground(COLOR_BG);
wrapper.setBorder(new EmptyBorder(30, 30, 30, 30));
JPanel titlePanel = new JPanel(new BorderLayout());
titlePanel.setOpaque(false);
JLabel titleLbl = new JLabel(title);
titleLbl.setFont(FONT_TITLE);
titleLbl.setForeground(COLOR_TEXT);
JLabel subtitleLbl = new JLabel(subtitle);
subtitleLbl.setFont(new Font("Segoe UI", Font.PLAIN, 13));
subtitleLbl.setForeground(COLOR_TEXT_MUTED);
subtitleLbl.setBorder(new EmptyBorder(6, 0, 0, 0));
JPanel textPanel = new JPanel();
textPanel.setOpaque(false);
textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.Y_AXIS));
textPanel.add(titleLbl);
textPanel.add(subtitleLbl);
titlePanel.add(textPanel, BorderLayout.WEST);
titlePanel.add(countLabel, BorderLayout.EAST);
wrapper.add(titlePanel, BorderLayout.NORTH);
JScrollPane scrollPane = new JScrollPane(table);
scrollPane.getViewport().setBackground(COLOR_SURFACE);
scrollPane.setBorder(new MatteBorder(1, 1, 1, 1, COLOR_BORDER));
scrollPane.setBorder(new CompoundBorder(
new EmptyBorder(20, 0, 0, 0),
new MatteBorder(1, 1, 1, 1, COLOR_BORDER)
));
wrapper.add(scrollPane, BorderLayout.CENTER);
return wrapper;
}
private JPanel createStatusBar() {
JPanel statusBar = new JPanel(new BorderLayout());
statusBar.setBackground(COLOR_SURFACE);
statusBar.setBorder(new CompoundBorder(
new MatteBorder(1, 0, 0, 0, COLOR_BORDER),
new EmptyBorder(8, 14, 8, 14)
));
JLabel left = new JLabel("Dashboard running locally");
left.setFont(FONT_SMALL);
left.setForeground(COLOR_TEXT_SUBTLE);
JLabel right = new JLabel("Tip: table columns are sortable");
right.setFont(FONT_SMALL);
right.setForeground(COLOR_TEXT_SUBTLE);
statusBar.add(left, BorderLayout.WEST);
statusBar.add(right, BorderLayout.EAST);
return statusBar;
}
private void setActiveCard(String cardName) {
selectedCardName = cardName;
cardLayout.show(cardsPanel, cardName);
navButtons.forEach((name, button) -> {
JLabel label = (JLabel) button.getComponent(0);
updateNavButtonStyle(name, button, label);
});
}
private void updateNavButtonStyle(String cardName, JPanel button, JLabel label) {
boolean active = cardName.equals(selectedCardName);
button.setBackground(active ? COLOR_PRIMARY_SOFT : COLOR_SURFACE);
label.setForeground(active ? COLOR_TEXT : COLOR_TEXT_MUTED);
}
private void updateMetrics() {
try {
EmulatorStatsService.Snapshot snapshot = EmulatorStatsService.collectSnapshot();
EmulatorStatsService.Overview overview = snapshot.overview;
Object[][] usersData = new Object[snapshot.users.size()][5];
for (int i = 0; i < snapshot.users.size(); i++) {
EmulatorStatsService.OnlineUserRow user = snapshot.users.get(i);
usersData[i] = new Object[]{user.id, user.username, user.rank, user.credits, user.roomId};
}
Object[][] roomsData = new Object[snapshot.rooms.size()][8];
for (int i = 0; i < snapshot.rooms.size(); i++) {
EmulatorStatsService.ActiveRoomRow room = snapshot.rooms.get(i);
roomsData[i] = new Object[]{
room.roomId,
room.name,
room.players,
room.items,
room.tickables,
String.format("%.2f", room.cpuMs),
room.estimatedRamKb,
room.thread
};
}
Object[][] wiredData = new Object[snapshot.wired.size()][7];
for (int i = 0; i < snapshot.wired.size(); i++) {
EmulatorStatsService.WiredRoomRow wiredRoom = snapshot.wired.get(i);
wiredData[i] = new Object[]{
wiredRoom.roomId,
wiredRoom.averageTickMs + " ms",
wiredRoom.peakTickMs + " ms",
wiredRoom.usagePercent + "%",
wiredRoom.delayedEventsPending,
wiredRoom.overloaded ? "YES" : "NO",
wiredRoom.heavy ? "YES" : "NO"
};
}
SwingUtilities.invokeLater(() -> {
memLabel.setText(String.format("%d MB / %d MB", overview.memoryUsedMb, overview.memoryMaxMb));
cpuLabel.setText(String.format("%.1f %%", overview.cpuLoadPercent));
threadLabel.setText(String.valueOf(overview.activeOsThreads));
usersLabel.setText(String.valueOf(overview.connectedPlayers));
roomsLabel.setText(String.valueOf(overview.loadedRooms));
wiredLabel.setText(String.valueOf(overview.wiredTickables));
uptimeLabel.setText(EmulatorStatsService.formatDuration(overview.uptimeSeconds));
lastUpdatedLabel.setText(LocalDateTime.now().format(TIME_FORMAT));
footerStatusLabel.setText(overview.guiStatus);
memoryGraph.addValue((long) overview.memoryUsedMb * 1024L * 1024L, (long) overview.memoryMaxMb * 1024L * 1024L);
usersTableModel.setDataVector(usersData, new String[]{"ID", "Username", "Rank", "Credits", "Room ID"});
roomsTableModel.setDataVector(roomsData, new String[]{"Room ID", "Name", "Players", "Items", "Tickables", "CPU (ms)", "Est. RAM (KB)", "Thread"});
wiredTableModel.setDataVector(wiredData, new String[]{"Room ID", "Avg Tick", "Peak Tick", "Usage %", "Delayed", "Overloaded?", "Heavy?"});
usersCountLabel.setText(snapshot.users.size() + " rows");
roomsCountLabel.setText(snapshot.rooms.size() + " rows");
wiredCountLabel.setText(snapshot.wired.size() + " rows");
});
} catch (Exception e) {
LOGGER.error("Error updating dashboard metrics", e);
}
}
public static void launch() {
if (instance == null) {
instance = new EmulatorDashboard();
}
SwingUtilities.invokeLater(() -> {
instance.setVisible(true);
});
}
private static class MemoryGraphPanel extends JPanel {
private final LinkedList<Double> history = new LinkedList<>();
private static final int MAX_POINTS = 100;
public MemoryGraphPanel() {
setOpaque(false);
}
public void addValue(long used, long max) {
double percent = (double) used / (double) max;
history.addLast(percent);
if (history.size() > MAX_POINTS) {
history.removeFirst();
}
repaint();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int width = getWidth();
int height = getHeight();
// Background grid and labels
g2.setFont(new Font("Segoe UI", Font.PLAIN, 10));
long maxMemRaw = Runtime.getRuntime().maxMemory();
for(int i = 0; i <= 4; i++) {
int y = i == 0 ? 15 : height * i / 4;
if (i == 4) y = height - 5;
g2.setColor(COLOR_BORDER);
g2.drawLine(0, y, width, y);
// Draw Y-axis numbers
g2.setColor(COLOR_TEXT_MUTED);
long labelVal = (long) (maxMemRaw * (1.0 - (double)i / 4.0)) / 1024 / 1024;
g2.drawString(labelVal + " MB", 5, y - 5);
}
if (history.size() < 2) return;
double dx = (double) width / (MAX_POINTS - 1);
Path2D path = new Path2D.Double();
path.moveTo(0, height);
int i = MAX_POINTS - history.size();
for (Double val : history) {
double x = i * dx;
double y = height - (val * height);
if (i == MAX_POINTS - history.size()) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
i++;
}
// Draw line
g2.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2.setColor(COLOR_PRIMARY);
g2.draw(path);
// Fill area
path.lineTo(width, height);
path.lineTo((MAX_POINTS - history.size()) * dx, height);
path.closePath();
GradientPaint fillPaint = new GradientPaint(
0, 0, new Color(COLOR_PRIMARY.getRed(), COLOR_PRIMARY.getGreen(), COLOR_PRIMARY.getBlue(), 120),
0, height, new Color(COLOR_PRIMARY.getRed(), COLOR_PRIMARY.getGreen(), COLOR_PRIMARY.getBlue(), 10)
);
g2.setPaint(fillPaint);
g2.fill(path);
Double lastValue = history.peekLast();
if (lastValue != null) {
String usageLabel = String.format("Usage %.1f%%", lastValue * 100.0);
g2.setFont(new Font("Segoe UI", Font.BOLD, 12));
FontMetrics metrics = g2.getFontMetrics();
int labelWidth = metrics.stringWidth(usageLabel) + 16;
int labelHeight = 24;
int labelX = Math.max(8, width - labelWidth - 8);
int labelY = 8;
g2.setColor(new Color(0, 0, 0, 130));
g2.fillRoundRect(labelX, labelY, labelWidth, labelHeight, 12, 12);
g2.setColor(COLOR_TEXT);
g2.drawString(usageLabel, labelX + 8, labelY + 16);
}
}
}
private static String formatDuration(long millis) {
long totalSeconds = Math.max(0L, millis / 1000L);
long hours = totalSeconds / 3600L;
long minutes = (totalSeconds % 3600L) / 60L;
long seconds = totalSeconds % 60L;
return String.format("%02dh %02dm %02ds", hours, minutes, seconds);
}
}
@@ -6,6 +6,8 @@ import com.eu.habbo.habbohotel.achievements.AchievementManager;
import com.eu.habbo.habbohotel.bots.BotManager;
import com.eu.habbo.habbohotel.campaign.calendar.CalendarManager;
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.commands.CommandHandler;
import com.eu.habbo.habbohotel.crafting.CraftingManager;
import com.eu.habbo.habbohotel.guides.GuideManager;
@@ -21,7 +23,10 @@ import com.eu.habbo.habbohotel.pets.PetManager;
import com.eu.habbo.habbohotel.polls.PollManager;
import com.eu.habbo.habbohotel.rooms.RoomChatBubbleManager;
import com.eu.habbo.habbohotel.rooms.RoomManager;
import com.eu.habbo.habbohotel.translations.GoogleTranslateManager;
import com.eu.habbo.habbohotel.users.HabboManager;
import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeManager;
import com.eu.habbo.habbohotel.users.infostand.InfostandBackgroundManager;
import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionManager;
import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionScheduler;
import org.slf4j.Logger;
@@ -58,6 +63,11 @@ public class GameEnvironment {
private SubscriptionManager subscriptionManager;
private CalendarManager calendarManager;
private RoomChatBubbleManager roomChatBubbleManager;
private GoogleTranslateManager googleTranslateManager;
private CustomBadgeManager customBadgeManager;
private InfostandBackgroundManager infostandBackgroundManager;
private WheelManager wheelManager;
private SoundboardManager soundboardManager;
public void load() throws Exception {
LOGGER.info("GameEnvironment -> Loading...");
@@ -84,6 +94,11 @@ public class GameEnvironment {
this.pollManager = new PollManager();
this.calendarManager = new CalendarManager();
this.roomChatBubbleManager = new RoomChatBubbleManager();
this.googleTranslateManager = new GoogleTranslateManager();
this.customBadgeManager = new CustomBadgeManager();
this.infostandBackgroundManager = new InfostandBackgroundManager();
this.wheelManager = new WheelManager();
this.soundboardManager = new SoundboardManager();
this.roomManager.loadPublicRooms();
this.navigatorManager.loadNavigator();
@@ -121,6 +136,9 @@ public class GameEnvironment {
this.hotelViewManager.dispose();
this.subscriptionManager.dispose();
this.calendarManager.dispose();
if (this.googleTranslateManager != null) {
this.googleTranslateManager.clearCache();
}
LOGGER.info("GameEnvironment -> Disposed!");
}
@@ -144,6 +162,14 @@ public class GameEnvironment {
return this.catalogManager;
}
public WheelManager getWheelManager() {
return this.wheelManager;
}
public SoundboardManager getSoundboardManager() {
return this.soundboardManager;
}
public HotelViewManager getHotelViewManager() {
return this.hotelViewManager;
}
@@ -219,4 +245,16 @@ public class GameEnvironment {
public RoomChatBubbleManager getRoomChatBubbleManager() {
return roomChatBubbleManager;
}
public GoogleTranslateManager getGoogleTranslateManager() {
return this.googleTranslateManager;
}
public CustomBadgeManager getCustomBadgeManager() {
return this.customBadgeManager;
}
public InfostandBackgroundManager getInfostandBackgroundManager() {
return this.infostandBackgroundManager;
}
}
@@ -1,7 +1,6 @@
package com.eu.habbo.habbohotel.achievements;
import com.eu.habbo.Emulator;
import com.eu.habbo.database.SqlQueries;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboBadge;
@@ -50,12 +49,16 @@ public class AchievementManager {
if (habbo != null) {
progressAchievement(habbo, achievement, amount);
} else {
try {
SqlQueries.update(
"INSERT INTO users_achievements_queue (user_id, achievement_id, amount) VALUES (?, ?, ?) "
+ "ON DUPLICATE KEY UPDATE amount = amount + ?",
habboId, achievement.id, amount, amount);
} catch (SqlQueries.DataAccessException e) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("" +
"INSERT INTO users_achievements_queue (user_id, achievement_id, amount) VALUES (?, ?, ?) " +
"ON DUPLICATE KEY UPDATE amount = amount + ?")) {
statement.setInt(1, habboId);
statement.setInt(2, achievement.id);
statement.setInt(3, amount);
statement.setInt(4, amount);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
@@ -200,41 +203,48 @@ public class AchievementManager {
}
public static void createUserEntry(Habbo habbo, Achievement achievement) {
try {
SqlQueries.update(
"INSERT INTO users_achievements (user_id, achievement_name, progress) VALUES (?, ?, ?)",
habbo.getHabboInfo().getId(), achievement.name, 1);
} catch (SqlQueries.DataAccessException e) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_achievements (user_id, achievement_name, progress) VALUES (?, ?, ?)")) {
statement.setInt(1, habbo.getHabboInfo().getId());
statement.setString(2, achievement.name);
statement.setInt(3, 1);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
public static void saveAchievements(Habbo habbo) {
int userId = habbo.getHabboInfo().getId();
try {
SqlQueries.batchUpdate(
"UPDATE users_achievements SET progress = ? WHERE achievement_name = ? AND user_id = ? LIMIT 1",
habbo.getHabboStats().getAchievementProgress().entrySet(),
(ps, entry) -> {
ps.setInt(1, entry.getValue());
ps.setString(2, entry.getKey().name);
ps.setInt(3, userId);
});
} catch (SqlQueries.DataAccessException e) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users_achievements SET progress = ? WHERE achievement_name = ? AND user_id = ? LIMIT 1")) {
statement.setInt(3, habbo.getHabboInfo().getId());
for (Map.Entry<Achievement, Integer> map : habbo.getHabboStats().getAchievementProgress().entrySet()) {
statement.setInt(1, map.getValue());
statement.setString(2, map.getKey().name);
statement.addBatch();
}
statement.executeBatch();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
public static int getAchievementProgressForHabbo(int userId, Achievement achievement) {
try {
return SqlQueries.queryOne(
"SELECT progress FROM users_achievements WHERE user_id = ? AND achievement_name = ? LIMIT 1",
rs -> rs.getInt("progress"),
userId, achievement.name).orElse(0);
} catch (SqlQueries.DataAccessException e) {
LOGGER.error("Caught SQL exception", e);
if (achievement == null) {
return 0;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT progress FROM users_achievements WHERE user_id = ? AND achievement_name = ? LIMIT 1")) {
statement.setInt(1, userId);
statement.setString(2, achievement.name);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) {
return set.getInt("progress");
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
return 0;
}
public void reload() {
@@ -138,18 +138,20 @@ public class Bot implements Runnable {
@Override
public void run() {
if (this.needsUpdate) {
Room localRoom = this.room;
RoomUnit localRoomUnit = this.roomUnit;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE bots SET name = ?, motto = ?, figure = ?, gender = ?, user_id = ?, room_id = ?, x = ?, y = ?, z = ?, rot = ?, dance = ?, freeroam = ?, chat_lines = ?, chat_auto = ?, chat_random = ?, chat_delay = ?, effect = ?, bubble_id = ? WHERE id = ?")) {
statement.setString(1, this.name);
statement.setString(2, this.motto);
statement.setString(3, this.figure);
statement.setString(4, this.gender.toString());
statement.setInt(5, this.ownerId);
statement.setInt(6, this.room == null ? 0 : this.room.getId());
statement.setInt(7, this.roomUnit == null ? 0 : this.roomUnit.getX());
statement.setInt(8, this.roomUnit == null ? 0 : this.roomUnit.getY());
statement.setDouble(9, this.roomUnit == null ? 0 : this.roomUnit.getZ());
statement.setInt(10, this.roomUnit == null ? 0 : this.roomUnit.getBodyRotation().getValue());
statement.setInt(11, this.roomUnit == null ? 0 : this.roomUnit.getDanceType().getType());
statement.setInt(6, localRoom == null ? 0 : localRoom.getId());
statement.setInt(7, localRoomUnit == null ? 0 : localRoomUnit.getX());
statement.setInt(8, localRoomUnit == null ? 0 : localRoomUnit.getY());
statement.setDouble(9, localRoomUnit == null ? 0 : localRoomUnit.getZ());
statement.setInt(10, localRoomUnit == null ? 0 : localRoomUnit.getBodyRotation().getValue());
statement.setInt(11, localRoomUnit == null ? 0 : localRoomUnit.getDanceType().getType());
statement.setString(12, this.canWalk ? "1" : "0");
StringBuilder text = new StringBuilder();
for (String s : this.chatLines) {
@@ -187,11 +189,7 @@ public class Bot implements Runnable {
int timeOut = Emulator.getRandom().nextInt(20) * 2;
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) {
@@ -216,7 +214,7 @@ public class Bot implements Runnable {
} else {
this.lastChatIndex++;
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) {
this.stopFollowingHabbo();
}
public void onUserSay(final RoomChatMessage message) {
@@ -308,9 +306,6 @@ public class Bot implements Runnable {
public void setName(String name) {
this.name = name;
this.needsUpdate = true;
//if(this.room != null)
//this.room.sendComposer(new ChangeNameUpdatedComposer(this.getRoomUnit(), this.getName()).compose());
}
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("bartender", ButlerBot.class);
addBotDefinition("visitor_log", VisitorBot.class);
addBotDefinition(FrankBot.BOT_TYPE, FrankBot.class);
this.reload();
@@ -71,13 +72,23 @@ public class BotManager {
}
public Bot createBot(THashMap<String, String> data, String type) {
return this.createBot(data, type, 0);
}
public Bot createBot(THashMap<String, String> data, String type, int ownerId) {
if (ownerId <= 0) {
LOGGER.error("Cannot create bot of type '{}' without a valid owner user id.", type);
return null;
}
Bot bot = null;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO bots (user_id, room_id, name, motto, figure, gender, type) VALUES (0, 0, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) {
statement.setString(1, data.get("name"));
statement.setString(2, data.get("motto"));
statement.setString(3, data.get("figure"));
statement.setString(4, data.get("gender").toUpperCase());
statement.setString(5, type);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO bots (user_id, room_id, name, motto, figure, gender, type) VALUES (?, 0, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, ownerId);
statement.setString(2, data.get("name"));
statement.setString(3, data.get("motto"));
statement.setString(4, data.get("figure"));
statement.setString(5, data.get("gender").toUpperCase());
statement.setString(6, type);
statement.execute();
try (ResultSet set = statement.getGeneratedKeys()) {
if (set.next()) {
@@ -20,10 +20,13 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
public class ButlerBot extends Bot {
private static final Logger LOGGER = LoggerFactory.getLogger(ButlerBot.class);
public static THashMap<THashSet<String>, Integer> serveItems = new THashMap<>();
private static final ConcurrentHashMap<Pattern, Integer> serveItemsCompiled = new ConcurrentHashMap<>();
public ButlerBot(ResultSet set) throws SQLException {
super(set);
@@ -38,6 +41,7 @@ public class ButlerBot extends Bot {
serveItems = new THashMap<>();
serveItems.clear();
serveItemsCompiled.clear();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM bot_serves")) {
while (set.next()) {
@@ -45,6 +49,17 @@ public class ButlerBot extends Bot {
THashSet<String> ks = new THashSet<>();
Collections.addAll(ks, keys);
serveItems.put(ks, set.getInt("item"));
for (String key : keys) {
if (key != null && !key.trim().isEmpty()) {
try {
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(key.toLowerCase()) + "\\b");
serveItemsCompiled.put(pattern, set.getInt("item"));
} catch (Exception e) {
LOGGER.error("Failed to compile butler bot keyword pattern: {}", key, e);
}
}
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
@@ -53,6 +68,7 @@ public class ButlerBot extends Bot {
public static void dispose() {
serveItems.clear();
serveItemsCompiled.clear();
}
@Override
@@ -66,74 +82,73 @@ public class ButlerBot extends Bot {
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.commanddistance")) {
if (message.getUnfilteredMessage() != null) {
for (Map.Entry<THashSet<String>, Integer> set : serveItems.entrySet()) {
for (String keyword : set.getKey()) {
String unfilteredLower = message.getUnfilteredMessage().toLowerCase();
for (Map.Entry<Pattern, Integer> entry : serveItemsCompiled.entrySet()) {
Pattern pattern = entry.getKey();
if (pattern.matcher(unfilteredLower).matches()) {
int itemId = entry.getValue();
String keyword = pattern.pattern().replace("\\b", "").replace("\\Q", "").replace("\\E", "");
// Check if the string contains a certain keyword using a regex.
// If keyword = tea, teapot wouldn't trigger it.
if (message.getUnfilteredMessage().toLowerCase().matches("\\b" + keyword + "\\b")) {
// Enable plugins to cancel this event
BotServerItemEvent serveEvent = new BotServerItemEvent(this, message.getHabbo(), set.getValue());
if (Emulator.getPluginManager().fireEvent(serveEvent).isCancelled()) {
return;
}
// Start give handitem process
if (this.getRoomUnit().canWalk()) {
final String key = keyword;
final Bot bot = this;
// Step 1: Look at Habbo
bot.lookAt(serveEvent.habbo);
// Step 2: Prepare tasks for when the Bot (carrying the handitem) reaches the Habbo
final List<Runnable> tasks = new ArrayList<>();
tasks.add(new RoomUnitGiveHanditem(serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
tasks.add(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), 0));
tasks.add(() -> {
if(this.getRoom() != null) {
String botMessage = Emulator.getTexts()
.getValue("bots.butler.given")
.replace("%key%", key)
.replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), botMessage)) {
bot.talk(botMessage);
}
}
});
List<Runnable> failedReached = new ArrayList<>();
failedReached.add(() -> {
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.servedistance", 8)) {
for (Runnable task : tasks) {
task.run();
}
}
});
// Give bot the handitem that it's going to give the Habbo
Emulator.getThreading().run(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
if (distanceBetweenBotAndHabbo > Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)) {
Emulator.getThreading().run(new RoomUnitWalkToRoomUnit(this.getRoomUnit(), serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), tasks, failedReached, Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)));
} else {
Emulator.getThreading().run(failedReached.get(0), 1000);
}
} else {
if(this.getRoom() != null) {
this.getRoom().giveHandItem(serveEvent.habbo, serveEvent.itemId);
String msg = Emulator.getTexts().getValue("bots.butler.given").replace("%key%", keyword).replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), msg)) {
this.talk(msg);
}
}
}
// Enable plugins to cancel this event
BotServerItemEvent serveEvent = new BotServerItemEvent(this, message.getHabbo(), itemId);
if (Emulator.getPluginManager().fireEvent(serveEvent).isCancelled()) {
return;
}
// Start give handitem process
if (this.getRoomUnit().canWalk()) {
final String key = keyword;
final Bot bot = this;
// Step 1: Look at Habbo
bot.lookAt(serveEvent.habbo);
// Step 2: Prepare tasks for when the Bot (carrying the handitem) reaches the Habbo
final List<Runnable> tasks = new ArrayList<>();
tasks.add(new RoomUnitGiveHanditem(serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
tasks.add(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), 0));
tasks.add(() -> {
if(this.getRoom() != null) {
String botMessage = Emulator.getTexts()
.getValue("bots.butler.given")
.replace("%key%", key)
.replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), botMessage)) {
bot.talk(botMessage);
}
}
});
List<Runnable> failedReached = new ArrayList<>();
failedReached.add(() -> {
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.servedistance", 8)) {
for (Runnable task : tasks) {
task.run();
}
}
});
// Give bot the handitem that it's going to give the Habbo
Emulator.getThreading().run(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
if (distanceBetweenBotAndHabbo > Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)) {
Emulator.getThreading().run(new RoomUnitWalkToRoomUnit(this.getRoomUnit(), serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), tasks, failedReached, Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)));
} else {
Emulator.getThreading().run(failedReached.get(0), 1000);
}
} else {
if(this.getRoom() != null) {
this.getRoom().giveHandItem(serveEvent.habbo, serveEvent.itemId);
String msg = Emulator.getTexts().getValue("bots.butler.given").replace("%key%", keyword).replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), msg)) {
this.talk(msg);
}
}
}
return;
}
}
}
@@ -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 THashMap<Integer, CatalogLimitedConfiguration> limitedNumbers;
private final List<Voucher> vouchers;
public final TIntObjectMap<int[]> furnitureValues;
private volatile byte[] rareValuesPayloadCache;
public CatalogManager() {
long millis = System.currentTimeMillis();
@@ -219,6 +221,7 @@ public class CatalogManager {
this.buildersClubOfferDefs = new TIntIntHashMap();
this.vouchers = new ArrayList<>();
this.limitedNumbers = new THashMap<>();
this.furnitureValues = new TIntObjectHashMap<>();
this.initialize();
@@ -243,6 +246,76 @@ public class CatalogManager {
this.loadClothing();
this.loadRecycler();
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() {
@@ -1046,10 +1119,19 @@ public class CatalogManager {
for (Item baseItem : item.getBaseItems()) {
for (int k = 0; k < item.getItemAmount(baseItem.getId()); k++) {
if (baseItem.getName().startsWith("rentable_bot_") || baseItem.getName().startsWith("bot_")) {
String baseName = baseItem.getName();
String type = item.getName().replace("rentable_bot_", "");
type = type.replace("bot_", "");
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<>();
for (String s : item.getExtradata().split(";")) {
@@ -1058,7 +1140,7 @@ public class CatalogManager {
}
}
Bot bot = Emulator.getGameEnvironment().getBotManager().createBot(data, type);
Bot bot = Emulator.getGameEnvironment().getBotManager().createBot(data, type, habbo.getHabboInfo().getId());
if (bot != null) {
bot.setOwnerId(habbo.getClient().getHabbo().getHabboInfo().getId());
@@ -72,7 +72,11 @@ public class ClubOffer implements ISerialize {
this.type = OfferType.fromDatabase(set.getString("type"));
this.vip = this.type == OfferType.VIP;
this.deal = set.getString("deal").equals("1");
this.giftable = set.getString("giftable").equals("1");
boolean giftable = false;
try {
giftable = "1".equals(set.getString("giftable"));
} catch (SQLException ignored) {}
this.giftable = giftable;
}
public int getId() {
@@ -42,14 +42,18 @@ public class RoomBundleLayout extends SingleBundle {
}
if (this.room == null) {
if (this.roomId > 0) {
this.room = Emulator.getGameEnvironment().getRoomManager().loadRoom(this.roomId);
RoomManager roomManager = Emulator.getGameEnvironment().getRoomManager();
if (this.roomId > 0 && roomManager != null) {
this.room = roomManager.loadRoom(this.roomId);
if (this.room != null)
this.room.preventUnloading = true;
} else {
} else if (this.roomId <= 0) {
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) {
@@ -3,9 +3,7 @@ package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.catalog.CatalogManager;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.messages.outgoing.generic.alerts.MessagesForYouComposer;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@@ -13,9 +11,10 @@ public class AboutCommand extends Command {
public AboutCommand() {
super(null, new String[]{"about", "info", "online", "server"});
}
public static String credits = "Arcturus Morningstar is an opensource project based on Arcturus By TheGeneral \n" +
"The Following people have all contributed to this emulator:\n" +
"TheGeneral\n Beny\n Alejandro\n Capheus\n Skeletor\n Harmonic\n Mike\n Remco\n zGrav \n Quadral \n Harmony\n Swirny\n ArpyAge\n Mikkel\n Rodolfo\n Rasmus\n Kitt Mustang\n Snaiker\n nttzx\n necmi\n Dome\n Jose Flores\n Cam\n Oliver\n Narzo\n Tenshie\n MartenM\n Ridge\n SenpaiDipper\n Snaiker\n Thijmen\n DuckieTM\n simoleo89\n Medievalshell\n Lorenzo (the wired master)";
public static final String NITRO_INFO_SENTINEL = "[NITRO_INFO_V1]";
public static final String REPORT_ISSUES_URL = "https://github.com/duckietm/Nitro-V3/issues";
@Override
public boolean handle(GameClient gameClient, String[] params) {
@@ -27,28 +26,31 @@ public class AboutCommand extends Command {
long minute = TimeUnit.SECONDS.toMinutes(seconds) - (TimeUnit.SECONDS.toHours(seconds) * 60);
long second = TimeUnit.SECONDS.toSeconds(seconds) - (TimeUnit.SECONDS.toMinutes(seconds) * 60);
String message = "<b>" + Emulator.version + "</b>\r\n";
StringBuilder message = new StringBuilder();
message.append(NITRO_INFO_SENTINEL).append("\r");
message.append("<b>").append(Emulator.version).append("</b>\r\n");
if (Emulator.getConfig().getBoolean("info.shown", true)) {
message += "<b>Hotel Statistics</b>\r" +
"- Online Users: " + Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "\r" +
"- Active Rooms: " + Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size() + "\r" +
"- Shop: " + Emulator.getGameEnvironment().getCatalogManager().catalogPages.size() + " pages and " + CatalogManager.catalogItemAmount + " items. \r" +
"- Furni: " + Emulator.getGameEnvironment().getItemManager().getItems().size() + " item definitions" + "\r" +
"\n" +
"<b>Server Statistics</b>\r" +
"- Uptime: " + day + (day > 1 ? " days, " : " day, ") + hours + (hours > 1 ? " hours, " : " hour, ") + minute + (minute > 1 ? " minutes, " : " minute, ") + second + (second > 1 ? " seconds!" : " second!") + "\r" +
"- RAM Usage: " + (Emulator.getRuntime().totalMemory() - Emulator.getRuntime().freeMemory()) / (1024 * 1024) + "/" + (Emulator.getRuntime().freeMemory()) / (1024 * 1024) + "MB\r" +
"- CPU Cores: " + Emulator.getRuntime().availableProcessors() + "\r" +
"- Total Memory: " + Emulator.getRuntime().maxMemory() / (1024 * 1024) + "MB" + "\r\n";
message.append("<b>Hotel Statistics</b>\r")
.append("- Online Users: ").append(Emulator.getGameEnvironment().getHabboManager().getOnlineCount()).append("\r")
.append("- Active Rooms: ").append(Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size()).append("\r")
.append("- Shop: ").append(Emulator.getGameEnvironment().getCatalogManager().catalogPages.size()).append(" pages and ").append(CatalogManager.catalogItemAmount).append(" items.\r")
.append("- Furni: ").append(Emulator.getGameEnvironment().getItemManager().getItems().size()).append(" item definitions\r")
.append("\n")
.append("<b>Server Statistics</b>\r")
.append("- Uptime: ").append(day).append(day == 1 ? " day, " : " days, ").append(hours).append(hours == 1 ? " hour, " : " hours, ").append(minute).append(minute == 1 ? " minute, " : " minutes, ").append(second).append(second == 1 ? " second!" : " seconds!").append("\r")
.append("- RAM Usage: ").append((Emulator.getRuntime().totalMemory() - Emulator.getRuntime().freeMemory()) / (1024 * 1024)).append("/").append((Emulator.getRuntime().freeMemory()) / (1024 * 1024)).append("MB\r")
.append("- CPU Cores: ").append(Emulator.getRuntime().availableProcessors()).append("\r")
.append("- Total Memory: ").append(Emulator.getRuntime().maxMemory() / (1024 * 1024)).append("MB\r\n");
}
message += "\r" +
message.append("<b>Credits</b>\r")
.append("- The General\r")
.append("- Krews Team\r")
.append("- DuckieTM, simoleo89, Medievalshell, Lorenzo (the wired master), Remco\r\n")
.append("Report issues at: ").append(REPORT_ISSUES_URL);
"<b>Thanks for using Arcturus. Report issues on the forums. http://arcturus.wf \r\r" +
" - The General";
gameClient.getHabbo().alert(message);
gameClient.sendResponse(new MessagesForYouComposer(Collections.singletonList(credits)));
gameClient.getHabbo().alert(message.toString());
return true;
}
}
@@ -1,18 +0,0 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.messages.outgoing.users.UserDataComposer;
public class ChangeNameCommand extends Command {
public ChangeNameCommand() {
super("cmd_changename", Emulator.getTexts().getValue("commands.keys.cmd_changename").split(";"));
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
gameClient.getHabbo().getHabboStats().allowNameChange = !gameClient.getHabbo().getHabboStats().allowNameChange;
gameClient.sendResponse(new UserDataComposer(gameClient.getHabbo()));
return true;
}
}
@@ -184,7 +184,6 @@ public class CommandHandler {
addCommand(new BlockAlertCommand());
addCommand(new BotsCommand());
addCommand(new CalendarCommand());
addCommand(new ChangeNameCommand());
addCommand(new ChatTypeCommand());
addCommand(new CommandsCommand());
addCommand(new ControlCommand());
@@ -197,6 +196,7 @@ public class CommandHandler {
addCommand(new EmptyInventoryCommand());
addCommand(new EmptyBotsInventoryCommand());
addCommand(new EmptyPetsInventoryCommand());
addCommand(new EmuStatsCommand());
addCommand(new EnableCommand());
addCommand(new EventCommand());
addCommand(new FacelessCommand());
@@ -0,0 +1,16 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.permissions.Permission;
public class EmuStatsCommand extends Command {
public EmuStatsCommand() {
super(Permission.ACC_MODTOOL_ROOM_INFO, new String[]{"emustats"});
}
@Override
public boolean handle(GameClient gameClient, String[] params) {
gameClient.getHabbo().whisper("Emulator stats are available in the Nitro stats window.");
return true;
}
}
@@ -2,7 +2,12 @@ package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.permissions.PermissionsManager;
import com.eu.habbo.habbohotel.permissions.Rank;
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboManager;
import com.eu.habbo.messages.outgoing.users.UserPermissionsComposer;
public class UpdatePermissionsCommand extends Command {
public UpdatePermissionsCommand() {
@@ -13,7 +18,41 @@ public class UpdatePermissionsCommand extends Command {
public boolean handle(GameClient gameClient, String[] params) throws Exception {
Emulator.getGameEnvironment().getPermissionsManager().reload();
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_update_permissions"), RoomChatMessageBubbles.ALERT);
// PermissionsManager.reload() rebuilt the rank table each online
// Habbo's HabboInfo still references the OLD Rank object, so
// server-side hasPermission() / wire composers would keep
// reporting stale data until relogin. Re-bind every connected
// user to the freshly-loaded Rank by id, then ship the new
// UserPermissionsComposer (which carries clubLevel,
// securityLevel, isAmbassador, rank metadata and the resolved
// permission_definitions map) so Nitro clients' React-side
// useHasPermission(key) / useUserRank() / useUserPermissions()
// consumers re-render against the updated tables without an F5.
HabboManager habboManager = Emulator.getGameEnvironment().getHabboManager();
PermissionsManager permissions = Emulator.getGameEnvironment().getPermissionsManager();
int refreshed = 0;
for (Habbo habbo : habboManager.getOnlineHabbos().values()) {
if (habbo == null || habbo.getHabboInfo() == null || habbo.getClient() == null) continue;
int currentRankId = habbo.getHabboInfo().getRank().getId();
// Defensive fallback: if the admin deleted the rank from the
// permission_ranks table between sessions, fall back to rank 1
// (Member) so the user isn't stranded with a null Rank.
Rank freshRank = permissions.rankExists(currentRankId)
? permissions.getRank(currentRankId)
: permissions.getRank(1);
habbo.getHabboInfo().setRank(freshRank);
habbo.getClient().sendResponse(new UserPermissionsComposer(habbo));
refreshed++;
}
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.succes.cmd_update_permissions") + " (" + refreshed + " online refreshed)",
RoomChatMessageBubbles.ALERT
);
return true;
}
@@ -153,7 +153,13 @@ public class GameClient {
this.channel.close();
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 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
boolean parked = SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket);
@@ -1,23 +1,16 @@
package com.eu.habbo.habbohotel.gameclients;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomUnit;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserEffectComposer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
/**
* Manages a grace period for disconnected users. Instead of immediately
* disposing a Habbo when their WebSocket drops, the Habbo is held in
* a "ghost" state for a configurable number of seconds. If the same
* user reconnects (via SSO ticket) within the grace window, their
* existing Habbo object is resumed on the new connection keeping
* them in their room, preserving inventory state, etc.
*
* Config key: session.reconnect.grace.seconds (default: 30)
*/
public class SessionResumeManager {
private static final Logger LOGGER = LoggerFactory.getLogger(SessionResumeManager.class);
@@ -37,12 +30,10 @@ public class SessionResumeManager {
return Emulator.getConfig().getInt("session.reconnect.grace.seconds", 30);
}
/**
* Park a disconnected Habbo in ghost mode. Their room presence is
* preserved, but the old GameClient channel is closed.
*
* @return true if the habbo was parked (grace period > 0), false if immediate dispose should happen
*/
public int getPausedEffectId() {
return Emulator.getConfig().getInt("session.reconnect.effect.id", 170);
}
public boolean parkHabbo(Habbo habbo, String ssoTicket) {
int graceSeconds = getGracePeriodSeconds();
if (graceSeconds <= 0) {
@@ -51,7 +42,6 @@ public class SessionResumeManager {
int userId = habbo.getHabboInfo().getId();
// Cancel any existing ghost session for this user
GhostSession existing = ghostSessions.remove(userId);
if (existing != null && existing.disposeFuture != null) {
existing.disposeFuture.cancel(false);
@@ -60,12 +50,18 @@ public class SessionResumeManager {
LOGGER.info("[SessionResume] Parking {} (id={}) for {}s grace period",
habbo.getHabboInfo().getUsername(), userId, graceSeconds);
// Restore the SSO ticket so the client can reconnect with the same ticket
if (ssoTicket != null && !ssoTicket.isEmpty()) {
restoreSsoTicket(userId, ssoTicket);
}
// Schedule the final disconnect after the grace period
int previousEffectId = 0;
int previousEffectEnd = 0;
RoomUnit unit = habbo.getRoomUnit();
if (unit != null) {
previousEffectId = unit.getEffectId();
previousEffectEnd = unit.getEffectEndTimestamp();
}
ScheduledFuture<?> future = Emulator.getThreading().run(() -> {
GhostSession ghost = ghostSessions.remove(userId);
if (ghost != null) {
@@ -75,22 +71,19 @@ public class SessionResumeManager {
}
}, graceSeconds * 1000);
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future));
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd));
applyPausedEffect(habbo);
return true;
}
/**
* Try to resume a ghost session for the given user ID.
*
* @return the parked Habbo if found within grace period, null otherwise
*/
public Habbo resumeSession(int userId) {
GhostSession ghost = ghostSessions.remove(userId);
if (ghost == null) {
return null;
}
// Cancel the scheduled dispose
if (ghost.disposeFuture != null) {
ghost.disposeFuture.cancel(false);
}
@@ -98,19 +91,15 @@ public class SessionResumeManager {
LOGGER.info("[SessionResume] Resuming session for {} (id={})",
ghost.habbo.getHabboInfo().getUsername(), userId);
restorePausedEffect(ghost);
return ghost.habbo;
}
/**
* Check if a user has a ghost session (is in grace period).
*/
public boolean hasGhostSession(int userId) {
return ghostSessions.containsKey(userId);
}
/**
* Immediately expire all ghost sessions (e.g. on emulator shutdown).
*/
public void disposeAll() {
for (GhostSession ghost : ghostSessions.values()) {
if (ghost.disposeFuture != null) {
@@ -121,9 +110,6 @@ public class SessionResumeManager {
ghostSessions.clear();
}
/**
* Perform the actual full disconnect that normally happens in Habbo.disconnect().
*/
private void performFullDisconnect(Habbo habbo) {
try {
habbo.getHabboInfo().setOnline(false);
@@ -132,22 +118,69 @@ public class SessionResumeManager {
LOGGER.error("[SessionResume] Error during deferred disconnect", e);
}
// Clear the SSO ticket now that the grace period is truly over
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) {
// 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();
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.setInt(2, userId);
statement.execute();
LOGGER.info("[SessionResume] Restored SSO ticket for user {} during grace period", userId);
int updated = statement.executeUpdate();
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) {
LOGGER.error("[SessionResume] Failed to restore SSO ticket for user " + userId, e);
}
}
private void applyPausedEffect(Habbo habbo) {
int effectId = getPausedEffectId();
if (effectId <= 0) return;
try {
RoomUnit unit = habbo.getRoomUnit();
Room room = habbo.getHabboInfo() == null ? null : habbo.getHabboInfo().getCurrentRoom();
if (unit == null || room == null) return;
int endTimestamp = Emulator.getIntUnixTimestamp() + getGracePeriodSeconds() + 10;
unit.setEffectId(effectId, endTimestamp);
room.sendComposer(new RoomUserEffectComposer(unit).compose());
} catch (Exception e) {
LOGGER.error("[SessionResume] Failed to apply paused effect", e);
}
}
private void restorePausedEffect(GhostSession ghost) {
try {
Habbo habbo = ghost.habbo;
RoomUnit unit = habbo.getRoomUnit();
Room room = habbo.getHabboInfo() == null ? null : habbo.getHabboInfo().getCurrentRoom();
if (unit == null || room == null) return;
int pausedEffectId = getPausedEffectId();
if (unit.getEffectId() == pausedEffectId) {
unit.setEffectId(ghost.previousEffectId, ghost.previousEffectEnd);
room.sendComposer(new RoomUserEffectComposer(unit).compose());
}
} catch (Exception e) {
LOGGER.error("[SessionResume] Failed to restore previous effect", e);
}
}
private void clearSsoTicket(int userId) {
try (var connection = Emulator.getDatabase().getDataSource().getConnection();
var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) {
@@ -163,11 +196,16 @@ public class SessionResumeManager {
final Habbo habbo;
final String ssoTicket;
final ScheduledFuture<?> disposeFuture;
final int previousEffectId;
final int previousEffectEnd;
GhostSession(Habbo habbo, String ssoTicket, ScheduledFuture<?> disposeFuture) {
GhostSession(Habbo habbo, String ssoTicket, ScheduledFuture<?> disposeFuture,
int previousEffectId, int previousEffectEnd) {
this.habbo = habbo;
this.ssoTicket = ssoTicket;
this.disposeFuture = disposeFuture;
this.previousEffectId = previousEffectId;
this.previousEffectEnd = previousEffectEnd;
}
}
}
@@ -208,10 +208,10 @@ public abstract class Game implements Runnable {
this.state = GameState.IDLE;
boolean gamesActive = false;
for (HabboItem timer : room.getFloorItems()) {
if (timer instanceof InteractionGameTimer) {
if (((InteractionGameTimer) timer).isRunning())
gamesActive = true;
for (InteractionGameTimer timer : room.getRoomSpecialTypes().getGameTimers().values()) {
if (timer.isRunning()) {
gamesActive = true;
break;
}
}
@@ -6,49 +6,55 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager;
public class GamePlayer {
private final Habbo habbo;
private GameTeamColors teamColor;
private int score;
private int wiredScore;
public GamePlayer(Habbo habbo, GameTeamColors teamColor) {
this.habbo = habbo;
this.teamColor = teamColor;
}
public void reset() {
this.score = 0;
this.wiredScore = 0;
}
public synchronized void addScore(int amount) {
public void addScore(int amount) {
addScore(amount, false);
}
public synchronized void addScore(int amount, boolean isWired) {
if (habbo.getHabboInfo().getGamePlayer() != null && this.habbo.getHabboInfo().getCurrentGame() != null && this.habbo.getHabboInfo().getCurrentRoom().getGame(this.habbo.getHabboInfo().getCurrentGame()).getTeamForHabbo(this.habbo) != null) {
this.score += amount;
public void addScore(int amount, boolean isWired) {
com.eu.habbo.habbohotel.rooms.Room roomToTrigger = null;
com.eu.habbo.habbohotel.rooms.RoomUnit roomUnitToTrigger = null;
int currentScore = 0;
if (this.score < 0) this.score = 0;
synchronized (this) {
if (this.habbo.getHabboInfo().getGamePlayer() != null && this.habbo.getHabboInfo().getCurrentGame() != null && this.habbo.getHabboInfo().getCurrentRoom().getGame(this.habbo.getHabboInfo().getCurrentGame()).getTeamForHabbo(this.habbo) != null) {
this.score += amount;
if(isWired) {
this.wiredScore += amount;
if (this.score < 0) this.score = 0;
if (this.wiredScore < 0) {
this.wiredScore = 0;
if (isWired) {
this.wiredScore += amount;
if (this.wiredScore < 0) {
this.wiredScore = 0;
}
if (this.wiredScore > this.score) {
this.wiredScore = this.score;
}
}
if (this.wiredScore > this.score) {
this.wiredScore = this.score;
}
roomToTrigger = this.habbo.getHabboInfo().getCurrentRoom();
roomUnitToTrigger = this.habbo.getRoomUnit();
currentScore = this.score;
}
}
WiredManager.triggerScoreAchieved(this.habbo.getHabboInfo().getCurrentRoom(), this.habbo.getRoomUnit(), this.score, amount);
if (roomToTrigger != null && roomUnitToTrigger != null) {
WiredManager.triggerScoreAchieved(roomToTrigger, roomUnitToTrigger, currentScore, amount);
}
}
@@ -56,12 +62,10 @@ public class GamePlayer {
return this.habbo;
}
public GameTeamColors getTeamColor() {
return this.teamColor;
}
public int getScore() {
return this.score;
}
@@ -2,11 +2,13 @@ package com.eu.habbo.habbohotel.guilds;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.guilds.forums.ForumThread;
import com.eu.habbo.habbohotel.guilds.forums.ForumView;
import com.eu.habbo.habbohotel.items.interactions.InteractionGuildFurni;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.outgoing.guilds.GuildJoinErrorComposer;
import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer;
import gnu.trove.TCollections;
import gnu.trove.iterator.TIntObjectIterator;
import gnu.trove.map.TIntObjectMap;
@@ -142,12 +144,36 @@ public class GuildManager {
deleteFavourite.execute();
}
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM guild_forum_views WHERE guild_id = ?")) {
statement.setInt(1, guild.getId());
statement.execute();
}
try (PreparedStatement statement = connection.prepareStatement("DELETE c FROM guilds_forums_comments c INNER JOIN guilds_forums_threads t ON c.thread_id = t.id WHERE t.guild_id = ?")) {
statement.setInt(1, guild.getId());
statement.execute();
}
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM guilds_forums_threads WHERE guild_id = ?")) {
statement.setInt(1, guild.getId());
statement.execute();
}
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM guilds_members WHERE guild_id = ?")) {
statement.setInt(1, guild.getId());
statement.execute();
}
try (PreparedStatement statement = connection.prepareStatement("UPDATE rooms SET guild_id = 0 WHERE guild_id = ?")) {
statement.setInt(1, guild.getId());
statement.execute();
}
try (PreparedStatement statement = connection.prepareStatement("UPDATE items SET guild_id = 0 WHERE guild_id = ?")) {
statement.setInt(1, guild.getId());
statement.execute();
}
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM guilds WHERE id = ?")) {
statement.setInt(1, guild.getId());
statement.execute();
@@ -161,6 +187,10 @@ public class GuildManager {
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
this.guilds.remove(guild.getId());
ForumThread.clearCacheForGuild(guild.getId());
GuildForumDataComposer.invalidateUnreadCache(guild.getId());
}
@@ -48,6 +48,12 @@ public class Item implements ISerialize {
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) {
if (item instanceof InteractionMultiHeight && item.getBaseItem().getMultiHeights().length > 0) {
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(() -> {
WiredManager.triggerUserWalksOn(room, unit, this);
if (room.isLoaded()) {
WiredManager.triggerUserWalksOn(room, unit, this);
}
}, 500);
});
@@ -29,6 +29,10 @@ public class InteractionRoomAds extends InteractionCustomValues {
{
this.put("offsetZ", "0");
}
{
this.put("scale", "100");
}
};
public InteractionRoomAds(ResultSet set, Item baseItem) throws SQLException {
@@ -93,23 +93,24 @@ public abstract class InteractionWired extends InteractionDefault {
@Override
public void run() {
if (this.needsUpdate()) {
String wiredData = this.getWiredData();
String wiredDataRaw = this.getWiredData();
final String wiredData = (wiredDataRaw == null) ? "" : wiredDataRaw;
final int currentRoomId = this.getRoomId();
final int currentId = this.getId();
if (wiredData == null) {
wiredData = "";
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE items SET wired_data = ? WHERE id = ?")) {
if (this.getRoomId() != 0) {
statement.setString(1, wiredData);
} else {
statement.setString(1, "");
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE items SET wired_data = ? WHERE id = ?")) {
if (currentRoomId != 0) {
statement.setString(1, wiredData);
} else {
statement.setString(1, "");
}
statement.setInt(2, currentId);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
statement.setInt(2, this.getId());
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
});
}
super.run();
}
@@ -216,6 +217,9 @@ public abstract class InteractionWired extends InteractionDefault {
public static WiredSettings readSettings(ClientMessage packet, boolean isEffect)
{
int intParamCount = packet.readInt();
if (intParamCount < 0 || intParamCount > 100) {
throw new IllegalArgumentException("Invalid intParamCount: " + intParamCount);
}
int[] intParams = new int[intParamCount];
for(int i = 0; i < intParamCount; i++)
@@ -226,6 +230,10 @@ public abstract class InteractionWired extends InteractionDefault {
String stringParam = packet.readString();
int itemCount = packet.readInt();
int selectionLimit = Emulator.getConfig() != null ? Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5) : 5;
if (itemCount < 0 || itemCount > selectionLimit * 20) {
throw new IllegalArgumentException("Invalid itemCount: " + itemCount + " exceeds maximum allowed limit");
}
int[] itemIds = new int[itemCount];
for(int i = 0; i < itemCount; i++)
@@ -154,6 +154,7 @@ public class InteractionGameTimer extends HabboItem {
@Override
public void onPickUp(Room room) {
this.endGame(room);
this.threadActive = false;
this.timeNow = this.getInitialTimeValue();
this.setExtradata(this.timeNow + "\t" + this.baseTime);
@@ -220,8 +221,7 @@ public class InteractionGameTimer extends HabboItem {
room.updateItem(this);
WiredManager.triggerGameStarts(room);
if (!this.threadActive) {
this.threadActive = true;
if (this.tryActivateTimerThread()) {
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
}
} else if (client != null) {
@@ -243,8 +243,7 @@ public class InteractionGameTimer extends HabboItem {
} else {
this.unpause(room);
if (!this.threadActive) {
this.threadActive = true;
if (this.tryActivateTimerThread()) {
this.scheduleTimerRunnable(this.getTimerResumeDelayMs());
}
}
@@ -257,8 +256,7 @@ public class InteractionGameTimer extends HabboItem {
this.createNewGame(room);
WiredManager.triggerGameStarts(room);
if (!this.threadActive) {
this.threadActive = true;
if (this.tryActivateTimerThread()) {
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
}
}
@@ -297,8 +295,7 @@ public class InteractionGameTimer extends HabboItem {
}
this.createNewGame(room);
WiredManager.triggerGameStarts(room);
if (!threadActive) {
threadActive = true;
if (this.tryActivateTimerThread()) {
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
}
}
@@ -321,8 +318,7 @@ public class InteractionGameTimer extends HabboItem {
this.isPaused = false;
this.unpause(room);
if (!this.threadActive) {
this.threadActive = true;
if (this.tryActivateTimerThread()) {
this.scheduleTimerRunnable(this.getTimerResumeDelayMs());
}
}
@@ -406,7 +402,9 @@ public class InteractionGameTimer extends HabboItem {
}
public void setThreadActive(boolean threadActive) {
this.threadActive = threadActive;
synchronized (this) {
this.threadActive = threadActive;
}
}
public boolean isPaused() {
@@ -428,4 +426,15 @@ public class InteractionGameTimer extends HabboItem {
public int getBaseTime() {
return this.baseTime;
}
public boolean tryActivateTimerThread() {
synchronized (this) {
if (this.threadActive) {
return false;
}
this.threadActive = true;
return true;
}
}
}
@@ -1,5 +1,6 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
@@ -11,10 +12,12 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.messages.ServerMessage;
import gnu.trove.set.hash.THashSet;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.stream.Collectors;
public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private static final int COMPARISON_LESS_THAN = 0;
@@ -23,9 +26,16 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private static final int SOURCE_GROUP_USERS = 0;
private static final int SOURCE_GROUP_FURNI = 1;
private static final int SOURCE_USER_TRIGGER = 0;
private static final int SOURCE_USER_SIGNAL = 1;
private static final int SOURCE_USER_CLICKED = 2;
private static final int SOURCE_FURNI_TRIGGER = 3;
private static final int SOURCE_FURNI_PICKED = 4;
private static final int SOURCE_FURNI_SIGNAL = 5;
public static final WiredConditionType type = WiredConditionType.SLC_QUANTITY;
private final THashSet<HabboItem> items;
private int comparison = COMPARISON_EQUAL;
private int quantity = 0;
private int sourceGroup = SOURCE_GROUP_USERS;
@@ -33,10 +43,12 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
public WiredConditionSelectionQuantity(ResultSet set, Item baseItem) throws SQLException {
super(set, baseItem);
this.items = new THashSet<>();
}
public WiredConditionSelectionQuantity(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
super(id, userId, item, extradata, limitedStack, limitedSells);
this.items = new THashSet<>();
}
@Override
@@ -46,9 +58,18 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
@Override
public void serializeWiredData(ServerMessage message, Room room) {
message.appendBoolean(false);
message.appendInt(5);
message.appendInt(0);
this.refresh(room);
boolean pickMode = this.sourceGroup == SOURCE_GROUP_FURNI && this.sourceType == WiredSourceUtil.SOURCE_SELECTED;
message.appendBoolean(pickMode);
message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION);
message.appendInt(pickMode ? this.items.size() : 0);
if (pickMode) {
for (HabboItem item : this.items) {
message.appendInt(item.getId());
}
}
message.appendInt(this.getBaseItem().getSpriteId());
message.appendInt(this.getId());
message.appendString("");
@@ -69,8 +90,36 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL;
this.quantity = (params.length > 1) ? this.normalizeQuantity(params[1]) : 0;
this.sourceGroup = (params.length > 2) ? this.normalizeSourceGroup(params[2]) : SOURCE_GROUP_USERS;
this.sourceType = (params.length > 3) ? this.normalizeSourceType(this.sourceGroup, params[3]) : WiredSourceUtil.SOURCE_TRIGGER;
this.items.clear();
if (params.length > 3) {
this.sourceGroup = this.normalizeSourceGroup(params[2]);
this.sourceType = this.normalizeSourceType(this.sourceGroup, params[3]);
} else {
this.setSourceSelection((params.length > 2) ? params[2] : SOURCE_USER_TRIGGER);
}
if (this.sourceGroup != SOURCE_GROUP_FURNI || this.sourceType != WiredSourceUtil.SOURCE_SELECTED) {
return true;
}
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId());
if (room == null) {
return false;
}
int count = settings.getFurniIds().length;
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) {
return false;
}
for (int itemId : settings.getFurniIds()) {
HabboItem item = room.getHabboItem(itemId);
if (item != null) {
this.items.add(item);
}
}
return true;
}
@@ -97,11 +146,14 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
@Override
public String getWiredData() {
this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()));
return WiredManager.getGson().toJson(new JsonData(
this.comparison,
this.quantity,
this.sourceGroup,
this.sourceType
this.sourceType,
this.items.stream().map(HabboItem::getId).collect(Collectors.toList())
));
}
@@ -125,6 +177,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
this.quantity = this.normalizeQuantity(data.quantity);
this.sourceGroup = this.normalizeSourceGroup(data.sourceGroup);
this.sourceType = this.normalizeSourceType(this.sourceGroup, data.sourceType);
this.loadSelectedItems(data.itemIds, room);
return;
}
@@ -150,6 +203,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
@Override
public void onPickUp() {
this.items.clear();
this.comparison = COMPARISON_EQUAL;
this.quantity = 0;
this.sourceGroup = SOURCE_GROUP_USERS;
@@ -158,7 +212,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private int resolveCount(WiredContext ctx) {
if (this.sourceGroup == SOURCE_GROUP_FURNI) {
List<HabboItem> items = WiredSourceUtil.resolveItems(ctx, this.sourceType, null);
List<HabboItem> items = WiredSourceUtil.resolveItems(ctx, this.sourceType, this.items);
return items.size();
}
@@ -188,10 +242,18 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private int normalizeSourceType(int group, int value) {
if (group == SOURCE_GROUP_USERS) {
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
switch (value) {
case WiredSourceUtil.SOURCE_CLICKED_USER:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_SELECTOR:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
}
switch (value) {
case WiredSourceUtil.SOURCE_SELECTED:
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER:
@@ -201,17 +263,104 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
}
}
private int getSourceSelection() {
if (this.sourceGroup == SOURCE_GROUP_FURNI) {
switch (this.sourceType) {
case WiredSourceUtil.SOURCE_SELECTED:
return SOURCE_FURNI_PICKED;
case WiredSourceUtil.SOURCE_SIGNAL:
return SOURCE_FURNI_SIGNAL;
default:
return SOURCE_FURNI_TRIGGER;
}
}
switch (this.sourceType) {
case WiredSourceUtil.SOURCE_CLICKED_USER:
return SOURCE_USER_CLICKED;
case WiredSourceUtil.SOURCE_SIGNAL:
return SOURCE_USER_SIGNAL;
default:
return SOURCE_USER_TRIGGER;
}
}
private void setSourceSelection(int value) {
switch (value) {
case SOURCE_USER_SIGNAL:
this.sourceGroup = SOURCE_GROUP_USERS;
this.sourceType = WiredSourceUtil.SOURCE_SIGNAL;
break;
case SOURCE_USER_CLICKED:
this.sourceGroup = SOURCE_GROUP_USERS;
this.sourceType = WiredSourceUtil.SOURCE_CLICKED_USER;
break;
case SOURCE_FURNI_TRIGGER:
this.sourceGroup = SOURCE_GROUP_FURNI;
this.sourceType = WiredSourceUtil.SOURCE_TRIGGER;
break;
case SOURCE_FURNI_PICKED:
this.sourceGroup = SOURCE_GROUP_FURNI;
this.sourceType = WiredSourceUtil.SOURCE_SELECTED;
break;
case SOURCE_FURNI_SIGNAL:
this.sourceGroup = SOURCE_GROUP_FURNI;
this.sourceType = WiredSourceUtil.SOURCE_SIGNAL;
break;
default:
this.sourceGroup = SOURCE_GROUP_USERS;
this.sourceType = WiredSourceUtil.SOURCE_TRIGGER;
break;
}
}
private void loadSelectedItems(List<Integer> itemIds, Room room) {
this.items.clear();
if (itemIds == null || room == null) {
return;
}
for (Integer itemId : itemIds) {
HabboItem item = room.getHabboItem(itemId);
if (item != null) {
this.items.add(item);
}
}
}
private void refresh(Room room) {
if (room == null || this.items.isEmpty()) {
return;
}
THashSet<HabboItem> itemsToRemove = new THashSet<>();
for (HabboItem item : this.items) {
if (item == null || room.getHabboItem(item.getId()) == null) {
itemsToRemove.add(item);
}
}
for (HabboItem item : itemsToRemove) {
this.items.remove(item);
}
}
static class JsonData {
int comparison;
int quantity;
int sourceGroup;
int sourceType;
List<Integer> itemIds;
public JsonData(int comparison, int quantity, int sourceGroup, int sourceType) {
public JsonData(int comparison, int quantity, int sourceGroup, int sourceType, List<Integer> itemIds) {
this.comparison = comparison;
this.quantity = quantity;
this.sourceGroup = sourceGroup;
this.sourceType = sourceType;
this.itemIds = itemIds;
}
}
}
@@ -82,7 +82,7 @@ public class WiredEffectBotTalk extends InteractionWiredEffect {
this.setDelay(delay);
this.botName = data[0].substring(0, Math.min(data[0].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.bot.message.max_length", 100)));
this.mode = mode;
return true;
@@ -105,7 +105,7 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect {
throw new WiredSaveException("Delay too long");
this.botName = data[0].substring(0, Math.min(data[0].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.bot.message.max_length", 100)));
this.mode = mode;
this.setDelay(delay);
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect;
import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameTimer;
import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameUpCounter;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
import com.eu.habbo.habbohotel.rooms.Room;
@@ -60,29 +61,74 @@ public class WiredEffectControlClock extends InteractionWiredEffect {
}
for (HabboItem item : effectiveItems) {
if (!(item instanceof InteractionGameUpCounter)) {
if (!(item instanceof InteractionGameTimer)) {
continue;
}
InteractionGameUpCounter counter = (InteractionGameUpCounter) item;
switch (this.action) {
case ACTION_START:
counter.restartFromZero(room);
break;
case ACTION_STOP:
counter.stopCounter(room);
break;
case ACTION_RESET:
counter.resetCounter(room);
break;
case ACTION_PAUSE:
counter.pauseCounter(room);
break;
case ACTION_RESUME:
counter.resumeCounter(room);
break;
if (item instanceof InteractionGameUpCounter) {
this.controlUpCounter((InteractionGameUpCounter) item, room);
continue;
}
this.controlGameTimer((InteractionGameTimer) item, room);
}
}
private void controlUpCounter(InteractionGameUpCounter counter, Room room) {
switch (this.action) {
case ACTION_START:
counter.restartFromZero(room);
break;
case ACTION_STOP:
counter.stopCounter(room);
break;
case ACTION_RESET:
counter.resetCounter(room);
break;
case ACTION_PAUSE:
counter.pauseCounter(room);
break;
case ACTION_RESUME:
counter.resumeCounter(room);
break;
}
}
private void controlGameTimer(InteractionGameTimer timer, Room room) {
switch (this.action) {
case ACTION_START:
timer.startTimer(room);
break;
case ACTION_STOP:
this.stopGameTimer(timer, room, false);
break;
case ACTION_RESET:
this.stopGameTimer(timer, room, true);
break;
case ACTION_PAUSE:
timer.pauseTimer(room);
break;
case ACTION_RESUME:
timer.resumeTimer(room);
break;
}
}
private void stopGameTimer(InteractionGameTimer timer, Room room, boolean resetTime) {
boolean wasActive = timer.isRunning() || timer.isPaused();
timer.endGame(room);
if (resetTime) {
timer.setTimeNow(timer.getBaseTime());
timer.setExtradata(timer.getTimeNow() + "\t" + timer.getBaseTime());
}
room.updateItem(timer);
timer.needsUpdate(true);
if (wasActive) {
WiredManager.triggerGameEnds(room);
}
}
@@ -206,7 +252,7 @@ public class WiredEffectControlClock extends InteractionWiredEffect {
throw new WiredSaveException(String.format("Item %s not found", itemId));
}
if (!(item instanceof InteractionGameUpCounter)) {
if (!(item instanceof InteractionGameTimer)) {
throw new WiredSaveException("wiredfurni.error.require_counter_furni");
}
@@ -53,26 +53,37 @@ public class WiredEffectFurniToFurni extends InteractionWiredEffect {
return;
}
HabboItem moveItem = this.resolveLastMoveItem(ctx);
HabboItem targetItem = this.resolveLastTargetItem(ctx);
List<HabboItem> moveItems = this.resolveMoveItems(ctx);
List<HabboItem> targetItems = this.resolveTargetItems(ctx);
if (moveItem == null || targetItem == null || moveItem.getId() == targetItem.getId()) {
if (moveItems.isEmpty() || targetItems.isEmpty()) {
return;
}
RoomTile targetTile = room.getLayout().getTile(targetItem.getX(), targetItem.getY());
if (targetTile == null) {
return;
}
int targetIndex = 0;
for (HabboItem moveItem : moveItems) {
if (moveItem == null) {
continue;
}
FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), null, false, ctx);
if (error == FurnitureMovementError.NONE) {
return;
}
HabboItem targetItem = targetItems.get(targetIndex % targetItems.size());
targetIndex++;
error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), targetItem.getZ(), null, false, ctx);
if (error == FurnitureMovementError.NONE) {
return;
if (targetItem == null || moveItem.getId() == targetItem.getId()) {
continue;
}
RoomTile targetTile = room.getLayout().getTile(targetItem.getX(), targetItem.getY());
if (targetTile == null) {
continue;
}
FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), null, false, ctx);
if (error == FurnitureMovementError.NONE) {
continue;
}
WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), targetItem.getZ(), null, false, ctx);
}
}
@@ -233,35 +244,23 @@ public class WiredEffectFurniToFurni extends InteractionWiredEffect {
return COOLDOWN_MOVEMENT;
}
private HabboItem resolveLastMoveItem(WiredContext ctx) {
return this.resolveLastItem(ctx, this.moveSource, this.moveItems);
private List<HabboItem> resolveMoveItems(WiredContext ctx) {
return this.resolveItems(ctx, this.moveSource, this.moveItems);
}
private HabboItem resolveLastTargetItem(WiredContext ctx) {
private List<HabboItem> resolveTargetItems(WiredContext ctx) {
int source = (this.targetSource == SOURCE_SECONDARY_SELECTED) ? WiredSourceUtil.SOURCE_SELECTED : this.targetSource;
return this.resolveLastItem(ctx, source, this.targetItems);
return this.resolveItems(ctx, source, this.targetItems);
}
private HabboItem resolveLastItem(WiredContext ctx, int source, List<HabboItem> items) {
private List<HabboItem> resolveItems(WiredContext ctx, int source, List<HabboItem> items) {
if (source == WiredSourceUtil.SOURCE_SELECTED) {
this.validateItems(items);
}
List<HabboItem> resolvedItems = WiredSourceUtil.resolveItems(ctx, source, items);
if (resolvedItems.isEmpty()) {
return null;
}
for (int index = resolvedItems.size() - 1; index >= 0; index--) {
HabboItem item = resolvedItems.get(index);
if (item != null) {
return item;
}
}
return null;
return WiredSourceUtil.resolveItems(ctx, source, items).stream()
.filter(item -> item != null && ctx.room().getHabboItem(item.getId()) != null)
.collect(Collectors.toList());
}
private List<HabboItem> parseItems(String data, Room room) throws WiredSaveException {
@@ -20,12 +20,13 @@ import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.incoming.wired.WiredSaveException;
import com.eu.habbo.messages.outgoing.generic.alerts.UpdateFailedComposer;
import gnu.trove.procedure.TObjectProcedure;
import gnu.trove.set.hash.THashSet;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
public class WiredEffectGiveReward extends InteractionWiredEffect {
public static final int LIMIT_ONCE = 0;
@@ -37,10 +38,10 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
public int limit;
public int limitationInterval;
public int given;
public AtomicInteger given = new AtomicInteger(0);
public int rewardTime;
public boolean uniqueRewards;
public THashSet<WiredGiveRewardItem> rewardItems = new THashSet<>();
public List<WiredGiveRewardItem> rewardItems = new CopyOnWriteArrayList<>();
public int userSource = WiredSourceUtil.SOURCE_TRIGGER;
public WiredEffectGiveReward(ResultSet set, Item baseItem) throws SQLException {
@@ -71,9 +72,8 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
@Override
public String getWiredData() {
ArrayList<WiredGiveRewardItem> rewards = new ArrayList<>(this.rewardItems);
return WiredManager.getGson().toJson(new JsonData(this.limit, this.given, this.rewardTime, this.uniqueRewards, this.limitationInterval, rewards, this.getDelay(), this.userSource));
return WiredManager.getGson().toJson(new JsonData(this.limit, this.given.get(), this.rewardTime, this.uniqueRewards, this.limitationInterval, rewards, this.getDelay(), this.userSource));
}
@Override
@@ -84,7 +84,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
this.setDelay(data.delay);
this.limit = data.limit;
this.given = data.given;
this.given.set(data.given);
this.rewardTime = data.reward_time;
this.uniqueRewards = data.unique_rewards;
this.limitationInterval = data.limit_interval;
@@ -96,7 +96,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
String[] data = wiredData.split(":");
if (data.length > 0) {
this.limit = Integer.parseInt(data[0]);
this.given = Integer.parseInt(data[1]);
this.given.set(Integer.parseInt(data[1]));
this.rewardTime = Integer.parseInt(data[2]);
this.uniqueRewards = data[3].equals("1");
this.limitationInterval = Integer.parseInt(data[4]);
@@ -127,7 +127,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
public void onPickUp() {
this.limit = 0;
this.limitationInterval = 0;
this.given = 0;
this.given.set(0);
this.rewardTime = 0;
this.uniqueRewards = false;
this.rewardItems.clear();
@@ -192,7 +192,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
this.limit = settings.getIntParams()[2];
this.limitationInterval = settings.getIntParams()[3];
this.userSource = settings.getIntParams()[4];
this.given = 0;
this.given.set(0);
String data = settings.getStringParam();
@@ -276,15 +276,15 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
}
public int getGiven() {
return this.given;
return this.given.get();
}
public void setGiven(int given) {
this.given = given;
this.given.set(given);
}
public void incrementGiven() {
this.given++;
this.given.incrementAndGet();
}
public int getRewardTime() {
@@ -303,11 +303,11 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
this.uniqueRewards = uniqueRewards;
}
public THashSet<WiredGiveRewardItem> getRewardItems() {
public List<WiredGiveRewardItem> getRewardItems() {
return this.rewardItems;
}
public void setRewardItems(THashSet<WiredGiveRewardItem> rewardItems) {
public void setRewardItems(List<WiredGiveRewardItem> rewardItems) {
this.rewardItems = rewardItems;
}
}
@@ -33,7 +33,7 @@ public class WiredEffectSendSignal extends InteractionWiredEffect {
public static final WiredEffectType type = WiredEffectType.SEND_SIGNAL;
private static final int MAX_SIGNAL_DEPTH = 10;
public static int MAX_SIGNAL_DEPTH = 100;
private static final int ANTENNA_PICKED = 0;
private static final int ANTENNA_TRIGGER = 1;
@@ -166,7 +166,7 @@ public class WiredEffectSendSignal extends InteractionWiredEffect {
.signalChannel(signalChannel)
.signalUserCount(signalUserCount)
.signalFurniCount(sourceItem != null ? 1 : 0)
.contextVariableScope(ctx.contextVariables())
.contextVariableScope(ctx.contextVariables().copy())
.triggeredByEffect(true);
if (actor != null) builder.actor(actor);
@@ -286,15 +286,6 @@ public class WiredEffectSendSignal extends InteractionWiredEffect {
}
}
if (room != null && room.getRoomSpecialTypes() != null) {
for (HabboItem receiver : newItems) {
int count = room.getRoomSpecialTypes().countSendersTargetingReceiver(receiver.getId(), this);
if (count >= RoomSpecialTypes.MAX_SENDERS_PER_RECEIVER) {
throw new WiredSaveException("Maximum of " + RoomSpecialTypes.MAX_SENDERS_PER_RECEIVER + " senders per receiver reached");
}
}
}
int delay = settings.getDelay();
if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) {
throw new WiredSaveException("Delay too long");
@@ -34,6 +34,8 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
private static final long DELIVERY_DEDUP_TTL_MS = 60_000L;
private static final int DELIVERY_DEDUP_CLEANUP_THRESHOLD = 512;
private static final ConcurrentHashMap<String, Long> DELIVERY_DEDUP = new ConcurrentHashMap<>();
private static final int DEFAULT_SHOW_MESSAGE_MAX_LENGTH = 200;
private static final int DEFAULT_SHOW_MESSAGE_MAX_LINES = 8;
protected String message = "";
protected int userSource = WiredSourceUtil.SOURCE_TRIGGER;
@@ -96,9 +98,12 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
if(gameClient.getHabbo() == null || !gameClient.getHabbo().hasPermission(Permission.ACC_SUPERWIRED)) {
message = Emulator.getGameEnvironment().getWordFilter().filter(message, null);
message = message.substring(0, Math.min(message.length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
}
int maxLength = Emulator.getConfig().getInt("hotel.wired.show_message.max_length", DEFAULT_SHOW_MESSAGE_MAX_LENGTH);
int maxLines = Emulator.getConfig().getInt("hotel.wired.show_message.max_lines", DEFAULT_SHOW_MESSAGE_MAX_LINES);
message = clampMessage(message, maxLength, maxLines);
int delay = settings.getDelay();
if(delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20))
@@ -109,6 +114,35 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
return true;
}
private static String clampMessage(String value, int maxLength, int maxLines) {
if (value == null || value.isEmpty()) {
return "";
}
int safeMaxLength = Math.max(1, maxLength);
int safeMaxLines = Math.max(1, maxLines);
String normalized = value.replace("\r\n", "\n").replace('\r', '\n');
String[] lines = normalized.split("\n", -1);
StringBuilder builder = new StringBuilder();
int linesToWrite = Math.min(lines.length, safeMaxLines);
for (int index = 0; index < linesToWrite; index++) {
if (builder.length() > 0) {
builder.append('\n');
}
builder.append(lines[index]);
}
if (builder.length() > safeMaxLength) {
builder.setLength(safeMaxLength);
}
return builder.toString();
}
protected List<RoomUnit> resolveUsers(WiredContext ctx) {
return WiredSourceUtil.resolveUsers(ctx, this.userSource);
}
@@ -212,7 +246,9 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
}
String msg = buildMessage(ctx, (sharedSourceHabbo != null) ? sharedSourceHabbo : habbo);
habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(msg, habbo, habbo, RoomChatMessageBubbles.getBubble(this.bubbleStyle))));
habbo.getClient().sendResponse(new RoomUserWhisperComposer(
new RoomChatMessage(msg, habbo.getRoomUnit(), RoomChatMessageBubbles.getBubble(this.bubbleStyle))
));
if (habbo.getRoomUnit().isIdle()) {
habbo.getRoomUnit().getRoom().unIdle(habbo);
@@ -170,13 +170,15 @@ public class WiredExtraTextInputVariable extends InteractionWiredExtra {
}
public Integer resolveCapturedValue(Room room, String rawValue) {
String normalizedValue = rawValue != null ? rawValue.trim() : "";
if (normalizedValue.isEmpty()) {
return null;
}
String capturedValue = rawValue != null ? rawValue : "";
String normalizedValue = capturedValue.trim();
if (this.getDisplayType(room) == DISPLAY_TEXTUAL) {
return WiredVariableTextConnectorSupport.toValue(room, this.variableItemId, normalizedValue);
return WiredVariableTextConnectorSupport.toValue(room, this.variableItemId, capturedValue);
}
if (normalizedValue.isEmpty()) {
return null;
}
try {
@@ -22,6 +22,7 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
public static final int CODE = 79;
public static final int MAX_MAPPING_LENGTH = 1000;
public static final int MAX_MAPPING_LINES = 30;
private static final String PRESERVED_SPACE = "\u00A0";
private String mappingsText = "";
private LinkedHashMap<Integer, String> mappings = new LinkedHashMap<>();
@@ -123,8 +124,12 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
return "";
}
String mappedValue = this.mappings.get(value);
return mappedValue != null ? mappedValue : String.valueOf(value);
if (this.mappings.containsKey(value)) {
String mappedValue = this.mappings.get(value);
return mappedValue != null ? preserveSpaces(mappedValue) : "";
}
return String.valueOf(value);
}
public Integer resolveValue(String text) {
@@ -132,17 +137,16 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
return null;
}
String normalizedText = text.trim();
if (normalizedText.isEmpty()) {
return null;
}
String normalizedText = normalizePreservedSpaces(text);
for (Map.Entry<Integer, String> entry : this.mappings.entrySet()) {
if (entry == null || entry.getKey() == null || entry.getValue() == null) {
continue;
}
if (entry.getValue().trim().equalsIgnoreCase(normalizedText)) {
String normalizedMappingValue = normalizePreservedSpaces(entry.getValue());
if (normalizedMappingValue.equalsIgnoreCase(normalizedText)) {
return entry.getKey();
}
}
@@ -195,8 +199,8 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
continue;
}
String line = rawLine.trim();
if (line.isEmpty()) {
String line = rawLine;
if (line.trim().isEmpty()) {
continue;
}
@@ -210,7 +214,7 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
}
String keyPart = line.substring(0, separatorIndex).trim();
String valuePart = line.substring(separatorIndex + 1).trim();
String valuePart = line.substring(separatorIndex + 1);
try {
result.put(Integer.parseInt(keyPart), valuePart);
@@ -221,6 +225,14 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
return result;
}
private static String preserveSpaces(String value) {
return value.replace(" ", PRESERVED_SPACE);
}
private static String normalizePreservedSpaces(String value) {
return value.replace(PRESERVED_SPACE, " ");
}
static class JsonData {
String mappingsText;
@@ -384,61 +384,69 @@ public final class WiredVariableReferenceSupport {
}
private static void upsertSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId, SharedUserAssignment assignment) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_user_wired_variables (room_id, user_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, userId);
statement.setInt(3, sourceVariableItemId);
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_user_wired_variables (room_id, user_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, userId);
statement.setInt(3, sourceVariableItemId);
if (assignment.getValue() == null) {
statement.setNull(4, java.sql.Types.INTEGER);
} else {
statement.setInt(4, assignment.getValue());
if (assignment.getValue() == null) {
statement.setNull(4, java.sql.Types.INTEGER);
} else {
statement.setInt(4, assignment.getValue());
}
statement.setInt(5, assignment.getCreatedAt());
statement.setInt(6, assignment.getUpdatedAt());
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to store shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
}
statement.setInt(5, assignment.getCreatedAt());
statement.setInt(6, assignment.getUpdatedAt());
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to store shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
}
});
}
private static void deleteSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ?")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, userId);
statement.setInt(3, sourceVariableItemId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to delete shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
}
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ?")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, userId);
statement.setInt(3, sourceVariableItemId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to delete shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
}
});
}
private static void upsertSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId, SharedRoomAssignment assignment) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_wired_variables (room_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, sourceVariableItemId);
statement.setInt(3, assignment.getValue());
statement.setInt(4, 0);
statement.setInt(5, assignment.getUpdatedAt());
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to store shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
}
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_wired_variables (room_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, sourceVariableItemId);
statement.setInt(3, assignment.getValue());
statement.setInt(4, 0);
statement.setInt(5, assignment.getUpdatedAt());
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to store shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
}
});
}
private static void deleteSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ?")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, sourceVariableItemId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to delete shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
}
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ?")) {
statement.setInt(1, sourceRoomId);
statement.setInt(2, sourceVariableItemId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to delete shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
}
});
}
private static String createDefinitionPrefix(int sourceRoomId, int sourceVariableItemId) {
@@ -95,6 +95,11 @@ public class WiredEffectFurniAltitude extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(
@@ -100,6 +100,11 @@ public class WiredEffectFurniArea extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay()));
@@ -155,6 +155,11 @@ public class WiredEffectFurniByType extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(
@@ -38,6 +38,7 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
private static final int MAX_PICKED_FURNI = 20;
private static final int MAX_TILE_OFFSETS = 64;
private static final int GRID_RANGE = 4;
private int sourceType = SOURCE_USER_TRIGGER;
private boolean filterExisting = false;
@@ -69,8 +70,20 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
int totalRaw = 0;
int wiredSkipped = 0;
Set<HabboItem> result = new LinkedHashSet<>();
Set<HabboItem> neighborhoodItems = new LinkedHashSet<>();
for (int[] src : sourcePositions) {
LOGGER.info("[FurniNeighborhood] Source: ({},{}), offsets: {}", src[0], src[1], tileOffsets.size());
for (int[] offset : getFullGridOffsets()) {
int tx = src[0] + (offset[0] - this.targetOffsetX);
int ty = src[1] + (offset[1] - this.targetOffsetY);
for (HabboItem item : room.getItemsAt(tx, ty)) {
if (item != null && (includeWiredItems || !(item instanceof InteractionWired))) {
neighborhoodItems.add(item);
}
}
}
for (int[] offset : tileOffsets) {
int tx = src[0] + (offset[0] - this.targetOffsetX);
int ty = src[1] + (offset[1] - this.targetOffsetY);
@@ -91,7 +104,7 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
}
LOGGER.info("[FurniNeighborhood] Raw={}, wiredSkipped={}, kept={}", totalRaw, wiredSkipped, result.size());
result = this.applySelectorModifiers(result, this.getSelectableFloorItems(room, ctx), ctx.targets().items(), filterExisting, invert);
result = this.applyNeighborhoodModifiers(result, neighborhoodItems, ctx.targets().items());
// Always set the selector result even if empty.
// An empty result means no items matched the neighborhood, so downstream
@@ -100,15 +113,51 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
LOGGER.info("[FurniNeighborhood] Set {} items as targets", result.size());
}
private List<int[]> getFullGridOffsets() {
List<int[]> offsets = new ArrayList<>();
for (int y = -GRID_RANGE; y <= GRID_RANGE; y++) {
for (int x = -GRID_RANGE; x <= GRID_RANGE; x++) {
offsets.add(new int[]{ x, y });
}
}
return offsets;
}
private LinkedHashSet<HabboItem> applyNeighborhoodModifiers(Set<HabboItem> matchedTargets,
Set<HabboItem> neighborhoodTargets,
Collection<HabboItem> existingTargets) {
LinkedHashSet<HabboItem> matched = new LinkedHashSet<>(matchedTargets);
if (this.invert) {
LinkedHashSet<HabboItem> base = new LinkedHashSet<>(neighborhoodTargets);
base.removeAll(matched);
if (this.filterExisting) {
base.retainAll(this.toLinkedHashSet(existingTargets));
}
return base;
}
if (this.filterExisting) {
matched.retainAll(this.toLinkedHashSet(existingTargets));
}
return matched;
}
private List<int[]> resolveSourcePositions(WiredContext ctx, Room room) {
switch (sourceType) {
case SOURCE_USER_TRIGGER: {
if (ctx.tile().isPresent()) {
return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y });
Optional<RoomUnit> actor = ctx.actor();
if (actor.isPresent()) {
return Collections.singletonList(new int[]{ actor.get().getX(), actor.get().getY() });
}
return ctx.actor()
.map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() }))
return ctx.tile()
.map(tile -> Collections.singletonList(new int[]{ tile.x, tile.y }))
.orElse(Collections.emptyList());
}
case SOURCE_USER_SIGNAL: {
@@ -260,6 +309,16 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
return true;
}
@Override
public boolean hasRequiredSelectorTargets(WiredContext ctx) {
return ctx != null && ctx.targets().hasItems();
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(
@@ -128,6 +128,11 @@ public class WiredEffectFurniOnFurni extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()));
@@ -86,6 +86,11 @@ public class WiredEffectFurniPicks extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(
@@ -77,6 +77,11 @@ public class WiredEffectFurniSignal extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay()));
@@ -86,6 +86,11 @@ public class WiredEffectUsersArea extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay()));
@@ -92,6 +92,11 @@ public class WiredEffectUsersByAction extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(
@@ -90,6 +90,11 @@ public class WiredEffectUsersByName extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.namesText, this.filterExisting, this.invert, this.getDelay()));
@@ -76,6 +76,11 @@ public class WiredEffectUsersByType extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.entityType, this.filterExisting, this.invert, this.getDelay()));
@@ -90,6 +90,11 @@ public class WiredEffectUsersGroup extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.groupType, this.selectedGroupId, this.filterExisting, this.invert, this.getDelay()));
@@ -73,6 +73,11 @@ public class WiredEffectUsersHandItem extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.handItemId, this.filterExisting, this.invert, this.getDelay()));
@@ -38,6 +38,7 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
private static final int MAX_PICKED_FURNI = 20;
private static final int MAX_TILE_OFFSETS = 64;
private static final int GRID_RANGE = 4;
private int sourceType = SOURCE_USER_TRIGGER;
private boolean filterExisting = false;
@@ -87,11 +88,25 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
LOGGER.debug("[Neighborhood] Target tiles: {}", targetTiles);
Set<String> neighborhoodTiles = new HashSet<>();
for (int[] src : sourcePositions) {
for (int[] offset : getFullGridOffsets()) {
int tx = src[0] + (offset[0] - this.targetOffsetX);
int ty = src[1] + (offset[1] - this.targetOffsetY);
neighborhoodTiles.add(tx + "," + ty);
}
}
List<RoomUnit> result = new ArrayList<>();
List<RoomUnit> neighborhoodUsers = new ArrayList<>();
for (RoomUnit unit : room.getRoomUnits()) {
String pos = unit.getX() + "," + unit.getY();
boolean onTile = targetTiles.contains(pos);
if (neighborhoodTiles.contains(pos)) {
neighborhoodUsers.add(unit);
}
LOGGER.debug("[Neighborhood] Unit id={} type={} pos={} onTile={}", unit.getId(), unit.getRoomUnitType(), pos, onTile);
if (onTile) {
@@ -99,7 +114,7 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
}
}
result = new ArrayList<>(this.applySelectorModifiers(result, room.getRoomUnits(), ctx.targets().users(), filterExisting, invert));
result = new ArrayList<>(this.applyNeighborhoodModifiers(result, neighborhoodUsers, ctx.targets().users()));
LOGGER.debug("[Neighborhood] Result: {} users selected", result.size());
@@ -110,15 +125,51 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
ctx.targets().setUsers(result);
}
private List<int[]> getFullGridOffsets() {
List<int[]> offsets = new ArrayList<>();
for (int y = -GRID_RANGE; y <= GRID_RANGE; y++) {
for (int x = -GRID_RANGE; x <= GRID_RANGE; x++) {
offsets.add(new int[]{ x, y });
}
}
return offsets;
}
private LinkedHashSet<RoomUnit> applyNeighborhoodModifiers(Collection<RoomUnit> matchedTargets,
Collection<RoomUnit> neighborhoodTargets,
Collection<RoomUnit> existingTargets) {
LinkedHashSet<RoomUnit> matched = new LinkedHashSet<>(matchedTargets);
if (this.invert) {
LinkedHashSet<RoomUnit> base = new LinkedHashSet<>(neighborhoodTargets);
base.removeAll(matched);
if (this.filterExisting) {
base.retainAll(this.toLinkedHashSet(existingTargets));
}
return base;
}
if (this.filterExisting) {
matched.retainAll(this.toLinkedHashSet(existingTargets));
}
return matched;
}
private List<int[]> resolveSourcePositions(WiredContext ctx, Room room) {
switch (sourceType) {
case SOURCE_USER_TRIGGER: {
if (ctx.tile().isPresent()) {
return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y });
Optional<RoomUnit> actor = ctx.actor();
if (actor.isPresent()) {
return Collections.singletonList(new int[]{ actor.get().getX(), actor.get().getY() });
}
return ctx.actor()
.map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() }))
return ctx.tile()
.map(tile -> Collections.singletonList(new int[]{ tile.x, tile.y }))
.orElse(Collections.emptyList());
}
case SOURCE_USER_SIGNAL: {
@@ -262,6 +313,16 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
return true;
}
@Override
public boolean hasRequiredSelectorTargets(WiredContext ctx) {
return ctx != null && ctx.targets().hasUsers();
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(
@@ -115,6 +115,11 @@ public class WiredEffectUsersOnFurni extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()));
@@ -71,6 +71,11 @@ public class WiredEffectUsersSignal extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay()));
@@ -76,6 +76,11 @@ public class WiredEffectUsersTeam extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.teamType, this.filterExisting, this.invert, this.getDelay()));
@@ -187,6 +187,11 @@ public abstract class WiredEffectVariableSelectorBase extends InteractionWiredEf
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
this.refreshReferenceItems();
@@ -123,7 +123,11 @@ public class WiredTriggerRepeaterLong extends InteractionWiredTrigger implements
@Override
public boolean saveData(WiredSettings settings) {
if (settings.getIntParams().length < 1) return false;
this.repeatTime = settings.getIntParams()[0] * 5000;
int interval = settings.getIntParams()[0];
if (interval < 1) {
interval = 1;
}
this.repeatTime = interval * 5000;
// No accumulated time reset needed - using global tick count
return true;
}
@@ -219,6 +219,10 @@ public class Messenger {
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
if (habbo.hasPermission(StaffChatBuddy.PERMISSION_KEY)) {
this.friends.putIfAbsent(StaffChatBuddy.BUDDY_ID, new StaffChatBuddy(habbo.getHabboInfo().getId()));
}
}
public MessengerBuddy loadFriend(Habbo habbo, int userId) {
@@ -0,0 +1,57 @@
package com.eu.habbo.habbohotel.messenger;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.commands.CommandHandler;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboGender;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.friends.FriendChatMessageComposer;
public class StaffChatBuddy extends MessengerBuddy {
public static final int BUDDY_ID = -1;
public static final String PERMISSION_KEY = "acc_staff_chat";
public static final String DISPLAY_NAME = "Staff Chat";
public static final String DEFAULT_LOOK = "ADM";
public StaffChatBuddy(int userOne) {
super(BUDDY_ID, DISPLAY_NAME, DEFAULT_LOOK, (short) 0, userOne);
this.setOnline(true);
}
@Override
public void onMessageReceived(Habbo from, String message) {
if (from == null || message == null || message.isEmpty()) return;
// Re-check permission so a staff member who was demoted mid-session
// can no longer broadcast to the staff channel.
if (!from.hasPermission(PERMISSION_KEY)) return;
if (message.charAt(0) == ':') {
CommandHandler.handleCommand(from.getClient(), message);
return;
}
Message chatMessage = new Message(from.getHabboInfo().getId(), BUDDY_ID, message);
Emulator.getGameServer().getGameClientManager().sendBroadcastResponse(
new FriendChatMessageComposer(chatMessage, BUDDY_ID, from.getHabboInfo().getId()).compose(),
PERMISSION_KEY,
from.getClient());
}
@Override
public void serialize(ServerMessage message) {
message.appendInt(this.getId());
message.appendString(this.getUsername());
message.appendInt(this.getGender().equals(HabboGender.M) ? 0 : 1);
message.appendBoolean(true); // online
message.appendBoolean(false); // not in room
message.appendString(this.getLook());
message.appendInt(0); // category
message.appendString(""); // motto
message.appendString(""); // last seen
message.appendString(""); // realname
message.appendBoolean(true); // offline messaging supported
message.appendBoolean(false);
message.appendBoolean(false);
message.appendShort(0); // relation
}
}
@@ -651,6 +651,10 @@ public class ModToolManager {
sender.getClient().sendResponse(new ModToolIssueHandledComposer(ModToolIssueHandledComposer.ABUSIVE));
}
// Reporter (the user who opened the CFH) gets their abusive
// counter bumped the legacy stat shown in the User Info table.
bumpUserSettingCounter(issue.senderId, "cfh_abusive");
this.updateTicketToMods(issue);
this.removeTicket(issue);
@@ -737,4 +741,38 @@ public class ModToolManager {
return issues;
}
/**
* Increments a single integer counter on `users_settings` for the
* given user. Used by the moderation sanction handlers to bump the
* legacy counters that `ModToolUserInfoComposer` surfaces (cfh_warnings,
* cfh_bans, cfh_abusive, tradelock_amount) historically these were
* only ever incremented by the CFH submission path, so a user could
* accumulate any number of bans/mutes without the User Info table
* reflecting it.
*
* Restricted to a whitelisted column name to keep the dynamic SQL
* safe; the caller passes a Permission-style constant.
*/
public static void bumpUserSettingCounter(int userId, String column) {
switch (column) {
case "cfh_warnings":
case "cfh_bans":
case "cfh_abusive":
case "tradelock_amount":
break;
default:
LOGGER.warn("Refusing to bump unrecognized user_settings column: {}", column);
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE users_settings SET " + column + " = " + column + " + 1 WHERE user_id = ?")) {
statement.setInt(1, userId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception bumping {} for user {}", column, userId, e);
}
}
}
@@ -8,6 +8,7 @@ public class Permission {
public static String ACC_SEE_WHISPERS = "acc_see_whispers";
public static String ACC_SEE_TENTCHAT = "acc_see_tentchat";
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_UNKICKABLE = "acc_unkickable";
public static String ACC_GUILDGATE = "acc_guildgate";
@@ -370,8 +370,14 @@ public class PetManager {
} else {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
LOGGER.error("Missing petdata for type {}. Adding this to the database...", type);
try (PreparedStatement statement = connection.prepareStatement("INSERT INTO pet_actions (pet_type) VALUES (?)")) {
try (PreparedStatement statement = connection.prepareStatement("INSERT INTO pet_actions (pet_type, pet_name, offspring_type, happy_actions, tired_actions, random_actions, can_swim) VALUES (?, ?, ?, ?, ?, ?, ?)")) {
statement.setInt(1, type);
statement.setString(2, getFallbackPetName(type));
statement.setInt(3, getFallbackOffspringType(type));
statement.setString(4, "");
statement.setString(5, "");
statement.setString(6, "");
statement.setString(7, "0");
statement.execute();
}
@@ -411,6 +417,42 @@ public class PetManager {
return this.petData.values();
}
private static String getFallbackPetName(int type) {
switch (type) {
case 0:
return "Dog";
case 1:
return "Cat";
case 2:
return "Crocodile";
case 3:
return "Terrier";
case 4:
return "Bear";
case 5:
return "Pig";
default:
return "pet_type_" + type;
}
}
private static int getFallbackOffspringType(int type) {
switch (type) {
case 0:
return 29;
case 1:
return 28;
case 3:
return 25;
case 4:
return 24;
case 5:
return 30;
default:
return -1;
}
}
public Pet createPet(Item item, String name, String race, String color, GameClient client) {
int type = Integer.parseInt(item.getName().toLowerCase().replace("a0 pet", ""));
@@ -540,4 +582,4 @@ public class PetManager {
return false;
}
}
}
@@ -115,19 +115,21 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
}
public final Object roomUnitLock = new Object();
public final ConcurrentHashMap<RoomTile, THashSet<HabboItem>> tileCache = new ConcurrentHashMap<>();
public final List<Integer> userVotes;
private final TIntArrayList rights;
private final TIntIntHashMap mutedHabbos;
private final TIntObjectHashMap<RoomBan> bannedHabbos;
private final Set<Game> games;
private final TIntObjectMap<RoomMoodlightData> moodlightData;
public volatile double lastCycleCpuMs = 0.0;
public volatile String lastCycleThread = "N/A";
private final Object loadLock = new Object();
//Use appropriately. Could potentially cause memory leaks when used incorrectly.
public volatile boolean preventUnloading = false;
public volatile boolean preventUncaching = false;
public Set<ServerMessage> scheduledComposers = ConcurrentHashMap.newKeySet();
public Set<Runnable> scheduledTasks = ConcurrentHashMap.newKeySet();
public final java.util.concurrent.ConcurrentLinkedQueue<Runnable> scheduledTasks = new java.util.concurrent.ConcurrentLinkedQueue<>();
public String wordQuiz = "";
public int noVotes = 0;
public int yesVotes = 0;
@@ -195,6 +197,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
private int wiredInspectMask = WIRED_ACCESS_DEFAULT_INSPECT_MASK;
private int wiredModifyMask = WIRED_ACCESS_DEFAULT_MODIFY_MASK;
private boolean youtubeEnabled = false;
private boolean soundboardEnabled = false;
private String youtubeCurrentVideo = "";
private String youtubeSenderName = "";
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 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 getYoutubeSenderName() { return this.youtubeSenderName; }
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.hideWall = set.getBoolean("allow_hidewall");
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.chatWeight = set.getInt("chat_weight");
this.chatSpeed = set.getInt("chat_speed");
@@ -981,8 +987,6 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
this.scheduledTasks.clear();
this.scheduledComposers.clear();
this.tileCache.clear();
synchronized (this.mutedHabbos) {
this.mutedHabbos.clear();
}
@@ -1160,10 +1164,13 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
synchronized (this.loadLock) {
if (this.loaded) {
try {
long startTime = System.nanoTime();
this.lastCycleThread = Thread.currentThread().getName();
// Run cycle directly instead of scheduling on thread pool
// This ensures all cycle tasks in the same tick execute synchronously
// preventing wired desync issues
this.cycle();
this.lastCycleCpuMs = (System.nanoTime() - startTime) / 1000000.0;
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
@@ -1177,7 +1184,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
if (this.needsUpdate) {
try (Connection connection = Emulator.getDatabase().getDataSource()
.getConnection(); PreparedStatement statement = connection.prepareStatement(
"UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ?, youtube_enabled = ? WHERE id = ?")) {
"UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ?, youtube_enabled = ?, builders_club_trial_locked = ?, builders_club_original_state = ? WHERE id = ?")) {
statement.setString(1, this.name);
statement.setString(2, this.description);
statement.setString(3, this.password);
@@ -1228,7 +1235,9 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
statement.setString(39, this.hideWired ? "1" : "0");
statement.setString(40, this.allowUnderpass ? "1" : "0");
statement.setString(41, this.youtubeEnabled ? "1" : "0");
statement.setInt(42, this.id);
statement.setString(42, this.buildersClubTrialLocked ? "1" : "0");
statement.setString(43, (this.buildersClubOriginalState != null ? this.buildersClubOriginalState : RoomState.OPEN).name().toLowerCase());
statement.setInt(44, this.id);
statement.executeUpdate();
this.needsUpdate = false;
} catch (SQLException e) {
@@ -2318,27 +2327,37 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
sanitizedInspectMask |= sanitizedModifyMask;
synchronized (this.wiredSettingsLock) {
int previousInspectMask = this.wiredInspectMask;
int previousModifyMask = this.wiredModifyMask;
final int finalInspectMask = sanitizedInspectMask;
final int finalModifyMask = sanitizedModifyMask;
final int finalId = this.id;
final int previousInspectMask = this.wiredInspectMask;
final int previousModifyMask = this.wiredModifyMask;
this.wiredInspectMask = sanitizedInspectMask;
this.wiredModifyMask = sanitizedModifyMask;
this.wiredSettingsLoaded = true;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO room_wired_settings (room_id, inspect_mask, modify_mask) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE inspect_mask = VALUES(inspect_mask), modify_mask = VALUES(modify_mask)")) {
statement.setInt(1, this.id);
statement.setInt(2, sanitizedInspectMask);
statement.setInt(3, sanitizedModifyMask);
statement.executeUpdate();
this.pushWiredSettingsToCurrentHabbos();
return true;
} catch (SQLException e) {
this.wiredInspectMask = previousInspectMask;
this.wiredModifyMask = previousModifyMask;
LOGGER.error("Caught SQL exception while saving wired room settings", e);
return false;
}
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO room_wired_settings (room_id, inspect_mask, modify_mask) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE inspect_mask = VALUES(inspect_mask), modify_mask = VALUES(modify_mask)")) {
statement.setInt(1, finalId);
statement.setInt(2, finalInspectMask);
statement.setInt(3, finalModifyMask);
statement.executeUpdate();
} catch (SQLException e) {
synchronized (this.wiredSettingsLock) {
if (this.wiredInspectMask == finalInspectMask && this.wiredModifyMask == finalModifyMask) {
this.wiredInspectMask = previousInspectMask;
this.wiredModifyMask = previousModifyMask;
}
}
LOGGER.error("Caught SQL exception while saving wired room settings", e);
}
});
this.pushWiredSettingsToCurrentHabbos();
return true;
}
}
@@ -2876,4 +2895,20 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
public Collection<RoomUnit> getRoomUnitsAt(RoomTile tile) {
return this.unitManager.getRoomUnitsAt(tile);
}
public long getEstimatedMemoryUsage() {
long bytes = 1024 * 10; // Base footprint
if (this.itemManager != null) {
bytes += this.itemManager.itemCount() * 512L;
}
bytes += this.getUserCount() * 2048L;
if (this.layout != null) {
bytes += this.layout.getMapSize() * 128L;
}
com.eu.habbo.habbohotel.wired.tick.WiredTickService wired = com.eu.habbo.habbohotel.wired.tick.WiredTickService.getInstance();
if (wired != null) {
bytes += wired.getTickableCount(this.getId()) * 256L;
}
return bytes;
}
}
@@ -313,27 +313,6 @@ public class RoomChatManager {
}
}
String wiredSayMessage = roomChatMessage.getMessage();
// Handle commands and wired
boolean suppressSaysOutput = false;
if (chatType != RoomChatType.WHISPER) {
if (CommandHandler.handleCommand(habbo.getClient(), roomChatMessage.getUnfilteredMessage())) {
WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), wiredSayMessage);
roomChatMessage.isCommand = true;
return;
}
if (!ignoreWired) {
suppressSaysOutput = WiredManager.shouldSuppressUserSaysOutput(
habbo.getHabboInfo().getCurrentRoom(),
habbo.getRoomUnit(),
wiredSayMessage,
chatType.ordinal(),
roomChatMessage.getBubble() != null ? roomChatMessage.getBubble().getType() : -1);
}
}
// Flood protection
if (!habbo.hasPermission(Permission.ACC_CHAT_NO_FLOOD)) {
final int chatCounter = habbo.getHabboStats().chatCounter.addAndGet(1);
@@ -357,6 +336,27 @@ public class RoomChatManager {
}
}
String wiredSayMessage = roomChatMessage.getMessage();
// Handle commands and wired
boolean suppressSaysOutput = false;
if (chatType != RoomChatType.WHISPER) {
if (CommandHandler.handleCommand(habbo.getClient(), roomChatMessage.getUnfilteredMessage())) {
WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), wiredSayMessage);
roomChatMessage.isCommand = true;
return;
}
if (!ignoreWired) {
suppressSaysOutput = WiredManager.shouldSuppressUserSaysOutput(
habbo.getHabboInfo().getCurrentRoom(),
habbo.getRoomUnit(),
wiredSayMessage,
chatType.ordinal(),
roomChatMessage.getBubble() != null ? roomChatMessage.getBubble().getType() : -1);
}
}
// Build prefix messages
ServerMessage prefixMessage = null;
@@ -615,6 +615,9 @@ public class RoomChatManager {
InteractionTalkingFurniture.class);
for (HabboItem item : items) {
if (item.getExtradata().equals("1")) {
continue;
}
if (this.room.getLayout().getTile(item.getX(), item.getY())
.distance(habbo.getRoomUnit().getCurrentLocation()) <= Emulator.getConfig()
.getInt("furniture.talking.range")) {
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.core.DatabaseLoggable;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserCustomizationData;
import com.eu.habbo.messages.ISerialize;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.incoming.Incoming;
@@ -204,23 +205,14 @@ public class RoomChatMessage implements Runnable, ISerialize, DatabaseLoggable {
message.appendInt(this.getMessage().length());
// Custom prefix data
String prefixText = "";
String prefixColor = "";
String prefixIcon = "";
String prefixEffect = "";
if (this.habbo != null && this.habbo.getInventory() != null && this.habbo.getInventory().getPrefixesComponent() != null) {
com.eu.habbo.habbohotel.users.UserPrefix activePrefix = this.habbo.getInventory().getPrefixesComponent().getActivePrefix();
if (activePrefix != null) {
prefixText = activePrefix.getText();
prefixColor = activePrefix.getColor();
prefixIcon = activePrefix.getIcon();
prefixEffect = activePrefix.getEffect();
}
}
message.appendString(prefixText);
message.appendString(prefixColor);
message.appendString(prefixIcon);
message.appendString(prefixEffect);
UserCustomizationData customizationData = (this.habbo != null) ? UserCustomizationData.fromHabbo(this.habbo) : UserCustomizationData.empty();
message.appendString(customizationData.prefixText);
message.appendString(customizationData.prefixColor);
message.appendString(customizationData.prefixIcon);
message.appendString(customizationData.prefixEffect);
message.appendString(customizationData.prefixFont);
message.appendString(customizationData.nickIcon);
message.appendString(customizationData.displayOrder);
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}

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