Compare commits

..

87 Commits

Author SHA1 Message Date
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
github-actions[bot] 6d203c1267 🆙 Bump version to 4.1.6 [skip ci] 2026-04-24 14:35:01 +00:00
DuckieTM a8bcb27d27 Merge pull request #90 from duckietm/dev
🆙 CryptoV2 - please red the how_things_work on DC !!!
2026-04-24 16:33:57 +02:00
Lorenzune f51617d092 Add secure mode config toggles 2026-04-24 15:55:39 +02:00
duckietm b18d65bd79 🆙 CryptoV2 - please red the how_things_work on DC !!! 2026-04-24 15:54:37 +02:00
github-actions[bot] 13958cb11e 🆙 Bump version to 4.1.5 [skip ci] 2026-04-24 09:19:58 +00:00
DuckieTM 7414bc2589 Merge pull request #89 from duckietm/dev
Dev
2026-04-24 11:19:14 +02:00
duckietm da2307f3b5 🆙 Updated Tokens to use JWT rotational tokens 2026-04-24 11:18:46 +02:00
Lorenzune 585af846c4 Add secure assets and remember login support 2026-04-23 16:27:01 +02:00
duckietm 030b5ec174 🆕 Handshake on connect - ECDH key exchange (P-256 so it works in every browser's crypto.subtle) 2026-04-23 15:53:30 +02:00
github-actions[bot] ec54dc5c85 🆙 Bump version to 4.1.4 [skip ci] 2026-04-23 08:20:27 +00:00
DuckieTM 50acf6217e Merge pull request #88 from duckietm/dev
Dev
2026-04-23 10:19:32 +02:00
duckietm dd06f2b15c 🆙 Token login added 2026-04-23 10:19:06 +02:00
Lorenzune dde2c4143c checkpoint: secure config gdm and api baseline 2026-04-23 07:01:09 +02:00
duckietm d5497e49ad 🆙 Update API and added Copy to Template room command 2026-04-22 16:03:40 +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
github-actions[bot] 1916c6c785 🆙 Bump version to 4.1.3 [skip ci] 2026-04-22 05:38:10 +00:00
DuckieTM 4c46c0cb00 Merge pull request #87 from duckietm/dev
Dev
2026-04-22 07:37:10 +02:00
DuckieTM fdcc33212f Merge branch 'main' into dev 2026-04-22 07:37:02 +02:00
duckietm bcee750ff8 🆙 Bump to version 4.1.2 2026-04-22 07:36:19 +02:00
duckietm 872dd11bd2 🆕 API installed
Api has been enabled over the websocket address :

/api/auth/login
/api/auth/register
/api/auth/forgot-password
/api/auth/logout
/api/auth/check-email
/api/health
2026-04-22 07:35:06 +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
github-actions[bot] da0523a794 🆙 Bump version to 4.1.5 [skip ci] 2026-04-20 19:57:00 +00:00
DuckieTM 1184ccdbae Merge pull request #86 from duckietm/dev
Dev
2026-04-20 21:56:08 +02:00
duckietm 1b08e083bf 🆙 Small update 2026-04-20 15:14:21 +02:00
duckietm 7347906786 🆕 Added UI login to the Emu 2026-04-20 14:27:19 +02:00
github-actions[bot] 3f47321583 🆙 Bump version to 4.1.4 [skip ci] 2026-04-19 09:01:05 +00:00
DuckieTM f6faf36709 Merge pull request #85 from duckietm/dev
Dev
2026-04-19 11:00:09 +02:00
DuckieTM e2c823253f 🆙 update SQL 2026-04-19 09:41:21 +02:00
DuckieTM 671ab04b50 🆙 Update SQL files 2026-04-19 09:39:51 +02:00
157 changed files with 41627 additions and 32429 deletions
@@ -1 +0,0 @@
INSERT INTO emulator_settings (`key`, `value`) VALUES ('wired.tick.workers', '6');
-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;
@@ -80,7 +80,6 @@ END//
DELIMITER ;
CALL `_add_fk_if_missing`('rooms', 'fk_rooms_owner', 'owner_id', 'users', 'id', 'CASCADE');
CALL `_add_fk_if_missing`('items', 'fk_items_user', 'user_id', 'users', 'id', 'CASCADE');
CALL `_add_fk_if_missing`('catalog_items', 'fk_catitems_page', 'page_id', 'catalog_pages', 'id', 'CASCADE');
CALL `_add_fk_if_missing`('guilds', 'fk_guilds_user', 'user_id', 'users', 'id', 'CASCADE');
@@ -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,2 @@
INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
('wired.tick.workers', '6', '');
@@ -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';
@@ -0,0 +1,135 @@
ALTER TABLE emulator_settings
CHANGE COLUMN `comment` `comment` TEXT NULL DEFAULT '' ;
CREATE TABLE IF NOT EXISTS `password_resets` (
`user_id` INT NOT NULL PRIMARY KEY,
`token` VARCHAR(128) NOT NULL,
`expires_at` TIMESTAMP NOT NULL,
`created_ip` VARCHAR(64) NOT NULL DEFAULT '',
UNIQUE KEY `idx_token` (`token`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
('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')
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);
INSERT INTO emulator_settings (`key`, `value`, `comment`) VALUES
('new_user_credits', '0' , 'This is the default setting for habbo credits when creating an account for the NitroV3 Login'),
('new_user_duckets', '0' , 'This is the default setting for habbo duckets when creating an account for the NitroV3 Login'),
('new_user_diamonds', '0' , 'This is the default setting for habbo diamonds when creating an account for the NitroV3 Login')
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);
-- Grant to rank 7 only (adjust rank_7 if your rank id differs)
INSERT INTO `permission_definitions` (`permission_key`, `rank_7`, `comment`) VALUES
('cmd_setroom_template', '1', 'Use the setroom_template to copy the room into the template')
ON DUPLICATE KEY UPDATE `rank_7` = VALUES(`rank_7`);
INSERT 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.')
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);
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 DEFAULT '',
`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 `enabled_sort` (`enabled`, `sort_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- --------------------------------------------------------
-- Items belonging to a template. Clone target is `items`.
-- `template_id` replaces `room_id`; `user_id` is absent because items
-- are re-owned by the new user at clone time.
-- --------------------------------------------------------
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 `template_id` (`template_id`),
CONSTRAINT `fk_rt_items_template`
FOREIGN KEY (`template_id`) REFERENCES `room_templates` (`template_id`) ON DELETE CASCADE,
CONSTRAINT `fk_rt_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 `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 `user_id` (`user_id`),
KEY `expires_at` (`expires_at`),
CONSTRAINT `fk_remember_family_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
DROP TABLE IF EXISTS `users_remember_tokens`;
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
('login.remember.duration.days', '30'),
('login.remember.rotate.interval.minutes', '15'),
('login.remember.jwt.secret', '')
ON DUPLICATE KEY UPDATE `value` = `value`;
@@ -0,0 +1,8 @@
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', '')
ON DUPLICATE KEY UPDATE `value` = `value`;
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';
@@ -0,0 +1,36 @@
-- ============================================================================
-- 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;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+18 -2
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.1.3</version>
<version>4.2.8</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -162,5 +162,21 @@
<artifactId>joda-time</artifactId>
<version>2.13.0</version>
</dependency>
<!-- 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>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>
<!-- Jakarta Mail used by the built-in forgot-password endpoint
when smtp.* keys are configured in emulator_settings -->
<dependency>
<groupId>org.eclipse.angus</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.3</version>
</dependency>
</dependencies>
</project>
</project>
@@ -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');
@@ -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());
@@ -21,7 +21,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 +61,9 @@ public class GameEnvironment {
private SubscriptionManager subscriptionManager;
private CalendarManager calendarManager;
private RoomChatBubbleManager roomChatBubbleManager;
private GoogleTranslateManager googleTranslateManager;
private CustomBadgeManager customBadgeManager;
private InfostandBackgroundManager infostandBackgroundManager;
public void load() throws Exception {
LOGGER.info("GameEnvironment -> Loading...");
@@ -84,6 +90,9 @@ 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.roomManager.loadPublicRooms();
this.navigatorManager.loadNavigator();
@@ -121,6 +130,9 @@ public class GameEnvironment {
this.hotelViewManager.dispose();
this.subscriptionManager.dispose();
this.calendarManager.dispose();
if (this.googleTranslateManager != null) {
this.googleTranslateManager.clearCache();
}
LOGGER.info("GameEnvironment -> Disposed!");
}
@@ -219,4 +231,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() {
@@ -71,13 +71,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()) {
@@ -1058,7 +1058,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());
@@ -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,13 +184,12 @@ public class CommandHandler {
addCommand(new BlockAlertCommand());
addCommand(new BotsCommand());
addCommand(new CalendarCommand());
addCommand(new ChangeNameCommand());
addCommand(new ChatTypeCommand());
addCommand(new CommandsCommand());
addCommand(new ControlCommand());
addCommand(new CoordsCommand());
addCommand(new CreditsCommand());
addCommand(new DanceCommand());
addCommand(new DanceCommand());
addCommand(new DiagonalCommand());
addCommand(new DisconnectCommand());
addCommand(new EjectAllCommand());
@@ -230,7 +229,7 @@ public class CommandHandler {
addCommand(new MutePetsCommand());
addCommand(new PetInfoCommand());
addCommand(new PickallCommand());
addCommand(new PingCommand());
addCommand(new PingCommand());
addCommand(new PixelCommand());
addCommand(new PluginsCommand());
addCommand(new PointsCommand());
@@ -253,6 +252,7 @@ public class CommandHandler {
addCommand(new SayCommand());
addCommand(new SetMaxCommand());
addCommand(new SetPollCommand());
addCommand(new SetRoomTemplateCommand());
addCommand(new SetSpeedCommand());
addCommand(new ShoutAllCommand());
addCommand(new ShoutCommand());
@@ -0,0 +1,116 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
public class SetRoomTemplateCommand extends Command {
private static final Logger LOGGER = LoggerFactory.getLogger(SetRoomTemplateCommand.class);
public SetRoomTemplateCommand() {
super("cmd_setroom_template", Emulator.getTexts().getValue("commands.keys.cmd_setroom_template").split(";"));
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
Room room = gameClient.getHabbo().getHabboInfo().getCurrentRoom();
if (room == null) {
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.error.cmd_setroom_template.no_room"),
RoomChatMessageBubbles.ALERT);
return true;
}
String yes = Emulator.getTexts().getValue("generic.yes");
if (params.length < 2 || !params[1].equalsIgnoreCase(yes)) {
gameClient.getHabbo().alert(
Emulator.getTexts().getValue("commands.succes.cmd_setroom_template.verify")
.replace("%generic.yes%", yes)
.replace("%roomname%", room.getName()));
return true;
}
int newTemplateId = 0;
int itemsCopied = 0;
int itemsSkipped = 0;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement insTemplate = connection.prepareStatement(
"INSERT INTO room_templates (title, description, thumbnail, sort_order, enabled, " +
"name, room_description, model, password, state, users_max, category, " +
"paper_floor, paper_wall, paper_landscape, thickness_wall, thickness_floor, " +
"moodlight_data, override_model, trade_mode) " +
"(SELECT name, description, '', 0, '1', " +
"name, description, model, password, state, users_max, category, " +
"paper_floor, paper_wall, paper_landscape, thickness_wall, thickness_floor, " +
"moodlight_data, override_model, trade_mode " +
"FROM rooms WHERE id = ?)",
Statement.RETURN_GENERATED_KEYS)) {
insTemplate.setInt(1, room.getId());
insTemplate.executeUpdate();
try (ResultSet keys = insTemplate.getGeneratedKeys()) {
if (keys.next()) newTemplateId = keys.getInt(1);
}
}
if (newTemplateId <= 0) {
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.error.cmd_setroom_template"),
RoomChatMessageBubbles.ALERT);
return true;
}
if (room.hasCustomLayout()) {
try (PreparedStatement updLayout = connection.prepareStatement(
"UPDATE room_templates t " +
"JOIN room_models_custom c ON c.id = ? " +
"SET t.heightmap = c.heightmap, t.door_x = c.door_x, " +
" t.door_y = c.door_y, t.door_dir = c.door_dir " +
"WHERE t.template_id = ?")) {
updLayout.setInt(1, room.getId());
updLayout.setInt(2, newTemplateId);
updLayout.executeUpdate();
}
}
try (PreparedStatement insItems = connection.prepareStatement(
"INSERT INTO room_templates_items (template_id, item_id, wall_pos, x, y, z, rot, extra_data, wired_data) " +
"SELECT ?, i.item_id, i.wall_pos, i.x, i.y, i.z, i.rot, i.extra_data, i.wired_data " +
"FROM items i JOIN items_base ib ON ib.id = i.item_id " +
"WHERE i.room_id = ?")) {
insItems.setInt(1, newTemplateId);
insItems.setInt(2, room.getId());
itemsCopied = insItems.executeUpdate();
}
try (PreparedStatement countTotal = connection.prepareStatement(
"SELECT COUNT(*) FROM items WHERE room_id = ?")) {
countTotal.setInt(1, room.getId());
try (ResultSet rs = countTotal.executeQuery()) {
if (rs.next()) itemsSkipped = Math.max(0, rs.getInt(1) - itemsCopied);
}
}
} catch (SQLException e) {
LOGGER.error("cmd_setroom_template failed for roomId=" + room.getId(), e);
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.error.cmd_setroom_template"),
RoomChatMessageBubbles.ALERT);
return true;
}
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.succes.cmd_setroom_template")
.replace("%id%", Integer.toString(newTemplateId))
.replace("%items%", Integer.toString(itemsCopied))
.replace("%skipped%", Integer.toString(itemsSkipped)),
RoomChatMessageBubbles.ALERT);
return true;
}
}
@@ -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,7 +118,6 @@ 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());
}
@@ -148,6 +133,38 @@ public class SessionResumeManager {
}
}
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 +180,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;
}
}
}
@@ -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());
}
@@ -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 {
@@ -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;
@@ -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();
@@ -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
}
}
@@ -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;
}
}
}
@@ -1177,7 +1177,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 +1228,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) {
@@ -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);
}
@@ -15,25 +15,19 @@ import com.eu.habbo.habbohotel.items.interactions.games.tag.InteractionTagField;
import com.eu.habbo.habbohotel.items.interactions.games.tag.InteractionTagPole;
import com.eu.habbo.habbohotel.items.interactions.pets.*;
import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectSendSignal;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredBlob;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFurniVariable;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRoomVariable;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUserVariable;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableEcho;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableReference;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableTextConnector;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraContextVariable;
import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.*;
import com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerReceiveSignal;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.habbohotel.users.HabboManager;
import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.core.WiredMovementPhysics;
import com.eu.habbo.habbohotel.wired.tick.WiredTickable;
import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer;
import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer;
import com.eu.habbo.messages.outgoing.rooms.items.*;
import com.eu.habbo.plugin.Event;
import com.eu.habbo.plugin.events.furniture.*;
@@ -94,7 +88,7 @@ public class RoomItemManager {
}
try (PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM items WHERE room_id = ?")) {
"SELECT * FROM items WHERE room_id = ?")) {
statement.setInt(1, this.room.getId());
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
@@ -106,8 +100,8 @@ public class RoomItemManager {
}
if (this.itemCount() > Room.MAXIMUM_FURNI) {
LOGGER.error("Room ID: {} has exceeded the furniture limit ({} > {}).",
this.room.getId(), this.itemCount(), Room.MAXIMUM_FURNI);
LOGGER.error("Room ID: {} has exceeded the furniture limit ({} > {}).",
this.room.getId(), this.itemCount(), Room.MAXIMUM_FURNI);
}
}
@@ -116,7 +110,7 @@ public class RoomItemManager {
*/
public void loadWiredData(Connection connection) {
try (PreparedStatement statement = connection.prepareStatement(
"SELECT id, wired_data FROM items WHERE room_id = ? AND wired_data<>''")) {
"SELECT id, wired_data FROM items WHERE room_id = ? AND wired_data<>''")) {
statement.setInt(1, this.room.getId());
try (ResultSet set = statement.executeQuery()) {
@@ -274,7 +268,7 @@ public class RoomItemManager {
}
if (iterator.value().getBaseItem().getInteractionType().getType()
== InteractionPostIt.class) {
== InteractionPostIt.class) {
items.add(iterator.value());
}
}
@@ -359,7 +353,7 @@ public class RoomItemManager {
}
if (!(tile.x >= item.getX() && tile.x <= item.getX() + width - 1 && tile.y >= item.getY()
&& tile.y <= item.getY() + length - 1)) {
&& tile.y <= item.getY() + length - 1)) {
continue;
}
@@ -447,7 +441,7 @@ public class RoomItemManager {
}
if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem)
> item.getZ() + Item.getCurrentHeight(item)) {
> item.getZ() + Item.getCurrentHeight(item)) {
continue;
}
@@ -516,7 +510,7 @@ public class RoomItemManager {
}
if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem)
> item.getZ() + Item.getCurrentHeight(item)) {
> item.getZ() + Item.getCurrentHeight(item)) {
continue;
}
@@ -598,7 +592,7 @@ public class RoomItemManager {
}
if (lowestChair != null && lowestChair.getZ() + Item.getCurrentHeight(lowestChair)
> item.getZ() + Item.getCurrentHeight(item)) {
> item.getZ() + Item.getCurrentHeight(item)) {
continue;
}
@@ -647,7 +641,7 @@ public class RoomItemManager {
this.furniOwnerNames.put(item.getUserId(), habbo.getUsername());
} else {
LOGGER.error("Failed to find username for item (ID: {}, UserID: {})",
item.getId(), item.getUserId());
item.getId(), item.getUserId());
}
}
}
@@ -665,7 +659,7 @@ public class RoomItemManager {
if (specialTypes == null) {
return;
}
boolean isWiredItem = false;
synchronized (specialTypes) {
@@ -714,29 +708,29 @@ public class RoomItemManager {
} else if (item instanceof InteractionPetTree) {
specialTypes.addPetTree((InteractionPetTree) item);
} else if (item instanceof InteractionMoodLight ||
item instanceof InteractionPyramid ||
item instanceof InteractionMusicDisc ||
item instanceof InteractionBattleBanzaiSphere ||
item instanceof InteractionTalkingFurniture ||
item instanceof InteractionWater ||
item instanceof InteractionWaterItem ||
item instanceof InteractionMuteArea ||
item instanceof InteractionBuildArea ||
item instanceof InteractionTagPole ||
item instanceof InteractionTagField ||
item instanceof InteractionJukeBox ||
item instanceof InteractionPetBreedingNest ||
item instanceof InteractionBlackHole ||
item instanceof InteractionWiredHighscore ||
item instanceof InteractionStickyPole ||
item instanceof WiredBlob ||
item instanceof InteractionTent ||
item instanceof InteractionSnowboardSlope ||
item instanceof InteractionFireworks) {
item instanceof InteractionPyramid ||
item instanceof InteractionMusicDisc ||
item instanceof InteractionBattleBanzaiSphere ||
item instanceof InteractionTalkingFurniture ||
item instanceof InteractionWater ||
item instanceof InteractionWaterItem ||
item instanceof InteractionMuteArea ||
item instanceof InteractionBuildArea ||
item instanceof InteractionTagPole ||
item instanceof InteractionTagField ||
item instanceof InteractionJukeBox ||
item instanceof InteractionPetBreedingNest ||
item instanceof InteractionBlackHole ||
item instanceof InteractionWiredHighscore ||
item instanceof InteractionStickyPole ||
item instanceof WiredBlob ||
item instanceof InteractionTent ||
item instanceof InteractionSnowboardSlope ||
item instanceof InteractionFireworks) {
specialTypes.addUndefined(item);
}
}
// Invalidate wired cache when wired items are added
if (isWiredItem) {
WiredManager.invalidateRoom(this.room);
@@ -810,7 +804,7 @@ public class RoomItemManager {
}
this.room.getFurniVariableManager().removeAssignmentsForFurni(item.getId());
boolean isWiredItem = false;
// Unregister from tick service for time-based wired triggers (new 50ms tick system)
@@ -822,53 +816,53 @@ public class RoomItemManager {
specialTypes.removeCycleTask((ICycleable) item);
}
if (item instanceof InteractionBattleBanzaiTeleporter) {
specialTypes.removeBanzaiTeleporter((InteractionBattleBanzaiTeleporter) item);
} else if (item instanceof InteractionWiredTrigger) {
specialTypes.removeTrigger((InteractionWiredTrigger) item);
isWiredItem = true;
} else if (item instanceof InteractionWiredEffect) {
specialTypes.removeEffect((InteractionWiredEffect) item);
isWiredItem = true;
} else if (item instanceof InteractionWiredCondition) {
specialTypes.removeCondition((InteractionWiredCondition) item);
isWiredItem = true;
} else if (item instanceof InteractionWiredExtra) {
boolean removedContextDefinition = false;
boolean removedVariableTextConnector = false;
if (item instanceof WiredExtraUserVariable) {
this.room.getUserVariableManager().removeDefinition(item.getId());
} else if (item instanceof WiredExtraFurniVariable) {
this.room.getFurniVariableManager().removeDefinition(item.getId());
} else if (item instanceof WiredExtraRoomVariable) {
this.room.getRoomVariableManager().removeDefinition(item.getId());
} else if (item instanceof WiredExtraContextVariable) {
removedContextDefinition = true;
} else if (item instanceof WiredExtraVariableTextConnector) {
removedVariableTextConnector = true;
} else if (item instanceof WiredExtraVariableReference) {
if (((WiredExtraVariableReference) item).isRoomReference()) {
this.room.getRoomVariableManager().removeDefinition(item.getId());
} else {
this.room.getUserVariableManager().removeDefinition(item.getId());
}
} else if (item instanceof WiredExtraVariableEcho) {
WiredExtraVariableEcho echo = (WiredExtraVariableEcho) item;
if (item instanceof InteractionBattleBanzaiTeleporter) {
specialTypes.removeBanzaiTeleporter((InteractionBattleBanzaiTeleporter) item);
} else if (item instanceof InteractionWiredTrigger) {
specialTypes.removeTrigger((InteractionWiredTrigger) item);
isWiredItem = true;
} else if (item instanceof InteractionWiredEffect) {
specialTypes.removeEffect((InteractionWiredEffect) item);
isWiredItem = true;
} else if (item instanceof InteractionWiredCondition) {
specialTypes.removeCondition((InteractionWiredCondition) item);
isWiredItem = true;
} else if (item instanceof InteractionWiredExtra) {
boolean removedContextDefinition = false;
boolean removedVariableTextConnector = false;
if (item instanceof WiredExtraUserVariable) {
this.room.getUserVariableManager().removeDefinition(item.getId());
} else if (item instanceof WiredExtraFurniVariable) {
this.room.getFurniVariableManager().removeDefinition(item.getId());
} else if (item instanceof WiredExtraRoomVariable) {
this.room.getRoomVariableManager().removeDefinition(item.getId());
} else if (item instanceof WiredExtraContextVariable) {
removedContextDefinition = true;
} else if (item instanceof WiredExtraVariableTextConnector) {
removedVariableTextConnector = true;
} else if (item instanceof WiredExtraVariableReference) {
if (((WiredExtraVariableReference) item).isRoomReference()) {
this.room.getRoomVariableManager().removeDefinition(item.getId());
} else {
this.room.getUserVariableManager().removeDefinition(item.getId());
}
} else if (item instanceof WiredExtraVariableEcho) {
WiredExtraVariableEcho echo = (WiredExtraVariableEcho) item;
if (echo.isRoomEcho()) {
this.room.getRoomVariableManager().removeDefinition(item.getId());
} else if (echo.isFurniEcho()) {
this.room.getFurniVariableManager().removeDefinition(item.getId());
} else {
this.room.getUserVariableManager().removeDefinition(item.getId());
}
}
specialTypes.removeExtra((InteractionWiredExtra) item);
if (removedContextDefinition || removedVariableTextConnector) {
WiredContextVariableSupport.broadcastDefinitions(this.room);
}
isWiredItem = true;
} else if (item instanceof InteractionRoller) {
if (echo.isRoomEcho()) {
this.room.getRoomVariableManager().removeDefinition(item.getId());
} else if (echo.isFurniEcho()) {
this.room.getFurniVariableManager().removeDefinition(item.getId());
} else {
this.room.getUserVariableManager().removeDefinition(item.getId());
}
}
specialTypes.removeExtra((InteractionWiredExtra) item);
if (removedContextDefinition || removedVariableTextConnector) {
WiredContextVariableSupport.broadcastDefinitions(this.room);
}
isWiredItem = true;
} else if (item instanceof InteractionRoller) {
specialTypes.removeRoller((InteractionRoller) item);
} else if (item instanceof InteractionGameScoreboard) {
specialTypes.removeScoreboard((InteractionGameScoreboard) item);
@@ -889,26 +883,26 @@ public class RoomItemManager {
} else if (item instanceof InteractionPetTree) {
specialTypes.removePetTree((InteractionPetTree) item);
} else if (item instanceof InteractionMoodLight ||
item instanceof InteractionPyramid ||
item instanceof InteractionMusicDisc ||
item instanceof InteractionBattleBanzaiSphere ||
item instanceof InteractionTalkingFurniture ||
item instanceof InteractionWaterItem ||
item instanceof InteractionWater ||
item instanceof InteractionMuteArea ||
item instanceof InteractionTagPole ||
item instanceof InteractionTagField ||
item instanceof InteractionJukeBox ||
item instanceof InteractionPetBreedingNest ||
item instanceof InteractionBlackHole ||
item instanceof InteractionWiredHighscore ||
item instanceof InteractionStickyPole ||
item instanceof WiredBlob ||
item instanceof InteractionTent ||
item instanceof InteractionSnowboardSlope) {
item instanceof InteractionPyramid ||
item instanceof InteractionMusicDisc ||
item instanceof InteractionBattleBanzaiSphere ||
item instanceof InteractionTalkingFurniture ||
item instanceof InteractionWaterItem ||
item instanceof InteractionWater ||
item instanceof InteractionMuteArea ||
item instanceof InteractionTagPole ||
item instanceof InteractionTagField ||
item instanceof InteractionJukeBox ||
item instanceof InteractionPetBreedingNest ||
item instanceof InteractionBlackHole ||
item instanceof InteractionWiredHighscore ||
item instanceof InteractionStickyPole ||
item instanceof WiredBlob ||
item instanceof InteractionTent ||
item instanceof InteractionSnowboardSlope) {
specialTypes.removeUndefined(item);
}
// Invalidate wired cache when wired items are removed
if (isWiredItem || cleanedSignalAntennaReferences) {
WiredManager.invalidateRoom(this.room);
@@ -936,9 +930,9 @@ public class RoomItemManager {
if (item.getBaseItem().getType() == FurnitureType.FLOOR) {
this.room.sendComposer(new FloorItemUpdateComposer(item).compose());
this.room.updateTiles(this.room.getLayout()
.getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()),
item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
item.getRotation()));
.getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()),
item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
item.getRotation()));
} else if (item.getBaseItem().getType() == FurnitureType.WALL) {
this.room.sendComposer(new WallItemUpdateComposer(item).compose());
}
@@ -963,9 +957,9 @@ public class RoomItemManager {
}
this.room.updateTiles(this.room.getLayout()
.getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()),
item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
item.getRotation()));
.getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()),
item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
item.getRotation()));
if (item instanceof InteractionMultiHeight) {
((InteractionMultiHeight) item).updateUnitsOnItem(this.room);
@@ -1032,7 +1026,7 @@ public class RoomItemManager {
if (Emulator.getPluginManager().isRegistered(FurniturePickedUpEvent.class, true)) {
FurniturePickedUpEvent event = Emulator.getPluginManager()
.fireEvent(new FurniturePickedUpEvent(item, picker));
.fireEvent(new FurniturePickedUpEvent(item, picker));
if (event.isCancelled()) {
return;
@@ -1060,10 +1054,10 @@ public class RoomItemManager {
}
THashSet<RoomTile> updatedTiles = this.room.getLayout().getTilesAt(
this.room.getLayout().getTile(item.getX(), item.getY()),
item.getBaseItem().getWidth(),
item.getBaseItem().getLength(),
item.getRotation());
this.room.getLayout().getTile(item.getX(), item.getY()),
item.getBaseItem().getWidth(),
item.getBaseItem().getLength(),
item.getRotation());
this.room.updateTiles(updatedTiles);
for (RoomTile tile : updatedTiles) {
@@ -1114,6 +1108,7 @@ public class RoomItemManager {
if (habbo != null && !inventoryItems.isEmpty()) {
habbo.getInventory().getItemsComponent().addItems(inventoryItems);
habbo.getClient().sendResponse(new AddHabboItemComposer(inventoryItems));
habbo.getClient().sendResponse(new InventoryRefreshComposer());
}
for (HabboItem i : items) {
@@ -1160,7 +1155,7 @@ public class RoomItemManager {
}
userItemsMap.computeIfAbsent(iterator.value().getUserId(), k -> new THashSet<>())
.add(iterator.value());
.add(iterator.value());
}
}
@@ -1182,6 +1177,7 @@ public class RoomItemManager {
if (user != null && !inventoryItems.isEmpty()) {
user.getInventory().getItemsComponent().addItems(inventoryItems);
user.getClient().sendResponse(new AddHabboItemComposer(inventoryItems));
user.getClient().sendResponse(new InventoryRefreshComposer());
}
}
}
@@ -1222,7 +1218,7 @@ public class RoomItemManager {
for (short y = 0; y < item.getBaseItem().getLength(); y++) {
for (short x = 0; x < item.getBaseItem().getWidth(); x++) {
RoomTile tile = this.room.getLayout().getTile(
(short) (item.getX() + x), (short) (item.getY() + y));
(short) (item.getX() + x), (short) (item.getY() + y));
if (tile != null) {
lockedTiles.add(tile);
@@ -1233,7 +1229,7 @@ public class RoomItemManager {
for (short y = 0; y < item.getBaseItem().getWidth(); y++) {
for (short x = 0; x < item.getBaseItem().getLength(); x++) {
RoomTile tile = this.room.getLayout().getTile(
(short) (item.getX() + x), (short) (item.getY() + y));
(short) (item.getX() + x), (short) (item.getY() + y));
if (tile != null) {
lockedTiles.add(tile);
@@ -1324,8 +1320,8 @@ public class RoomItemManager {
rotation %= 8;
if (this.room.hasRights(habbo) || this.room.getGuildRightLevel(habbo)
.isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || habbo.hasPermission(
Permission.ACC_MOVEROTATE) || BuildersClubRoomSupport.canPlaceInRoom(habbo, this.room)) {
.isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || habbo.hasPermission(
Permission.ACC_MOVEROTATE) || BuildersClubRoomSupport.canPlaceInRoom(habbo, this.room)) {
return FurnitureMovementError.NONE;
}
@@ -1334,10 +1330,10 @@ public class RoomItemManager {
if (rentSpace != null) {
if (!RoomLayout.squareInSquare(RoomLayout.getRectangle(rentSpace.getX(), rentSpace.getY(),
rentSpace.getBaseItem().getWidth(), rentSpace.getBaseItem().getLength(),
rentSpace.getRotation()),
RoomLayout.getRectangle(tile.x, tile.y, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation))) {
rentSpace.getBaseItem().getWidth(), rentSpace.getBaseItem().getLength(),
rentSpace.getRotation()),
RoomLayout.getRectangle(tile.x, tile.y, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation))) {
return FurnitureMovementError.NO_RIGHTS;
} else {
return FurnitureMovementError.NONE;
@@ -1347,7 +1343,7 @@ public class RoomItemManager {
for (HabboItem area : this.room.getRoomSpecialTypes().getItemsOfType(InteractionBuildArea.class)) {
if (((InteractionBuildArea) area).inSquare(tile) && ((InteractionBuildArea) area).isBuilder(
habbo.getHabboInfo().getUsername())) {
habbo.getHabboInfo().getUsername())) {
return FurnitureMovementError.NONE;
}
}
@@ -1438,14 +1434,14 @@ public class RoomItemManager {
}
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation);
item.getBaseItem().getLength(), rotation);
for (RoomTile t : occupiedTiles) {
if (t.state == RoomTileState.INVALID) {
return FurnitureMovementError.INVALID_MOVE;
}
if (!Emulator.getConfig().getBoolean("wired.place.under", false) || (
Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable()
&& !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) {
Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable()
&& !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) {
if (checkForUnits && this.room.hasHabbosAt(t.x, t.y)) {
return FurnitureMovementError.TILE_HAS_HABBOS;
}
@@ -1490,7 +1486,7 @@ public class RoomItemManager {
}
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation);
item.getBaseItem().getLength(), rotation);
for (RoomTile t : occupiedTiles) {
if (t.state == RoomTileState.INVALID) {
return FurnitureMovementError.INVALID_MOVE;
@@ -1542,7 +1538,7 @@ public class RoomItemManager {
boolean pluginHelper = false;
if (Emulator.getPluginManager().isRegistered(FurniturePlacedEvent.class, true)) {
FurniturePlacedEvent event = Emulator.getPluginManager()
.fireEvent(new FurniturePlacedEvent(item, owner, tile));
.fireEvent(new FurniturePlacedEvent(item, owner, tile));
if (event.isCancelled()) {
return FurnitureMovementError.CANCEL_PLUGIN_PLACE;
@@ -1553,7 +1549,7 @@ public class RoomItemManager {
RoomLayout layout = this.room.getLayout();
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation);
item.getBaseItem().getLength(), rotation);
FurnitureMovementError fits = furnitureFitsAt(tile, item, rotation);
@@ -1572,7 +1568,7 @@ public class RoomItemManager {
if (Emulator.getPluginManager().isRegistered(FurnitureBuildheightEvent.class, true)) {
FurnitureBuildheightEvent event = Emulator.getPluginManager()
.fireEvent(new FurnitureBuildheightEvent(item, owner, 0.00, height));
.fireEvent(new FurnitureBuildheightEvent(item, owner, 0.00, height));
if (event.hasChangedHeight()) {
height = layout.getHeightAtSquare(tile.x, tile.y) + event.getUpdatedHeight();
}
@@ -1592,7 +1588,7 @@ public class RoomItemManager {
item.onPlace(this.room);
this.room.updateTiles(occupiedTiles);
this.room.sendComposer(
new AddFloorItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose());
new AddFloorItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose());
if (RoomConfInvisSupport.isControllerItem(item) || RoomConfInvisSupport.isTarget(item)) {
RoomConfInvisSupport.sendState(this.room);
@@ -1620,7 +1616,7 @@ public class RoomItemManager {
*/
public FurnitureMovementError placeWallFurniAt(HabboItem item, String wallPosition, Habbo owner) {
if (!(this.room.hasRights(owner) || this.room.getGuildRightLevel(owner)
.isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || BuildersClubRoomSupport.canPlaceInRoom(owner, this.room))) {
.isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || BuildersClubRoomSupport.canPlaceInRoom(owner, this.room))) {
return FurnitureMovementError.NO_RIGHTS;
}
@@ -1638,7 +1634,7 @@ public class RoomItemManager {
this.furniOwnerNames.put(item.getUserId(), this.resolveOwnerName(item, owner));
}
this.room.sendComposer(
new AddWallItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose());
new AddWallItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose());
item.needsUpdate(true);
this.addHabboItem(item);
item.setRoomId(this.room.getId());
@@ -1989,7 +1985,7 @@ public class RoomItemManager {
boolean pluginHelper = false;
if (Emulator.getPluginManager().isRegistered(FurnitureMovedEvent.class, true)) {
FurnitureMovedEvent event = Emulator.getPluginManager()
.fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile));
.fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile));
if (event.isCancelled()) {
return FurnitureMovementError.CANCEL_PLUGIN_MOVE;
}
@@ -2002,9 +1998,9 @@ public class RoomItemManager {
// Check if can be placed at new position
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation);
item.getBaseItem().getLength(), rotation);
THashSet<RoomTile> newOccupiedTiles = layout.getTilesAt(tile,
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation);
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation);
HabboItem topItem = this.getTopItemAt(occupiedTiles, null);
@@ -2013,15 +2009,15 @@ public class RoomItemManager {
for (RoomTile t : occupiedTiles) {
HabboItem tileTopItem = this.getTopItemAt(t.x, t.y);
if (!magicTile && ((tileTopItem != null && tileTopItem != item ? (
t.state.equals(RoomTileState.INVALID) || !t.getAllowStack()
t.state.equals(RoomTileState.INVALID) || !t.getAllowStack()
|| !tileTopItem.getBaseItem().allowStack())
: this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) {
: this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) {
return FurnitureMovementError.CANT_STACK;
}
if (!Emulator.getConfig().getBoolean("wired.place.under", false) || (
Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable()
&& !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) {
Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable()
&& !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) {
if (checkForUnits) {
if (!magicTile && this.room.hasHabbosAt(t.x, t.y)) {
return FurnitureMovementError.TILE_HAS_HABBOS;
@@ -2048,8 +2044,8 @@ public class RoomItemManager {
}
THashSet<RoomTile> oldOccupiedTiles = layout.getTilesAt(
layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), item.getRotation());
layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), item.getRotation());
int oldRotation = item.getRotation();
@@ -2066,9 +2062,9 @@ public class RoomItemManager {
}
if ((stackHelper == null && topItem != null && topItem != item && !topItem.getBaseItem()
.allowStack()) || (topItem != null && topItem != item
&& topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item)
> Room.MAXIMUM_FURNI_HEIGHT)) {
.allowStack()) || (topItem != null && topItem != item
&& topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item)
> Room.MAXIMUM_FURNI_HEIGHT)) {
item.setRotation(oldRotation);
return FurnitureMovementError.CANT_STACK;
}
@@ -2117,7 +2113,7 @@ public class RoomItemManager {
if (Emulator.getPluginManager().isRegistered(FurnitureBuildheightEvent.class, true)) {
FurnitureBuildheightEvent event = Emulator.getPluginManager()
.fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height));
.fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height));
if (event.hasChangedHeight()) {
height = layout.getHeightAtSquare(tile.x, tile.y) + event.getUpdatedHeight();
pluginHeight = true;
@@ -2138,7 +2134,7 @@ public class RoomItemManager {
if (item.getZ() > Room.MAXIMUM_FURNI_HEIGHT) {
item.setZ(Room.MAXIMUM_FURNI_HEIGHT);
}
// Update wired spatial index and invalidate cache when wired items are moved
if (item instanceof InteractionWiredTrigger) {
this.room.getRoomSpecialTypes().updateTriggerLocation((InteractionWiredTrigger) item, oldLocation.x, oldLocation.y);
@@ -2198,7 +2194,7 @@ public class RoomItemManager {
boolean pluginHelper = false;
if (Emulator.getPluginManager().isRegistered(FurnitureMovedEvent.class, true)) {
FurnitureMovedEvent event = Emulator.getPluginManager()
.fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile));
.fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile));
if (event.isCancelled()) {
return FurnitureMovementError.CANCEL_PLUGIN_MOVE;
}
@@ -2210,9 +2206,9 @@ public class RoomItemManager {
HabboItem stackHelper = this.findStackHeightHelperAt(tile, item);
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation);
item.getBaseItem().getLength(), rotation);
THashSet<RoomTile> newOccupiedTiles = layout.getTilesAt(tile,
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation);
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation);
HabboItem topItem = this.getTopPhysicsItemAt(occupiedTiles, null, physics);
@@ -2221,9 +2217,9 @@ public class RoomItemManager {
for (RoomTile t : occupiedTiles) {
HabboItem tileTopItem = this.getTopPhysicsItemAt(t.x, t.y, item, physics);
if (!magicTile && ((tileTopItem != null && tileTopItem != item ? (
t.state.equals(RoomTileState.INVALID) || !t.getAllowStack()
t.state.equals(RoomTileState.INVALID) || !t.getAllowStack()
|| !tileTopItem.getBaseItem().allowStack())
: this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) {
: this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) {
return FurnitureMovementError.CANT_STACK;
}
@@ -2251,8 +2247,8 @@ public class RoomItemManager {
}
THashSet<RoomTile> oldOccupiedTiles = layout.getTilesAt(
layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), item.getRotation());
layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), item.getRotation());
int oldRotation = item.getRotation();
@@ -2269,9 +2265,9 @@ public class RoomItemManager {
}
if ((stackHelper == null && topItem != null && topItem != item && !topItem.getBaseItem()
.allowStack()) || (topItem != null && topItem != item
&& topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item)
> Room.MAXIMUM_FURNI_HEIGHT)) {
.allowStack()) || (topItem != null && topItem != item
&& topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item)
> Room.MAXIMUM_FURNI_HEIGHT)) {
item.setRotation(oldRotation);
return FurnitureMovementError.CANT_STACK;
}
@@ -2319,7 +2315,7 @@ public class RoomItemManager {
if (Emulator.getPluginManager().isRegistered(FurnitureBuildheightEvent.class, true)) {
FurnitureBuildheightEvent event = Emulator.getPluginManager()
.fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height));
.fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height));
if (event.hasChangedHeight()) {
height = layout.getHeightAtSquare(tile.x, tile.y) + event.getUpdatedHeight();
pluginHeight = true;
@@ -2391,10 +2387,10 @@ public class RoomItemManager {
boolean magicTile = this.isStackPlacementBypassItem(item);
RoomLayout layout = this.room.getLayout();
// Check if can be placed at new position
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation);
item.getBaseItem().getLength(), rotation);
java.util.List<Pair<RoomTile, THashSet<HabboItem>>> tileFurniList = new java.util.ArrayList<>();
for (RoomTile t : occupiedTiles) {
@@ -2438,8 +2434,8 @@ public class RoomItemManager {
}
return !item.isWalkable()
&& !item.getBaseItem().allowSit()
&& !item.getBaseItem().allowLay();
&& !item.getBaseItem().allowSit()
&& !item.getBaseItem().allowLay();
}
private FurnitureMovementError getPhysicsUnitCollision(RoomTile tile, WiredMovementPhysics physics) {
@@ -2515,7 +2511,7 @@ public class RoomItemManager {
for (HabboItem item : this.getPhysicsItemsAt(tile, exclude, physics)) {
if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem)
> item.getZ() + Item.getCurrentHeight(item)) {
> item.getZ() + Item.getCurrentHeight(item)) {
continue;
}
@@ -2539,7 +2535,7 @@ public class RoomItemManager {
}
if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem)
> topItem.getZ() + Item.getCurrentHeight(topItem)) {
> topItem.getZ() + Item.getCurrentHeight(topItem)) {
continue;
}
@@ -343,18 +343,16 @@ public class RoomSpecialTypes {
* Adds a wired trigger to the room.
* @param trigger The trigger to add
*/
public static final int MAX_SIGNAL_SENDERS_PER_ROOM = 25;
public static final int MAX_SIGNAL_RECEIVERS_PER_ROOM = 5;
public static final int MAX_SENDERS_PER_RECEIVER = 5;
public static final int MAX_SIGNAL_SENDERS_PER_ROOM = 0;
public static final int MAX_SIGNAL_RECEIVERS_PER_ROOM = 0;
public static final int MAX_SENDERS_PER_RECEIVER = 0;
public boolean isSignalSenderLimitReached() {
Set<InteractionWiredEffect> existing = this.getSignalSenders();
return existing != null && existing.size() >= MAX_SIGNAL_SENDERS_PER_ROOM;
return false;
}
public boolean isSignalReceiverLimitReached() {
Set<InteractionWiredTrigger> existing = this.wiredTriggers.get(WiredTriggerType.RECEIVE_SIGNAL);
return existing != null && existing.size() >= MAX_SIGNAL_RECEIVERS_PER_ROOM;
return false;
}
public int countSendersTargetingReceiver(int receiverItemId, InteractionWiredEffect excludeSender) {
@@ -0,0 +1,469 @@
package com.eu.habbo.habbohotel.translations;
import com.eu.habbo.Emulator;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class GoogleTranslateManager {
private static final Logger LOGGER = LoggerFactory.getLogger(GoogleTranslateManager.class);
private static final int DEFAULT_TIMEOUT_MS = 5000;
private static final long CACHE_TTL_MS = 1000L * 60L * 60L * 6L;
private static final int MAX_TRANSLATION_CACHE_SIZE = 2048;
private static final int MAX_LANGUAGE_CACHE_SIZE = 32;
private static final String FREE_TRANSLATE_ENDPOINT = "https://translate.googleapis.com/translate_a/single";
private static final List<SupportedLanguage> FREE_SUPPORTED_LANGUAGES = buildFreeSupportedLanguages();
private final Map<String, CachedTranslation> translationCache = Collections.synchronizedMap(
new LinkedHashMap<String, CachedTranslation>(128, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, CachedTranslation> eldest) {
return this.size() > MAX_TRANSLATION_CACHE_SIZE;
}
});
private final Map<String, CachedLanguages> languagesCache = Collections.synchronizedMap(
new LinkedHashMap<String, CachedLanguages>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, CachedLanguages> eldest) {
return this.size() > MAX_LANGUAGE_CACHE_SIZE;
}
});
public SupportedLanguagesResponse getSupportedLanguages(String displayLanguage) {
String normalizedDisplayLanguage = normalizeLanguageCode(displayLanguage, "en");
CachedLanguages cachedLanguages = this.languagesCache.get(normalizedDisplayLanguage);
if ((cachedLanguages != null) && !cachedLanguages.isExpired()) {
return SupportedLanguagesResponse.success(new ArrayList<>(cachedLanguages.languages));
}
ArrayList<SupportedLanguage> supportedLanguages = new ArrayList<>(FREE_SUPPORTED_LANGUAGES);
this.languagesCache.put(normalizedDisplayLanguage, new CachedLanguages(supportedLanguages));
return SupportedLanguagesResponse.success(supportedLanguages);
}
public TranslationResponse translate(String text, String targetLanguage) {
String safeText = text == null ? "" : text;
String normalizedTargetLanguage = normalizeLanguageCode(targetLanguage, "en");
if (safeText.trim().isEmpty()) {
return TranslationResponse.success(safeText, safeText, "", normalizedTargetLanguage);
}
String cacheKey = normalizedTargetLanguage + '\u0000' + safeText;
CachedTranslation cachedTranslation = this.translationCache.get(cacheKey);
if ((cachedTranslation != null) && !cachedTranslation.isExpired()) {
return cachedTranslation.response;
}
try {
String requestUrl = FREE_TRANSLATE_ENDPOINT
+ "?client=gtx"
+ "&sl=auto"
+ "&tl=" + encode(normalizedTargetLanguage)
+ "&dt=t"
+ "&q=" + encode(safeText);
HttpsURLConnection connection = this.openGet(requestUrl);
int statusCode = connection.getResponseCode();
if (statusCode != 200) {
return TranslationResponse.failure(safeText, normalizedTargetLanguage, this.readErrorMessage(connection));
}
JsonArray response = this.readJsonArray(connection.getInputStream());
JsonArray translatedParts = response.size() > 0 && response.get(0).isJsonArray()
? response.get(0).getAsJsonArray()
: new JsonArray();
StringBuilder translatedText = new StringBuilder();
for (int index = 0; index < translatedParts.size(); index++) {
if (!translatedParts.get(index).isJsonArray()) {
continue;
}
JsonArray translatedPart = translatedParts.get(index).getAsJsonArray();
if (translatedPart.size() > 0 && !translatedPart.get(0).isJsonNull()) {
translatedText.append(translatedPart.get(0).getAsString());
}
}
String detectedLanguage = "";
if (response.size() > 2 && !response.get(2).isJsonNull()) {
detectedLanguage = response.get(2).getAsString();
}
String resolvedTranslation = translatedText.length() > 0 ? translatedText.toString() : safeText;
TranslationResponse translationResponse = TranslationResponse.success(safeText, resolvedTranslation, detectedLanguage, normalizedTargetLanguage);
this.translationCache.put(cacheKey, new CachedTranslation(translationResponse));
return translationResponse;
} catch (Exception e) {
LOGGER.error("Failed to translate text with Google Translate", e);
return TranslationResponse.failure(safeText, normalizedTargetLanguage, "Failed to translate text with Google Translate.");
}
}
public void clearCache() {
this.translationCache.clear();
this.languagesCache.clear();
}
private int getTimeoutMs() {
return Math.max(1000, Emulator.getConfig().getInt("translate.google.timeout.ms", DEFAULT_TIMEOUT_MS));
}
private HttpsURLConnection openGet(String requestUrl) throws IOException {
HttpsURLConnection connection = (HttpsURLConnection) URI.create(requestUrl).toURL().openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(this.getTimeoutMs());
connection.setReadTimeout(this.getTimeoutMs());
connection.setRequestProperty("Accept", "application/json");
return connection;
}
private JsonObject readJson(InputStream inputStream) throws IOException {
try (InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
return JsonParser.parseReader(bufferedReader).getAsJsonObject();
}
}
private JsonArray readJsonArray(InputStream inputStream) throws IOException {
try (InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
return JsonParser.parseReader(bufferedReader).getAsJsonArray();
}
}
private String readErrorMessage(HttpsURLConnection connection) {
try {
InputStream errorStream = connection.getErrorStream();
if (errorStream == null) {
return "Google Translate request failed with HTTP " + connection.getResponseCode() + '.';
}
try {
JsonObject errorResponse = this.readJson(errorStream);
if (errorResponse.has("error") && errorResponse.get("error").isJsonObject()) {
JsonObject errorObject = errorResponse.getAsJsonObject("error");
if (errorObject.has("message")) {
return errorObject.get("message").getAsString();
}
}
} catch (Exception ignored) {
try (InputStreamReader inputStreamReader = new InputStreamReader(errorStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
StringBuilder responseText = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
responseText.append(line);
}
if (responseText.length() > 0) {
return responseText.toString();
}
}
}
} catch (Exception e) {
LOGGER.warn("Failed to parse Google Translate error response", e);
}
try {
return "Google Translate request failed with HTTP " + connection.getResponseCode() + '.';
} catch (IOException e) {
return "Google Translate request failed.";
}
}
private static String normalizeLanguageCode(String languageCode, String fallback) {
if (languageCode == null || languageCode.trim().isEmpty()) {
return fallback;
}
String normalized = languageCode.trim().replace('_', '-');
String[] split = normalized.split("-");
if (split.length <= 1) {
return normalized;
}
return split[0] + '-' + split[1].toUpperCase();
}
private static String encode(String value) {
return URLEncoder.encode(value == null ? "" : value, StandardCharsets.UTF_8);
}
private static List<SupportedLanguage> buildFreeSupportedLanguages() {
ArrayList<SupportedLanguage> languages = new ArrayList<>();
addLanguage(languages, "af", "Afrikaans");
addLanguage(languages, "sq", "Albanian");
addLanguage(languages, "am", "Amharic");
addLanguage(languages, "ar", "Arabic");
addLanguage(languages, "hy", "Armenian");
addLanguage(languages, "az", "Azerbaijani");
addLanguage(languages, "eu", "Basque");
addLanguage(languages, "be", "Belarusian");
addLanguage(languages, "bn", "Bengali");
addLanguage(languages, "bs", "Bosnian");
addLanguage(languages, "bg", "Bulgarian");
addLanguage(languages, "ca", "Catalan");
addLanguage(languages, "ceb", "Cebuano");
addLanguage(languages, "ny", "Chichewa");
addLanguage(languages, "zh-CN", "Chinese (Simplified)");
addLanguage(languages, "zh-TW", "Chinese (Traditional)");
addLanguage(languages, "co", "Corsican");
addLanguage(languages, "hr", "Croatian");
addLanguage(languages, "cs", "Czech");
addLanguage(languages, "da", "Danish");
addLanguage(languages, "nl", "Dutch");
addLanguage(languages, "en", "English");
addLanguage(languages, "eo", "Esperanto");
addLanguage(languages, "et", "Estonian");
addLanguage(languages, "tl", "Filipino");
addLanguage(languages, "fi", "Finnish");
addLanguage(languages, "fr", "French");
addLanguage(languages, "fy", "Frisian");
addLanguage(languages, "gl", "Galician");
addLanguage(languages, "ka", "Georgian");
addLanguage(languages, "de", "German");
addLanguage(languages, "el", "Greek");
addLanguage(languages, "gu", "Gujarati");
addLanguage(languages, "ht", "Haitian Creole");
addLanguage(languages, "ha", "Hausa");
addLanguage(languages, "haw", "Hawaiian");
addLanguage(languages, "iw", "Hebrew");
addLanguage(languages, "hi", "Hindi");
addLanguage(languages, "hmn", "Hmong");
addLanguage(languages, "hu", "Hungarian");
addLanguage(languages, "is", "Icelandic");
addLanguage(languages, "ig", "Igbo");
addLanguage(languages, "id", "Indonesian");
addLanguage(languages, "ga", "Irish");
addLanguage(languages, "it", "Italian");
addLanguage(languages, "ja", "Japanese");
addLanguage(languages, "jw", "Javanese");
addLanguage(languages, "kn", "Kannada");
addLanguage(languages, "kk", "Kazakh");
addLanguage(languages, "km", "Khmer");
addLanguage(languages, "rw", "Kinyarwanda");
addLanguage(languages, "ko", "Korean");
addLanguage(languages, "ku", "Kurdish");
addLanguage(languages, "ky", "Kyrgyz");
addLanguage(languages, "lo", "Lao");
addLanguage(languages, "la", "Latin");
addLanguage(languages, "lv", "Latvian");
addLanguage(languages, "lt", "Lithuanian");
addLanguage(languages, "lb", "Luxembourgish");
addLanguage(languages, "mk", "Macedonian");
addLanguage(languages, "mg", "Malagasy");
addLanguage(languages, "ms", "Malay");
addLanguage(languages, "ml", "Malayalam");
addLanguage(languages, "mt", "Maltese");
addLanguage(languages, "mi", "Maori");
addLanguage(languages, "mr", "Marathi");
addLanguage(languages, "mn", "Mongolian");
addLanguage(languages, "my", "Myanmar");
addLanguage(languages, "ne", "Nepali");
addLanguage(languages, "no", "Norwegian");
addLanguage(languages, "or", "Odia");
addLanguage(languages, "ps", "Pashto");
addLanguage(languages, "fa", "Persian");
addLanguage(languages, "pl", "Polish");
addLanguage(languages, "pt", "Portuguese");
addLanguage(languages, "pa", "Punjabi");
addLanguage(languages, "ro", "Romanian");
addLanguage(languages, "ru", "Russian");
addLanguage(languages, "sm", "Samoan");
addLanguage(languages, "gd", "Scots");
addLanguage(languages, "sr", "Serbian");
addLanguage(languages, "st", "Sesotho");
addLanguage(languages, "sn", "Shona");
addLanguage(languages, "sd", "Sindhi");
addLanguage(languages, "si", "Sinhala");
addLanguage(languages, "sk", "Slovak");
addLanguage(languages, "sl", "Slovenian");
addLanguage(languages, "so", "Somali");
addLanguage(languages, "es", "Spanish");
addLanguage(languages, "su", "Sundanese");
addLanguage(languages, "sw", "Swahili");
addLanguage(languages, "sv", "Swedish");
addLanguage(languages, "tg", "Tajik");
addLanguage(languages, "ta", "Tamil");
addLanguage(languages, "tt", "Tatar");
addLanguage(languages, "te", "Telugu");
addLanguage(languages, "th", "Thai");
addLanguage(languages, "tr", "Turkish");
addLanguage(languages, "tk", "Turkmen");
addLanguage(languages, "uk", "Ukrainian");
addLanguage(languages, "ur", "Urdu");
addLanguage(languages, "ug", "Uyghur");
addLanguage(languages, "uz", "Uzbek");
addLanguage(languages, "vi", "Vietnamese");
addLanguage(languages, "cy", "Welsh");
addLanguage(languages, "xh", "Xhosa");
addLanguage(languages, "yi", "Yiddish");
addLanguage(languages, "yo", "Yoruba");
addLanguage(languages, "zu", "Zulu");
languages.sort(Comparator.comparing(SupportedLanguage::getName, String.CASE_INSENSITIVE_ORDER));
return Collections.unmodifiableList(languages);
}
private static void addLanguage(List<SupportedLanguage> languages, String code, String name) {
languages.add(new SupportedLanguage(code, name));
}
public static class SupportedLanguage {
private final String code;
private final String name;
public SupportedLanguage(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return this.code;
}
public String getName() {
return this.name;
}
}
public static class SupportedLanguagesResponse {
private final boolean success;
private final String errorMessage;
private final List<SupportedLanguage> languages;
private SupportedLanguagesResponse(boolean success, String errorMessage, List<SupportedLanguage> languages) {
this.success = success;
this.errorMessage = errorMessage == null ? "" : errorMessage;
this.languages = languages == null ? Collections.emptyList() : languages;
}
public static SupportedLanguagesResponse success(List<SupportedLanguage> languages) {
return new SupportedLanguagesResponse(true, "", languages);
}
public static SupportedLanguagesResponse failure(String errorMessage) {
return new SupportedLanguagesResponse(false, errorMessage, Collections.emptyList());
}
public boolean isSuccess() {
return this.success;
}
public String getErrorMessage() {
return this.errorMessage;
}
public List<SupportedLanguage> getLanguages() {
return this.languages;
}
}
public static class TranslationResponse {
private final boolean success;
private final String errorMessage;
private final String originalText;
private final String translatedText;
private final String detectedLanguage;
private final String targetLanguage;
private TranslationResponse(boolean success, String errorMessage, String originalText, String translatedText, String detectedLanguage, String targetLanguage) {
this.success = success;
this.errorMessage = errorMessage == null ? "" : errorMessage;
this.originalText = originalText == null ? "" : originalText;
this.translatedText = translatedText == null ? "" : translatedText;
this.detectedLanguage = detectedLanguage == null ? "" : detectedLanguage;
this.targetLanguage = targetLanguage == null ? "" : targetLanguage;
}
public static TranslationResponse success(String originalText, String translatedText, String detectedLanguage, String targetLanguage) {
return new TranslationResponse(true, "", originalText, translatedText, detectedLanguage, targetLanguage);
}
public static TranslationResponse failure(String originalText, String targetLanguage, String errorMessage) {
return new TranslationResponse(false, errorMessage, originalText, originalText, "", targetLanguage);
}
public boolean isSuccess() {
return this.success;
}
public String getErrorMessage() {
return this.errorMessage;
}
public String getOriginalText() {
return this.originalText;
}
public String getTranslatedText() {
return this.translatedText;
}
public String getDetectedLanguage() {
return this.detectedLanguage;
}
public String getTargetLanguage() {
return this.targetLanguage;
}
}
private static class CachedTranslation {
private final long createdAt;
private final TranslationResponse response;
private CachedTranslation(TranslationResponse response) {
this.createdAt = System.currentTimeMillis();
this.response = response;
}
private boolean isExpired() {
return (System.currentTimeMillis() - this.createdAt) > CACHE_TTL_MS;
}
}
private static class CachedLanguages {
private final long createdAt;
private final List<SupportedLanguage> languages;
private CachedLanguages(List<SupportedLanguage> languages) {
this.createdAt = System.currentTimeMillis();
this.languages = languages;
}
private boolean isExpired() {
return (System.currentTimeMillis() - this.createdAt) > CACHE_TTL_MS;
}
}
}
@@ -45,6 +45,7 @@ public class HabboInfo implements Runnable {
private int InfostandBg;
private int InfostandStand;
private int InfostandOverlay;
private int InfostandCardBg;
private int loadingRoom;
private Room currentRoom;
private String roomEntryMethod = "door";
@@ -91,6 +92,7 @@ public class HabboInfo implements Runnable {
this.InfostandBg = set.getInt("background_id");
this.InfostandStand = set.getInt("background_stand_id");
this.InfostandOverlay = set.getInt("background_overlay_id");
this.InfostandCardBg = set.getInt("background_card_id");
this.currentRoom = null;
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
@@ -290,6 +292,14 @@ public class HabboInfo implements Runnable {
public void setInfostandOverlay(int infostandOverlay) {
InfostandOverlay = infostandOverlay;
}
public int getInfostandCardBg() {
return InfostandCardBg;
}
public void setInfostandCardBg(int infostandCardBg) {
InfostandCardBg = infostandCardBg;
}
public Rank getRank() {
return this.rank;
}
@@ -577,7 +587,7 @@ public class HabboInfo implements Runnable {
try {
SqlQueries.update(
"UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ? WHERE id = ?",
"UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ?, background_card_id = ? WHERE id = ?",
this.motto,
this.online ? "1" : "0",
this.look,
@@ -593,6 +603,7 @@ public class HabboInfo implements Runnable {
this.InfostandBg,
this.InfostandStand,
this.InfostandOverlay,
this.InfostandCardBg,
this.id);
} catch (SqlQueries.DataAccessException e) {
LOGGER.error("Caught SQL exception", e);
@@ -23,6 +23,8 @@ public class HabboInventory {
private ItemsComponent itemsComponent;
private PetsComponent petsComponent;
private PrefixesComponent prefixesComponent;
private NickIconsComponent nickIconsComponent;
private UserVisualSettingsComponent userVisualSettingsComponent;
public HabboInventory(Habbo habbo) {
this.habbo = habbo;
@@ -68,6 +70,18 @@ public class HabboInventory {
LOGGER.error("Caught exception", e);
}
try {
this.nickIconsComponent = new NickIconsComponent(this.habbo);
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
try {
this.userVisualSettingsComponent = new UserVisualSettingsComponent(this.habbo);
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
this.items = MarketPlace.getOwnOffers(this.habbo);
}
@@ -127,6 +141,22 @@ public class HabboInventory {
this.prefixesComponent = prefixesComponent;
}
public NickIconsComponent getNickIconsComponent() {
return this.nickIconsComponent;
}
public void setNickIconsComponent(NickIconsComponent nickIconsComponent) {
this.nickIconsComponent = nickIconsComponent;
}
public UserVisualSettingsComponent getUserVisualSettingsComponent() {
return this.userVisualSettingsComponent;
}
public void setUserVisualSettingsComponent(UserVisualSettingsComponent userVisualSettingsComponent) {
this.userVisualSettingsComponent = userVisualSettingsComponent;
}
public void dispose() {
this.badgesComponent.dispose();
this.botsComponent.dispose();
@@ -135,6 +165,8 @@ public class HabboInventory {
this.petsComponent.dispose();
this.wardrobeComponent.dispose();
this.prefixesComponent.dispose();
this.nickIconsComponent.dispose();
this.userVisualSettingsComponent.dispose();
this.badgesComponent = null;
this.botsComponent = null;
@@ -143,6 +175,8 @@ public class HabboInventory {
this.petsComponent = null;
this.wardrobeComponent = null;
this.prefixesComponent = null;
this.nickIconsComponent = null;
this.userVisualSettingsComponent = null;
}
public void addMarketplaceOffer(MarketPlaceOffer marketPlaceOffer) {
@@ -95,7 +95,7 @@ public class HabboManager {
int userId = 0;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) {
PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
statement.setString(1, sso);
try (ResultSet s = statement.executeQuery()) {
if (s.next()) {
@@ -121,7 +121,7 @@ public class HabboManager {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE auth_ticket = ? LIMIT 1")) {
PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
statement.setString(1, sso);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) {
@@ -0,0 +1,121 @@
package com.eu.habbo.habbohotel.users;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.inventory.UserVisualSettingsComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class UserCustomizationData {
private static final Logger LOGGER = LoggerFactory.getLogger(UserCustomizationData.class);
public final String nickIcon;
public final String displayOrder;
public final String prefixText;
public final String prefixColor;
public final String prefixIcon;
public final String prefixEffect;
public final String prefixFont;
private UserCustomizationData(String nickIcon, String displayOrder, String prefixText, String prefixColor, String prefixIcon, String prefixEffect, String prefixFont) {
this.nickIcon = nickIcon != null ? nickIcon : "";
this.displayOrder = UserVisualSettingsComponent.sanitizeDisplayOrder(displayOrder);
this.prefixText = prefixText != null ? prefixText : "";
this.prefixColor = prefixColor != null ? prefixColor : "";
this.prefixIcon = prefixIcon != null ? prefixIcon : "";
this.prefixEffect = prefixEffect != null ? prefixEffect : "";
this.prefixFont = prefixFont != null ? prefixFont : "";
}
public static UserCustomizationData fromHabbo(Habbo habbo) {
if (habbo == null) {
return empty();
}
String nickIcon = "";
String displayOrder = UserVisualSettingsComponent.DEFAULT_DISPLAY_ORDER;
String prefixText = "";
String prefixColor = "";
String prefixIcon = "";
String prefixEffect = "";
String prefixFont = "";
if (habbo.getInventory() != null) {
if (habbo.getInventory().getNickIconsComponent() != null) {
UserNickIcon activeNickIcon = habbo.getInventory().getNickIconsComponent().getActiveNickIcon();
if (activeNickIcon != null && activeNickIcon.getIconKey() != null) {
nickIcon = activeNickIcon.getIconKey();
}
}
if (habbo.getInventory().getPrefixesComponent() != null) {
UserPrefix activePrefix = habbo.getInventory().getPrefixesComponent().getActivePrefix();
if (activePrefix != null) {
prefixText = activePrefix.getText();
prefixColor = activePrefix.getColor();
prefixIcon = activePrefix.getIcon();
prefixEffect = activePrefix.getEffect();
prefixFont = activePrefix.getFont();
}
}
if (habbo.getInventory().getUserVisualSettingsComponent() != null) {
displayOrder = habbo.getInventory().getUserVisualSettingsComponent().getDisplayOrder();
}
}
return new UserCustomizationData(nickIcon, displayOrder, prefixText, prefixColor, prefixIcon, prefixEffect, prefixFont);
}
public static UserCustomizationData fromUserId(int userId) {
String nickIcon = "";
String prefixText = "";
String prefixColor = "";
String prefixIcon = "";
String prefixEffect = "";
String prefixFont = "";
String displayOrder = UserVisualSettingsComponent.loadDisplayOrder(userId);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement nickStatement = connection.prepareStatement(
"SELECT icon_key FROM user_nick_icons WHERE user_id = ? AND active = 1 LIMIT 1")) {
nickStatement.setInt(1, userId);
try (ResultSet set = nickStatement.executeQuery()) {
if (set.next()) {
nickIcon = set.getString("icon_key");
}
}
}
try (PreparedStatement prefixStatement = connection.prepareStatement(
"SELECT text, color, icon, effect, font FROM user_prefixes WHERE user_id = ? AND active = 1 LIMIT 1")) {
prefixStatement.setInt(1, userId);
try (ResultSet set = prefixStatement.executeQuery()) {
if (set.next()) {
prefixText = set.getString("text");
prefixColor = set.getString("color");
prefixIcon = set.getString("icon");
prefixEffect = set.getString("effect");
prefixFont = set.getString("font");
}
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception while loading user customization data", e);
}
return new UserCustomizationData(nickIcon, displayOrder, prefixText, prefixColor, prefixIcon, prefixEffect, prefixFont);
}
public static UserCustomizationData empty() {
return new UserCustomizationData("", UserVisualSettingsComponent.DEFAULT_DISPLAY_ORDER, "", "", "", "", "");
}
}
@@ -0,0 +1,118 @@
package com.eu.habbo.habbohotel.users;
import com.eu.habbo.Emulator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
public class UserNickIcon implements Runnable {
private static final Logger LOGGER = LoggerFactory.getLogger(UserNickIcon.class);
private int id;
private final int userId;
private String iconKey;
private boolean active;
private boolean needsInsert;
private boolean needsUpdate;
private boolean needsDelete;
public UserNickIcon(ResultSet set) throws SQLException {
this.id = set.getInt("id");
this.userId = set.getInt("user_id");
this.iconKey = set.getString("icon_key");
this.active = set.getBoolean("active");
this.needsInsert = false;
this.needsUpdate = false;
this.needsDelete = false;
}
public UserNickIcon(int userId, String iconKey) {
this.id = 0;
this.userId = userId;
this.iconKey = iconKey;
this.active = false;
this.needsInsert = true;
this.needsUpdate = false;
this.needsDelete = false;
}
@Override
public void run() {
try {
if (this.needsInsert) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO user_nick_icons (user_id, icon_key, active) VALUES (?, ?, ?)",
Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, this.userId);
statement.setString(2, this.iconKey);
statement.setBoolean(3, this.active);
statement.execute();
try (ResultSet set = statement.getGeneratedKeys()) {
if (set.next()) {
this.id = set.getInt(1);
}
}
}
this.needsInsert = false;
} else if (this.needsDelete) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"DELETE FROM user_nick_icons WHERE id = ? AND user_id = ?")) {
statement.setInt(1, this.id);
statement.setInt(2, this.userId);
statement.execute();
}
this.needsDelete = false;
} else if (this.needsUpdate) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE user_nick_icons SET icon_key = ?, active = ? WHERE id = ? AND user_id = ?")) {
statement.setString(1, this.iconKey);
statement.setBoolean(2, this.active);
statement.setInt(3, this.id);
statement.setInt(4, this.userId);
statement.execute();
}
this.needsUpdate = false;
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
public int getId() {
return this.id;
}
public int getUserId() {
return this.userId;
}
public String getIconKey() {
return this.iconKey;
}
public void setIconKey(String iconKey) {
this.iconKey = iconKey;
this.needsUpdate = true;
}
public boolean isActive() {
return this.active;
}
public void setActive(boolean active) {
this.active = active;
this.needsUpdate = true;
}
public void needsDelete(boolean needsDelete) {
this.needsDelete = needsDelete;
}
}
@@ -15,6 +15,12 @@ public class UserPrefix implements Runnable {
private String color;
private String icon;
private String effect;
private String font;
private int catalogPrefixId;
private String displayName;
private int points;
private int pointsType;
private boolean custom;
private boolean active;
private boolean needsInsert;
private boolean needsUpdate;
@@ -29,6 +35,12 @@ public class UserPrefix implements Runnable {
if (this.icon == null) this.icon = "";
this.effect = set.getString("effect");
if (this.effect == null) this.effect = "";
this.font = readString(set, "font", "");
this.catalogPrefixId = readInt(set, "catalog_prefix_id", 0);
this.displayName = readString(set, "display_name", this.text);
this.points = readInt(set, "points", 0);
this.pointsType = readInt(set, "points_type", 0);
this.custom = readBoolean(set, "is_custom", true);
this.active = set.getBoolean("active");
this.needsInsert = false;
this.needsUpdate = false;
@@ -36,12 +48,22 @@ public class UserPrefix implements Runnable {
}
public UserPrefix(int userId, String text, String color, String icon, String effect) {
this(userId, text, color, icon, effect, "", 0, text, 0, 0, true);
}
public UserPrefix(int userId, String text, String color, String icon, String effect, String font, int catalogPrefixId, String displayName, int points, int pointsType, boolean custom) {
this.id = 0;
this.userId = userId;
this.text = text;
this.color = color;
this.icon = icon != null ? icon : "";
this.effect = effect != null ? effect : "";
this.font = font != null ? font : "";
this.catalogPrefixId = catalogPrefixId;
this.displayName = (displayName != null && !displayName.isEmpty()) ? displayName : text;
this.points = points;
this.pointsType = pointsType;
this.custom = custom;
this.active = false;
this.needsInsert = true;
this.needsUpdate = false;
@@ -54,14 +76,20 @@ public class UserPrefix implements Runnable {
if (this.needsInsert) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO user_prefixes (user_id, text, color, icon, effect, active) VALUES (?, ?, ?, ?, ?, ?)",
"INSERT INTO user_prefixes (user_id, text, color, icon, effect, font, active, catalog_prefix_id, display_name, points, points_type, is_custom) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, this.userId);
statement.setString(2, this.text);
statement.setString(3, this.color);
statement.setString(4, this.icon);
statement.setString(5, this.effect);
statement.setBoolean(6, this.active);
statement.setString(6, this.font);
statement.setBoolean(7, this.active);
statement.setInt(8, this.catalogPrefixId);
statement.setString(9, this.displayName);
statement.setInt(10, this.points);
statement.setInt(11, this.pointsType);
statement.setBoolean(12, this.custom);
statement.execute();
try (ResultSet set = statement.getGeneratedKeys()) {
if (set.next()) {
@@ -82,14 +110,20 @@ public class UserPrefix implements Runnable {
} else if (this.needsUpdate) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE user_prefixes SET text = ?, color = ?, icon = ?, effect = ?, active = ? WHERE id = ? AND user_id = ?")) {
"UPDATE user_prefixes SET text = ?, color = ?, icon = ?, effect = ?, font = ?, active = ?, catalog_prefix_id = ?, display_name = ?, points = ?, points_type = ?, is_custom = ? WHERE id = ? AND user_id = ?")) {
statement.setString(1, this.text);
statement.setString(2, this.color);
statement.setString(3, this.icon);
statement.setString(4, this.effect);
statement.setBoolean(5, this.active);
statement.setInt(6, this.id);
statement.setInt(7, this.userId);
statement.setString(5, this.font);
statement.setBoolean(6, this.active);
statement.setInt(7, this.catalogPrefixId);
statement.setString(8, this.displayName);
statement.setInt(9, this.points);
statement.setInt(10, this.pointsType);
statement.setBoolean(11, this.custom);
statement.setInt(12, this.id);
statement.setInt(13, this.userId);
statement.execute();
}
this.needsUpdate = false;
@@ -109,6 +143,13 @@ public class UserPrefix implements Runnable {
public void setIcon(String icon) { this.icon = icon != null ? icon : ""; }
public String getEffect() { return this.effect; }
public void setEffect(String effect) { this.effect = effect != null ? effect : ""; }
public String getFont() { return this.font; }
public void setFont(String font) { this.font = font != null ? font : ""; }
public int getCatalogPrefixId() { return this.catalogPrefixId; }
public String getDisplayName() { return this.displayName; }
public int getPoints() { return this.points; }
public int getPointsType() { return this.pointsType; }
public boolean isCustom() { return this.custom; }
public boolean isActive() { return this.active; }
public void setActive(boolean active) {
@@ -119,4 +160,29 @@ public class UserPrefix implements Runnable {
public void needsUpdate(boolean needsUpdate) { this.needsUpdate = needsUpdate; }
public void needsInsert(boolean needsInsert) { this.needsInsert = needsInsert; }
public void needsDelete(boolean needsDelete) { this.needsDelete = needsDelete; }
private static int readInt(ResultSet set, String columnName, int defaultValue) {
try {
return set.getInt(columnName);
} catch (SQLException e) {
return defaultValue;
}
}
private static String readString(ResultSet set, String columnName, String defaultValue) {
try {
String value = set.getString(columnName);
return value != null ? value : defaultValue;
} catch (SQLException e) {
return defaultValue;
}
}
private static boolean readBoolean(ResultSet set, String columnName, boolean defaultValue) {
try {
return set.getBoolean(columnName);
} catch (SQLException e) {
return defaultValue;
}
}
}
@@ -0,0 +1,75 @@
package com.eu.habbo.habbohotel.users.custombadge;
import java.sql.ResultSet;
import java.sql.SQLException;
public class CustomBadge {
private final int id;
private final int userId;
private final String badgeId;
private String badgeName;
private String badgeDescription;
private final int dateCreated;
private int dateEdit;
public CustomBadge(ResultSet set) throws SQLException {
this.id = set.getInt("id");
this.userId = set.getInt("user_id");
this.badgeId = set.getString("badge_id");
this.badgeName = set.getString("badge_name");
this.badgeDescription = set.getString("badge_description");
this.dateCreated = set.getInt("date_created");
this.dateEdit = set.getInt("date_edit");
}
public CustomBadge(int id, int userId, String badgeId, String badgeName, String badgeDescription, int dateCreated, int dateEdit) {
this.id = id;
this.userId = userId;
this.badgeId = badgeId;
this.badgeName = badgeName;
this.badgeDescription = badgeDescription;
this.dateCreated = dateCreated;
this.dateEdit = dateEdit;
}
public int getId() {
return this.id;
}
public int getUserId() {
return this.userId;
}
public String getBadgeId() {
return this.badgeId;
}
public String getBadgeName() {
return this.badgeName;
}
public String getBadgeDescription() {
return this.badgeDescription;
}
public int getDateCreated() {
return this.dateCreated;
}
public int getDateEdit() {
return this.dateEdit;
}
public void setBadgeName(String badgeName) {
this.badgeName = badgeName;
}
public void setBadgeDescription(String badgeDescription) {
this.badgeDescription = badgeDescription;
}
public void setDateEdit(int dateEdit) {
this.dateEdit = dateEdit;
}
}
@@ -0,0 +1,15 @@
package com.eu.habbo.habbohotel.users.custombadge;
public class CustomBadgeException extends Exception {
private final String code;
public CustomBadgeException(String code, String message) {
super(message);
this.code = code;
}
public String getCode() {
return this.code;
}
}
@@ -0,0 +1,588 @@
package com.eu.habbo.habbohotel.users.custombadge;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboBadge;
import com.eu.habbo.habbohotel.users.inventory.BadgesComponent;
import com.eu.habbo.messages.outgoing.inventory.InventoryBadgesComposer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
public class CustomBadgeManager {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomBadgeManager.class);
public static final int MAX_PER_USER = 5;
public static final int BADGE_WIDTH = 40;
public static final int BADGE_HEIGHT = 40;
public static final int MAX_BADGE_SIZE_BYTES = 40960;
private static final int RANDOM_SUFFIX_LENGTH = 5;
private static final char[] RANDOM_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
private static final Pattern BADGE_ID_PATTERN = Pattern.compile("^CUST[A-Z0-9]{" + RANDOM_SUFFIX_LENGTH + "}-\\d+$");
private static final byte[] PNG_MAGIC = { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
private static final int RATE_LIMIT_OPS = 5;
private static final long RATE_LIMIT_WINDOW_MS = 60_000L;
private final SecureRandom random = new SecureRandom();
private final Map<Integer, long[]> rateBuckets = new ConcurrentHashMap<>();
private final Map<String, BadgeText> textCache = new ConcurrentHashMap<>();
private final java.util.concurrent.atomic.AtomicLong textCacheVersion = new java.util.concurrent.atomic.AtomicLong();
private volatile CustomBadgeSettings settings;
public CustomBadgeManager() {
this.reload();
}
public static final class BadgeText {
public final String name;
public final String description;
public BadgeText(String name, String description) {
this.name = name == null ? "" : name;
this.description = description == null ? "" : description;
}
}
public Map<String, BadgeText> getTextCache() {
return java.util.Collections.unmodifiableMap(this.textCache);
}
public long getTextCacheVersion() {
return this.textCacheVersion.get();
}
private void loadTextCache() {
Map<String, BadgeText> next = new java.util.HashMap<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT `badge_id`, `badge_name`, `badge_description` FROM `user_custom_badge`")) {
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
next.put(resultSet.getString("badge_id"),
new BadgeText(
resultSet.getString("badge_name"),
resultSet.getString("badge_description")));
}
}
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to load badge text cache.", e);
return;
}
this.textCache.clear();
this.textCache.putAll(next);
this.textCacheVersion.incrementAndGet();
LOGGER.info("CustomBadgeManager -> loaded {} custom badge texts into memory.", next.size());
}
public void reload() {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT `badge_path`, `badge_url`, `price_badge`, `currency_type` FROM `users_custom_badge_settings` ORDER BY `id` ASC LIMIT 1")) {
try (ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
this.settings = new CustomBadgeSettings(
resultSet.getString("badge_path"),
resultSet.getString("badge_url"),
resultSet.getInt("price_badge"),
resultSet.getInt("currency_type"));
} else {
this.settings = new CustomBadgeSettings(
"/var/www/gamedata/c_images/album1584",
"/gamedata/c_images/album1584",
0, -1);
LOGGER.warn("CustomBadgeManager -> No row found in users_custom_badge_settings, falling back to defaults.");
}
}
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to load settings.", e);
}
loadTextCache();
}
public CustomBadgeSettings getSettings() {
return this.settings;
}
public List<CustomBadge> listForUser(int userId) {
List<CustomBadge> result = new ArrayList<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM `user_custom_badge` WHERE `user_id` = ? ORDER BY `date_created` ASC")) {
statement.setInt(1, userId);
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
result.add(new CustomBadge(resultSet));
}
}
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to list badges for user " + userId, e);
}
return result;
}
public CustomBadge getByBadgeId(String badgeId) {
if (badgeId == null || badgeId.isEmpty()) return null;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM `user_custom_badge` WHERE `badge_id` = ? LIMIT 1")) {
statement.setString(1, badgeId);
try (ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
return new CustomBadge(resultSet);
}
}
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to load badge " + badgeId, e);
}
return null;
}
public int countForUser(int userId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT COUNT(*) FROM `user_custom_badge` WHERE `user_id` = ?")) {
statement.setInt(1, userId);
try (ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
return resultSet.getInt(1);
}
}
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to count badges for user " + userId, e);
}
return 0;
}
public CustomBadge create(int userId, String name, String description, byte[] pngBytes) throws CustomBadgeException {
enforceRateLimit(userId);
if (this.countForUser(userId) >= MAX_PER_USER) {
throw new CustomBadgeException("limit_reached", "Maximum of " + MAX_PER_USER + " custom badges reached.");
}
BufferedImage image = validatePng(pngBytes);
chargeForCreate(userId);
String badgeId = generateBadgeId();
int now = (int) (System.currentTimeMillis() / 1000L);
try {
writeBadgeFile(badgeId, image);
} catch (CustomBadgeException e) {
refundForCreate(userId);
throw e;
}
String safeName = sanitize(name, 64);
String safeDesc = sanitize(description, 255);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO `user_custom_badge` (`user_id`, `badge_id`, `badge_name`, `badge_description`, `date_created`, `date_edit`) VALUES (?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, userId);
statement.setString(2, badgeId);
statement.setString(3, safeName);
statement.setString(4, safeDesc);
statement.setInt(5, now);
statement.setInt(6, now);
statement.executeUpdate();
int generatedId = 0;
try (ResultSet keys = statement.getGeneratedKeys()) {
if (keys.next()) generatedId = keys.getInt(1);
}
this.textCache.put(badgeId, new BadgeText(safeName, safeDesc));
this.textCacheVersion.incrementAndGet();
issueBadgeToInventory(userId, badgeId);
return new CustomBadge(generatedId, userId, badgeId, safeName, safeDesc, now, now);
} catch (SQLException e) {
deleteBadgeFileQuietly(badgeId);
refundForCreate(userId);
LOGGER.error("CustomBadgeManager -> Failed to insert badge for user " + userId, e);
throw new CustomBadgeException("db_error", "Could not save the badge.");
}
}
public CustomBadge update(int userId, String oldBadgeId, String name, String description, byte[] pngBytes) throws CustomBadgeException {
enforceRateLimit(userId);
CustomBadge existing = getByBadgeId(oldBadgeId);
if (existing == null || existing.getUserId() != userId) {
throw new CustomBadgeException("not_found", "Badge not found.");
}
BufferedImage image = validatePng(pngBytes);
String newBadgeId = generateBadgeId();
int now = (int) (System.currentTimeMillis() / 1000L);
writeBadgeFile(newBadgeId, image);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE `user_custom_badge` SET `badge_id` = ?, `badge_name` = ?, `badge_description` = ?, `date_edit` = ? WHERE `id` = ?")) {
statement.setString(1, newBadgeId);
statement.setString(2, sanitize(name, 64));
statement.setString(3, sanitize(description, 255));
statement.setInt(4, now);
statement.setInt(5, existing.getId());
statement.executeUpdate();
} catch (SQLException e) {
deleteBadgeFileQuietly(newBadgeId);
LOGGER.error("CustomBadgeManager -> Failed to update badge " + oldBadgeId, e);
throw new CustomBadgeException("db_error", "Could not update the badge.");
}
String safeName = sanitize(name, 64);
String safeDesc = sanitize(description, 255);
this.textCache.remove(oldBadgeId);
this.textCache.put(newBadgeId, new BadgeText(safeName, safeDesc));
this.textCacheVersion.incrementAndGet();
renameBadgeInInventory(userId, oldBadgeId, newBadgeId);
deleteBadgeFileQuietly(oldBadgeId);
return new CustomBadge(existing.getId(), userId, newBadgeId, safeName, safeDesc, existing.getDateCreated(), now);
}
public void delete(int userId, String badgeId) throws CustomBadgeException {
enforceRateLimit(userId);
CustomBadge existing = getByBadgeId(badgeId);
if (existing == null || existing.getUserId() != userId) {
throw new CustomBadgeException("not_found", "Badge not found.");
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"DELETE FROM `user_custom_badge` WHERE `id` = ?")) {
statement.setInt(1, existing.getId());
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to delete badge " + badgeId, e);
throw new CustomBadgeException("db_error", "Could not delete the badge.");
}
this.textCache.remove(badgeId);
this.textCacheVersion.incrementAndGet();
revokeBadgeFromInventory(userId, badgeId);
deleteBadgeFileQuietly(badgeId);
}
public boolean isCustomBadgeId(String badgeId) {
return badgeId != null && BADGE_ID_PATTERN.matcher(badgeId).matches();
}
public String generateBadgeId() {
long timestamp = System.currentTimeMillis() / 1000L;
for (int attempt = 0; attempt < 8; attempt++) {
StringBuilder suffix = new StringBuilder(RANDOM_SUFFIX_LENGTH);
for (int i = 0; i < RANDOM_SUFFIX_LENGTH; i++) {
suffix.append(RANDOM_ALPHABET[this.random.nextInt(RANDOM_ALPHABET.length)]);
}
String candidate = "CUST" + suffix + "-" + timestamp;
if (getByBadgeId(candidate) == null) return candidate;
timestamp++;
}
throw new IllegalStateException("Could not allocate a unique custom badge id after 8 attempts.");
}
public String publicUrlFor(String badgeId) {
CustomBadgeSettings current = this.settings;
if (current == null) return "";
String base = current.getBadgeUrl();
if (base == null || base.isEmpty()) return "";
if (base.endsWith("/")) return base + badgeId + ".gif";
return base + "/" + badgeId + ".gif";
}
private void chargeForCreate(int userId) throws CustomBadgeException {
CustomBadgeSettings current = this.settings;
if (current == null) return;
int price = current.getPriceBadge();
if (price <= 0) return;
Habbo habbo = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (habbo == null) {
throw new CustomBadgeException("must_be_online",
"You must be online in the hotel to create a paid badge.");
}
int currencyType = current.getCurrencyType();
if (currencyType == -1) {
if (habbo.getHabboInfo().getCredits() < price) {
throw new CustomBadgeException("insufficient_funds",
"You don't have enough credits (need " + price + ").");
}
habbo.giveCredits(-price);
} else {
if (habbo.getHabboInfo().getCurrencyAmount(currencyType) < price) {
throw new CustomBadgeException("insufficient_funds",
"You don't have enough of that currency (need " + price + ").");
}
habbo.givePoints(currencyType, -price);
}
}
private void issueBadgeToInventory(int userId, String badgeId) {
Habbo online = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (online != null) {
BadgesComponent.createBadge(badgeId, online);
if (online.getClient() != null) {
online.getClient().sendResponse(new InventoryBadgesComposer(online));
}
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO `users_badges` (`user_id`, `slot_id`, `badge_code`) VALUES (?, 0, ?)")) {
statement.setInt(1, userId);
statement.setString(2, badgeId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to issue offline badge " + badgeId + " to user " + userId, e);
}
}
private void renameBadgeInInventory(int userId, String oldBadgeId, String newBadgeId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE `users_badges` SET `badge_code` = ? WHERE `user_id` = ? AND `badge_code` = ?")) {
statement.setString(1, newBadgeId);
statement.setInt(2, userId);
statement.setString(3, oldBadgeId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to rename badge in users_badges " + oldBadgeId + " -> " + newBadgeId, e);
}
Habbo online = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (online == null) return;
HabboBadge existing = online.getInventory().getBadgesComponent().getBadge(oldBadgeId);
if (existing != null) existing.setCode(newBadgeId);
if (online.getClient() != null) {
online.getClient().sendResponse(new InventoryBadgesComposer(online));
}
}
private void revokeBadgeFromInventory(int userId, String badgeId) {
BadgesComponent.deleteBadge(userId, badgeId);
Habbo online = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (online == null) return;
online.getInventory().getBadgesComponent().removeBadge(badgeId);
if (online.getClient() != null) {
online.getClient().sendResponse(new InventoryBadgesComposer(online));
}
}
private BufferedImage validatePng(byte[] data) throws CustomBadgeException {
if (data == null || data.length == 0) {
throw new CustomBadgeException("empty", "Badge image is empty.");
}
if (data.length > MAX_BADGE_SIZE_BYTES) {
throw new CustomBadgeException("too_large", "Badge image exceeds " + MAX_BADGE_SIZE_BYTES + " bytes.");
}
if (data.length < PNG_MAGIC.length) {
throw new CustomBadgeException("invalid_image", "Badge image must be a PNG.");
}
for (int i = 0; i < PNG_MAGIC.length; i++) {
if (data[i] != PNG_MAGIC[i]) {
throw new CustomBadgeException("invalid_image", "Badge image must be a PNG.");
}
}
try (ImageInputStream peek = ImageIO.createImageInputStream(new ByteArrayInputStream(data))) {
if (peek == null) throw new IOException("no input stream");
Iterator<ImageReader> readers = ImageIO.getImageReaders(peek);
if (!readers.hasNext()) {
throw new CustomBadgeException("invalid_image", "Badge image format not recognised.");
}
ImageReader reader = readers.next();
try {
reader.setInput(peek, true, true);
int w = reader.getWidth(0);
int h = reader.getHeight(0);
if (w != BADGE_WIDTH || h != BADGE_HEIGHT) {
throw new CustomBadgeException("wrong_dimensions",
"Badge image must be " + BADGE_WIDTH + "x" + BADGE_HEIGHT + " pixels.");
}
} finally {
reader.dispose();
}
} catch (IOException e) {
throw new CustomBadgeException("invalid_image", "Badge image header could not be read.");
}
BufferedImage image;
try {
image = ImageIO.read(new ByteArrayInputStream(data));
} catch (IOException e) {
throw new CustomBadgeException("invalid_image", "Badge image could not be decoded.");
}
if (image == null
|| image.getWidth() != BADGE_WIDTH
|| image.getHeight() != BADGE_HEIGHT) {
throw new CustomBadgeException("invalid_image", "Badge image could not be decoded.");
}
return image;
}
private void enforceRateLimit(int userId) throws CustomBadgeException {
long now = System.currentTimeMillis();
long[] bucket = this.rateBuckets.computeIfAbsent(userId, id -> new long[RATE_LIMIT_OPS]);
synchronized (bucket) {
long oldest = Long.MAX_VALUE;
int oldestIdx = 0;
for (int i = 0; i < bucket.length; i++) {
if (bucket[i] < oldest) { oldest = bucket[i]; oldestIdx = i; }
}
if (oldest > now - RATE_LIMIT_WINDOW_MS) {
throw new CustomBadgeException("rate_limited",
"Too many badge operations. Try again in a moment.");
}
bucket[oldestIdx] = now;
}
}
private void refundForCreate(int userId) {
CustomBadgeSettings current = this.settings;
if (current == null) return;
int price = current.getPriceBadge();
if (price <= 0) return;
Habbo habbo = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (habbo == null) {
LOGGER.warn("CustomBadgeManager -> Could not refund {} (price {}): user offline", userId, price);
return;
}
int currencyType = current.getCurrencyType();
if (currencyType == -1) habbo.giveCredits(price);
else habbo.givePoints(currencyType, price);
}
private void writeBadgeFile(String badgeId, BufferedImage source) throws CustomBadgeException {
CustomBadgeSettings current = this.settings;
if (current == null || current.getBadgePath() == null || current.getBadgePath().isEmpty()) {
throw new CustomBadgeException("not_configured", "Custom badge storage path is not configured.");
}
try {
Path dir = Paths.get(current.getBadgePath()).toAbsolutePath();
Files.createDirectories(dir);
Path target = dir.resolve(badgeId + ".gif");
BufferedImage indexed = toIndexedGifImage(source);
if (!ImageIO.write(indexed, "gif", target.toFile())) {
throw new IOException("No GIF ImageWriter available.");
}
LOGGER.info("CustomBadgeManager -> wrote badge {} ({} bytes) to {}",
badgeId, Files.size(target), target);
} catch (IOException e) {
LOGGER.error("CustomBadgeManager -> Failed to write badge " + badgeId
+ " to " + current.getBadgePath(), e);
throw new CustomBadgeException("write_failed", "Could not save the badge file.");
}
}
private static BufferedImage toIndexedGifImage(BufferedImage source) {
int w = source.getWidth();
int h = source.getHeight();
int[] pixels = source.getRGB(0, 0, w, h, null, 0, w);
Map<Integer, Integer> indexByColor = new LinkedHashMap<>();
indexByColor.put(0, 0);
for (int p : pixels) {
int alpha = (p >>> 24) & 0xff;
int key = (alpha < 128) ? 0 : (p | 0xFF000000);
if (key == 0) continue;
if (indexByColor.size() >= 256) break;
indexByColor.computeIfAbsent(key, k -> indexByColor.size());
}
int n = indexByColor.size();
byte[] r = new byte[n];
byte[] g = new byte[n];
byte[] b = new byte[n];
int i = 0;
for (Integer color : indexByColor.keySet()) {
r[i] = (byte) ((color >>> 16) & 0xff);
g[i] = (byte) ((color >>> 8) & 0xff);
b[i] = (byte) (color & 0xff);
i++;
}
IndexColorModel colorModel = new IndexColorModel(8, n, r, g, b, 0);
BufferedImage out = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_INDEXED, colorModel);
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int p = pixels[y * w + x];
int alpha = (p >>> 24) & 0xff;
int key = (alpha < 128) ? 0 : (p | 0xFF000000);
Integer idx = indexByColor.get(key);
out.getRaster().setSample(x, y, 0, idx == null ? 0 : idx);
}
}
return out;
}
private void deleteBadgeFileQuietly(String badgeId) {
CustomBadgeSettings current = this.settings;
if (current == null || current.getBadgePath() == null) return;
File file = new File(current.getBadgePath(), badgeId + ".gif");
if (file.exists() && !file.delete()) {
LOGGER.warn("CustomBadgeManager -> Could not delete stale badge file: {}", file.getAbsolutePath());
}
}
private static String sanitize(String value, int maxLength) {
if (value == null) return "";
StringBuilder out = new StringBuilder(Math.min(value.length(), maxLength));
for (int i = 0; i < value.length() && out.length() < maxLength; i++) {
char c = value.charAt(i);
if (c < 0x20 || c == 0x7F) continue;
out.append(c);
}
return out.toString().trim();
}
}
@@ -0,0 +1,32 @@
package com.eu.habbo.habbohotel.users.custombadge;
public class CustomBadgeSettings {
private final String badgePath;
private final String badgeUrl;
private final int priceBadge;
private final int currencyType;
public CustomBadgeSettings(String badgePath, String badgeUrl, int priceBadge, int currencyType) {
this.badgePath = badgePath;
this.badgeUrl = badgeUrl;
this.priceBadge = priceBadge;
this.currencyType = currencyType;
}
public String getBadgePath() {
return this.badgePath;
}
public String getBadgeUrl() {
return this.badgeUrl;
}
public int getPriceBadge() {
return this.priceBadge;
}
public int getCurrencyType() {
return this.currencyType;
}
}
@@ -0,0 +1,136 @@
package com.eu.habbo.habbohotel.users.infostand;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboStats;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
public class InfostandBackgroundManager {
private static final Logger LOGGER = LoggerFactory.getLogger(InfostandBackgroundManager.class);
public enum Category {
BACKGROUND("background"),
STAND("stand"),
OVERLAY("overlay"),
CARD("card");
public final String dbValue;
Category(String dbValue) {
this.dbValue = dbValue;
}
public static Category fromDbValue(String value) {
for (Category category : values()) {
if (category.dbValue.equalsIgnoreCase(value)) return category;
}
return null;
}
}
private final Map<Category, Map<Integer, Entry>> entries = new EnumMap<>(Category.class);
private boolean enforce = false;
public InfostandBackgroundManager() {
for (Category category : Category.values()) {
this.entries.put(category, Collections.emptyMap());
}
this.reload();
}
public void reload() {
Map<Category, Map<Integer, Entry>> next = new EnumMap<>(Category.class);
for (Category category : Category.values()) {
next.put(category, new HashMap<>());
}
int loaded = 0;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT id, category, min_rank, is_hc_only, is_ambassador_only FROM infostand_backgrounds");
ResultSet set = statement.executeQuery()) {
while (set.next()) {
Category category = Category.fromDbValue(set.getString("category"));
if (category == null) continue;
int id = set.getInt("id");
int minRank = set.getInt("min_rank");
boolean isHcOnly = set.getBoolean("is_hc_only");
boolean isAmbassadorOnly = set.getBoolean("is_ambassador_only");
next.get(category).put(id, new Entry(minRank, isHcOnly, isAmbassadorOnly));
loaded++;
}
} catch (SQLException e) {
this.enforce = false;
for (Category category : Category.values()) {
this.entries.put(category, Collections.emptyMap());
}
LOGGER.error("InfostandBackgroundManager -> Failed to load infostand_backgrounds, server-side validation disabled.", e);
return;
}
for (Category category : Category.values()) {
this.entries.put(category, next.get(category));
}
this.enforce = loaded > 0;
if (this.enforce) {
LOGGER.info("InfostandBackgroundManager -> Loaded {} backgrounds, {} stands, {} overlays, {} cards from infostand_backgrounds.",
this.entries.get(Category.BACKGROUND).size(),
this.entries.get(Category.STAND).size(),
this.entries.get(Category.OVERLAY).size(),
this.entries.get(Category.CARD).size());
} else {
LOGGER.info("InfostandBackgroundManager -> infostand_backgrounds is empty, server-side validation disabled (only range clamp will apply).");
}
}
public boolean canUse(Habbo habbo, Category category, int id) {
if (id == 0) return true;
if (!this.enforce) return true;
if (habbo == null) return false;
Map<Integer, Entry> categoryEntries = this.entries.get(category);
if (categoryEntries == null) return false;
Entry entry = categoryEntries.get(id);
if (entry == null) return false;
HabboInfo info = habbo.getHabboInfo();
int rankId = (info != null && info.getRank() != null) ? info.getRank().getId() : 0;
HabboStats stats = habbo.getHabboStats();
boolean hasClub = stats != null && stats.hasActiveClub();
if (entry.isHcOnly && !hasClub) return false;
if (entry.isAmbassadorOnly && !habbo.hasPermission(Permission.ACC_AMBASSADOR)) return false;
if (rankId < entry.minRank) return false;
return true;
}
public static final class Entry {
public final int minRank;
public final boolean isHcOnly;
public final boolean isAmbassadorOnly;
public Entry(int minRank, boolean isHcOnly, boolean isAmbassadorOnly) {
this.minRank = minRank;
this.isHcOnly = isHcOnly;
this.isAmbassadorOnly = isAmbassadorOnly;
}
}
}
@@ -1,7 +1,6 @@
package com.eu.habbo.habbohotel.users.inventory;
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.HabboInventory;
@@ -10,6 +9,7 @@ import com.eu.habbo.plugin.events.inventory.InventoryItemAddedEvent;
import com.eu.habbo.plugin.events.inventory.InventoryItemRemovedEvent;
import com.eu.habbo.plugin.events.inventory.InventoryItemsAddedEvent;
import gnu.trove.TCollections;
import gnu.trove.iterator.TIntObjectIterator;
import gnu.trove.map.TIntObjectMap;
import gnu.trove.map.hash.THashMap;
import gnu.trove.map.hash.TIntObjectHashMap;
@@ -18,9 +18,11 @@ import gnu.trove.set.hash.THashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
public class ItemsComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(ItemsComponent.class);
@@ -37,23 +39,25 @@ public class ItemsComponent {
public static THashMap<Integer, HabboItem> loadItems(Habbo habbo) {
THashMap<Integer, HabboItem> itemsList = new THashMap<>();
try {
SqlQueries.forEach(
"SELECT * FROM items WHERE room_id = ? AND user_id = ?",
rs -> {
try {
HabboItem item = Emulator.getGameEnvironment().getItemManager().loadHabboItem(rs);
if (item != null) {
itemsList.put(rs.getInt("id"), item);
} else {
LOGGER.error("Failed to load HabboItem: {}", rs.getInt("id"));
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT items.* FROM items LEFT JOIN builders_club_items ON builders_club_items.item_id = items.id WHERE items.room_id = ? AND items.user_id = ? AND builders_club_items.item_id IS NULL")) {
statement.setInt(1, 0);
statement.setInt(2, habbo.getHabboInfo().getId());
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
try {
HabboItem item = Emulator.getGameEnvironment().getItemManager().loadHabboItem(set);
if (item != null) {
itemsList.put(set.getInt("id"), item);
} else {
LOGGER.error("Failed to load HabboItem: {}", set.getInt("id"));
}
},
0, habbo.getHabboInfo().getId());
} catch (SqlQueries.DataAccessException e) {
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
@@ -147,45 +151,70 @@ public class ItemsComponent {
public void dispose() {
synchronized (this.items) {
if (!this.items.isEmpty()) {
List<HabboItem> updates = new ArrayList<>();
List<HabboItem> deletes = new ArrayList<>();
for (HabboItem item : this.items.valueCollection()) {
if (item.needsDelete()) {
deletes.add(item);
item.needsUpdate(false);
item.needsDelete(false);
} else if (item.needsUpdate()) {
updates.add(item);
item.needsUpdate(false);
}
}
TIntObjectIterator<HabboItem> items = this.items.iterator();
try {
if (!deletes.isEmpty()) {
SqlQueries.batchUpdate(
"DELETE FROM items WHERE id = ?",
deletes,
(ps, item) -> ps.setInt(1, item.getId()));
if (items == null) {
LOGGER.error("Items is NULL!");
return;
}
if (!this.items.isEmpty()) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement updateStmt = connection.prepareStatement(
"UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?")) {
try (PreparedStatement deleteStmt = connection.prepareStatement(
"DELETE FROM items WHERE id = ?")) {
int updateCount = 0;
int deleteCount = 0;
for (int i = this.items.size(); i-- > 0; ) {
try {
items.advance();
} catch (NoSuchElementException e) {
break;
}
HabboItem item = items.value();
if (item.needsDelete()) {
deleteStmt.setInt(1, item.getId());
deleteStmt.addBatch();
deleteCount++;
item.needsUpdate(false);
item.needsDelete(false);
} else if (item.needsUpdate()) {
updateStmt.setInt(1, item.getUserId());
updateStmt.setInt(2, item.getRoomId());
updateStmt.setString(3, item.getWallPosition());
updateStmt.setInt(4, item.getX());
updateStmt.setInt(5, item.getY());
updateStmt.setDouble(6, item.getZ());
updateStmt.setInt(7, item.getRotation());
updateStmt.setString(8, item.getExtradata());
updateStmt.setString(9, item.getLimitedStack() + ":" + item.getLimitedSells());
updateStmt.setInt(10, item.getId());
updateStmt.addBatch();
updateCount++;
item.needsUpdate(false);
}
if (updateCount > 0 && updateCount % 100 == 0) {
updateStmt.executeBatch();
}
if (deleteCount > 0 && deleteCount % 100 == 0) {
deleteStmt.executeBatch();
}
}
if (deleteCount % 100 != 0) {
deleteStmt.executeBatch();
}
if (updateCount % 100 != 0) {
updateStmt.executeBatch();
}
}
}
if (!updates.isEmpty()) {
SqlQueries.batchUpdate(
"UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?",
updates,
(ps, item) -> {
ps.setInt(1, item.getUserId());
ps.setInt(2, item.getRoomId());
ps.setString(3, item.getWallPosition());
ps.setInt(4, item.getX());
ps.setInt(5, item.getY());
ps.setDouble(6, item.getZ());
ps.setInt(7, item.getRotation());
ps.setString(8, item.getExtradata());
ps.setString(9, item.getLimitedStack() + ":" + item.getLimitedSells());
ps.setInt(10, item.getId());
});
}
} catch (SqlQueries.DataAccessException e) {
} catch (SQLException e) {
LOGGER.error("Caught SQL exception during batch item save", e);
}
}
@@ -0,0 +1,119 @@
package com.eu.habbo.habbohotel.users.inventory;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserNickIcon;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class NickIconsComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(NickIconsComponent.class);
private final List<UserNickIcon> nickIcons = new ArrayList<>();
private final Habbo habbo;
public NickIconsComponent(Habbo habbo) {
this.habbo = habbo;
this.loadNickIcons();
}
private void loadNickIcons() {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT * FROM user_nick_icons WHERE user_id = ?")) {
statement.setInt(1, this.habbo.getHabboInfo().getId());
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
this.nickIcons.add(new UserNickIcon(set));
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
public List<UserNickIcon> getNickIcons() {
synchronized (this.nickIcons) {
return new ArrayList<>(this.nickIcons);
}
}
public UserNickIcon getActiveNickIcon() {
synchronized (this.nickIcons) {
for (UserNickIcon nickIcon : this.nickIcons) {
if (nickIcon.isActive()) {
return nickIcon;
}
}
}
return null;
}
public UserNickIcon getNickIcon(int id) {
synchronized (this.nickIcons) {
for (UserNickIcon nickIcon : this.nickIcons) {
if (nickIcon.getId() == id) {
return nickIcon;
}
}
}
return null;
}
public UserNickIcon getNickIconByKey(String iconKey) {
synchronized (this.nickIcons) {
for (UserNickIcon nickIcon : this.nickIcons) {
if (nickIcon.getIconKey().equalsIgnoreCase(iconKey)) {
return nickIcon;
}
}
}
return null;
}
public void addNickIcon(UserNickIcon nickIcon) {
synchronized (this.nickIcons) {
this.nickIcons.add(nickIcon);
}
}
public void setActive(int nickIconId) {
synchronized (this.nickIcons) {
for (UserNickIcon nickIcon : this.nickIcons) {
boolean shouldBeActive = (nickIcon.getId() == nickIconId);
if (nickIcon.isActive() != shouldBeActive) {
nickIcon.setActive(shouldBeActive);
Emulator.getThreading().run(nickIcon);
}
}
}
}
public void deactivateAll() {
synchronized (this.nickIcons) {
for (UserNickIcon nickIcon : this.nickIcons) {
if (nickIcon.isActive()) {
nickIcon.setActive(false);
Emulator.getThreading().run(nickIcon);
}
}
}
}
public void dispose() {
synchronized (this.nickIcons) {
this.nickIcons.clear();
}
}
}
@@ -62,6 +62,15 @@ public class PrefixesComponent {
return null;
}
public UserPrefix getPrefixByCatalogId(int catalogPrefixId) {
synchronized (this.prefixes) {
for (UserPrefix prefix : this.prefixes) {
if (prefix.getCatalogPrefixId() == catalogPrefixId) return prefix;
}
}
return null;
}
public void addPrefix(UserPrefix prefix) {
synchronized (this.prefixes) {
this.prefixes.add(prefix);
@@ -0,0 +1,94 @@
package com.eu.habbo.habbohotel.users.inventory;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class UserVisualSettingsComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(UserVisualSettingsComponent.class);
public static final String DEFAULT_DISPLAY_ORDER = "icon-prefix-name";
private static final Set<String> ALLOWED_PARTS = new HashSet<>(Arrays.asList("icon", "prefix", "name"));
private final Habbo habbo;
private String displayOrder = DEFAULT_DISPLAY_ORDER;
public UserVisualSettingsComponent(Habbo habbo) {
this.habbo = habbo;
this.loadSettings();
}
private void loadSettings() {
this.displayOrder = loadDisplayOrder(this.habbo.getHabboInfo().getId());
}
public String getDisplayOrder() {
return sanitizeDisplayOrder(this.displayOrder);
}
public void setDisplayOrder(String displayOrder) {
this.displayOrder = sanitizeDisplayOrder(displayOrder);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO user_visual_settings (user_id, display_order) VALUES (?, ?) ON DUPLICATE KEY UPDATE display_order = VALUES(display_order)")) {
statement.setInt(1, this.habbo.getHabboInfo().getId());
statement.setString(2, this.displayOrder);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception while saving user visual settings", e);
}
}
public static String loadDisplayOrder(int userId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT display_order FROM user_visual_settings WHERE user_id = ? LIMIT 1")) {
statement.setInt(1, userId);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) {
return sanitizeDisplayOrder(set.getString("display_order"));
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception while loading user visual settings", e);
}
return DEFAULT_DISPLAY_ORDER;
}
public static String sanitizeDisplayOrder(String displayOrder) {
if (displayOrder == null || displayOrder.trim().isEmpty()) {
return DEFAULT_DISPLAY_ORDER;
}
String[] parts = displayOrder.trim().toLowerCase().split("-");
if (parts.length != 3) {
return DEFAULT_DISPLAY_ORDER;
}
Set<String> uniqueParts = new HashSet<>();
for (String part : parts) {
if (!ALLOWED_PARTS.contains(part) || !uniqueParts.add(part)) {
return DEFAULT_DISPLAY_ORDER;
}
}
return String.join("-", parts);
}
public void dispose() {
this.displayOrder = DEFAULT_DISPLAY_ORDER;
}
}
@@ -77,6 +77,22 @@ public interface IWiredEffect {
return false;
}
/**
* Selectors can use this to gate stack execution after their target list has
* been resolved. Returning false stops the stack before conditions/effects.
*/
default boolean hasRequiredSelectorTargets(WiredContext ctx) {
return true;
}
/**
* Selectors that filter the current selector result should run after
* selectors that create/replace that result.
*/
default boolean usesExistingSelectorTargets() {
return false;
}
/**
* Simulate this effect's execution and record intended state changes.
* <p>
@@ -112,7 +112,7 @@ public final class WiredContext {
this.state = state;
this.legacySettings = legacySettings;
this.contextVariables = (event.getContextVariableScope() != null)
? event.getContextVariableScope()
? event.getContextVariableScope().copy()
: new WiredContextVariableScope();
this.targets = new WiredTargets();
@@ -26,6 +26,7 @@ import com.eu.habbo.habbohotel.wired.api.WiredStack;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer;
import com.eu.habbo.messages.outgoing.rooms.items.ItemStateComposer;
import com.eu.habbo.plugin.events.furniture.wired.WiredStackExecutedEvent;
import com.eu.habbo.plugin.events.furniture.wired.WiredStackTriggeredEvent;
import gnu.trove.map.hash.THashMap;
@@ -130,6 +131,9 @@ public final class WiredEngine {
/** Cache room+eventType+sourceItemId -> matching stacks for source-triggered timer events */
private final ConcurrentHashMap<String, List<WiredStack>> sourceStacksByTriggerKey;
/** Track filter-selector animation tokens so rapid executions do not reset newer animations */
private final ConcurrentHashMap<Integer, Long> filteredSelectorAnimationTokens;
/**
* Create a new wired engine.
*
@@ -151,6 +155,7 @@ public final class WiredEngine {
this.bannedRooms = new ConcurrentHashMap<>();
this.roomDiagnostics = new ConcurrentHashMap<>();
this.sourceStacksByTriggerKey = new ConcurrentHashMap<>();
this.filteredSelectorAnimationTokens = new ConcurrentHashMap<>();
}
/**
@@ -426,6 +431,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors);
}
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions);
List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution);
boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event);
@@ -541,6 +550,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors);
}
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions);
List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution);
boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event);
@@ -627,6 +640,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors);
}
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions);
List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution);
return !executableEffects.isEmpty();
@@ -660,6 +677,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors);
}
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, false);
if (!conditionsPassedForExecution) {
return false;
@@ -1011,9 +1032,27 @@ public final class WiredEngine {
if (effects.isEmpty()) return Collections.emptyList();
List<InteractionWiredEffect> executedSelectors = new ArrayList<>();
List<IWiredEffect> immediateSelectors = new ArrayList<>();
List<IWiredEffect> deferredSelectors = new ArrayList<>();
for (IWiredEffect effect : effects) {
if (!effect.isSelector()) continue;
if (effect.usesExistingSelectorTargets()) {
deferredSelectors.add(effect);
} else {
immediateSelectors.add(effect);
}
}
executeSelectorList(immediateSelectors, ctx, executedSelectors);
executeSelectorList(deferredSelectors, ctx, executedSelectors);
return executedSelectors;
}
private void executeSelectorList(List<IWiredEffect> selectors, WiredContext ctx, List<InteractionWiredEffect> executedSelectors) {
for (IWiredEffect effect : selectors) {
if (effect.requiresActor() && !ctx.hasActor()) {
continue;
}
@@ -1022,14 +1061,17 @@ public final class WiredEngine {
try {
effect.execute(ctx);
if (effect instanceof InteractionWiredEffect) {
executedSelectors.add((InteractionWiredEffect) effect);
InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect;
executedSelectors.add(wiredEffect);
if (wiredEffect.usesExistingSelectorTargets()) {
setFilteredSelectorState(ctx.room(), wiredEffect, "3");
}
}
} catch (Exception e) {
LOGGER.warn("Error executing selector: {}", e.getMessage());
}
}
return executedSelectors;
}
private void finalizeSelectors(List<InteractionWiredEffect> executedSelectors, WiredContext ctx, long currentTime) {
@@ -1042,7 +1084,56 @@ public final class WiredEngine {
for (InteractionWiredEffect wiredEffect : executedSelectors) {
wiredEffect.setCooldown(currentTime);
wiredEffect.activateBox(room, actor, currentTime);
if (wiredEffect.usesExistingSelectorTargets()) {
animateFilteredSelectorBox(room, wiredEffect);
} else {
wiredEffect.activateBox(room, actor, currentTime);
}
}
}
private void animateFilteredSelectorBox(Room room, InteractionWiredEffect wiredEffect) {
if (room == null || wiredEffect == null || room.isHideWired()) {
return;
}
long animationToken = System.nanoTime();
this.filteredSelectorAnimationTokens.put(wiredEffect.getId(), animationToken);
setFilteredSelectorState(room, wiredEffect, "3", animationToken, false);
scheduleFilteredSelectorState(room, wiredEffect, "4", animationToken, 80L, false);
scheduleFilteredSelectorState(room, wiredEffect, "5", animationToken, 160L, false);
scheduleFilteredSelectorState(room, wiredEffect, "3", animationToken, 240L, true);
}
private void scheduleFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state, long animationToken, long delay, boolean clearToken) {
Emulator.getThreading().run(() -> setFilteredSelectorState(room, wiredEffect, state, animationToken, clearToken), delay);
}
private void setFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state) {
setFilteredSelectorState(room, wiredEffect, state, 0L, false);
}
private void setFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state, long animationToken, boolean clearToken) {
if (room == null || wiredEffect == null || room.isHideWired()) {
return;
}
if (animationToken != 0L) {
Long currentToken = this.filteredSelectorAnimationTokens.get(wiredEffect.getId());
if (currentToken == null || currentToken != animationToken) {
return;
}
}
if (!state.equals(wiredEffect.getExtradata())) {
wiredEffect.setExtradata(state);
room.sendComposer(new ItemStateComposer(wiredEffect).compose());
}
if (clearToken) {
this.filteredSelectorAnimationTokens.remove(wiredEffect.getId(), animationToken);
}
}
@@ -1059,6 +1150,20 @@ public final class WiredEngine {
WiredSelectionFilterSupport.applySelectorFilters(room, stack.triggerItem(), ctx);
}
private boolean selectorsHaveRequiredTargets(List<InteractionWiredEffect> executedSelectors, WiredContext ctx) {
if (executedSelectors == null || executedSelectors.isEmpty()) {
return true;
}
for (InteractionWiredEffect selector : executedSelectors) {
if (!selector.hasRequiredSelectorTargets(ctx)) {
return false;
}
}
return true;
}
/**
* Schedule a delayed effect execution.
*/
@@ -151,8 +151,20 @@ public final class WiredSourceUtil {
selectorCtx.setIncludeWiredSelectorItems(originalCtx.includeWiredSelectorItems());
List<InteractionWiredEffect> selectorEffects = getOrderedSelectorEffects(originalCtx, room, triggerItem);
executeSelectorEffects(selectorCtx, selectorEffects, false);
executeSelectorEffects(selectorCtx, selectorEffects, true);
applySelectionFilterExtras(room, triggerItem, selectorCtx);
return selectorCtx;
}
private static void executeSelectorEffects(WiredContext selectorCtx, List<InteractionWiredEffect> selectorEffects, boolean deferred) {
for (InteractionWiredEffect effect : selectorEffects) {
if (effect == null || effect.usesExistingSelectorTargets() != deferred) {
continue;
}
if (effect.requiresActor() && !selectorCtx.hasActor()) {
continue;
}
@@ -163,10 +175,6 @@ public final class WiredSourceUtil {
} catch (Exception ignored) {
}
}
applySelectionFilterExtras(room, triggerItem, selectorCtx);
return selectorCtx;
}
private static WiredContext cloneSelectorContext(WiredContext originalCtx, boolean includeWiredItems) {
@@ -64,8 +64,17 @@ public final class WiredTextInputCaptureSupport {
return trigger.matches(stack.triggerItem(), event) ? CaptureResult.matched(new LinkedHashMap<>()) : CaptureResult.noMatch();
}
MatchResult matchResult = matchTemplate(trigger, text, capturersByName);
MatchResult matchResult = matchTemplate(trigger, text, capturersByName, room);
if (!matchResult.matches) {
if (WiredManager.isDebugEnabled()) {
WiredManager.debug("[TextCapture] NO_MATCH room={} triggerId={} mode={} key='{}' text='{}' len={}",
room.getId(),
stack.triggerItem().getId(),
trigger.getMatchMode(),
safeForLog(trigger.getKey()),
safeForLog(text),
(text != null ? text.length() : 0));
}
return CaptureResult.noMatch();
}
@@ -78,12 +87,28 @@ public final class WiredTextInputCaptureSupport {
Integer resolvedValue = capturer.resolveCapturedValue(room, capture.getValue());
if (resolvedValue == null) {
if (WiredManager.isDebugEnabled()) {
WiredManager.debug("[TextCapture] RESOLVE_FAIL room={} triggerId={} capturer='{}' raw='{}' rawLen={}",
room.getId(),
stack.triggerItem().getId(),
capture.getKey(),
safeForLog(capture.getValue()),
(capture.getValue() != null ? capture.getValue().length() : 0));
}
return CaptureResult.noMatch();
}
capturedValues.put(capturer.getVariableItemId(), resolvedValue);
}
if (WiredManager.isDebugEnabled()) {
WiredManager.debug("[TextCapture] MATCH_OK room={} triggerId={} captures={} textLen={}",
room.getId(),
stack.triggerItem().getId(),
capturedValues.size(),
(text != null ? text.length() : 0));
}
return CaptureResult.matched(capturedValues);
}
@@ -108,12 +133,13 @@ public final class WiredTextInputCaptureSupport {
return capturers;
}
private static MatchResult matchTemplate(WiredTriggerHabboSaysKeyword trigger, String rawText, Map<String, WiredExtraTextInputVariable> capturersByName) {
String text = rawText != null ? rawText.trim() : "";
private static MatchResult matchTemplate(WiredTriggerHabboSaysKeyword trigger, String rawText, Map<String, WiredExtraTextInputVariable> capturersByName, Room room) {
String text = rawText != null ? rawText : "";
String normalizedText = text.trim();
String template = trigger.getKey() != null ? trigger.getKey().trim() : "";
if (trigger.getMatchMode() == MATCH_ALL_WORDS && template.isEmpty()) {
if (capturersByName.size() != 1 || text.isEmpty()) {
if (capturersByName.size() != 1 || normalizedText.isEmpty()) {
return MatchResult.noMatch();
}
@@ -123,12 +149,24 @@ public final class WiredTextInputCaptureSupport {
return MatchResult.matched(captures);
}
MatchResult adjacentCaptureResult = matchAdjacentCapturers(template, rawText, capturersByName, room, trigger.getMatchMode());
if (adjacentCaptureResult != null) {
if (WiredManager.isDebugEnabled()) {
WiredManager.debug("[TextCapture] ADJACENT mode used key='{}' textLen={} matched={}",
safeForLog(template),
(rawText != null ? rawText.length() : 0),
adjacentCaptureResult.matches);
}
return adjacentCaptureResult;
}
TemplatePattern pattern = buildPattern(template);
if (pattern == null) {
return MatchResult.noMatch();
}
Matcher matcher = pattern.pattern.matcher(text);
String matchText = pattern.placeholderNames.isEmpty() ? normalizedText : text;
Matcher matcher = pattern.pattern.matcher(matchText);
boolean matches = (trigger.getMatchMode() == MATCH_CONTAINS) ? matcher.find() : matcher.matches();
if (!matches) {
return MatchResult.noMatch();
@@ -142,12 +180,136 @@ public final class WiredTextInputCaptureSupport {
}
String capturedValue = matcher.group(index + 1);
captures.put(placeholderName, capturedValue != null ? capturedValue.trim() : "");
captures.put(placeholderName, normalizeCapturedValue(capturedValue));
}
return MatchResult.matched(captures);
}
private static MatchResult matchAdjacentCapturers(String template, String rawText, Map<String, WiredExtraTextInputVariable> capturersByName, Room room, int matchMode) {
if (template == null || template.isEmpty() || rawText == null || capturersByName == null || capturersByName.isEmpty() || room == null) {
return null;
}
Matcher matcher = PLACEHOLDER_PATTERN.matcher(template);
List<String> placeholderNames = new ArrayList<>();
int cursor = 0;
while (matcher.find()) {
if (matcher.start() != cursor) {
return null;
}
String placeholderName = matcher.group(1) != null ? matcher.group(1).trim().toLowerCase() : "";
if (placeholderName.isEmpty() || !capturersByName.containsKey(placeholderName)) {
return null;
}
placeholderNames.add(placeholderName);
cursor = matcher.end();
}
if (placeholderNames.isEmpty() || cursor != template.length()) {
return null;
}
int placeholderCount = placeholderNames.size();
int textLength = rawText.length();
boolean[][] reachable = new boolean[placeholderCount + 1][textLength + 1];
int[][] previousIndex = new int[placeholderCount + 1][textLength + 1];
String[][] capturedValues = new String[placeholderCount + 1][textLength + 1];
for (int placeholderIndex = 0; placeholderIndex <= placeholderCount; placeholderIndex++) {
for (int textIndex = 0; textIndex <= textLength; textIndex++) {
previousIndex[placeholderIndex][textIndex] = -1;
}
}
reachable[0][0] = true;
for (int placeholderIndex = 0; placeholderIndex < placeholderCount; placeholderIndex++) {
String placeholderName = placeholderNames.get(placeholderIndex);
WiredExtraTextInputVariable capturer = capturersByName.get(placeholderName);
if (capturer == null) {
return MatchResult.noMatch();
}
for (int textIndex = 0; textIndex <= textLength; textIndex++) {
if (!reachable[placeholderIndex][textIndex]) {
continue;
}
int minEndIndex = (textIndex < textLength) ? (textIndex + 1) : textIndex;
for (int endIndex = minEndIndex; endIndex <= textLength; endIndex++) {
if (reachable[placeholderIndex + 1][endIndex]) {
continue;
}
String candidate = rawText.substring(textIndex, endIndex);
if (capturer.resolveCapturedValue(room, candidate) == null) {
continue;
}
reachable[placeholderIndex + 1][endIndex] = true;
previousIndex[placeholderIndex + 1][endIndex] = textIndex;
capturedValues[placeholderIndex + 1][endIndex] = candidate;
}
}
}
int resultEndIndex = -1;
if (matchMode == MATCH_CONTAINS) {
for (int endIndex = textLength; endIndex >= 0; endIndex--) {
if (reachable[placeholderCount][endIndex]) {
resultEndIndex = endIndex;
break;
}
}
} else if (reachable[placeholderCount][textLength]) {
resultEndIndex = textLength;
}
if (resultEndIndex < 0) {
return MatchResult.noMatch();
}
LinkedHashMap<String, String> captures = new LinkedHashMap<>();
int backtrackTextIndex = resultEndIndex;
for (int placeholderIndex = placeholderCount; placeholderIndex > 0; placeholderIndex--) {
String placeholderName = placeholderNames.get(placeholderIndex - 1);
String capturedValue = capturedValues[placeholderIndex][backtrackTextIndex];
captures.put(placeholderName, capturedValue != null ? capturedValue : "");
backtrackTextIndex = previousIndex[placeholderIndex][backtrackTextIndex];
if (backtrackTextIndex < 0) {
return MatchResult.noMatch();
}
}
return MatchResult.matched(captures);
}
private static String normalizeCapturedValue(String value) {
return value != null ? value : "";
}
private static String safeForLog(String value) {
if (value == null) {
return "";
}
String normalized = value
.replace("\r", "\\r")
.replace("\n", "\\n")
.replace("\u00A0", "");
if (normalized.length() > 180) {
return normalized.substring(0, 180) + "...(" + normalized.length() + ")";
}
return normalized;
}
private static TemplatePattern buildPattern(String template) {
if (template == null || template.isEmpty()) {
return null;
@@ -160,7 +322,7 @@ public final class WiredTextInputCaptureSupport {
while (matcher.find()) {
regex.append(Pattern.quote(template.substring(cursor, matcher.start())));
regex.append("(.+?)");
regex.append(hasPlaceholderAfter(template, matcher.end()) ? "(.+?)" : "(.+)");
String placeholderName = matcher.group(1) != null ? matcher.group(1).trim().toLowerCase() : "";
placeholderNames.add(placeholderName);
@@ -176,6 +338,10 @@ public final class WiredTextInputCaptureSupport {
return new TemplatePattern(Pattern.compile(regex.toString(), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE), placeholderNames);
}
private static boolean hasPlaceholderAfter(String template, int cursor) {
return PLACEHOLDER_PATTERN.matcher(template.substring(cursor)).find();
}
public static void applyToContext(WiredContext ctx, Room room, CaptureResult captureResult) {
if (ctx == null || room == null || captureResult == null || !captureResult.matches || captureResult.capturedValues.isEmpty()) {
return;
@@ -32,6 +32,8 @@ import java.util.List;
import java.util.Locale;
public final class WiredTextPlaceholderUtil {
private static final char PRESERVED_SPACE = '\u00A0';
private WiredTextPlaceholderUtil() {
}
@@ -87,7 +89,41 @@ public final class WiredTextPlaceholderUtil {
}
}
return resolvedText;
return preserveRepeatedSpaces(resolvedText);
}
private static String preserveRepeatedSpaces(String text) {
if (text == null || text.length() < 2) {
return text;
}
StringBuilder result = new StringBuilder(text.length());
int index = 0;
while (index < text.length()) {
char currentChar = text.charAt(index);
if (currentChar != ' ') {
result.append(currentChar);
index++;
continue;
}
int startIndex = index;
while (index < text.length() && text.charAt(index) == ' ') {
index++;
}
int spaceCount = index - startIndex;
if (spaceCount == 1) {
result.append(' ');
continue;
}
for (int spaceIndex = 0; spaceIndex < spaceCount; spaceIndex++) {
result.append(PRESERVED_SPACE);
}
}
return result.toString();
}
public static boolean requiresActor(Room room, HabboItem stackItem) {
@@ -275,7 +311,7 @@ public final class WiredTextPlaceholderUtil {
}
String value = resolveRoomVariableValue(room, extra);
return (value == null || value.isEmpty()) ? List.of() : List.of(value);
return value == null ? List.of() : List.of(value);
}
private static List<String> collectContextVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) {
@@ -284,7 +320,7 @@ public final class WiredTextPlaceholderUtil {
}
String value = resolveContextVariableValue(ctx, extra);
return (value == null || value.isEmpty()) ? List.of() : List.of(value);
return value == null ? List.of() : List.of(value);
}
private static String resolveUserVariableValue(Room room, RoomUnit roomUnit, WiredExtraTextOutputVariable extra) {
@@ -11,6 +11,8 @@ import java.util.List;
import java.util.Map;
public final class WiredVariableTextConnectorSupport {
private static final String PRESERVED_SPACE = "\u00A0";
private WiredVariableTextConnectorSupport() {
}
@@ -71,7 +73,7 @@ public final class WiredVariableTextConnectorSupport {
Map<Integer, String> mappings = connector.getMappings();
if (mappings.containsKey(value)) {
String mappedValue = mappings.get(value);
return mappedValue != null ? mappedValue : String.valueOf(value);
return mappedValue != null ? preserveSpaces(mappedValue) : "";
}
}
@@ -83,10 +85,7 @@ public final class WiredVariableTextConnectorSupport {
return null;
}
String normalizedText = text.trim();
if (normalizedText.isEmpty()) {
return null;
}
String normalizedText = normalizePreservedSpaces(text);
for (WiredExtraVariableTextConnector connector : getConnectors(room, definitionItemId)) {
Integer mappedValue = connector.resolveValue(normalizedText);
@@ -97,4 +96,12 @@ public final class WiredVariableTextConnectorSupport {
return null;
}
private static String preserveSpaces(String value) {
return value.replace(" ", PRESERVED_SPACE);
}
private static String normalizePreservedSpaces(String value) {
return value.replace(PRESERVED_SPACE, " ");
}
}
@@ -36,6 +36,7 @@ import com.eu.habbo.messages.incoming.helper.MySanctionStatusEvent;
import com.eu.habbo.messages.incoming.helper.RequestTalentTrackEvent;
import com.eu.habbo.messages.incoming.hotelview.*;
import com.eu.habbo.messages.incoming.inventory.*;
import com.eu.habbo.messages.incoming.inventory.nickicons.*;
import com.eu.habbo.messages.incoming.inventory.prefixes.*;
import com.eu.habbo.messages.incoming.modtool.*;
import com.eu.habbo.messages.incoming.navigator.*;
@@ -61,6 +62,8 @@ import com.eu.habbo.messages.incoming.rooms.promotions.RequestPromotionRoomsEven
import com.eu.habbo.messages.incoming.rooms.promotions.UpdateRoomPromotionEvent;
import com.eu.habbo.messages.incoming.rooms.users.*;
import com.eu.habbo.messages.incoming.trading.*;
import com.eu.habbo.messages.incoming.translation.TranslationLanguagesRequestEvent;
import com.eu.habbo.messages.incoming.translation.TranslationTextRequestEvent;
import com.eu.habbo.messages.incoming.unknown.RequestResolutionEvent;
import com.eu.habbo.messages.incoming.unknown.UnknownEvent1;
import com.eu.habbo.messages.incoming.users.*;
@@ -117,6 +120,7 @@ public class PacketManager {
this.registerGuilds();
this.registerPets();
this.registerWired();
this.registerTranslation();
this.registerAchievements();
this.registerFloorPlanEditor();
this.registerAmbassadors();
@@ -409,6 +413,13 @@ public class PacketManager {
this.registerHandler(Incoming.SetActivePrefixEvent, SetActivePrefixEvent.class);
this.registerHandler(Incoming.DeletePrefixEvent, DeletePrefixEvent.class);
this.registerHandler(Incoming.PurchasePrefixEvent, PurchasePrefixEvent.class);
this.registerHandler(Incoming.PurchaseCatalogPrefixEvent, PurchaseCatalogPrefixEvent.class);
this.registerHandler(Incoming.SetDisplayOrderEvent, SetDisplayOrderEvent.class);
// Nick Icons
this.registerHandler(Incoming.RequestUserNickIconsEvent, RequestUserNickIconsEvent.class);
this.registerHandler(Incoming.PurchaseNickIconEvent, PurchaseNickIconEvent.class);
this.registerHandler(Incoming.SetActiveNickIconEvent, SetActiveNickIconEvent.class);
}
void registerRooms() throws Exception {
@@ -635,6 +646,11 @@ public class PacketManager {
this.registerHandler(Incoming.WiredUserInspectMoveEvent, WiredUserInspectMoveEvent.class);
}
void registerTranslation() throws Exception {
this.registerHandler(Incoming.TranslationLanguagesRequestEvent, TranslationLanguagesRequestEvent.class);
this.registerHandler(Incoming.TranslationTextRequestEvent, TranslationTextRequestEvent.class);
}
void registerUnknown() throws Exception {
this.registerHandler(Incoming.RequestResolutionEvent, RequestResolutionEvent.class);
this.registerHandler(Incoming.RequestTalenTrackEvent, RequestTalentTrackEvent.class);
@@ -419,6 +419,8 @@ public class Incoming {
public static final int WiredUserVariableUpdateEvent = 10025;
public static final int WiredUserVariableManageEvent = 10026;
public static final int WiredUserInspectMoveEvent = 10027;
public static final int TranslationLanguagesRequestEvent = 10032;
public static final int TranslationTextRequestEvent = 10033;
public static final int RequestInventoryPetDelete = 10030;
public static final int RequestInventoryBadgeDelete = 10031;
@@ -448,6 +450,11 @@ public class Incoming {
public static final int SetActivePrefixEvent = 7012;
public static final int DeletePrefixEvent = 7013;
public static final int PurchasePrefixEvent = 7014;
public static final int RequestUserNickIconsEvent = 7015;
public static final int PurchaseNickIconEvent = 7016;
public static final int SetActiveNickIconEvent = 7017;
public static final int PurchaseCatalogPrefixEvent = 7018;
public static final int SetDisplayOrderEvent = 7019;
// YouTube Room Broadcast
public static final int YouTubeRoomPlayEvent = 8001;
@@ -13,107 +13,317 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
/**
* Manages reading and writing of FurnitureData.json entries.
* Resolves the file path from emulator config keys.
* Manages reading and writing of FurnitureData entries.
*
* Accepts both legacy single-file layouts (FurnitureData.json) and the split
* directory layout introduced by the split-aware loader on the Nitro V3 side:
*
* <base>/
* manifest.json5 OPTIONAL { "tiers": ["core", "custom", "seasonal"] }
* core/manifest.json5 REQUIRED { "files": ["floor-001.json5", ...] }
* core/*.json5
* custom/manifest.json5 OPTIONAL
* seasonal/manifest.json5 OPTIONAL
*
* The path is resolved from the emulator config:
*
* furni.editor.renderer.config.path -> renderer-config.json (read for the
* furnidata.url value)
* furni.editor.asset.base.path -> filesystem base used to derive the
* local path from an http(s) URL
*/
public class FurniDataManager {
private static final Logger LOGGER = LoggerFactory.getLogger(FurniDataManager.class);
private static final List<String> DEFAULT_TIERS = Arrays.asList("core", "custom", "seasonal");
private static final List<String> MANIFEST_NAMES = Arrays.asList("manifest.json5", "manifest.json");
private static final List<String> SECTIONS = Arrays.asList("roomitemtypes", "wallitemtypes");
/**
* Get the JSON string for a specific item from FurnitureData.json.
* Get the JSON string for a specific item.
* Returns "{}" if not found or on error.
*/
public static String getItemJson(int itemId) {
try {
Path furniDataPath = resolveFurniDataPath();
if (furniDataPath == null || !Files.exists(furniDataPath)) {
return "{}";
ResolvedSource source = resolveSource();
if (source == null) return "{}";
if (source.directory) {
return findItemInSplitDir(source.path, itemId);
}
String content = Files.readString(furniDataPath, StandardCharsets.UTF_8);
JsonObject root = JsonParser.parseString(content).getAsJsonObject();
if (!Files.exists(source.path)) return "{}";
// Search in both "roomitemtypes" and "wallitemtypes"
for (String section : new String[]{"roomitemtypes", "wallitemtypes"}) {
if (!root.has(section)) continue;
JsonObject sectionObj = root.getAsJsonObject(section);
if (!sectionObj.has("furnitype")) continue;
JsonArray types = sectionObj.getAsJsonArray("furnitype");
for (JsonElement el : types) {
JsonObject obj = el.getAsJsonObject();
if (obj.has("id") && obj.get("id").getAsInt() == itemId) {
return obj.toString();
}
}
}
String content = readJson5(source.path);
return findItemInRoot(JsonParser.parseString(content).getAsJsonObject(), itemId);
} catch (Exception e) {
LOGGER.warn("Failed to read FurnitureData.json for item " + itemId, e);
LOGGER.warn("Failed to read FurnitureData for item " + itemId, e);
}
return "{}";
}
private static String findItemInRoot(JsonObject root, int itemId) {
for (String section : SECTIONS) {
if (!root.has(section)) continue;
JsonObject sectionObj = root.getAsJsonObject(section);
if (!sectionObj.has("furnitype")) continue;
JsonArray types = sectionObj.getAsJsonArray("furnitype");
for (JsonElement el : types) {
JsonObject obj = el.getAsJsonObject();
if (obj.has("id") && obj.get("id").getAsInt() == itemId) {
return obj.toString();
}
}
}
return null;
}
/**
* Resolve the path to FurnitureData.json from emulator config.
* Walk the split directory layout looking for an item by id.
* Later tiers (custom, then seasonal) override earlier ones.
*/
private static Path resolveFurniDataPath() {
private static String findItemInSplitDir(Path baseDir, int itemId) {
if (!Files.isDirectory(baseDir)) return "{}";
List<String> tiers = readTiersManifest(baseDir);
String found = null;
for (String tier : tiers) {
Path tierDir = baseDir.resolve(tier);
if (!Files.isDirectory(tierDir)) continue;
List<String> files = readFilesManifest(tierDir);
for (String fileName : files) {
Path file = tierDir.resolve(fileName);
if (!Files.exists(file)) continue;
try {
String content = readJson5(file);
JsonObject obj = JsonParser.parseString(content).getAsJsonObject();
String match = findItemInRoot(obj, itemId);
if (match != null) found = match;
} catch (Exception e) {
LOGGER.warn("Failed to parse split gamedata file " + file, e);
}
}
}
return found != null ? found : "{}";
}
@SuppressWarnings("unchecked")
private static List<String> readTiersManifest(Path baseDir) {
Path manifest = firstExisting(baseDir, MANIFEST_NAMES);
if (manifest == null) return DEFAULT_TIERS;
try {
String content = readJson5(manifest);
JsonObject obj = JsonParser.parseString(content).getAsJsonObject();
if (obj.has("tiers") && obj.get("tiers").isJsonArray()) {
JsonArray arr = obj.getAsJsonArray("tiers");
List<String> out = new java.util.ArrayList<>();
for (JsonElement el : arr) out.add(el.getAsString());
if (!out.isEmpty()) return out;
}
} catch (Exception e) {
LOGGER.warn("Failed to read root manifest " + manifest + ", falling back to default tiers", e);
}
return DEFAULT_TIERS;
}
private static List<String> readFilesManifest(Path tierDir) {
Path manifest = firstExisting(tierDir, MANIFEST_NAMES);
if (manifest == null) return java.util.Collections.emptyList();
try {
String content = readJson5(manifest);
JsonObject obj = JsonParser.parseString(content).getAsJsonObject();
if (obj.has("files") && obj.get("files").isJsonArray()) {
JsonArray arr = obj.getAsJsonArray("files");
List<String> out = new java.util.ArrayList<>();
for (JsonElement el : arr) out.add(el.getAsString());
return out;
}
} catch (Exception e) {
LOGGER.warn("Failed to read tier manifest " + manifest, e);
}
return java.util.Collections.emptyList();
}
private static Path firstExisting(Path dir, List<String> names) {
for (String name : names) {
Path p = dir.resolve(name);
if (Files.exists(p)) return p;
}
return null;
}
/**
* Read a JSON or JSON5 file. Strips line and block comments and trailing
* commas so Gson can parse the result. String contents are preserved
* verbatim; comments embedded inside strings are not removed.
*/
private static String readJson5(Path path) throws IOException {
String raw = Files.readString(path, StandardCharsets.UTF_8);
return stripJson5(raw);
}
static String stripJson5(String content) {
if (content == null || content.isEmpty()) return content;
StringBuilder out = new StringBuilder(content.length());
int i = 0;
int len = content.length();
boolean inString = false;
char stringChar = 0;
boolean escape = false;
while (i < len) {
char c = content.charAt(i);
if (inString) {
out.append(c);
if (escape) {
escape = false;
} else if (c == '\\') {
escape = true;
} else if (c == stringChar) {
inString = false;
}
i++;
continue;
}
if (c == '"' || c == '\'') {
inString = true;
stringChar = c;
out.append(c);
i++;
continue;
}
if (c == '/' && i + 1 < len) {
char next = content.charAt(i + 1);
if (next == '/') {
int eol = content.indexOf('\n', i + 2);
if (eol < 0) { i = len; break; }
i = eol;
continue;
}
if (next == '*') {
int end = content.indexOf("*/", i + 2);
if (end < 0) { i = len; break; }
i = end + 2;
continue;
}
}
out.append(c);
i++;
}
String stripped = out.toString();
// Remove trailing commas before } or ]
stripped = stripped.replaceAll(",(\\s*[}\\]])", "$1");
return stripped;
}
/**
* Represents the resolved location of the furnidata source: either a single
* file or a directory in split-layout mode.
*/
private static class ResolvedSource {
final Path path;
final boolean directory;
ResolvedSource(Path path, boolean directory) {
this.path = path;
this.directory = directory;
}
}
/**
* Resolve the location of the furnidata source. Returns null if no
* candidate can be found.
*/
private static ResolvedSource resolveSource() {
try {
String configPath = Emulator.getConfig().getValue("furni.editor.renderer.config.path", "");
if (configPath.isEmpty()) {
// Fallback: try common locations
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
if (!basePath.isEmpty()) {
Path candidate = Paths.get(basePath, "FurnitureData.json");
if (Files.exists(candidate)) return candidate;
}
return null;
Path fallback = fallbackToBasePath();
return fallback != null ? new ResolvedSource(fallback, Files.isDirectory(fallback)) : null;
}
// Read the renderer config to find the furnidata URL/path
Path rendererConfig = Paths.get(configPath);
if (!Files.exists(rendererConfig)) return null;
String rendererContent = Files.readString(rendererConfig, StandardCharsets.UTF_8);
String rendererContent = readJson5(rendererConfig);
JsonObject rendererObj = JsonParser.parseString(rendererContent).getAsJsonObject();
if (rendererObj.has("furnidata.url")) {
String furniUrl = rendererObj.get("furnidata.url").getAsString();
if (!rendererObj.has("furnidata.url")) return null;
// Skip unresolved placeholders like ${gamedata.url}
if (furniUrl.contains("${")) {
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
if (!basePath.isEmpty()) {
Path candidate = Paths.get(basePath, "FurnitureData.json");
if (Files.exists(candidate)) return candidate;
}
return null;
}
String furniUrl = rendererObj.get("furnidata.url").getAsString();
// Strip query string (?v=1 etc.)
String cleanUrl = furniUrl.contains("?") ? furniUrl.substring(0, furniUrl.indexOf('?')) : furniUrl;
// If it's a local file path (not http), use it directly
if (!cleanUrl.startsWith("http")) {
return Paths.get(cleanUrl);
}
// For http URLs, try to derive local path from base path
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
if (!basePath.isEmpty()) {
// Extract filename from URL (without query string)
String filename = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1);
return Paths.get(basePath, filename);
}
if (furniUrl.contains("${")) {
Path fallback = fallbackToBasePath();
return fallback != null ? new ResolvedSource(fallback, Files.isDirectory(fallback)) : null;
}
// Strip query string and fragment (e.g. ?v=123 or #anchor)
String cleanUrl = furniUrl;
int q = cleanUrl.indexOf('?');
if (q >= 0) cleanUrl = cleanUrl.substring(0, q);
int h = cleanUrl.indexOf('#');
if (h >= 0) cleanUrl = cleanUrl.substring(0, h);
boolean splitMode = cleanUrl.endsWith("/");
// Local file path (not http) return as-is, the caller will check
// whether it points at a file or a directory.
if (!cleanUrl.startsWith("http")) {
Path local = Paths.get(cleanUrl);
return new ResolvedSource(local, splitMode || Files.isDirectory(local));
}
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
if (basePath.isEmpty()) return null;
if (splitMode) {
// Derive the directory name from the URL: take the last non-empty
// segment before the trailing slash. e.g. https://x/y/furnidata/ -> "furnidata"
String trimmed = cleanUrl.endsWith("/") ? cleanUrl.substring(0, cleanUrl.length() - 1) : cleanUrl;
String dirName = trimmed.substring(trimmed.lastIndexOf('/') + 1);
Path candidate = Paths.get(basePath, dirName);
return new ResolvedSource(candidate, true);
}
String filename = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1);
Path candidate = Paths.get(basePath, filename);
return new ResolvedSource(candidate, false);
} catch (Exception e) {
LOGGER.warn("Failed to resolve FurnitureData.json path", e);
LOGGER.warn("Failed to resolve FurnitureData source", e);
}
return null;
}
private static Path fallbackToBasePath() {
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
if (basePath.isEmpty()) return null;
Path dir = Paths.get(basePath);
// Prefer the split layout if it exists, then the legacy file.
Path splitCandidate = dir.resolve("furnidata");
if (Files.isDirectory(splitCandidate)) return splitCandidate;
Path legacy = dir.resolve("FurnitureData.json");
if (Files.exists(legacy)) return legacy;
return null;
}
}

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