You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
Compare commits
274 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 510e0d082e | |||
| e13c7fdbb6 | |||
| 2a28fbd2e5 | |||
| cd60cba355 | |||
| e62f461962 | |||
| 7f8c98e4f3 | |||
| d95e09e64f | |||
| ebe0690e46 | |||
| 0dda0ae0f7 | |||
| 54ab6613f1 | |||
| 9fda766ba5 | |||
| 3da9325344 | |||
| 770739c256 | |||
| 3ec468993a | |||
| 0e0f1cbb15 | |||
| daeda761cd | |||
| 0906048a3a | |||
| 19cde45d3e | |||
| 8161e3d7e5 | |||
| 5c0f2d2855 | |||
| d984461cc0 | |||
| 61ea33ac28 | |||
| b6ee400b83 | |||
| 62104596ac | |||
| fad6be158a | |||
| a9f1903465 | |||
| af82352f24 | |||
| dcc23ba744 | |||
| f7556138aa | |||
| a0910d822c | |||
| 4eb1484daf | |||
| 45d01876c1 | |||
| 1c4449fb88 | |||
| 373d0399c1 | |||
| 01c17c0511 | |||
| d1570d3574 | |||
| c98d3a3205 | |||
| da1fd01074 | |||
| f7bd452cb0 | |||
| 48fcd3f78b | |||
| 1275254fa0 | |||
| 7ed7a1ec5a | |||
| d383c43bbf | |||
| 44bfcc49b4 | |||
| b0ffb64cb2 | |||
| 1f4eef8e2e | |||
| bfc6ff21a5 | |||
| ea88934e9e | |||
| bb4b9fb7f4 | |||
| 84d7968b76 | |||
| f5bf4baa79 | |||
| 4a02d22061 | |||
| 14854efaeb | |||
| 564c8d647e | |||
| 0e7138a721 | |||
| 76eb1ecd05 | |||
| 4621ed62b7 | |||
| 2b8ce3cd91 | |||
| 57c36da795 | |||
| 17629c210c | |||
| 50444003bb | |||
| f55b182d8e | |||
| 1416cd7464 | |||
| 392d24b9c5 | |||
| 9dcd58d027 | |||
| 3b85d5fa34 | |||
| 43c2c2b0f1 | |||
| a815c1b99d | |||
| caf6ad35fa | |||
| 258a95a269 | |||
| 4944d41410 | |||
| 8fb117ae73 | |||
| 7f4f7d6da9 | |||
| 0cf46471f2 | |||
| 3a505cd559 | |||
| f2e0f6e2d5 | |||
| d73573e7c5 | |||
| efb88e5957 | |||
| e7e75a285b | |||
| 28c3e93945 | |||
| 5bf1d42cfb | |||
| b162b3f4d8 | |||
| 86498b6b4c | |||
| 964f388594 | |||
| f9644d83b7 | |||
| 0b142d184c | |||
| 867c8ff857 | |||
| 5094d6ce4f | |||
| 2c0ef9873c | |||
| dadc1b8aaf | |||
| 85758b53fa | |||
| 2171b5f2ec | |||
| 46306c8205 | |||
| fadec887cd | |||
| e614c1d64f | |||
| e7deea7d9d | |||
| 44ea3abd4e | |||
| 609cd20ab2 | |||
| 717a7f184f | |||
| 2862446686 | |||
| e97e680006 | |||
| 7e59dca192 | |||
| 1109d53720 | |||
| f12363a5b7 | |||
| 7d4ffec74e | |||
| 281fede58c | |||
| edf152485b | |||
| 18a1bfbe90 | |||
| 7c32bbfd2d | |||
| 4eae206b64 | |||
| 155b2202c7 | |||
| 10c291eb9f | |||
| 349a8c727e | |||
| 68f2b71d14 | |||
| 69a6c0d060 | |||
| 9f36d95dbc | |||
| 885bdca0c4 | |||
| db035294a7 | |||
| 3216ba1df6 | |||
| c9a47b1fac | |||
| 8d6b969d75 | |||
| b9723e0298 | |||
| c4aae676b2 | |||
| 7624d3fbc3 | |||
| 585f4dd3aa | |||
| afa114d511 | |||
| e9129576a9 | |||
| 0aadd01493 | |||
| 9d98fbf9ee | |||
| b38274e134 | |||
| 02ab30180c | |||
| da63439d53 | |||
| bf1a29a6e8 | |||
| 6391d721ff | |||
| dfea6bcf83 | |||
| a7f207bb76 | |||
| b7915884b6 | |||
| 478f7bdba0 | |||
| c255f1e1b4 | |||
| 9c831a9da4 | |||
| 08d1ae97a7 | |||
| f8fe1e3e22 | |||
| be77cdf4aa | |||
| 1ba2e43d4d | |||
| 8dd5155562 | |||
| 4f4f581371 | |||
| 9705b3e42a | |||
| e626a7fc50 | |||
| d6ebb632e6 | |||
| 014ca9ca48 | |||
| d189d66f9e | |||
| c272a36cc5 | |||
| 1d6e05ee57 | |||
| ea44771d69 | |||
| 1da783aff9 | |||
| e772686c4b | |||
| a00f7b01f5 | |||
| 6b4089cace | |||
| 9ea7acf05c | |||
| bab43af41e | |||
| 10a2b2b872 | |||
| 458b37dbed | |||
| 55b38e7b85 | |||
| 4a96c5baaf | |||
| 539c5b5b96 | |||
| 7b7154e68f | |||
| 4aabb738a3 | |||
| 691dc42627 | |||
| 226873c1fb | |||
| a06a204b39 | |||
| e213609609 | |||
| 44d38b8661 | |||
| ccadb81970 | |||
| 0a3a940946 | |||
| 4613fbe80c | |||
| 9328f4a355 | |||
| da8b947ddf | |||
| b9658d0407 | |||
| 68d3731393 | |||
| 4ef4ed1a96 | |||
| c20e273a2c | |||
| 83d418e712 | |||
| 38d05e8a06 | |||
| c83a3bad8e | |||
| 0bd23ad244 | |||
| d8f74b2477 | |||
| dac09e92d1 | |||
| fbf979419e | |||
| 6126c35779 | |||
| a1749c9eda | |||
| 525c124fa5 | |||
| 57087a31f2 | |||
| c4b3295a45 | |||
| 418c753e6c | |||
| 8419f11883 | |||
| 1a0d783ff7 | |||
| 655e039df7 | |||
| 7726691cde | |||
| 67503aeb2a | |||
| b206b32748 | |||
| ad60861a3f | |||
| b77290f5e7 | |||
| b14730d37f | |||
| 9126396973 | |||
| d321ff3b85 | |||
| 7f38a25eef | |||
| 4820ab15f3 | |||
| 8d989e7a19 | |||
| 1f7ec96e1c | |||
| 969f177108 | |||
| e485c2747c | |||
| d99a51899b | |||
| 29677a19be | |||
| 21ee36e089 | |||
| 4e47dbee16 | |||
| e7ba4d0926 | |||
| 67d2f52f64 | |||
| 69d770b65e | |||
| 2492569e16 | |||
| 9c215bea6b | |||
| 7dc3581f8f | |||
| f38eb32eee | |||
| 222e356ff0 | |||
| c8022ccc45 | |||
| 9579833775 | |||
| 87ad289a54 | |||
| fd28af5f69 | |||
| 99c938b98f | |||
| 82d90418cd | |||
| 8b51be4940 | |||
| 54259f89bd | |||
| 272a9b9f42 | |||
| 9c94402f78 | |||
| 7271506262 | |||
| 09710fc5d6 | |||
| d958fbc0ab | |||
| dca405ffb5 | |||
| 4190fa96d4 | |||
| 033faaeab6 | |||
| 98326e11af | |||
| 0f2666916f | |||
| 46041eedfe | |||
| e334a3e0ac | |||
| 53b7dba185 | |||
| efb4997bdb | |||
| 7617f8483e | |||
| 4f9fa9fc93 | |||
| d1d8d14bec | |||
| 1909f6d3c1 | |||
| 8709a72b6e | |||
| c331da9fbe | |||
| f9a079da02 | |||
| 89eb989c26 | |||
| 47be392d8e | |||
| d9465a0a65 | |||
| 90314d00fe | |||
| 56c73b9d98 | |||
| e6093f959f | |||
| c854770561 | |||
| a0b59134ee | |||
| 67924289ac | |||
| 26326bcc0e | |||
| ee0613a480 | |||
| 37d7885663 | |||
| 59ce829fe0 | |||
| 9bad1eb3f6 | |||
| f51617d092 | |||
| 585af846c4 | |||
| dde2c4143c | |||
| 26999c254b | |||
| dd96523496 | |||
| 02f3ded44e | |||
| 8bbe8640b0 | |||
| 078fb3db60 |
@@ -0,0 +1,46 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- 020_auth_ticket_ttl.sql
|
||||||
|
--
|
||||||
|
-- Adds an explicit expiry timestamp to the SSO auth_ticket on `users`.
|
||||||
|
--
|
||||||
|
-- The CMS issuing the ticket is expected to populate auth_ticket_expires_at
|
||||||
|
-- (e.g. NOW() + INTERVAL 60 SECOND) on every login redirect. The emulator-
|
||||||
|
-- side SELECT queries that look up a user by auth_ticket have been changed to
|
||||||
|
--
|
||||||
|
-- WHERE auth_ticket = ?
|
||||||
|
-- AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW())
|
||||||
|
--
|
||||||
|
-- The NULL branch keeps backward-compatibility with CMS deployments that do
|
||||||
|
-- not populate the column yet: existing rows continue to authenticate the
|
||||||
|
-- same way they always did, and the TTL kicks in only once the CMS starts
|
||||||
|
-- writing the expiry value.
|
||||||
|
--
|
||||||
|
-- Idempotent: skips the ALTER if the column already exists.
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
SET @col_exists = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'users'
|
||||||
|
AND COLUMN_NAME = 'auth_ticket_expires_at'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @ddl = IF(@col_exists = 0,
|
||||||
|
'ALTER TABLE `users` ADD COLUMN `auth_ticket_expires_at` TIMESTAMP NULL DEFAULT NULL AFTER `auth_ticket`',
|
||||||
|
'SELECT ''auth_ticket_expires_at already present, skipping'' AS info'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
|
||||||
|
UPDATE emulator_settings SET `key`='ws.whitelist' WHERE `key`='websockets.whitelist';
|
||||||
|
UPDATE emulator_settings SET `key`='ws.host' WHERE `key`='ws.nitro.host';
|
||||||
|
UPDATE emulator_settings SET `key`='ws.port' WHERE `key`='ws.nitro.port';
|
||||||
|
INSERT IGNORE INTO emulator_settings (`key`, `value`)
|
||||||
|
VALUES ('ws.ip.header', 'X-Forwarded-For');
|
||||||
|
|
||||||
|
INSERT IGNORE INTO emulator_settings (`key`, `value`)
|
||||||
|
VALUES ('ws.enabled', 'true');
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN `background_border_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_id`;
|
||||||
|
|
||||||
|
ALTER TABLE infostand_backgrounds
|
||||||
|
CHANGE COLUMN `category` `category` ENUM('background', 'stand', 'overlay', 'card', 'border') NOT NULL ;
|
||||||
|
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `infostand_backgrounds` (`id`, `category`, `min_rank`, `is_hc_only`, `is_ambassador_only`) VALUES
|
||||||
|
(1, 'border', 1, 0, 0),
|
||||||
|
(2, 'border', 1, 0, 0),
|
||||||
|
(3, 'border', 1, 0, 0),
|
||||||
|
(4, 'border', 1, 0, 0),
|
||||||
|
(5, 'border', 1, 0, 0),
|
||||||
|
(6, 'border', 1, 0, 0),
|
||||||
|
(7, 'border', 1, 0, 0),
|
||||||
|
(8, 'border', 1, 0, 0),
|
||||||
|
(9, 'border', 1, 0, 0),
|
||||||
|
(10, 'border', 1, 0, 0),
|
||||||
|
(11, 'border', 1, 0, 0),
|
||||||
|
(12, 'border', 1, 0, 0),
|
||||||
|
(13, 'border', 1, 0, 0),
|
||||||
|
(14, 'border', 1, 0, 0),
|
||||||
|
(15, 'border', 1, 0, 0),
|
||||||
|
(16, 'border', 1, 0, 0),
|
||||||
|
(17, 'border', 1, 0, 0),
|
||||||
|
(18, 'border', 1, 0, 0),
|
||||||
|
(19, 'border', 1, 0, 0),
|
||||||
|
(20, 'border', 1, 0, 0),
|
||||||
|
(21, 'border', 1, 0, 0),
|
||||||
|
(22, 'border', 1, 0, 0),
|
||||||
|
(23, 'border', 1, 0, 0),
|
||||||
|
(24, 'border', 1, 0, 0),
|
||||||
|
(25, 'border', 1, 0, 0);
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Live required schema
|
||||||
|
-- ============================================================
|
||||||
|
-- Consolidated schema for the currently used Nitro/Arcturus live
|
||||||
|
-- additions. This file intentionally excludes old/unused migration
|
||||||
|
-- artifacts and dump-only data.
|
||||||
|
--
|
||||||
|
-- Scope:
|
||||||
|
-- - tables/columns currently referenced by Java code
|
||||||
|
-- - runtime settings required by secure assets/API, login, wired, and UI
|
||||||
|
-- - safe CREATE IF NOT EXISTS / ADD COLUMN IF NOT EXISTS statements
|
||||||
|
--
|
||||||
|
-- Assumes the base Arcturus database already exists.
|
||||||
|
-- Tested for MariaDB-style syntax used by this project.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- Core settings support
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
|
||||||
|
ALTER TABLE `emulator_settings`
|
||||||
|
ADD COLUMN IF NOT EXISTS `comment` TEXT NULL DEFAULT '' AFTER `value`;
|
||||||
|
|
||||||
|
ALTER TABLE catalog_pages
|
||||||
|
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL', 'BUILDER', 'BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `includes`;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `wired_emulator_settings` (
|
||||||
|
`key` VARCHAR(255) NOT NULL,
|
||||||
|
`value` TEXT NOT NULL,
|
||||||
|
`comment` TEXT NULL DEFAULT '',
|
||||||
|
PRIMARY KEY (`key`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
|
||||||
|
('crypto.ws.enabled', '0'),
|
||||||
|
('crypto.ws.signing.enabled', '0'),
|
||||||
|
('crypto.ws.signing.public_key', ''),
|
||||||
|
('crypto.ws.signing.private_key', ''),
|
||||||
|
('login.access.jwt.secret', ''),
|
||||||
|
('login.remember.duration.days', '30'),
|
||||||
|
('login.remember.rotate.interval.minutes', '15'),
|
||||||
|
('login.remember.jwt.secret', ''),
|
||||||
|
('login.turnstile.enabled', '0'),
|
||||||
|
('login.turnstile.sitekey', ''),
|
||||||
|
('login.turnstile.secretkey', ''),
|
||||||
|
('login.ratelimit.enabled', '1'),
|
||||||
|
('login.ratelimit.max_attempts', '5'),
|
||||||
|
('login.ratelimit.window_sec', '60'),
|
||||||
|
('login.ratelimit.lockout_sec', '120'),
|
||||||
|
('login.register.enabled', '1'),
|
||||||
|
('register.max_per_ip', '5'),
|
||||||
|
('register.default.look', 'hr-100-7.hd-180-1.ch-210-66.lg-270-82.sh-290-80'),
|
||||||
|
('register.default.motto', 'I love Habbo!'),
|
||||||
|
('password.reset.url', 'http://localhost/reset-password'),
|
||||||
|
('smtp.provider', 'own'),
|
||||||
|
('smtp.host', 'localhost'),
|
||||||
|
('smtp.port', '587'),
|
||||||
|
('smtp.username', ''),
|
||||||
|
('smtp.password', ''),
|
||||||
|
('smtp.from_address', 'no-reply@example.com'),
|
||||||
|
('smtp.from_name', 'Habbo Hotel'),
|
||||||
|
('smtp.use_tls', '1'),
|
||||||
|
('smtp.use_ssl', '0'),
|
||||||
|
('new_user_credits', '0'),
|
||||||
|
('new_user_duckets', '0'),
|
||||||
|
('new_user_diamonds', '0')
|
||||||
|
ON DUPLICATE KEY UPDATE `value` = `value`;
|
||||||
|
|
||||||
|
INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||||
|
('wired.engine.enabled', '1', 'Compatibility flag. The runtime uses the new wired engine.'),
|
||||||
|
('wired.engine.exclusive', '1', 'Compatibility flag. The runtime uses exclusive wired engine execution.'),
|
||||||
|
('wired.engine.maxStepsPerStack', '100', 'Maximum internal processing steps allowed for a single wired stack execution.'),
|
||||||
|
('wired.engine.debug', '0', 'Enable verbose debug logging for the wired engine.'),
|
||||||
|
('wired.custom.enabled', '0', 'Enable custom legacy wired compatibility behavior.'),
|
||||||
|
('hotel.wired.furni.selection.count', '5', 'Maximum number of furni that a wired box can store or select.'),
|
||||||
|
('hotel.wired.max_delay', '20', 'Maximum delay value accepted by wired effects that support delayed execution.'),
|
||||||
|
('hotel.wired.message.max_length', '512', 'Maximum length of wired message text fields.'),
|
||||||
|
('wired.effect.teleport.delay', '500', 'Delay in milliseconds used by wired teleport movement.'),
|
||||||
|
('wired.place.under', '0', 'Allow placing wired furniture underneath other items when room rules permit it.'),
|
||||||
|
('wired.tick.interval.ms', '50', 'Global wired tick interval in milliseconds.'),
|
||||||
|
('wired.tick.resolution', '100', 'Legacy wired tick resolution value.'),
|
||||||
|
('wired.tick.debug', '0', 'Enable verbose logging for the wired tick service.'),
|
||||||
|
('wired.tick.thread.priority', '6', 'Java thread priority used by the wired tick service.'),
|
||||||
|
('wired.highscores.displaycount', '25', 'Maximum number of wired highscore entries shown to users.'),
|
||||||
|
('wired.abuse.max.recursion.depth', '10', 'Maximum recursive wired depth before execution is stopped.'),
|
||||||
|
('wired.abuse.max.events.per.window', '100', 'Maximum identical wired events allowed inside the abuse rate-limit window.'),
|
||||||
|
('wired.abuse.rate.limit.window.ms', '10000', 'Wired abuse rate-limit window in milliseconds.'),
|
||||||
|
('wired.abuse.ban.duration.ms', '600000', 'Temporary wired ban duration after abuse detection.'),
|
||||||
|
('wired.monitor.usage.window.ms', '1000', 'Rolling window size for wired usage monitoring.'),
|
||||||
|
('wired.monitor.usage.limit', '1000', 'Maximum wired usage budget in one monitor window.'),
|
||||||
|
('wired.monitor.delayed.events.limit', '100', 'Maximum delayed wired events queued in one room.'),
|
||||||
|
('wired.monitor.overload.average.ms', '50', 'Average execution time threshold for overload tracking.'),
|
||||||
|
('wired.monitor.overload.peak.ms', '150', 'Peak execution time threshold for overload tracking.'),
|
||||||
|
('wired.monitor.overload.consecutive.windows', '2', 'Consecutive overloaded windows required before logging overload.'),
|
||||||
|
('wired.monitor.heavy.usage.percent', '70', 'Usage percentage threshold for heavy-room tracking.'),
|
||||||
|
('wired.monitor.heavy.consecutive.windows', '5', 'Consecutive windows above heavy usage threshold.'),
|
||||||
|
('wired.monitor.heavy.delayed.percent', '60', 'Delayed queue percentage threshold for heavy-room tracking.')
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`value` = VALUES(`value`),
|
||||||
|
`comment` = VALUES(`comment`);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- Login API, room templates, remember-me, and news
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `password_resets` (
|
||||||
|
`user_id` INT(11) NOT NULL,
|
||||||
|
`token` VARCHAR(128) NOT NULL,
|
||||||
|
`expires_at` TIMESTAMP NOT NULL,
|
||||||
|
`created_ip` VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
PRIMARY KEY (`user_id`),
|
||||||
|
UNIQUE KEY `idx_password_resets_token` (`token`),
|
||||||
|
CONSTRAINT `fk_password_resets_user`
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `users_remember_families` (
|
||||||
|
`family_id` CHAR(36) NOT NULL,
|
||||||
|
`user_id` INT(11) NOT NULL,
|
||||||
|
`current_version` INT(11) NOT NULL DEFAULT 1,
|
||||||
|
`created_at` INT(11) NOT NULL,
|
||||||
|
`expires_at` INT(11) NOT NULL,
|
||||||
|
`revoked` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`last_ip` VARCHAR(45) NOT NULL DEFAULT '',
|
||||||
|
PRIMARY KEY (`family_id`),
|
||||||
|
KEY `idx_users_remember_families_user_id` (`user_id`),
|
||||||
|
KEY `idx_users_remember_families_expires_at` (`expires_at`),
|
||||||
|
CONSTRAINT `fk_users_remember_families_user`
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `room_templates` (
|
||||||
|
`template_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`title` VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
`description` VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
`thumbnail` VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`enabled` ENUM('0','1') NOT NULL DEFAULT '1',
|
||||||
|
`name` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`room_description` VARCHAR(250) NOT NULL DEFAULT '',
|
||||||
|
`model` VARCHAR(100) NOT NULL,
|
||||||
|
`password` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`state` ENUM('open','locked','password','invisible') NOT NULL DEFAULT 'open',
|
||||||
|
`users_max` INT(11) NOT NULL DEFAULT 25,
|
||||||
|
`category` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`paper_floor` VARCHAR(50) NOT NULL DEFAULT '0.0',
|
||||||
|
`paper_wall` VARCHAR(50) NOT NULL DEFAULT '0.0',
|
||||||
|
`paper_landscape` VARCHAR(50) NOT NULL DEFAULT '0.0',
|
||||||
|
`thickness_wall` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`thickness_floor` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`moodlight_data` VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
|
`override_model` ENUM('0','1') NOT NULL DEFAULT '0',
|
||||||
|
`trade_mode` INT(2) NOT NULL DEFAULT 2,
|
||||||
|
`heightmap` MEDIUMTEXT NOT NULL,
|
||||||
|
`door_x` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`door_y` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`door_dir` INT(4) NOT NULL DEFAULT 2,
|
||||||
|
PRIMARY KEY (`template_id`),
|
||||||
|
KEY `idx_room_templates_enabled_sort` (`enabled`, `sort_order`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `room_templates_items` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`template_id` INT(11) NOT NULL,
|
||||||
|
`item_id` INT(11) UNSIGNED NOT NULL,
|
||||||
|
`wall_pos` VARCHAR(20) NOT NULL DEFAULT '',
|
||||||
|
`x` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`y` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`z` DOUBLE(10,6) NOT NULL DEFAULT 0.000000,
|
||||||
|
`rot` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`extra_data` VARCHAR(2096) NOT NULL DEFAULT '',
|
||||||
|
`wired_data` VARCHAR(4096) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_room_templates_items_template_id` (`template_id`),
|
||||||
|
KEY `idx_room_templates_items_item_id` (`item_id`),
|
||||||
|
CONSTRAINT `fk_room_templates_items_template`
|
||||||
|
FOREIGN KEY (`template_id`) REFERENCES `room_templates` (`template_id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_room_templates_items_item_base`
|
||||||
|
FOREIGN KEY (`item_id`) REFERENCES `items_base` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ui_news` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`title` VARCHAR(150) NOT NULL,
|
||||||
|
`body` TEXT NOT NULL,
|
||||||
|
`image` MEDIUMTEXT DEFAULT NULL,
|
||||||
|
`link_text` VARCHAR(80) NOT NULL DEFAULT '',
|
||||||
|
`link_url` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_ui_news_enabled_sort` (`enabled`, `sort_order`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
|
INSERT INTO `ui_news` (`title`, `body`, `image`, `link_text`, `link_url`, `enabled`, `sort_order`)
|
||||||
|
SELECT 'Welcome to the Hotel!', 'Catch up on the latest events, updates and competitions happening right now in the hotel.', '', '', '', 1, 0
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `ui_news`);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- Wired runtime data
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `room_wired_settings` (
|
||||||
|
`room_id` INT(11) NOT NULL,
|
||||||
|
`inspect_mask` INT(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can open and inspect Wired in the room. 1=everyone, 2=users with rights, 4=group members, 8=group admins.',
|
||||||
|
`modify_mask` INT(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can modify Wired in the room. 2=users with rights, 4=group members, 8=group admins.',
|
||||||
|
PRIMARY KEY (`room_id`),
|
||||||
|
CONSTRAINT `fk_room_wired_settings_room`
|
||||||
|
FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `room_wired_variables` (
|
||||||
|
`room_id` INT(11) NOT NULL,
|
||||||
|
`variable_item_id` INT(11) NOT NULL,
|
||||||
|
`value` INT(11) DEFAULT NULL,
|
||||||
|
`created_at` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`updated_at` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`room_id`, `variable_item_id`),
|
||||||
|
KEY `idx_room_wired_variables_room_item` (`room_id`, `variable_item_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `room_user_wired_variables` (
|
||||||
|
`room_id` INT(11) NOT NULL,
|
||||||
|
`user_id` INT(11) NOT NULL,
|
||||||
|
`variable_item_id` INT(11) NOT NULL,
|
||||||
|
`value` INT(11) DEFAULT NULL,
|
||||||
|
`created_at` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`updated_at` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`room_id`, `user_id`, `variable_item_id`),
|
||||||
|
KEY `idx_room_user_wired_variables_room_item` (`room_id`, `variable_item_id`),
|
||||||
|
KEY `idx_room_user_wired_variables_user` (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `room_furni_wired_variables` (
|
||||||
|
`room_id` INT(11) NOT NULL,
|
||||||
|
`furni_id` INT(11) NOT NULL,
|
||||||
|
`variable_item_id` INT(11) NOT NULL,
|
||||||
|
`value` INT(11) DEFAULT NULL,
|
||||||
|
`created_at` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`updated_at` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`room_id`, `furni_id`, `variable_item_id`),
|
||||||
|
KEY `idx_room_furni_wired_variables_room_item` (`room_id`, `variable_item_id`),
|
||||||
|
KEY `idx_room_furni_wired_variables_furni` (`furni_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- User customization: prefixes, nick icons, profile backgrounds
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
|
||||||
|
ALTER TABLE `users`
|
||||||
|
ADD COLUMN IF NOT EXISTS `background_id` INT(11) NOT NULL DEFAULT 0 AFTER `machine_id`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `background_stand_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_id`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `background_overlay_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_stand_id`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `background_card_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_overlay_id`;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `infostand_backgrounds` (
|
||||||
|
`id` INT(11) NOT NULL,
|
||||||
|
`category` ENUM('background','stand','overlay','card') NOT NULL,
|
||||||
|
`min_rank` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`is_hc_only` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`is_ambassador_only` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`, `category`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `infostand_backgrounds` (`id`, `category`, `min_rank`, `is_hc_only`, `is_ambassador_only`) VALUES
|
||||||
|
(0, 'background', 0, 0, 0),
|
||||||
|
(0, 'stand', 0, 0, 0),
|
||||||
|
(0, 'overlay', 0, 0, 0),
|
||||||
|
(0, 'card', 0, 0, 0);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_prefixes` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` INT(11) NOT NULL,
|
||||||
|
`text` VARCHAR(50) NOT NULL,
|
||||||
|
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
|
||||||
|
`icon` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`effect` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`font` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`catalog_prefix_id` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
`points` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`points_type` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`is_custom` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`active` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_prefixes_user_id` (`user_id`),
|
||||||
|
KEY `idx_user_prefixes_user_active` (`user_id`, `active`),
|
||||||
|
CONSTRAINT `fk_user_prefixes_user`
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `custom_prefixes_catalog` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
`text` VARCHAR(50) NOT NULL,
|
||||||
|
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
|
||||||
|
`icon` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`effect` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`font` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`points` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`points_type` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_visual_settings` (
|
||||||
|
`user_id` INT(11) NOT NULL,
|
||||||
|
`display_order` VARCHAR(50) NOT NULL DEFAULT 'icon-prefix-name',
|
||||||
|
PRIMARY KEY (`user_id`),
|
||||||
|
CONSTRAINT `fk_user_visual_settings_user`
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
|
||||||
|
`key_name` VARCHAR(100) NOT NULL,
|
||||||
|
`value` VARCHAR(255) NOT NULL,
|
||||||
|
PRIMARY KEY (`key_name`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES
|
||||||
|
('max_length', '15'),
|
||||||
|
('min_rank_to_buy', '1'),
|
||||||
|
('price_credits', '5'),
|
||||||
|
('price_points', '0'),
|
||||||
|
('points_type', '0'),
|
||||||
|
('font_price_credits', '10'),
|
||||||
|
('font_price_points', '0'),
|
||||||
|
('font_points_type', '0');
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `custom_prefixes_catalog`
|
||||||
|
(`id`, `display_name`, `text`, `color`, `icon`, `effect`, `font`, `points`, `points_type`, `enabled`, `sort_order`)
|
||||||
|
VALUES
|
||||||
|
(1, 'VIP', 'VIP', '#FFD700', '', 'glow', '', 10, 0, 1, 1),
|
||||||
|
(2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2),
|
||||||
|
(3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `custom_nick_icons_catalog` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`icon_key` VARCHAR(50) NOT NULL,
|
||||||
|
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
`points` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`points_type` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_custom_nick_icons_catalog_icon_key` (`icon_key`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_nick_icons` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` INT(11) NOT NULL,
|
||||||
|
`icon_key` VARCHAR(50) NOT NULL,
|
||||||
|
`active` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_user_nick_icons_user_icon` (`user_id`, `icon_key`),
|
||||||
|
KEY `idx_user_nick_icons_user_id` (`user_id`),
|
||||||
|
KEY `idx_user_nick_icons_user_active` (`user_id`, `active`),
|
||||||
|
CONSTRAINT `fk_user_nick_icons_user`
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `custom_nick_icons_catalog` (`icon_key`, `display_name`, `points`, `points_type`, `enabled`, `sort_order`) VALUES
|
||||||
|
('1', 'Icon 1', 10, 0, 1, 1),
|
||||||
|
('2', 'Icon 2', 10, 0, 1, 2),
|
||||||
|
('3', 'Icon 3', 10, 0, 1, 3),
|
||||||
|
('4', 'Icon 4', 10, 0, 1, 4),
|
||||||
|
('5', 'Icon 5', 10, 0, 1, 5),
|
||||||
|
('6', 'Icon 6', 10, 0, 1, 6);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- Custom badge maker
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `users_custom_badge_settings` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`badge_path` VARCHAR(255) NOT NULL DEFAULT '/var/www/gamedata/c_images/album1584',
|
||||||
|
`badge_url` VARCHAR(255) NOT NULL DEFAULT '/gamedata/c_images/album1584',
|
||||||
|
`price_badge` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`currency_type` INT(11) NOT NULL DEFAULT -1,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
|
INSERT INTO `users_custom_badge_settings` (`id`, `badge_path`, `badge_url`, `price_badge`, `currency_type`)
|
||||||
|
SELECT 1, '/var/www/gamedata/c_images/album1584', '/gamedata/c_images/album1584', 50, 5
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `users_custom_badge_settings` WHERE `id` = 1);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_custom_badge` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` INT(11) NOT NULL,
|
||||||
|
`badge_id` VARCHAR(64) NOT NULL,
|
||||||
|
`badge_name` VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
`badge_description` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
`date_created` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`date_edit` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_user_custom_badge_badge_id` (`badge_id`),
|
||||||
|
KEY `idx_user_custom_badge_user_id` (`user_id`),
|
||||||
|
CONSTRAINT `fk_user_custom_badge_user`
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- UI/catalog compatibility values used by the current client
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT INTO `chat_bubbles` (`type`, `name`, `permission`, `overridable`, `triggers_talking_furniture`) VALUES
|
||||||
|
(200, 'SHOW_MESSAGE_RED', '', 1, 0),
|
||||||
|
(201, 'SHOW_MESSAGE_GREEN', '', 1, 0),
|
||||||
|
(202, 'SHOW_MESSAGE_BLUE', '', 1, 0),
|
||||||
|
(210, 'SHOW_MESSAGE_ALERT', '', 1, 0),
|
||||||
|
(211, 'SHOW_MESSAGE_INFO', '', 1, 0),
|
||||||
|
(212, 'SHOW_MESSAGE_WARNING', '', 1, 0),
|
||||||
|
(220, 'SHOW_MESSAGE_WRONG', '', 1, 0),
|
||||||
|
(221, 'SHOW_MESSAGE_WRONG_CIRCLED', '', 1, 0),
|
||||||
|
(222, 'SHOW_MESSAGE_CORRECT', '', 1, 0),
|
||||||
|
(223, 'SHOW_MESSAGE_CORRECT_CIRCLED', '', 1, 0),
|
||||||
|
(224, 'SHOW_MESSAGE_QUESTION', '', 1, 0),
|
||||||
|
(225, 'SHOW_MESSAGE_QUESTION_CIRCLED', '', 1, 0),
|
||||||
|
(226, 'SHOW_MESSAGE_ARROW_UP', '', 1, 0),
|
||||||
|
(227, 'SHOW_MESSAGE_ARROW_UP_CIRCLED', '', 1, 0),
|
||||||
|
(228, 'SHOW_MESSAGE_ARROW_DOWN', '', 1, 0),
|
||||||
|
(229, 'SHOW_MESSAGE_ARROW_DOWN_CIRCLED', '', 1, 0),
|
||||||
|
(250, 'SHOW_MESSAGE_SKULL', '', 1, 0),
|
||||||
|
(251, 'SHOW_MESSAGE_SKULL_ALT', '', 1, 0),
|
||||||
|
(252, 'SHOW_MESSAGE_MAGNIFIER', '', 1, 0)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`name` = VALUES(`name`),
|
||||||
|
`permission` = VALUES(`permission`),
|
||||||
|
`overridable` = VALUES(`overridable`),
|
||||||
|
`triggers_talking_furniture` = VALUES(`triggers_talking_furniture`);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
|
||||||
|
('commands.keys.cmd_setroom_template', 'setroom_template;set_room_template'),
|
||||||
|
('commands.succes.cmd_setroom_template.verify', 'Copy the current room "%roomname%" to room_templates? Type :setroom_template %generic.yes% to confirm.'),
|
||||||
|
('commands.succes.cmd_setroom_template', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'),
|
||||||
|
('commands.error.cmd_setroom_template', 'Could not save room as template. Check the server log for details.'),
|
||||||
|
('commands.error.cmd_setroom_template.no_room', 'You must be inside a room to use this command.'),
|
||||||
|
('commands.keys.cmd_give_prefix', 'giveprefix'),
|
||||||
|
('commands.keys.cmd_list_prefixes', 'listprefixes'),
|
||||||
|
('commands.keys.cmd_remove_prefix', 'removeprefix'),
|
||||||
|
('commands.keys.cmd_prefix_blacklist', 'prefixblacklist'),
|
||||||
|
('wiredfurni.badgereceived.body', 'You have just received a new Badge! Check your Inventory!'),
|
||||||
|
('wiredfurni.badgereceived.title', 'Badge received!');
|
||||||
|
|
||||||
|
-- Optional permission metadata for normalized permission schemas.
|
||||||
|
-- Actual rank values still belong in the permissions/permission_ranks setup.
|
||||||
|
CREATE TABLE IF NOT EXISTS `permission_definitions` (
|
||||||
|
`permission_key` VARCHAR(64) NOT NULL,
|
||||||
|
`max_value` TINYINT(3) UNSIGNED NOT NULL DEFAULT 1,
|
||||||
|
`comment` TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (`permission_key`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`) VALUES
|
||||||
|
('cmd_setroom_template', 1, 'Allows using :setroom_template to copy a room into the login room-template table.'),
|
||||||
|
('cmd_give_prefix', 1, 'Allows granting custom prefixes to users.'),
|
||||||
|
('cmd_list_prefixes', 1, 'Allows listing custom prefixes assigned to users.'),
|
||||||
|
('cmd_remove_prefix', 1, 'Allows removing custom prefixes from users.'),
|
||||||
|
('cmd_prefix_blacklist', 1, 'Allows managing the custom prefix blacklist.')
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`max_value` = VALUES(`max_value`),
|
||||||
|
`comment` = VALUES(`comment`);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- Explicitly obsolete table from older remember-me attempts.
|
||||||
|
-- The current Java uses users_remember_families only.
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `users_remember_tokens`;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Fix: acc_supporttool wrongly granted to VIP / wrongly denied to Super Mod
|
||||||
|
-- ============================================================
|
||||||
|
-- The default permission_definitions seed shipped acc_supporttool
|
||||||
|
-- with rank pattern (0, 1, 1, 1, 1, 0, 1) — i.e. rank_2 (VIP) and
|
||||||
|
-- rank_3 (X, junior helper) had ALLOWED, while rank_6 (Super Mod)
|
||||||
|
-- did NOT. That's two bugs:
|
||||||
|
--
|
||||||
|
-- * VIP users see the ModTools button on the toolbar and can
|
||||||
|
-- open Room/User info windows. The actual sanction endpoints
|
||||||
|
-- still gate on ACC_SUPPORTTOOL server-side so they can't
|
||||||
|
-- actually moderate, but the UI exposure is wrong and lets a
|
||||||
|
-- VIP request user info / room info / chatlogs they have no
|
||||||
|
-- business reading.
|
||||||
|
-- * Super Mod is denied the tool entirely, which is obviously
|
||||||
|
-- unintended given the rank name.
|
||||||
|
--
|
||||||
|
-- Intended pattern: only Support (4) and up — (0, 0, 0, 1, 1, 1, 1).
|
||||||
|
--
|
||||||
|
-- Run on existing deployments to align with the corrected default
|
||||||
|
-- seed in `Default Database/FullDatabase.sql`. Idempotent.
|
||||||
|
|
||||||
|
UPDATE `permission_definitions`
|
||||||
|
SET `rank_1` = 0,
|
||||||
|
`rank_2` = 0,
|
||||||
|
`rank_3` = 0,
|
||||||
|
`rank_4` = 1,
|
||||||
|
`rank_5` = 1,
|
||||||
|
`rank_6` = 1,
|
||||||
|
`rank_7` = 1
|
||||||
|
WHERE `permission_key` = 'acc_supporttool';
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE catalog_club_offers
|
||||||
|
ADD COLUMN IF NOT EXISTS giftable ENUM('0','1') NOT NULL DEFAULT '0';
|
||||||
|
|
||||||
|
INSERT INTO emulator_texts (`key`, `value`)
|
||||||
|
VALUES ('prereg.reward.you.received', 'You have recived:'),
|
||||||
|
('generic.days', 'days');
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`, `rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`) VALUES ('acc_housekeeping', '1', 'Allow housekeeping in the client', '0', '0', '0', '0', '0', '0', '1');
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `housekeeping_log` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`timestamp` INT NOT NULL,
|
||||||
|
`actor_id` INT NOT NULL,
|
||||||
|
`actor_name` VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
`target_type` VARCHAR(16) NOT NULL DEFAULT 'user',
|
||||||
|
`target_id` INT NOT NULL DEFAULT 0,
|
||||||
|
`target_label` VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
`action` VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
`detail` VARCHAR(500) NOT NULL DEFAULT '',
|
||||||
|
`success` TINYINT NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `timestamp` (`timestamp`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
ALTER TABLE `bots`
|
||||||
|
MODIFY COLUMN `type` ENUM('generic','visitor_log','bartender','weapons_dealer','frank')
|
||||||
|
NOT NULL DEFAULT 'generic';
|
||||||
|
|
||||||
|
INSERT INTO `permission_definitions`
|
||||||
|
(`permission_key`, `max_value`, `comment`,
|
||||||
|
`rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`)
|
||||||
|
VALUES
|
||||||
|
('acc_bot_frank', 1, 'Required to purchase the Frank mascot bot from the catalog.',
|
||||||
|
0, 0, 0, 0, 0, 0, 1)
|
||||||
|
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `bot_chat_responses` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`bot_type` VARCHAR(32) NOT NULL,
|
||||||
|
`keys` VARCHAR(255) NOT NULL COMMENT 'semicolon-separated trigger words',
|
||||||
|
`responses` TEXT NOT NULL COMMENT 'newline-separated replies; bot picks one at random',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `bot_type` (`bot_type`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT INTO `bot_chat_responses` (`bot_type`, `keys`, `responses`) VALUES
|
||||||
|
('frank', '__door_triggers', 'show me the door\nkick me\ni want to leave\nlet me out'),
|
||||||
|
('frank', '__door_lines', 'Right this way - mind the step!\nAnd out you go. Come back soon!\nAllow me to escort you to the exit.\nThere''s the door. Farewell, true believer!'),
|
||||||
|
('frank', '__busy_whisper', 'Sorry, I am currently busy. Please wait until I am available.'),
|
||||||
|
('frank', 'frank', 'Hello, I''m Frank! Welcome to Habbo.'),
|
||||||
|
('frank', 'help', 'What do you need help with?'),
|
||||||
|
('frank', 'thanks;thank you', 'Just doing my job, true believer!'),
|
||||||
|
('frank', 'new', 'Welcome to Habbo! I hope you have a great time here.'),
|
||||||
|
('frank', 'rooms', 'Looking for somewhere fun? Try the Navigator - thousands of rooms to explore!'),
|
||||||
|
('frank', 'sulake', 'Sulake is the company behind Habbo. Take a look: https://www.sulake.com'),
|
||||||
|
('frank', 'vip;hc', 'VIP gets you more outfits, more furni, more everything. Worth it!'),
|
||||||
|
('frank', 'music', 'Snoop Dogg, Frank Sinatra and a little Beethoven on Sundays.'),
|
||||||
|
('frank', 'movie', 'I''m a Casablanca man. Black and white films are an underrated art.'),
|
||||||
|
('frank', 'game', 'Battleship. Always Battleship.'),
|
||||||
|
('frank', 'snowstorm', 'Honestly? I''m terrible at Snowstorm. Don''t tell anyone.'),
|
||||||
|
('frank', 'furni', 'Best furniture maker in town - hands down, the folks at Sulake.'),
|
||||||
|
('frank', 'animal;cat;pet','I have a cat called Mr. Whiskers. He runs the place, really.'),
|
||||||
|
('frank', 'miranda', 'Miranda. The love of my life. Don''t get me started.'),
|
||||||
|
('frank', 'frank black', 'Named after the man himself. Frank Black is a hero of mine.'),
|
||||||
|
('frank', 'life', 'Life is like a bowl of popcorn - warm, salty and buttery.'),
|
||||||
|
('frank', 'job;work', 'I''m sure you can find work in one of the guest rooms!'),
|
||||||
|
('frank', 'snouthill', 'Snouthill... so many memories.'),
|
||||||
|
('frank', 'wife', 'I had a wife once. She broke my stereo.'),
|
||||||
|
('frank', 'baseball', 'Oh, I used to love to go down to the old ball park and watch Christy Mathewson and Honus Wagner at bat.'),
|
||||||
|
('frank', 'mark', 'I don''t trust Mark.'),
|
||||||
|
('frank', 'vietnam', 'Vietnam? Don''t ask. Worst trip of my life.'),
|
||||||
|
('frank', 'pills;drugs', 'Drugs are bad, mmkay?');
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `bot_serves` (`keys`, `item`) VALUES
|
||||||
|
('sunflower', 1002),
|
||||||
|
('cola;habbo cola', 19),
|
||||||
|
('rose', 1000),
|
||||||
|
('book', 1003),
|
||||||
|
('tea', 27),
|
||||||
|
('coffee', 8),
|
||||||
|
('migraine;headache;pills', 1015),
|
||||||
|
('radioactive liquid;radioactive', 30),
|
||||||
|
('turkey;can of turkey', 70);
|
||||||
|
|
||||||
|
-- VERY IMPORTANT !!!!
|
||||||
|
-- First check if the items_base ID and catalog_items ID is not in use !
|
||||||
|
-- After the SQL please go to the catalog_items table and change the page_id to where your BOTS are located
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `items_base` (`id`, `sprite_id`, `item_name`, `public_name`, `width`, `length`, `stack_height`, `allow_stack`, `allow_sit`, `allow_lay`, `allow_walk`, `allow_gift`, `allow_trade`, `allow_recycle`, `allow_marketplace_sell`, `allow_inventory_stack`, `type`, `interaction_type`, `interaction_modes_count`, `vending_ids`, `multiheight`, `customparams`)
|
||||||
|
VALUES (19001, 0, 'bot_frank', 'Frank', 1, 1, 0.00, '0', '0', '0', '1', '0', '0', '0', '0', '0', 'r', 'default', 1, '0', '0', '0');
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `catalog_items` (`item_ids`, `page_id`, `offer_id`, `catalog_name`, `cost_credits`, `cost_points`, `points_type`, `amount`, `extradata`)
|
||||||
|
VALUES ('19001', 8, 19001, 'Frank', 0, 0, 0, 1, 'name:Frank;motto:Welcome to Habbo!;figure:hr-3499-33.sh-290-90.ch-3971-72-73.lg-270-73.hd-205-1-1.fa-1206-67.ha-3409-73-72;gender:m');
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
ALTER TABLE `rooms`
|
||||||
|
ADD COLUMN IF NOT EXISTS `soundboard_enabled` TINYINT(1) NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `soundboard_sounds` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` VARCHAR(64) NOT NULL DEFAULT '', -- pad label shown in the client
|
||||||
|
`url` VARCHAR(255) NOT NULL DEFAULT '', -- audio url (uploaded via CMS, like custom badges)
|
||||||
|
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
-- Fortune Wheel — tables
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS `wheel_prizes` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`type` VARCHAR(16) NOT NULL DEFAULT 'nothing', -- item | badge | credits | points | spin | nothing
|
||||||
|
`value` VARCHAR(64) NOT NULL DEFAULT '', -- item: base item id ; badge: badge code ; others: unused
|
||||||
|
`amount` INT(11) NOT NULL DEFAULT 1, -- item qty / credits / points / extra spins
|
||||||
|
`points_type` INT(11) NOT NULL DEFAULT 5, -- for type=points (diamond default 5)
|
||||||
|
`weight` INT(11) NOT NULL DEFAULT 1, -- relative probability
|
||||||
|
`label` VARCHAR(64) NOT NULL DEFAULT '', -- slice label override (optional)
|
||||||
|
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `wheel_user_state` (
|
||||||
|
`user_id` INT(11) NOT NULL,
|
||||||
|
`free_spins` INT(11) NOT NULL DEFAULT 0, -- remaining free spins for the current day
|
||||||
|
`extra_spins` INT(11) NOT NULL DEFAULT 0, -- bought / won spins
|
||||||
|
`last_reset` INT(11) NOT NULL DEFAULT 0, -- day index of last daily reset (unix / 86400)
|
||||||
|
PRIMARY KEY (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `wheel_recent_wins` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` INT(11) NOT NULL,
|
||||||
|
`username` VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
`look` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
`prize_label` VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
`won_at` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||||
|
('wheel.free_spins_per_day', '1', 'Fortune wheel: free spins granted each day.'),
|
||||||
|
('wheel.spin_cost', '50', 'Fortune wheel: cost of one extra spin.'),
|
||||||
|
('wheel.spin_cost_type', '5', 'Fortune wheel: currency type for the spin cost (5 = diamonds; -1 = credits).')
|
||||||
|
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
|
||||||
|
|
||||||
|
INSERT INTO `wheel_prizes` (`type`, `amount`, `points_type`, `weight`, `label`, `sort_order`)
|
||||||
|
SELECT `type`, `amount`, `points_type`, `weight`, `label`, `sort_order`
|
||||||
|
FROM (
|
||||||
|
SELECT 'points' AS `type`, 25 AS `amount`, 5 AS `points_type`, 20 AS `weight`, '25 diamonds' AS `label`, 1 AS `sort_order`
|
||||||
|
UNION ALL SELECT 'points', 50, 5, 12, '50 diamonds', 2
|
||||||
|
UNION ALL SELECT 'points', 200, 5, 3, '200 diamonds', 3
|
||||||
|
UNION ALL SELECT 'credits', 100, 0, 15, '100 credits', 4
|
||||||
|
UNION ALL SELECT 'spin', 1, 0, 15, '1 Extra spin', 5
|
||||||
|
UNION ALL SELECT 'spin', 2, 0, 6, '2 Extra spins', 6
|
||||||
|
UNION ALL SELECT 'nothing', 0, 0, 29, 'Oh to bad!', 7
|
||||||
|
) AS seed
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `wheel_prizes`);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `permission_definitions` (`permission_key`, `max_value`, `comment`)
|
||||||
|
VALUES (
|
||||||
|
'acc_wheeladmin',
|
||||||
|
1,
|
||||||
|
'Allows opening the Fortune Wheel prize editor (FortuneWheelSettingsView) to add/edit prize slices. Gated server-side by the same key.'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @cols := NULL;
|
||||||
|
SELECT GROUP_CONCAT(CONCAT('dst.`', `column_name`, '` = src.`', `column_name`, '`') SEPARATOR ', ')
|
||||||
|
INTO @cols
|
||||||
|
FROM `information_schema`.`columns`
|
||||||
|
WHERE `table_schema` = DATABASE()
|
||||||
|
AND `table_name` = 'permission_definitions'
|
||||||
|
AND `column_name` REGEXP '^rank_[0-9]+$';
|
||||||
|
|
||||||
|
SET @sql := CONCAT(
|
||||||
|
'UPDATE `permission_definitions` dst ',
|
||||||
|
'JOIN `permission_definitions` src ON src.`permission_key` = ''acc_ads_background'' ',
|
||||||
|
'SET ', @cols, ' ',
|
||||||
|
'WHERE dst.`permission_key` = ''acc_wheeladmin'''
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `habbo_mentions` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`target_user_id` INT(11) NOT NULL,
|
||||||
|
`sender_user_id` INT(11) NOT NULL,
|
||||||
|
`sender_username` VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
`room_id` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`room_name` VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
`message` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
`mention_type` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '0 = direct (@nick), 1 = broadcast (@all/@friends/@room)',
|
||||||
|
`timestamp` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`read` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_target_id` (`target_user_id`, `id`),
|
||||||
|
KEY `idx_target_unread` (`target_user_id`, `read`),
|
||||||
|
KEY `idx_target_timestamp` (`target_user_id`, `timestamp`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO `permission_definitions`
|
||||||
|
(`permission_key`, `max_value`, `comment`,
|
||||||
|
`rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`, `rank_8`)
|
||||||
|
VALUES
|
||||||
|
('acc_mention_everyone', 1,
|
||||||
|
'Allow sending @all / @everyone / @tutti broadcast mentions (hotel-wide).',
|
||||||
|
0, 0, 0, 0, 1, 1, 1, 1),
|
||||||
|
('acc_mention_friends', 1,
|
||||||
|
'Allow sending @friends / @amici broadcast mentions (sender''s online buddies).',
|
||||||
|
0, 0, 0, 0, 1, 1, 1, 1),
|
||||||
|
('cmd_disablementions', 1,
|
||||||
|
'Allow toggling :disablementions to stop receiving any @mention notifications.',
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1),
|
||||||
|
('cmd_disablemassmentions', 1,
|
||||||
|
'Allow toggling :disablemassmentions to stop receiving broadcast mentions (direct @nick still works).',
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`comment` = VALUES(`comment`);
|
||||||
|
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
-- 3. Emulator settings: cooldowns, caps and alias lists
|
||||||
|
--
|
||||||
|
-- Only inserted when missing - existing tuned values are preserved.
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||||
|
('mentions.enabled', '1',
|
||||||
|
'Master switch. 1 = process @mentions, 0 = disable the feature entirely.'),
|
||||||
|
('mentions.max.targets', '50',
|
||||||
|
'Hard cap on how many users a single broadcast (@all / @friends / @room) can fan out to.'),
|
||||||
|
('mentions.cooldown.ms', '3000',
|
||||||
|
'Per-sender cooldown between any two mentions, in milliseconds.'),
|
||||||
|
('mentions.room.cooldown.ms', '15000',
|
||||||
|
'Extra per-sender cooldown for broadcast mentions (@all / @friends / @room) on top of mentions.cooldown.ms.'),
|
||||||
|
('mentions.store.limit', '50',
|
||||||
|
'Number of mentions returned in the initial RequestMentionsList response.'),
|
||||||
|
('mentions.request.cooldown.ms', '2000',
|
||||||
|
'Per-user cooldown between RequestMentionsList packets.'),
|
||||||
|
('mentions.markread.cooldown.ms', '500',
|
||||||
|
'Per-user cooldown between mark-single-as-read packets.'),
|
||||||
|
('mentions.markall.cooldown.ms', '5000',
|
||||||
|
'Per-user cooldown between mark-all-as-read packets (bulk DB update).'),
|
||||||
|
('mentions.delete.cooldown.ms', '500',
|
||||||
|
'Per-user cooldown between delete-mention packets.'),
|
||||||
|
('mentions.everyone.aliases', 'all,everyone,tutti',
|
||||||
|
'Comma-separated aliases that trigger an @everyone broadcast (requires acc_mention_everyone).'),
|
||||||
|
('mentions.friends.aliases', 'friends,amici',
|
||||||
|
'Comma-separated aliases that trigger an @friends broadcast (requires acc_mention_friends).'),
|
||||||
|
('mentions.room.aliases', 'room,stanza',
|
||||||
|
'Comma-separated aliases that trigger an @room broadcast (no permission required, room scope only).');
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE `wordfilter`
|
||||||
|
ADD COLUMN `prefix_only` ENUM('0','1') NOT NULL DEFAULT '0'
|
||||||
|
COMMENT 'When 1, this word only applies to custom prefixes, not to chat/motto/guild.' AFTER `mute`;
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
-- 5. Per-user mention preferences (:disablementions / :disablemassmentions)
|
||||||
|
--
|
||||||
|
-- Read by HabboStats (default '1' = enabled), toggled by the commands.
|
||||||
|
-- Without these columns the toggle commands cannot persist.
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ALTER TABLE `users_settings`
|
||||||
|
ADD COLUMN IF NOT EXISTS `mentions_enabled` ENUM('0','1') NOT NULL DEFAULT '1'
|
||||||
|
COMMENT 'Receive @nick mention notifications.',
|
||||||
|
ADD COLUMN IF NOT EXISTS `mass_mentions_enabled` ENUM('0','1') NOT NULL DEFAULT '1'
|
||||||
|
COMMENT 'Receive broadcast (@all / @friends / @room) mentions.';
|
||||||
@@ -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';
|
|
||||||
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
-- 020_furnidata_edit_log.sql
|
||||||
|
-- Audit trail for furnidata name/description edits made through the furni editor,
|
||||||
|
-- plus config keys for the editor write path. NOTE: *.enabled keys elsewhere are
|
||||||
|
-- read via Boolean.parseBoolean (true/false), but these two are numeric.
|
||||||
|
CREATE TABLE IF NOT EXISTS `furnidata_edit_log` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int(11) NOT NULL,
|
||||||
|
`classname` varchar(255) NOT NULL,
|
||||||
|
`action` enum('edit','revert') NOT NULL DEFAULT 'edit',
|
||||||
|
`old_name` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`new_name` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`old_description` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`new_description` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`timestamp` int(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
INDEX `idx_classname` (`classname`),
|
||||||
|
INDEX `idx_user` (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `emulator_settings` (`key`,`value`) VALUES
|
||||||
|
('items.furnidata.edit.backup.keep','10'),
|
||||||
|
('items.furnidata.edit.ratelimit.ms','2000'),
|
||||||
|
-- Server-authoritative furni names (source of truth = furnidata JSON)
|
||||||
|
('items.furnidata.names.enabled','true'),
|
||||||
|
('items.furnidata.path',''),
|
||||||
|
('items.furnidata.max.bytes','67108864'),
|
||||||
|
-- Live-reload watcher
|
||||||
|
('items.furnidata.watch.enabled','true'),
|
||||||
|
('items.furnidata.watch.debounce.ms','750'),
|
||||||
|
('items.furnidata.watch.min.interval.ms','5000'),
|
||||||
|
('items.furnidata.delta.cap','500'),
|
||||||
|
-- Furni editor: import official names/descriptions from Habbo
|
||||||
|
('furni.editor.import.url','https://www.habbo.com/gamedata/furnidata_json/1'),
|
||||||
|
('furni.editor.import.cache.ms','600000');
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS cleanup_furnidata_settings;
|
||||||
|
CREATE TEMPORARY TABLE cleanup_furnidata_settings (
|
||||||
|
`key` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL PRIMARY KEY
|
||||||
|
);
|
||||||
|
INSERT INTO cleanup_furnidata_settings (`key`) VALUES
|
||||||
|
('items.furnidata.names.enabled'),
|
||||||
|
('items.furnidata.path'),
|
||||||
|
('items.furnidata.max.bytes'),
|
||||||
|
('items.furnidata.watch.enabled'),
|
||||||
|
('items.furnidata.watch.debounce.ms'),
|
||||||
|
('items.furnidata.watch.min.interval.ms'),
|
||||||
|
('items.furnidata.delta.cap'),
|
||||||
|
('furni.editor.import.url'),
|
||||||
|
('furni.editor.import.cache.ms');
|
||||||
|
|
||||||
|
-- Preview rows that will be removed.
|
||||||
|
SELECT es.`key`, es.`value`
|
||||||
|
FROM emulator_settings es
|
||||||
|
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`
|
||||||
|
ORDER BY es.`key`;
|
||||||
|
|
||||||
|
DELETE es
|
||||||
|
FROM emulator_settings es
|
||||||
|
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`;
|
||||||
|
|
||||||
|
-- Preview remaining matching rows inside the transaction.
|
||||||
|
SELECT COUNT(*) AS remaining_furnidata_settings
|
||||||
|
FROM emulator_settings es
|
||||||
|
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`;
|
||||||
|
|
||||||
|
-- Safe default. Change to COMMIT after reviewing the preview.
|
||||||
|
ROLLBACK;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Navigator search filters - companion to the gameserver fix for the catalog
|
||||||
|
-- 'Find groups to join!' button (navigator/search/hotel_view/group:).
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `navigator_filter` (`key`, `field`, `compare`, `database_query`) VALUES
|
||||||
|
('anything', 'getName', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.name LIKE ?'),
|
||||||
|
('roomname', 'getName', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.name LIKE ?'),
|
||||||
|
('owner', 'getOwnerName', 'equals_ignore_case', 'SELECT rooms.* FROM rooms WHERE rooms.owner_name LIKE ?'),
|
||||||
|
('tag', 'getTags', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.tags LIKE ?'),
|
||||||
|
('group', 'getGuildName', 'contains', 'SELECT rooms.* FROM rooms INNER JOIN guilds ON guilds.room_id = rooms.id WHERE guilds.name LIKE ?');
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS `user_prefixes` (
|
|
||||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`user_id` INT(11) NOT NULL,
|
|
||||||
`text` VARCHAR(50) NOT NULL,
|
|
||||||
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
|
|
||||||
`icon` VARCHAR(50) NOT NULL DEFAULT '',
|
|
||||||
`effect` VARCHAR(50) NOT NULL DEFAULT '',
|
|
||||||
`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');
|
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
+6
@@ -121,6 +121,12 @@ INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
|
|||||||
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%"}');
|
('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
|
-- Camera emulator texts
|
||||||
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
|
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
|
||||||
('camera.permission', 'You do not have permission to use the camera.');
|
('camera.permission', 'You do not have permission to use the camera.');
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
|
INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
|
||||||
('furni.editor.renderer.config.path', '/var/www/Gamedata/config/renderer-config.json'),
|
('furni.editor.renderer.config.path', '/var/www/Nitro-V3/dist/configuration/renderer-config.json'),
|
||||||
('furni.editor.asset.base.path', '/var/www/Gamedata/furniture/nitro-assets/');
|
('furni.editor.asset.base.path', '/var/www/gamedata/furniture/');
|
||||||
|
|
||||||
ALTER TABLE permissions
|
ALTER TABLE permissions
|
||||||
ADD COLUMN `acc_catalogfurni` ENUM('0','1') NOT NULL DEFAULT '0' AFTER `acc_catalog_ids`;
|
ADD COLUMN `acc_catalogfurni` ENUM('0','1') NOT NULL DEFAULT '0' AFTER `acc_catalog_ids`;
|
||||||
+162
-163
@@ -1,15 +1,31 @@
|
|||||||
-- Normalizes the legacy `permissions` table into:
|
-- Normalizes the legacy `permissions` table into:
|
||||||
-- 1. `permission_ranks` -> one row per rank with rank metadata.
|
-- 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.
|
-- This version uses NO stored procedures and NO DELIMITER directives, so it
|
||||||
-- It also cleans up the older experimental normalized objects if they were already created.
|
-- 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 VIEW IF EXISTS `permissions_matrix_view`;
|
||||||
DROP PROCEDURE IF EXISTS `refresh_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_rank_values`;
|
||||||
DROP TABLE IF EXISTS `permission_nodes`;
|
DROP TABLE IF EXISTS `permission_nodes`;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------------------------
|
||||||
|
-- Target tables.
|
||||||
|
-- --------------------------------------------------------------------------
|
||||||
CREATE TABLE IF NOT EXISTS `permission_ranks` (
|
CREATE TABLE IF NOT EXISTS `permission_ranks` (
|
||||||
`id` int(11) NOT NULL,
|
`id` int(11) NOT NULL,
|
||||||
`rank_name` varchar(25) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci 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 `value_type`,
|
||||||
DROP COLUMN IF EXISTS `sort_order`;
|
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` (
|
INSERT INTO `permission_ranks` (
|
||||||
`id`,
|
`id`, `rank_name`, `hidden_rank`, `badge`, `job_description`,
|
||||||
`rank_name`,
|
`staff_color`, `staff_background`, `level`, `room_effect`, `log_commands`,
|
||||||
`hidden_rank`,
|
`prefix`, `prefix_color`,
|
||||||
`badge`,
|
`auto_credits_amount`, `auto_pixels_amount`, `auto_gotw_amount`, `auto_points_amount`
|
||||||
`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
|
SELECT
|
||||||
`id`,
|
`id`, `rank_name`, `hidden_rank`, `badge`, `job_description`,
|
||||||
`rank_name`,
|
`staff_color`, `staff_background`, `level`, `room_effect`, `log_commands`,
|
||||||
`hidden_rank`,
|
`prefix`, `prefix_color`,
|
||||||
`badge`,
|
`auto_credits_amount`, `auto_pixels_amount`, `auto_gotw_amount`, `auto_points_amount`
|
||||||
`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`
|
FROM `permissions`
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
`rank_name` = VALUES(`rank_name`),
|
`rank_name` = VALUES(`rank_name`),
|
||||||
@@ -95,55 +100,30 @@ ON DUPLICATE KEY UPDATE
|
|||||||
`auto_gotw_amount` = VALUES(`auto_gotw_amount`),
|
`auto_gotw_amount` = VALUES(`auto_gotw_amount`),
|
||||||
`auto_points_amount` = VALUES(`auto_points_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 $$
|
SELECT GROUP_CONCAT(
|
||||||
CREATE PROCEDURE `refresh_permission_definition_rank_columns`()
|
CONCAT('ADD COLUMN IF NOT EXISTS `rank_', `id`, '` tinyint(3) unsigned NOT NULL DEFAULT 0')
|
||||||
BEGIN
|
ORDER BY `id` ASC
|
||||||
DECLARE done INT DEFAULT 0;
|
SEPARATOR ', '
|
||||||
DECLARE current_rank_id INT;
|
)
|
||||||
DECLARE current_column_name VARCHAR(32);
|
INTO @add_rank_columns_sql
|
||||||
DECLARE column_exists INT DEFAULT 0;
|
FROM `permission_ranks`;
|
||||||
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;
|
SET @add_rank_columns_sql = CONCAT('ALTER TABLE `permission_definitions` ', @add_rank_columns_sql);
|
||||||
|
|
||||||
rank_loop: LOOP
|
PREPARE add_rank_columns_stmt FROM @add_rank_columns_sql;
|
||||||
FETCH rank_cursor INTO current_rank_id;
|
EXECUTE add_rank_columns_stmt;
|
||||||
|
DEALLOCATE PREPARE add_rank_columns_stmt;
|
||||||
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`();
|
|
||||||
|
|
||||||
|
-- --------------------------------------------------------------------------
|
||||||
|
-- Seed `permission_definitions` from the columns of the legacy table.
|
||||||
|
-- --------------------------------------------------------------------------
|
||||||
INSERT INTO `permission_definitions` (
|
INSERT INTO `permission_definitions` (
|
||||||
`permission_key`,
|
`permission_key`,
|
||||||
`max_value`,
|
`max_value`,
|
||||||
@@ -187,27 +167,18 @@ FROM `information_schema`.`columns`
|
|||||||
WHERE `table_schema` = DATABASE()
|
WHERE `table_schema` = DATABASE()
|
||||||
AND `table_name` = 'permissions'
|
AND `table_name` = 'permissions'
|
||||||
AND `column_name` NOT IN (
|
AND `column_name` NOT IN (
|
||||||
'id',
|
'id', 'rank_name', 'hidden_rank', 'badge', 'job_description',
|
||||||
'rank_name',
|
'staff_color', 'staff_background', 'level', 'room_effect', 'log_commands',
|
||||||
'hidden_rank',
|
'prefix', 'prefix_color',
|
||||||
'badge',
|
'auto_credits_amount', 'auto_pixels_amount', 'auto_gotw_amount', 'auto_points_amount'
|
||||||
'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
|
ON DUPLICATE KEY UPDATE
|
||||||
`max_value` = VALUES(`max_value`),
|
`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`;
|
DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`;
|
||||||
|
|
||||||
CREATE TEMPORARY TABLE `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 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 $$
|
-- We need to loop over rank ids without a stored procedure. We do that by
|
||||||
CREATE PROCEDURE `refresh_permission_definition_values`()
|
-- selecting all rank ids into a comma-separated string and then iterating
|
||||||
BEGIN
|
-- with substring math using a CTE-driven counter. Simpler: build one giant
|
||||||
DECLARE done INT DEFAULT 0;
|
-- UPDATE per rank by hand using GROUP_CONCAT and then EXECUTE each in turn.
|
||||||
DECLARE current_rank_id INT;
|
--
|
||||||
DECLARE current_column_name VARCHAR(32);
|
-- To avoid a procedural loop entirely, we instead emit a *single* UPDATE
|
||||||
DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC;
|
-- that uses CASE expressions to set every `rank_<id>` column from a single
|
||||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
|
-- derived table containing all ranks' values. This is one PREPARE / EXECUTE.
|
||||||
|
|
||||||
OPEN rank_cursor;
|
SET @permission_columns_sql = NULL;
|
||||||
|
|
||||||
rank_loop: LOOP
|
-- All the permission columns from the legacy table, comma separated and quoted.
|
||||||
FETCH rank_cursor INTO current_rank_id;
|
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
|
-- Build the UNPIVOT body: one "SELECT id, 'col' AS k, `col` AS v FROM permissions UNION ALL ..." per column.
|
||||||
LEAVE rank_loop;
|
SET @unpivot_sql = NULL;
|
||||||
END IF;
|
|
||||||
|
|
||||||
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(
|
-- Build the SET clause: `rank_<id>` = MAX(CASE WHEN rank_id = <id> THEN permission_value END) for each rank.
|
||||||
CONCAT(
|
SET @set_clause_sql = NULL;
|
||||||
'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(
|
SELECT GROUP_CONCAT(
|
||||||
'UPDATE `permission_definitions` pd ',
|
CONCAT(
|
||||||
'INNER JOIN (',
|
'pd.`rank_', `id`,
|
||||||
@permission_rank_source_sql,
|
'` = COALESCE(src.`rank_', `id`, '`, pd.`rank_', `id`, '`)'
|
||||||
') src ON src.permission_key = pd.permission_key ',
|
)
|
||||||
'SET pd.`',
|
ORDER BY `id` ASC
|
||||||
current_column_name,
|
SEPARATOR ', '
|
||||||
'` = src.permission_value'
|
)
|
||||||
);
|
INTO @set_clause_sql
|
||||||
|
FROM `permission_ranks`;
|
||||||
|
|
||||||
PREPARE permission_rank_update_stmt FROM @permission_rank_update_sql;
|
-- Pivot subquery: one row per permission_key, one column per rank_<id>.
|
||||||
EXECUTE permission_rank_update_stmt;
|
SET @pivot_sql = NULL;
|
||||||
DEALLOCATE PREPARE permission_rank_update_stmt;
|
|
||||||
END LOOP;
|
|
||||||
|
|
||||||
CLOSE rank_cursor;
|
SELECT GROUP_CONCAT(
|
||||||
END$$
|
CONCAT(
|
||||||
DELIMITER ;
|
'MAX(CASE WHEN rank_id = ', `id`,
|
||||||
|
' THEN permission_value END) AS `rank_', `id`, '`'
|
||||||
|
)
|
||||||
|
ORDER BY `id` ASC
|
||||||
|
SEPARATOR ', '
|
||||||
|
)
|
||||||
|
INTO @pivot_sql
|
||||||
|
FROM `permission_ranks`;
|
||||||
|
|
||||||
CALL `refresh_permission_definition_values`();
|
SET @final_update_sql = CONCAT(
|
||||||
|
'UPDATE `permission_definitions` pd ',
|
||||||
|
'INNER JOIN ( ',
|
||||||
|
'SELECT permission_key, ', @pivot_sql, ' ',
|
||||||
|
'FROM ( ', @unpivot_sql, ' ) u ',
|
||||||
|
'GROUP BY permission_key ',
|
||||||
|
') src ON src.permission_key = pd.`permission_key` ',
|
||||||
|
'SET ', @set_clause_sql
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE final_update_stmt FROM @final_update_sql;
|
||||||
|
EXECUTE final_update_stmt;
|
||||||
|
DEALLOCATE PREPARE final_update_stmt;
|
||||||
@@ -0,0 +1,500 @@
|
|||||||
|
UPDATE emulator_settings SET `value` = '1' WHERE (`key` = 'wired.engine.enabled');
|
||||||
|
UPDATE emulator_settings SET `value` = '1' WHERE (`key` = 'wired.engine.exclusive');
|
||||||
|
|
||||||
|
ALTER TABLE emulator_settings
|
||||||
|
ADD COLUMN IF NOT EXISTS `comment` VARCHAR(255) NOT NULL AFTER `value`;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `catalog_items_bc` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`item_ids` varchar(666) NOT NULL,
|
||||||
|
`page_id` int(11) NOT NULL,
|
||||||
|
`catalog_name` varchar(100) NOT NULL,
|
||||||
|
`order_number` int(11) NOT NULL DEFAULT 1,
|
||||||
|
`extradata` varchar(500) NOT NULL DEFAULT '',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `catalog_pages_bc` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`parent_id` int(11) NOT NULL DEFAULT -1,
|
||||||
|
`caption` varchar(128) NOT NULL,
|
||||||
|
`page_layout` enum(
|
||||||
|
'default_3x3','club_buy','club_gift','frontpage','spaces','recycler',
|
||||||
|
'recycler_info','recycler_prizes','trophies','plasto','marketplace',
|
||||||
|
'marketplace_own_items','spaces_new','soundmachine','guilds','guild_furni',
|
||||||
|
'info_duckets','info_rentables','info_pets','roomads','single_bundle',
|
||||||
|
'sold_ltd_items','badge_display','bots','pets','pets2','pets3',
|
||||||
|
'productpage1','room_bundle','recent_purchases',
|
||||||
|
'default_3x3_color_grouping','guild_forum','vip_buy','info_loyalty',
|
||||||
|
'loyalty_vip_buy','collectibles','petcustomization','frontpage_featured'
|
||||||
|
) NOT NULL DEFAULT 'default_3x3',
|
||||||
|
`icon_color` int(11) NOT NULL DEFAULT 1,
|
||||||
|
`icon_image` int(11) NOT NULL DEFAULT 1,
|
||||||
|
`order_num` int(11) NOT NULL DEFAULT 1,
|
||||||
|
`visible` enum('0','1') NOT NULL DEFAULT '1',
|
||||||
|
`enabled` enum('0','1') NOT NULL DEFAULT '1',
|
||||||
|
`page_headline` varchar(1024) NOT NULL DEFAULT '',
|
||||||
|
`page_teaser` varchar(64) NOT NULL DEFAULT '',
|
||||||
|
`page_special` varchar(2048) DEFAULT '' COMMENT 'Gold Bubble: catalog_special_txtbg1 // Speech Bubble: catalog_special_txtbg2 // Place normal text in page_text_teaser',
|
||||||
|
`page_text1` text DEFAULT NULL,
|
||||||
|
`page_text2` text DEFAULT NULL,
|
||||||
|
`page_text_details` text DEFAULT NULL,
|
||||||
|
`page_text_teaser` text DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
|
ALTER TABLE `catalog_club_offers`
|
||||||
|
MODIFY COLUMN `type` ENUM('HC','VIP','BUILDERS_CLUB','BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC';
|
||||||
|
|
||||||
|
ALTER TABLE `catalog_pages`
|
||||||
|
MODIFY COLUMN `page_layout` ENUM(
|
||||||
|
'default_3x3',
|
||||||
|
'club_buy',
|
||||||
|
'club_gift',
|
||||||
|
'frontpage',
|
||||||
|
'spaces',
|
||||||
|
'recycler',
|
||||||
|
'recycler_info',
|
||||||
|
'recycler_prizes',
|
||||||
|
'trophies',
|
||||||
|
'plasto',
|
||||||
|
'marketplace',
|
||||||
|
'marketplace_own_items',
|
||||||
|
'spaces_new',
|
||||||
|
'soundmachine',
|
||||||
|
'guilds',
|
||||||
|
'guild_furni',
|
||||||
|
'info_duckets',
|
||||||
|
'info_rentables',
|
||||||
|
'info_pets',
|
||||||
|
'roomads',
|
||||||
|
'single_bundle',
|
||||||
|
'sold_ltd_items',
|
||||||
|
'badge_display',
|
||||||
|
'bots',
|
||||||
|
'pets',
|
||||||
|
'pets2',
|
||||||
|
'pets3',
|
||||||
|
'productpage1',
|
||||||
|
'room_bundle',
|
||||||
|
'recent_purchases',
|
||||||
|
'default_3x3_color_grouping',
|
||||||
|
'guild_forum',
|
||||||
|
'vip_buy',
|
||||||
|
'info_loyalty',
|
||||||
|
'loyalty_vip_buy',
|
||||||
|
'collectibles',
|
||||||
|
'petcustomization',
|
||||||
|
'frontpage_featured',
|
||||||
|
'builders_club_frontpage',
|
||||||
|
'builders_club_addons',
|
||||||
|
'builders_club_loyalty'
|
||||||
|
) NOT NULL DEFAULT 'default_3x3';
|
||||||
|
|
||||||
|
ALTER TABLE `catalog_pages`
|
||||||
|
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL','BUILDER','BOTH') NOT NULL DEFAULT 'NORMAL'
|
||||||
|
AFTER `club_only`;
|
||||||
|
|
||||||
|
ALTER TABLE `catalog_pages_bc`
|
||||||
|
MODIFY COLUMN `page_layout` ENUM(
|
||||||
|
'default_3x3',
|
||||||
|
'club_buy',
|
||||||
|
'club_gift',
|
||||||
|
'frontpage',
|
||||||
|
'spaces',
|
||||||
|
'recycler',
|
||||||
|
'recycler_info',
|
||||||
|
'recycler_prizes',
|
||||||
|
'trophies',
|
||||||
|
'plasto',
|
||||||
|
'marketplace',
|
||||||
|
'marketplace_own_items',
|
||||||
|
'spaces_new',
|
||||||
|
'soundmachine',
|
||||||
|
'guilds',
|
||||||
|
'guild_furni',
|
||||||
|
'info_duckets',
|
||||||
|
'info_rentables',
|
||||||
|
'info_pets',
|
||||||
|
'roomads',
|
||||||
|
'single_bundle',
|
||||||
|
'sold_ltd_items',
|
||||||
|
'badge_display',
|
||||||
|
'bots',
|
||||||
|
'pets',
|
||||||
|
'pets2',
|
||||||
|
'pets3',
|
||||||
|
'productpage1',
|
||||||
|
'room_bundle',
|
||||||
|
'recent_purchases',
|
||||||
|
'default_3x3_color_grouping',
|
||||||
|
'guild_forum',
|
||||||
|
'vip_buy',
|
||||||
|
'info_loyalty',
|
||||||
|
'loyalty_vip_buy',
|
||||||
|
'collectibles',
|
||||||
|
'petcustomization',
|
||||||
|
'frontpage_featured',
|
||||||
|
'builders_club_frontpage',
|
||||||
|
'builders_club_addons',
|
||||||
|
'builders_club_loyalty'
|
||||||
|
) NOT NULL DEFAULT 'default_3x3';
|
||||||
|
|
||||||
|
SET @col_exists := (
|
||||||
|
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'users_settings'
|
||||||
|
AND COLUMN_NAME = 'builders_club_bonus_furni'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@col_exists = 0,
|
||||||
|
'ALTER TABLE `users_settings` ADD COLUMN `builders_club_bonus_furni` INT NOT NULL DEFAULT 0;',
|
||||||
|
'SELECT "exists";'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `wired_emulator_settings` (
|
||||||
|
`key` varchar(191) NOT NULL,
|
||||||
|
`value` text NOT NULL,
|
||||||
|
`comment` text NOT NULL,
|
||||||
|
PRIMARY KEY (`key`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
|
||||||
|
|
||||||
|
INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`)
|
||||||
|
SELECT 'wired.engine.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.enabled' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.engine.exclusive', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.exclusive' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.engine.maxStepsPerStack', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.maxStepsPerStack' LIMIT 1), '100'), 'Maximum amount of internal processing steps allowed for a single wired stack execution.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.engine.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.debug' LIMIT 1), '0'), 'Enable verbose debug logging for the new wired engine.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.custom.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.custom.enabled' LIMIT 1), '0'), 'Enable custom legacy wired behaviour such as user-based cooldown exceptions and compatibility logic.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'hotel.wired.furni.selection.count', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.furni.selection.count' LIMIT 1), '5'), 'Maximum number of furni that a wired box can store or select.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'hotel.wired.max_delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.max_delay' LIMIT 1), '20'), 'Maximum delay value accepted by wired effects that support delayed execution.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'hotel.wired.message.max_length', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.message.max_length' LIMIT 1), '100'), 'Maximum length of text fields used by wired messages and bot text effects.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.effect.teleport.delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.effect.teleport.delay' LIMIT 1), '500'), 'Delay in milliseconds used by wired teleport movement.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.place.under', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.place.under' LIMIT 1), '0'), 'Allow placing wired furniture underneath other items when room rules permit it.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.tick.interval.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.interval.ms' LIMIT 1), '50'), 'Global wired tick interval in milliseconds used by repeaters and other tick-driven wired items.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.tick.resolution', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.resolution' LIMIT 1), '100'), 'Legacy wired tick resolution value kept for compatibility with older wired timing setups.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.tick.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.debug' LIMIT 1), '0'), 'Enable verbose logging for the wired tick service.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.tick.thread.priority', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.thread.priority' LIMIT 1), '6'), 'Java thread priority used by the wired tick service.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.highscores.displaycount', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.highscores.displaycount' LIMIT 1), '25'), 'Maximum number of wired highscore entries shown to users when a highscore is displayed.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.abuse.max.recursion.depth', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.recursion.depth' LIMIT 1), '10'), 'Maximum recursive wired depth allowed before execution is stopped.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.abuse.max.events.per.window', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.events.per.window' LIMIT 1), '100'), 'Maximum amount of identical wired events allowed inside the abuse rate-limit window before a room ban is applied.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.abuse.rate.limit.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.rate.limit.window.ms' LIMIT 1), '10000'), 'Time window in milliseconds used by the wired abuse rate limiter.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.abuse.ban.duration.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.ban.duration.ms' LIMIT 1), '600000'), 'Duration in milliseconds of the temporary wired ban after abuse detection.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.monitor.usage.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.window.ms' LIMIT 1), '1000'), 'Rolling window size in milliseconds used to calculate wired usage in the :wired monitor.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.monitor.usage.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.limit' LIMIT 1), '1000'), 'Maximum wired usage budget allowed in one monitor window before EXECUTION_CAP is raised.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.monitor.delayed.events.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.delayed.events.limit' LIMIT 1), '100'), 'Maximum number of delayed wired events that can be queued in one room at the same time.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.monitor.overload.average.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.average.ms' LIMIT 1), '50'), 'Average execution time threshold in milliseconds that starts overload tracking.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.monitor.overload.peak.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.peak.ms' LIMIT 1), '150'), 'Peak single execution time threshold in milliseconds that starts overload tracking.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.monitor.overload.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.consecutive.windows' LIMIT 1), '2'), 'Number of consecutive overloaded monitor windows required before logging EXECUTOR_OVERLOAD.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.monitor.heavy.usage.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.usage.percent' LIMIT 1), '70'), 'Usage percentage threshold that contributes to marking a room as heavy in the :wired monitor.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.monitor.heavy.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.consecutive.windows' LIMIT 1), '5'), 'Number of consecutive windows above the heavy usage threshold required before the room is marked as heavy.'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'wired.monitor.heavy.delayed.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.delayed.percent' LIMIT 1), '60'), 'Delayed queue percentage threshold that also contributes to the heavy-room calculation.'
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`value` = VALUES(`value`),
|
||||||
|
`comment` = VALUES(`comment`);
|
||||||
|
|
||||||
|
DELETE FROM `emulator_settings`
|
||||||
|
WHERE `key` IN (
|
||||||
|
'wired.engine.enabled',
|
||||||
|
'wired.engine.exclusive',
|
||||||
|
'wired.engine.maxStepsPerStack',
|
||||||
|
'wired.engine.debug',
|
||||||
|
'wired.custom.enabled',
|
||||||
|
'hotel.wired.furni.selection.count',
|
||||||
|
'hotel.wired.max_delay',
|
||||||
|
'hotel.wired.message.max_length',
|
||||||
|
'wired.effect.teleport.delay',
|
||||||
|
'wired.place.under',
|
||||||
|
'wired.tick.interval.ms',
|
||||||
|
'wired.tick.resolution',
|
||||||
|
'wired.tick.debug',
|
||||||
|
'wired.tick.thread.priority',
|
||||||
|
'wired.highscores.displaycount',
|
||||||
|
'wired.abuse.max.recursion.depth',
|
||||||
|
'wired.abuse.max.events.per.window',
|
||||||
|
'wired.abuse.rate.limit.window.ms',
|
||||||
|
'wired.abuse.ban.duration.ms',
|
||||||
|
'wired.monitor.usage.window.ms',
|
||||||
|
'wired.monitor.usage.limit',
|
||||||
|
'wired.monitor.delayed.events.limit',
|
||||||
|
'wired.monitor.overload.average.ms',
|
||||||
|
'wired.monitor.overload.peak.ms',
|
||||||
|
'wired.monitor.overload.consecutive.windows',
|
||||||
|
'wired.monitor.heavy.usage.percent',
|
||||||
|
'wired.monitor.heavy.consecutive.windows',
|
||||||
|
'wired.monitor.heavy.delayed.percent'
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Allow whispering while a user stands inside a mute area.' WHERE `key` = 'room.chat.mutearea.allow_whisper';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'HTML or text format used for room chat prefixes.' WHERE `key` = 'room.chat.prefix.format';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Badge code displayed on promoted rooms.' WHERE `key` = 'room.promotion.badge';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Image used by Rosie bubble notifications.' WHERE `key` = 'rosie.bubble.image.url';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Currency type used by Rosie when buying a room or room package.' WHERE `key` = 'rosie.buyroom.currency.type';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `runtime.threads`.' WHERE `key` = 'runtime.threads';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.private.chats`.' WHERE `key` = 'save.private.chats';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.room.chats`.' WHERE `key` = 'save.room.chats';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Expose moderation tickets to the scripter or automation tooling.' WHERE `key` = 'scripter.modtool.tickets';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for diamonds.' WHERE `key` = 'seasonal.currency.diamond';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for duckets.' WHERE `key` = 'seasonal.currency.ducket';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated display names for seasonal currency types.' WHERE `key` = 'seasonal.currency.names';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for pixels.' WHERE `key` = 'seasonal.currency.pixel';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for shells.' WHERE `key` = 'seasonal.currency.shell';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Primary seasonal currency type ID.' WHERE `key` = 'seasonal.primary.type';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated list of currency type IDs treated as seasonal currencies.' WHERE `key` = 'seasonal.types';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Achievement code granted for the HC subscription tier.' WHERE `key` = 'subscriptions.hc.achievement';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Number of days before expiry when HC discount offers become available.' WHERE `key` = 'subscriptions.hc.discount.days_before_end';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Enable discounted HC renewal offers.' WHERE `key` = 'subscriptions.hc.discount.enabled';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Reset tracked credits spent when the HC subscription expires.' WHERE `key` = 'subscriptions.hc.payday.creditsspent_reset_on_expire';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Currency rewarded by the HC payday system.' WHERE `key` = 'subscriptions.hc.payday.currency';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Enable the HC payday reward system.' WHERE `key` = 'subscriptions.hc.payday.enabled';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Date interval used between HC payday reward runs.' WHERE `key` = 'subscriptions.hc.payday.interval';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Next scheduled execution date for HC payday rewards.' WHERE `key` = 'subscriptions.hc.payday.next_date';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Percentage of eligible spending returned by HC payday.' WHERE `key` = 'subscriptions.hc.payday.percentage';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated streak thresholds and rewards for HC payday.' WHERE `key` = 'subscriptions.hc.payday.streak';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Enable the subscription background scheduler.' WHERE `key` = 'subscriptions.scheduler.enabled';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Interval in minutes between subscription scheduler runs.' WHERE `key` = 'subscriptions.scheduler.interval';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'Compatibility marker used by the custom team wired implementation. Do not remove.' WHERE `key` = 'team.wired.update.rc-1';
|
||||||
|
UPDATE `emulator_settings` SET `comment` = 'API key used by the YouTube integration.' WHERE `key` = 'youtube.apikey';
|
||||||
|
|
||||||
|
-- =============================================================
|
||||||
|
-- Permissions normalization is handled by 005_normalize_permissions_schema.sql
|
||||||
|
-- (Removed from this file to avoid DELIMITER issues in HeidiSQL.)
|
||||||
|
-- =============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `room_wired_settings` (
|
||||||
|
`room_id` int(11) NOT NULL,
|
||||||
|
`inspect_mask` int(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can open and inspect Wired in the room. 1=everyone, 2=users with rights, 4=group members, 8=group admins.',
|
||||||
|
`modify_mask` int(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can modify Wired in the room. 2=users with rights, 4=group members, 8=group admins.',
|
||||||
|
PRIMARY KEY (`room_id`),
|
||||||
|
CONSTRAINT `fk_room_wired_settings_room_id` FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `room_user_wired_variables` (
|
||||||
|
`room_id` int(11) NOT NULL,
|
||||||
|
`user_id` int(11) NOT NULL,
|
||||||
|
`variable_item_id` int(11) NOT NULL,
|
||||||
|
`value` int(11) DEFAULT NULL,
|
||||||
|
`created_at` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`updated_at` int(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`room_id`, `user_id`, `variable_item_id`),
|
||||||
|
KEY `idx_room_user_wired_variables_room_item` (`room_id`, `variable_item_id`),
|
||||||
|
KEY `idx_room_user_wired_variables_user` (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `room_furni_wired_variables` (
|
||||||
|
`room_id` int(11) NOT NULL,
|
||||||
|
`furni_id` int(11) NOT NULL,
|
||||||
|
`variable_item_id` int(11) NOT NULL,
|
||||||
|
`value` int(11) DEFAULT NULL,
|
||||||
|
`created_at` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`updated_at` int(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`room_id`, `furni_id`, `variable_item_id`),
|
||||||
|
KEY `idx_room_furni_wired_variables_room_item` (`room_id`, `variable_item_id`),
|
||||||
|
KEY `idx_room_furni_wired_variables_furni` (`furni_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `room_wired_variables` (
|
||||||
|
`room_id` int(11) NOT NULL,
|
||||||
|
`variable_item_id` int(11) NOT NULL,
|
||||||
|
`value` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`created_at` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`updated_at` int(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`room_id`, `variable_item_id`),
|
||||||
|
KEY `idx_room_wired_variables_room_item` (`room_id`, `variable_item_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE `room_user_wired_variables`
|
||||||
|
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
|
||||||
|
|
||||||
|
ALTER TABLE `room_user_wired_variables`
|
||||||
|
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
|
||||||
|
|
||||||
|
UPDATE `room_user_wired_variables`
|
||||||
|
SET
|
||||||
|
`created_at` = IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()),
|
||||||
|
`updated_at` = IF(`updated_at` > 0, `updated_at`, IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()));
|
||||||
|
|
||||||
|
ALTER TABLE `room_furni_wired_variables`
|
||||||
|
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
|
||||||
|
|
||||||
|
ALTER TABLE `room_furni_wired_variables`
|
||||||
|
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
|
||||||
|
|
||||||
|
UPDATE `room_furni_wired_variables`
|
||||||
|
SET
|
||||||
|
`created_at` = IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()),
|
||||||
|
`updated_at` = IF(`updated_at` > 0, `updated_at`, IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()));
|
||||||
|
|
||||||
|
ALTER TABLE `room_wired_variables`
|
||||||
|
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
|
||||||
|
|
||||||
|
ALTER TABLE `room_wired_variables`
|
||||||
|
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
|
||||||
|
|
||||||
|
UPDATE `room_wired_variables`
|
||||||
|
SET
|
||||||
|
`created_at` = 0,
|
||||||
|
`updated_at` = IF(`updated_at` > 0, `updated_at`, UNIX_TIMESTAMP());
|
||||||
|
|
||||||
|
INSERT INTO `chat_bubbles` (`type`, `name`, `permission`, `overridable`, `triggers_talking_furniture`) VALUES
|
||||||
|
(200, 'SHOW_MESSAGE_RED', '', 1, 0),
|
||||||
|
(201, 'SHOW_MESSAGE_GREEN', '', 1, 0),
|
||||||
|
(202, 'SHOW_MESSAGE_BLUE', '', 1, 0),
|
||||||
|
(210, 'SHOW_MESSAGE_ALERT', '', 1, 0),
|
||||||
|
(211, 'SHOW_MESSAGE_INFO', '', 1, 0),
|
||||||
|
(212, 'SHOW_MESSAGE_WARNING', '', 1, 0),
|
||||||
|
(220, 'SHOW_MESSAGE_WRONG', '', 1, 0),
|
||||||
|
(221, 'SHOW_MESSAGE_WRONG_CIRCLED', '', 1, 0),
|
||||||
|
(222, 'SHOW_MESSAGE_CORRECT', '', 1, 0),
|
||||||
|
(223, 'SHOW_MESSAGE_CORRECT_CIRCLED', '', 1, 0),
|
||||||
|
(224, 'SHOW_MESSAGE_QUESTION', '', 1, 0),
|
||||||
|
(225, 'SHOW_MESSAGE_QUESTION_CIRCLED', '', 1, 0),
|
||||||
|
(226, 'SHOW_MESSAGE_ARROW_UP', '', 1, 0),
|
||||||
|
(227, 'SHOW_MESSAGE_ARROW_UP_CIRCLED', '', 1, 0),
|
||||||
|
(228, 'SHOW_MESSAGE_ARROW_DOWN', '', 1, 0),
|
||||||
|
(229, 'SHOW_MESSAGE_ARROW_DOWN_CIRCLED', '', 1, 0),
|
||||||
|
(250, 'SHOW_MESSAGE_SKULL', '', 1, 0),
|
||||||
|
(251, 'SHOW_MESSAGE_SKULL_ALT', '', 1, 0),
|
||||||
|
(252, 'SHOW_MESSAGE_MAGNIFIER', '', 1, 0)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`name` = VALUES(`name`),
|
||||||
|
`permission` = VALUES(`permission`),
|
||||||
|
`overridable` = VALUES(`overridable`),
|
||||||
|
`triggers_talking_furniture` = VALUES(`triggers_talking_furniture`);
|
||||||
|
|
||||||
|
ALTER TABLE `catalog_club_offers`
|
||||||
|
MODIFY COLUMN `type` ENUM('HC', 'VIP', 'BUILDERS_CLUB', 'BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC';
|
||||||
|
|
||||||
|
ALTER TABLE `catalog_pages`
|
||||||
|
MODIFY COLUMN `page_layout` ENUM(
|
||||||
|
'default_3x3',
|
||||||
|
'club_buy',
|
||||||
|
'club_gift',
|
||||||
|
'frontpage',
|
||||||
|
'spaces',
|
||||||
|
'recycler',
|
||||||
|
'recycler_info',
|
||||||
|
'recycler_prizes',
|
||||||
|
'trophies',
|
||||||
|
'plasto',
|
||||||
|
'marketplace',
|
||||||
|
'marketplace_own_items',
|
||||||
|
'spaces_new',
|
||||||
|
'soundmachine',
|
||||||
|
'guilds',
|
||||||
|
'guild_furni',
|
||||||
|
'info_duckets',
|
||||||
|
'info_rentables',
|
||||||
|
'info_pets',
|
||||||
|
'roomads',
|
||||||
|
'single_bundle',
|
||||||
|
'sold_ltd_items',
|
||||||
|
'badge_display',
|
||||||
|
'bots',
|
||||||
|
'pets',
|
||||||
|
'pets2',
|
||||||
|
'pets3',
|
||||||
|
'productpage1',
|
||||||
|
'room_bundle',
|
||||||
|
'recent_purchases',
|
||||||
|
'default_3x3_color_grouping',
|
||||||
|
'guild_forum',
|
||||||
|
'vip_buy',
|
||||||
|
'info_loyalty',
|
||||||
|
'loyalty_vip_buy',
|
||||||
|
'collectibles',
|
||||||
|
'petcustomization',
|
||||||
|
'frontpage_featured',
|
||||||
|
'builders_club_frontpage',
|
||||||
|
'builders_club_addons',
|
||||||
|
'builders_club_loyalty'
|
||||||
|
) NOT NULL DEFAULT 'default_3x3';
|
||||||
|
|
||||||
|
ALTER TABLE `catalog_pages`
|
||||||
|
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL', 'BUILDER', 'BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `club_only`;
|
||||||
|
|
||||||
|
ALTER TABLE `rooms`
|
||||||
|
ADD COLUMN IF NOT EXISTS `builders_club_trial_locked` TINYINT(1) NOT NULL DEFAULT 0 AFTER `allow_underpass`,
|
||||||
|
ADD COLUMN IF NOT EXISTS `builders_club_original_state` VARCHAR(16) NOT NULL DEFAULT 'open' AFTER `builders_club_trial_locked`;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `builders_club_items` (
|
||||||
|
`item_id` INT(11) NOT NULL,
|
||||||
|
`user_id` INT(11) NOT NULL,
|
||||||
|
`room_id` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`item_id`),
|
||||||
|
KEY `idx_builders_club_items_user_id` (`user_id`),
|
||||||
|
KEY `idx_builders_club_items_room_id` (`room_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `catalog_pages_bc`
|
||||||
|
MODIFY COLUMN `page_layout` ENUM(
|
||||||
|
'default_3x3',
|
||||||
|
'club_buy',
|
||||||
|
'club_gift',
|
||||||
|
'frontpage',
|
||||||
|
'spaces',
|
||||||
|
'recycler',
|
||||||
|
'recycler_info',
|
||||||
|
'recycler_prizes',
|
||||||
|
'trophies',
|
||||||
|
'plasto',
|
||||||
|
'marketplace',
|
||||||
|
'marketplace_own_items',
|
||||||
|
'spaces_new',
|
||||||
|
'soundmachine',
|
||||||
|
'guilds',
|
||||||
|
'guild_furni',
|
||||||
|
'info_duckets',
|
||||||
|
'info_rentables',
|
||||||
|
'info_pets',
|
||||||
|
'roomads',
|
||||||
|
'single_bundle',
|
||||||
|
'sold_ltd_items',
|
||||||
|
'badge_display',
|
||||||
|
'bots',
|
||||||
|
'pets',
|
||||||
|
'pets2',
|
||||||
|
'pets3',
|
||||||
|
'productpage1',
|
||||||
|
'room_bundle',
|
||||||
|
'recent_purchases',
|
||||||
|
'default_3x3_color_grouping',
|
||||||
|
'guild_forum',
|
||||||
|
'vip_buy',
|
||||||
|
'info_loyalty',
|
||||||
|
'loyalty_vip_buy',
|
||||||
|
'collectibles',
|
||||||
|
'petcustomization',
|
||||||
|
'frontpage_featured',
|
||||||
|
'builders_club_frontpage',
|
||||||
|
'builders_club_addons',
|
||||||
|
'builders_club_loyalty'
|
||||||
|
) NOT NULL DEFAULT 'default_3x3';
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- 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;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 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);
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 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,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,22 @@
|
|||||||
|
-- 020_furnidata_edit_log.sql
|
||||||
|
-- Audit trail for furnidata name/description edits made through the furni editor,
|
||||||
|
-- plus config keys for the editor write path. NOTE: *.enabled keys elsewhere are
|
||||||
|
-- read via Boolean.parseBoolean (true/false), but these two are numeric.
|
||||||
|
CREATE TABLE IF NOT EXISTS `furnidata_edit_log` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int(11) NOT NULL,
|
||||||
|
`classname` varchar(255) NOT NULL,
|
||||||
|
`action` enum('edit','revert') NOT NULL DEFAULT 'edit',
|
||||||
|
`old_name` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`new_name` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`old_description` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`new_description` varchar(256) NOT NULL DEFAULT '',
|
||||||
|
`timestamp` int(11) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
INDEX `idx_classname` (`classname`),
|
||||||
|
INDEX `idx_user` (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `emulator_settings` (`key`,`value`) VALUES
|
||||||
|
('items.furnidata.edit.backup.keep','10'),
|
||||||
|
('items.furnidata.edit.ratelimit.ms','2000');
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
-- 021_furnidata_config_cleanup.sql
|
||||||
|
-- Reverts the emulator_settings rows inserted by 021_furnidata_config.sql.
|
||||||
|
--
|
||||||
|
-- Safe default:
|
||||||
|
-- This script ends with ROLLBACK. Run it once to preview the exact rows, then
|
||||||
|
-- change the final ROLLBACK to COMMIT only if the preview is correct.
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS cleanup_furnidata_settings;
|
||||||
|
CREATE TEMPORARY TABLE cleanup_furnidata_settings (
|
||||||
|
`key` VARCHAR(255) NOT NULL PRIMARY KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO cleanup_furnidata_settings (`key`) VALUES
|
||||||
|
('items.furnidata.names.enabled'),
|
||||||
|
('items.furnidata.path'),
|
||||||
|
('items.furnidata.max.bytes'),
|
||||||
|
('items.furnidata.watch.enabled'),
|
||||||
|
('items.furnidata.watch.debounce.ms'),
|
||||||
|
('items.furnidata.watch.min.interval.ms'),
|
||||||
|
('items.furnidata.delta.cap'),
|
||||||
|
('furni.editor.import.url'),
|
||||||
|
('furni.editor.import.cache.ms');
|
||||||
|
|
||||||
|
-- Preview rows that will be removed.
|
||||||
|
SELECT es.`key`, es.`value`
|
||||||
|
FROM emulator_settings es
|
||||||
|
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`
|
||||||
|
ORDER BY es.`key`;
|
||||||
|
|
||||||
|
DELETE es
|
||||||
|
FROM emulator_settings es
|
||||||
|
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`;
|
||||||
|
|
||||||
|
-- Preview remaining matching rows inside the transaction.
|
||||||
|
SELECT COUNT(*) AS remaining_furnidata_settings
|
||||||
|
FROM emulator_settings es
|
||||||
|
JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`;
|
||||||
|
|
||||||
|
-- Safe default. Change to COMMIT after reviewing the preview.
|
||||||
|
ROLLBACK;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- Fix NULL room paint columns
|
||||||
|
--
|
||||||
|
-- Some legacy/imported rooms have NULL in paper_wall / paper_floor / paper_landscape.
|
||||||
|
-- The server compares these with .equals("0.0") on room entry, which throws a
|
||||||
|
-- NullPointerException (RoomManager.openRoom) and prevents the room from loading.
|
||||||
|
-- This normalizes existing NULL values and re-enforces the NOT NULL DEFAULT '0.0'
|
||||||
|
-- constraint so it cannot happen again.
|
||||||
|
|
||||||
|
UPDATE `rooms` SET `paper_wall` = '0.0' WHERE `paper_wall` IS NULL;
|
||||||
|
UPDATE `rooms` SET `paper_floor` = '0.0' WHERE `paper_floor` IS NULL;
|
||||||
|
UPDATE `rooms` SET `paper_landscape` = '0.0' WHERE `paper_landscape` IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE `rooms` MODIFY COLUMN `paper_wall` VARCHAR(50) NOT NULL DEFAULT '0.0';
|
||||||
|
ALTER TABLE `rooms` MODIFY COLUMN `paper_floor` VARCHAR(50) NOT NULL DEFAULT '0.0';
|
||||||
|
ALTER TABLE `rooms` MODIFY COLUMN `paper_landscape` VARCHAR(50) NOT NULL DEFAULT '0.0';
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+27
-19
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>com.eu.habbo</groupId>
|
<groupId>com.eu.habbo</groupId>
|
||||||
<artifactId>Habbo</artifactId>
|
<artifactId>Habbo</artifactId>
|
||||||
<version>4.1.13</version>
|
<version>4.2.44</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
<archive>
|
<archive>
|
||||||
<manifest>
|
<manifest>
|
||||||
<mainClass>com.eu.habbo.Emulator</mainClass>
|
<mainClass>com.eu.habbo.Emulator</mainClass>
|
||||||
|
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||||
</manifest>
|
</manifest>
|
||||||
</archive>
|
</archive>
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -61,6 +62,12 @@
|
|||||||
<show>public</show>
|
<show>public</show>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.5.2</version>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
@@ -76,21 +83,21 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.netty</groupId>
|
<groupId>io.netty</groupId>
|
||||||
<artifactId>netty-all</artifactId>
|
<artifactId>netty-all</artifactId>
|
||||||
<version>4.1.115.Final</version>
|
<version>4.2.15.Final</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- GSON -->
|
<!-- GSON -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.code.gson</groupId>
|
<groupId>com.google.code.gson</groupId>
|
||||||
<artifactId>gson</artifactId>
|
<artifactId>gson</artifactId>
|
||||||
<version>2.11.0</version>
|
<version>2.14.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- MariaDB Connector/J (native driver for MariaDB) -->
|
<!-- MariaDB Connector/J (native driver for MariaDB) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mariadb.jdbc</groupId>
|
<groupId>org.mariadb.jdbc</groupId>
|
||||||
<artifactId>mariadb-java-client</artifactId>
|
<artifactId>mariadb-java-client</artifactId>
|
||||||
<version>3.5.1</version>
|
<version>3.5.8</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -106,7 +113,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.zaxxer</groupId>
|
<groupId>com.zaxxer</groupId>
|
||||||
<artifactId>HikariCP</artifactId>
|
<artifactId>HikariCP</artifactId>
|
||||||
<version>6.2.1</version>
|
<version>7.0.2</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -114,7 +121,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-lang3</artifactId>
|
<artifactId>commons-lang3</artifactId>
|
||||||
<version>3.17.0</version>
|
<version>3.20.0</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -130,7 +137,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jsoup</groupId>
|
<groupId>org.jsoup</groupId>
|
||||||
<artifactId>jsoup</artifactId>
|
<artifactId>jsoup</artifactId>
|
||||||
<version>1.18.3</version>
|
<version>1.22.2</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -138,14 +145,14 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>slf4j-api</artifactId>
|
<artifactId>slf4j-api</artifactId>
|
||||||
<version>2.0.16</version>
|
<version>2.0.18</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Logback -->
|
<!-- Logback -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>ch.qos.logback</groupId>
|
<groupId>ch.qos.logback</groupId>
|
||||||
<artifactId>logback-classic</artifactId>
|
<artifactId>logback-classic</artifactId>
|
||||||
<version>1.5.15</version>
|
<version>1.5.34</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -153,17 +160,10 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.fusesource.jansi</groupId>
|
<groupId>org.fusesource.jansi</groupId>
|
||||||
<artifactId>jansi</artifactId>
|
<artifactId>jansi</artifactId>
|
||||||
<version>2.4.1</version>
|
<version>2.4.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Joda Time -->
|
<!-- jBCrypt � used by the built-in /api/auth/* HTTP login handler
|
||||||
<dependency>
|
|
||||||
<groupId>joda-time</groupId>
|
|
||||||
<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 -->
|
to verify Laravel-style $2y$ BCrypt hashes from users.password -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mindrot</groupId>
|
<groupId>org.mindrot</groupId>
|
||||||
@@ -176,7 +176,15 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.eclipse.angus</groupId>
|
<groupId>org.eclipse.angus</groupId>
|
||||||
<artifactId>jakarta.mail</artifactId>
|
<artifactId>jakarta.mail</artifactId>
|
||||||
<version>2.0.3</version>
|
<version>2.0.5</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JUnit Jupiter -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>6.1.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import ch.qos.logback.core.ConsoleAppender;
|
|||||||
import com.eu.habbo.core.*;
|
import com.eu.habbo.core.*;
|
||||||
import com.eu.habbo.core.consolecommands.ConsoleCommand;
|
import com.eu.habbo.core.consolecommands.ConsoleCommand;
|
||||||
import com.eu.habbo.database.Database;
|
import com.eu.habbo.database.Database;
|
||||||
|
import com.eu.habbo.gui.EmulatorDashboard;
|
||||||
import com.eu.habbo.habbohotel.GameEnvironment;
|
import com.eu.habbo.habbohotel.GameEnvironment;
|
||||||
import com.eu.habbo.habbohotel.gameclients.SessionResumeManager;
|
import com.eu.habbo.habbohotel.gameclients.SessionResumeManager;
|
||||||
import com.eu.habbo.networking.gameserver.GameServer;
|
import com.eu.habbo.networking.gameserver.GameServer;
|
||||||
@@ -38,12 +39,23 @@ public final class Emulator {
|
|||||||
private static final String OS_NAME = (System.getProperty("os.name") != null ? System.getProperty("os.name") : "Unknown");
|
private static final String OS_NAME = (System.getProperty("os.name") != null ? System.getProperty("os.name") : "Unknown");
|
||||||
private static final String CLASS_PATH = (System.getProperty("java.class.path") != null ? System.getProperty("java.class.path") : "Unknown");
|
private static final String CLASS_PATH = (System.getProperty("java.class.path") != null ? System.getProperty("java.class.path") : "Unknown");
|
||||||
|
|
||||||
|
// Fallback version, only used when running outside a packaged jar (e.g. from
|
||||||
|
// the IDE). In production the version comes from the jar manifest below.
|
||||||
public final static int MAJOR = 4;
|
public final static int MAJOR = 4;
|
||||||
public final static int MINOR = 1;
|
public final static int MINOR = 1;
|
||||||
public final static int BUILD = 0;
|
public final static int BUILD = 0;
|
||||||
public final static String PREVIEW = "";
|
public final static String PREVIEW = "";
|
||||||
|
|
||||||
public static final String version = "Arcturus Morningstar" + " " + MAJOR + "." + MINOR + "." + BUILD + " " + PREVIEW;
|
// Tied to the Maven project version: read from the jar manifest
|
||||||
|
// (Implementation-Version = ${project.version}, see pom assembly plugin).
|
||||||
|
private static String resolveVersionNumber() {
|
||||||
|
String implementation = Emulator.class.getPackage().getImplementationVersion();
|
||||||
|
if (implementation != null && !implementation.isEmpty()) return implementation;
|
||||||
|
String fallback = MAJOR + "." + MINOR + "." + BUILD;
|
||||||
|
return PREVIEW.isEmpty() ? fallback : fallback + " " + PREVIEW;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String version = "Arcturus Morningstar Extended " + resolveVersionNumber();
|
||||||
private static final String logo =
|
private static final String logo =
|
||||||
"\n" +
|
"\n" +
|
||||||
"███╗ ███╗ ██████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗████████╗ █████╗ ██████╗ \n" +
|
"███╗ ███╗ ██████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗████████╗ █████╗ ██████╗ \n" +
|
||||||
@@ -186,6 +198,10 @@ public final class Emulator {
|
|||||||
Emulator.isReady = true;
|
Emulator.isReady = true;
|
||||||
Emulator.timeStarted = getIntUnixTimestamp();
|
Emulator.timeStarted = getIntUnixTimestamp();
|
||||||
|
|
||||||
|
if (Emulator.getConfig().getBoolean("gui.enabled", true)) {
|
||||||
|
EmulatorDashboard.launch();
|
||||||
|
}
|
||||||
|
|
||||||
if (Emulator.getConfig().getInt("runtime.threads") < (Runtime.getRuntime().availableProcessors() * 2)) {
|
if (Emulator.getConfig().getInt("runtime.threads") < (Runtime.getRuntime().availableProcessors() * 2)) {
|
||||||
LOGGER.warn("Emulator settings runtime.threads ({}) can be increased to ({}) to possibly increase performance.",
|
LOGGER.warn("Emulator settings runtime.threads ({}) can be increased to ({}) to possibly increase performance.",
|
||||||
Emulator.getConfig().getInt("runtime.threads"),
|
Emulator.getConfig().getInt("runtime.threads"),
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public class RoomUserPetComposer extends MessageComposer {
|
|||||||
this.response.appendString("");
|
this.response.appendString("");
|
||||||
this.response.appendString("unknown");
|
this.response.appendString("unknown");
|
||||||
this.response.appendInt(0);
|
this.response.appendInt(0);
|
||||||
|
this.response.appendInt(0);
|
||||||
return this.response;
|
return this.response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ class DatabasePool {
|
|||||||
databaseConfiguration.addDataSourceProperty("useLocalSessionState", "true");
|
databaseConfiguration.addDataSourceProperty("useLocalSessionState", "true");
|
||||||
databaseConfiguration.addDataSourceProperty("cacheResultSetMetadata", "true");
|
databaseConfiguration.addDataSourceProperty("cacheResultSetMetadata", "true");
|
||||||
databaseConfiguration.addDataSourceProperty("elideSetAutoCommits", "true");
|
databaseConfiguration.addDataSourceProperty("elideSetAutoCommits", "true");
|
||||||
|
|
||||||
|
// Fail fast instead of pinning a pooled connection (and its worker
|
||||||
|
// thread) indefinitely on a stalled/slow MariaDB. HikariCP's
|
||||||
|
// connectionTimeout only bounds the pool *borrow*; these bound the
|
||||||
|
// actual socket/connect round-trip. Overridable via db.params.
|
||||||
|
databaseConfiguration.addDataSourceProperty("socketTimeout", "30000");
|
||||||
|
databaseConfiguration.addDataSourceProperty("connectTimeout", "10000");
|
||||||
|
databaseConfiguration.addDataSourceProperty("tcpKeepAlive", "true");
|
||||||
databaseConfiguration.addDataSourceProperty("maintainTimeStats", "false");
|
databaseConfiguration.addDataSourceProperty("maintainTimeStats", "false");
|
||||||
|
|
||||||
databaseConfiguration.setPoolName("HabboHikariPool");
|
databaseConfiguration.setPoolName("HabboHikariPool");
|
||||||
|
|||||||
@@ -0,0 +1,631 @@
|
|||||||
|
package com.eu.habbo.gui;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
|
import com.eu.habbo.monitoring.EmulatorStatsService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.border.CompoundBorder;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import javax.swing.border.MatteBorder;
|
||||||
|
import javax.swing.table.DefaultTableCellRenderer;
|
||||||
|
import javax.swing.table.DefaultTableModel;
|
||||||
|
import javax.swing.table.JTableHeader;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.ComponentAdapter;
|
||||||
|
import java.awt.event.ComponentEvent;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.awt.geom.Path2D;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class EmulatorDashboard extends JFrame {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(EmulatorDashboard.class);
|
||||||
|
|
||||||
|
// Modern Dark Theme Colors
|
||||||
|
private static final Color COLOR_BG = new Color(18, 18, 18);
|
||||||
|
private static final Color COLOR_SURFACE = new Color(30, 30, 30);
|
||||||
|
private static final Color COLOR_SURFACE_HOVER = new Color(45, 45, 45);
|
||||||
|
private static final Color COLOR_PRIMARY = new Color(99, 102, 241);
|
||||||
|
private static final Color COLOR_PRIMARY_SOFT = new Color(99, 102, 241, 45);
|
||||||
|
private static final Color COLOR_SUCCESS = new Color(34, 197, 94);
|
||||||
|
private static final Color COLOR_WARNING = new Color(245, 158, 11);
|
||||||
|
private static final Color COLOR_TEXT = new Color(240, 240, 240);
|
||||||
|
private static final Color COLOR_TEXT_MUTED = new Color(150, 150, 150);
|
||||||
|
private static final Color COLOR_TEXT_SUBTLE = new Color(110, 110, 110);
|
||||||
|
private static final Color COLOR_BORDER = new Color(50, 50, 50);
|
||||||
|
private static final Font FONT_TITLE = new Font("Segoe UI", Font.BOLD, 26);
|
||||||
|
private static final Font FONT_SECTION = new Font("Segoe UI", Font.BOLD, 16);
|
||||||
|
private static final Font FONT_SMALL = new Font("Segoe UI", Font.PLAIN, 12);
|
||||||
|
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
|
||||||
|
|
||||||
|
private static EmulatorDashboard instance;
|
||||||
|
|
||||||
|
// Overview Tab
|
||||||
|
private final JLabel memLabel = createMetricLabel();
|
||||||
|
private final JLabel cpuLabel = createMetricLabel();
|
||||||
|
private final JLabel threadLabel = createMetricLabel();
|
||||||
|
private final JLabel usersLabel = createMetricLabel();
|
||||||
|
private final JLabel roomsLabel = createMetricLabel();
|
||||||
|
private final JLabel wiredLabel = createMetricLabel();
|
||||||
|
private final JLabel uptimeLabel = createStatusValueLabel();
|
||||||
|
private final JLabel lastUpdatedLabel = createStatusValueLabel();
|
||||||
|
private final JLabel footerStatusLabel = createStatusValueLabel();
|
||||||
|
private final MemoryGraphPanel memoryGraph = new MemoryGraphPanel();
|
||||||
|
|
||||||
|
// Tables
|
||||||
|
private final DefaultTableModel usersTableModel;
|
||||||
|
private final DefaultTableModel roomsTableModel;
|
||||||
|
private final DefaultTableModel wiredTableModel;
|
||||||
|
private final JTable usersTable;
|
||||||
|
private final JTable roomsTable;
|
||||||
|
private final JTable wiredTable;
|
||||||
|
private final JLabel usersCountLabel = createCountLabel();
|
||||||
|
private final JLabel roomsCountLabel = createCountLabel();
|
||||||
|
private final JLabel wiredCountLabel = createCountLabel();
|
||||||
|
|
||||||
|
// UI Components
|
||||||
|
private final JPanel cardsPanel;
|
||||||
|
private final CardLayout cardLayout;
|
||||||
|
private final Map<String, JPanel> navButtons = new HashMap<>();
|
||||||
|
private String selectedCardName = "Overview";
|
||||||
|
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||||
|
Thread t = new Thread(r, "Dashboard-Updater");
|
||||||
|
t.setDaemon(true);
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
|
private EmulatorDashboard() {
|
||||||
|
setTitle("Arcturus Morningstar - System Dashboard");
|
||||||
|
setSize(1100, 700);
|
||||||
|
setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
|
||||||
|
setLocationRelativeTo(null);
|
||||||
|
getContentPane().setBackground(COLOR_BG);
|
||||||
|
setLayout(new BorderLayout());
|
||||||
|
|
||||||
|
// Setup custom Look & Feel basics to remove weird Swing borders
|
||||||
|
UIManager.put("ScrollBar.background", COLOR_BG);
|
||||||
|
UIManager.put("ScrollBar.thumb", COLOR_SURFACE_HOVER);
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
JPanel sidebar = new JPanel();
|
||||||
|
sidebar.setLayout(new BoxLayout(sidebar, BoxLayout.Y_AXIS));
|
||||||
|
sidebar.setBackground(COLOR_SURFACE);
|
||||||
|
sidebar.setPreferredSize(new Dimension(220, 0));
|
||||||
|
sidebar.setBorder(new MatteBorder(0, 0, 0, 1, COLOR_BORDER));
|
||||||
|
|
||||||
|
// Sidebar Header
|
||||||
|
JPanel brandPanel = new JPanel(new BorderLayout());
|
||||||
|
brandPanel.setBackground(COLOR_SURFACE);
|
||||||
|
brandPanel.setBorder(new EmptyBorder(20, 20, 30, 20));
|
||||||
|
|
||||||
|
JLabel brandTitle = new JLabel("Arcturus");
|
||||||
|
brandTitle.setFont(new Font("Segoe UI", Font.BOLD, 22));
|
||||||
|
brandTitle.setForeground(COLOR_TEXT);
|
||||||
|
|
||||||
|
JLabel brandSub = new JLabel("v" + Emulator.version);
|
||||||
|
brandSub.setFont(new Font("Segoe UI", Font.PLAIN, 12));
|
||||||
|
brandSub.setForeground(COLOR_PRIMARY);
|
||||||
|
|
||||||
|
brandPanel.add(brandTitle, BorderLayout.NORTH);
|
||||||
|
brandPanel.add(brandSub, BorderLayout.SOUTH);
|
||||||
|
sidebar.add(brandPanel);
|
||||||
|
|
||||||
|
// Main Cards
|
||||||
|
cardLayout = new CardLayout();
|
||||||
|
cardsPanel = new JPanel(cardLayout);
|
||||||
|
cardsPanel.setBackground(COLOR_BG);
|
||||||
|
|
||||||
|
// Setup Tabs
|
||||||
|
usersTableModel = createTableModel(new String[]{"ID", "Username", "Rank", "Credits", "Room ID"});
|
||||||
|
roomsTableModel = createTableModel(new String[]{"Room ID", "Name", "Players", "Items", "Tickables", "CPU (ms)", "Est. RAM (KB)", "Thread"});
|
||||||
|
wiredTableModel = createTableModel(new String[]{"Room ID", "Avg Tick", "Peak Tick", "Usage %", "Delayed", "Overloaded?", "Heavy?"});
|
||||||
|
usersTable = createDashboardTable(usersTableModel);
|
||||||
|
roomsTable = createDashboardTable(roomsTableModel);
|
||||||
|
wiredTable = createDashboardTable(wiredTableModel);
|
||||||
|
|
||||||
|
cardsPanel.add(createOverviewTab(), "Overview");
|
||||||
|
cardsPanel.add(createTableTab("Online Users", "Players currently connected to the emulator.", usersTable, usersCountLabel), "Online Users");
|
||||||
|
cardsPanel.add(createTableTab("Active Rooms", "Loaded rooms with lightweight performance indicators.", roomsTable, roomsCountLabel), "Active Rooms");
|
||||||
|
cardsPanel.add(createTableTab("Wired Diagnostics", "Rooms currently using wired timing, delay and execution budget.", wiredTable, wiredCountLabel), "Wired Diagnostics");
|
||||||
|
|
||||||
|
// Sidebar Navigation
|
||||||
|
sidebar.add(createNavButton("Overview", "Overview"));
|
||||||
|
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
|
||||||
|
sidebar.add(createNavButton("Online Users", "Online Users"));
|
||||||
|
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
|
||||||
|
sidebar.add(createNavButton("Active Rooms", "Active Rooms"));
|
||||||
|
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
|
||||||
|
sidebar.add(createNavButton("Wired Diagnostics", "Wired Diagnostics"));
|
||||||
|
sidebar.add(Box.createVerticalGlue());
|
||||||
|
|
||||||
|
add(sidebar, BorderLayout.WEST);
|
||||||
|
add(cardsPanel, BorderLayout.CENTER);
|
||||||
|
add(createStatusBar(), BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
addComponentListener(new ComponentAdapter() {
|
||||||
|
@Override
|
||||||
|
public void componentShown(ComponentEvent e) {
|
||||||
|
setActiveCard("Overview");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start updates
|
||||||
|
scheduler.scheduleAtFixedRate(this::updateMetrics, 0, 1, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultTableModel createTableModel(String[] cols) {
|
||||||
|
return new DefaultTableModel(cols, 0) {
|
||||||
|
@Override public boolean isCellEditable(int row, int column) { return false; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel createNavButton(String text, String cardName) {
|
||||||
|
JPanel btn = new JPanel(new BorderLayout());
|
||||||
|
btn.setBackground(COLOR_SURFACE);
|
||||||
|
btn.setMaximumSize(new Dimension(220, 45));
|
||||||
|
btn.setBorder(new EmptyBorder(0, 18, 0, 0));
|
||||||
|
btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
|
||||||
|
navButtons.put(cardName, btn);
|
||||||
|
|
||||||
|
JLabel lbl = new JLabel(text);
|
||||||
|
lbl.setFont(new Font("Segoe UI", Font.BOLD, 14));
|
||||||
|
lbl.setForeground(COLOR_TEXT_MUTED);
|
||||||
|
btn.add(lbl, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
btn.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseEntered(MouseEvent e) {
|
||||||
|
btn.setBackground(COLOR_SURFACE_HOVER);
|
||||||
|
lbl.setForeground(COLOR_TEXT);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void mouseExited(MouseEvent e) {
|
||||||
|
updateNavButtonStyle(cardName, btn, lbl);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
setActiveCard(cardName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateNavButtonStyle(cardName, btn, lbl);
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel createOverviewTab() {
|
||||||
|
JPanel wrapper = new JPanel(new BorderLayout());
|
||||||
|
wrapper.setBackground(COLOR_BG);
|
||||||
|
wrapper.setBorder(new EmptyBorder(30, 30, 30, 30));
|
||||||
|
|
||||||
|
JPanel header = new JPanel(new BorderLayout(0, 14));
|
||||||
|
header.setOpaque(false);
|
||||||
|
|
||||||
|
JLabel title = new JLabel("Dashboard Overview");
|
||||||
|
title.setFont(FONT_TITLE);
|
||||||
|
title.setForeground(COLOR_TEXT);
|
||||||
|
|
||||||
|
JLabel subtitle = new JLabel("Operational view of emulator health, activity and wired performance.");
|
||||||
|
subtitle.setFont(new Font("Segoe UI", Font.PLAIN, 13));
|
||||||
|
subtitle.setForeground(COLOR_TEXT_MUTED);
|
||||||
|
|
||||||
|
JPanel titleBlock = new JPanel();
|
||||||
|
titleBlock.setOpaque(false);
|
||||||
|
titleBlock.setLayout(new BoxLayout(titleBlock, BoxLayout.Y_AXIS));
|
||||||
|
titleBlock.add(title);
|
||||||
|
titleBlock.add(Box.createRigidArea(new Dimension(0, 4)));
|
||||||
|
titleBlock.add(subtitle);
|
||||||
|
|
||||||
|
header.add(titleBlock, BorderLayout.NORTH);
|
||||||
|
header.add(createOverviewMetaPanel(), BorderLayout.SOUTH);
|
||||||
|
wrapper.add(header, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
JPanel content = new JPanel(new GridLayout(1, 2, 20, 20));
|
||||||
|
content.setOpaque(false);
|
||||||
|
content.setBorder(new EmptyBorder(20, 0, 0, 0));
|
||||||
|
|
||||||
|
// Left Stats
|
||||||
|
JPanel statsPanel = new JPanel(new GridLayout(3, 2, 12, 12));
|
||||||
|
statsPanel.setOpaque(false);
|
||||||
|
statsPanel.add(createMetricCard("Memory Allocation", memLabel));
|
||||||
|
statsPanel.add(createMetricCard("CPU Load", cpuLabel));
|
||||||
|
statsPanel.add(createMetricCard("Active OS Threads", threadLabel));
|
||||||
|
statsPanel.add(createMetricCard("Connected Players", usersLabel));
|
||||||
|
statsPanel.add(createMetricCard("Loaded Rooms", roomsLabel));
|
||||||
|
statsPanel.add(createMetricCard("Wired Tickables", wiredLabel));
|
||||||
|
content.add(statsPanel);
|
||||||
|
|
||||||
|
// Right Graph
|
||||||
|
JPanel graphContainer = new JPanel(new BorderLayout());
|
||||||
|
graphContainer.setBackground(COLOR_SURFACE);
|
||||||
|
graphContainer.setBorder(BorderFactory.createCompoundBorder(
|
||||||
|
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
|
||||||
|
new EmptyBorder(15, 15, 15, 15)
|
||||||
|
));
|
||||||
|
|
||||||
|
JLabel gTitle = new JLabel("Realtime Memory Usage");
|
||||||
|
gTitle.setFont(FONT_SECTION);
|
||||||
|
gTitle.setForeground(COLOR_TEXT_MUTED);
|
||||||
|
gTitle.setBorder(new EmptyBorder(0, 0, 15, 0));
|
||||||
|
graphContainer.add(gTitle, BorderLayout.NORTH);
|
||||||
|
graphContainer.add(memoryGraph, BorderLayout.CENTER);
|
||||||
|
content.add(graphContainer);
|
||||||
|
|
||||||
|
wrapper.add(content, BorderLayout.CENTER);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel createMetricCard(String title, JLabel valueLabel) {
|
||||||
|
JPanel card = new JPanel(new BorderLayout());
|
||||||
|
card.setBackground(COLOR_SURFACE);
|
||||||
|
card.setBorder(BorderFactory.createCompoundBorder(
|
||||||
|
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
|
||||||
|
new EmptyBorder(15, 20, 15, 20)
|
||||||
|
));
|
||||||
|
|
||||||
|
JLabel tLabel = new JLabel(title);
|
||||||
|
tLabel.setFont(new Font("Segoe UI", Font.BOLD, 13));
|
||||||
|
tLabel.setForeground(COLOR_TEXT_MUTED);
|
||||||
|
|
||||||
|
card.add(tLabel, BorderLayout.NORTH);
|
||||||
|
card.add(valueLabel, BorderLayout.SOUTH);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JLabel createMetricLabel() {
|
||||||
|
JLabel label = new JLabel("-");
|
||||||
|
label.setFont(new Font("Segoe UI", Font.BOLD, 28));
|
||||||
|
label.setForeground(COLOR_TEXT);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel createOverviewMetaPanel() {
|
||||||
|
JPanel panel = new JPanel(new GridLayout(1, 3, 12, 12));
|
||||||
|
panel.setOpaque(false);
|
||||||
|
panel.add(createStatusCard("Uptime", uptimeLabel, COLOR_PRIMARY));
|
||||||
|
panel.add(createStatusCard("Last Refresh", lastUpdatedLabel, COLOR_SUCCESS));
|
||||||
|
panel.add(createStatusCard("GUI Status", footerStatusLabel, COLOR_WARNING));
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel createStatusCard(String title, JLabel valueLabel, Color accent) {
|
||||||
|
JPanel panel = new JPanel(new BorderLayout());
|
||||||
|
panel.setBackground(COLOR_SURFACE);
|
||||||
|
panel.setBorder(BorderFactory.createCompoundBorder(
|
||||||
|
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
|
||||||
|
new EmptyBorder(12, 14, 12, 14)
|
||||||
|
));
|
||||||
|
|
||||||
|
JPanel accentBar = new JPanel();
|
||||||
|
accentBar.setBackground(accent);
|
||||||
|
accentBar.setPreferredSize(new Dimension(6, 0));
|
||||||
|
|
||||||
|
JPanel content = new JPanel();
|
||||||
|
content.setOpaque(false);
|
||||||
|
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
|
||||||
|
|
||||||
|
JLabel label = new JLabel(title);
|
||||||
|
label.setFont(FONT_SMALL);
|
||||||
|
label.setForeground(COLOR_TEXT_MUTED);
|
||||||
|
|
||||||
|
content.add(label);
|
||||||
|
content.add(Box.createRigidArea(new Dimension(0, 6)));
|
||||||
|
content.add(valueLabel);
|
||||||
|
|
||||||
|
panel.add(accentBar, BorderLayout.WEST);
|
||||||
|
panel.add(content, BorderLayout.CENTER);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JLabel createStatusValueLabel() {
|
||||||
|
JLabel label = new JLabel("-");
|
||||||
|
label.setFont(new Font("Segoe UI", Font.BOLD, 16));
|
||||||
|
label.setForeground(COLOR_TEXT);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JLabel createCountLabel() {
|
||||||
|
JLabel label = new JLabel("0 rows");
|
||||||
|
label.setFont(FONT_SMALL);
|
||||||
|
label.setForeground(COLOR_TEXT_MUTED);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JTable createDashboardTable(DefaultTableModel model) {
|
||||||
|
JTable table = new JTable(model);
|
||||||
|
table.setBackground(COLOR_SURFACE);
|
||||||
|
table.setForeground(COLOR_TEXT);
|
||||||
|
table.setGridColor(COLOR_BORDER);
|
||||||
|
table.setRowHeight(34);
|
||||||
|
table.setFont(new Font("Segoe UI", Font.PLAIN, 13));
|
||||||
|
table.setFillsViewportHeight(true);
|
||||||
|
table.setSelectionBackground(COLOR_PRIMARY);
|
||||||
|
table.setSelectionForeground(Color.WHITE);
|
||||||
|
table.setShowVerticalLines(false);
|
||||||
|
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||||
|
table.setAutoCreateRowSorter(true);
|
||||||
|
|
||||||
|
JTableHeader header = table.getTableHeader();
|
||||||
|
header.setBackground(new Color(22, 22, 22));
|
||||||
|
header.setForeground(COLOR_TEXT_MUTED);
|
||||||
|
header.setFont(new Font("Segoe UI", Font.BOLD, 13));
|
||||||
|
header.setPreferredSize(new Dimension(0, 38));
|
||||||
|
header.setBorder(BorderFactory.createMatteBorder(1, 0, 1, 0, COLOR_BORDER));
|
||||||
|
((DefaultTableCellRenderer) header.getDefaultRenderer()).setHorizontalAlignment(JLabel.LEFT);
|
||||||
|
((DefaultTableCellRenderer) header.getDefaultRenderer()).setBorder(new EmptyBorder(0, 10, 0, 0));
|
||||||
|
|
||||||
|
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
|
||||||
|
@Override
|
||||||
|
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
|
||||||
|
JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||||
|
label.setBorder(new EmptyBorder(0, 10, 0, 10));
|
||||||
|
label.setForeground(isSelected ? Color.WHITE : COLOR_TEXT);
|
||||||
|
label.setBackground(isSelected ? COLOR_PRIMARY : ((row % 2 == 0) ? COLOR_SURFACE : new Color(35, 35, 35)));
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel createTableTab(String title, String subtitle, JTable table, JLabel countLabel) {
|
||||||
|
JPanel wrapper = new JPanel(new BorderLayout());
|
||||||
|
wrapper.setBackground(COLOR_BG);
|
||||||
|
wrapper.setBorder(new EmptyBorder(30, 30, 30, 30));
|
||||||
|
|
||||||
|
JPanel titlePanel = new JPanel(new BorderLayout());
|
||||||
|
titlePanel.setOpaque(false);
|
||||||
|
|
||||||
|
JLabel titleLbl = new JLabel(title);
|
||||||
|
titleLbl.setFont(FONT_TITLE);
|
||||||
|
titleLbl.setForeground(COLOR_TEXT);
|
||||||
|
|
||||||
|
JLabel subtitleLbl = new JLabel(subtitle);
|
||||||
|
subtitleLbl.setFont(new Font("Segoe UI", Font.PLAIN, 13));
|
||||||
|
subtitleLbl.setForeground(COLOR_TEXT_MUTED);
|
||||||
|
subtitleLbl.setBorder(new EmptyBorder(6, 0, 0, 0));
|
||||||
|
|
||||||
|
JPanel textPanel = new JPanel();
|
||||||
|
textPanel.setOpaque(false);
|
||||||
|
textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.Y_AXIS));
|
||||||
|
textPanel.add(titleLbl);
|
||||||
|
textPanel.add(subtitleLbl);
|
||||||
|
|
||||||
|
titlePanel.add(textPanel, BorderLayout.WEST);
|
||||||
|
titlePanel.add(countLabel, BorderLayout.EAST);
|
||||||
|
wrapper.add(titlePanel, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
JScrollPane scrollPane = new JScrollPane(table);
|
||||||
|
scrollPane.getViewport().setBackground(COLOR_SURFACE);
|
||||||
|
scrollPane.setBorder(new MatteBorder(1, 1, 1, 1, COLOR_BORDER));
|
||||||
|
scrollPane.setBorder(new CompoundBorder(
|
||||||
|
new EmptyBorder(20, 0, 0, 0),
|
||||||
|
new MatteBorder(1, 1, 1, 1, COLOR_BORDER)
|
||||||
|
));
|
||||||
|
wrapper.add(scrollPane, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel createStatusBar() {
|
||||||
|
JPanel statusBar = new JPanel(new BorderLayout());
|
||||||
|
statusBar.setBackground(COLOR_SURFACE);
|
||||||
|
statusBar.setBorder(new CompoundBorder(
|
||||||
|
new MatteBorder(1, 0, 0, 0, COLOR_BORDER),
|
||||||
|
new EmptyBorder(8, 14, 8, 14)
|
||||||
|
));
|
||||||
|
|
||||||
|
JLabel left = new JLabel("Dashboard running locally");
|
||||||
|
left.setFont(FONT_SMALL);
|
||||||
|
left.setForeground(COLOR_TEXT_SUBTLE);
|
||||||
|
|
||||||
|
JLabel right = new JLabel("Tip: table columns are sortable");
|
||||||
|
right.setFont(FONT_SMALL);
|
||||||
|
right.setForeground(COLOR_TEXT_SUBTLE);
|
||||||
|
|
||||||
|
statusBar.add(left, BorderLayout.WEST);
|
||||||
|
statusBar.add(right, BorderLayout.EAST);
|
||||||
|
return statusBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setActiveCard(String cardName) {
|
||||||
|
selectedCardName = cardName;
|
||||||
|
cardLayout.show(cardsPanel, cardName);
|
||||||
|
navButtons.forEach((name, button) -> {
|
||||||
|
JLabel label = (JLabel) button.getComponent(0);
|
||||||
|
updateNavButtonStyle(name, button, label);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateNavButtonStyle(String cardName, JPanel button, JLabel label) {
|
||||||
|
boolean active = cardName.equals(selectedCardName);
|
||||||
|
button.setBackground(active ? COLOR_PRIMARY_SOFT : COLOR_SURFACE);
|
||||||
|
label.setForeground(active ? COLOR_TEXT : COLOR_TEXT_MUTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMetrics() {
|
||||||
|
try {
|
||||||
|
EmulatorStatsService.Snapshot snapshot = EmulatorStatsService.collectSnapshot();
|
||||||
|
EmulatorStatsService.Overview overview = snapshot.overview;
|
||||||
|
|
||||||
|
Object[][] usersData = new Object[snapshot.users.size()][5];
|
||||||
|
for (int i = 0; i < snapshot.users.size(); i++) {
|
||||||
|
EmulatorStatsService.OnlineUserRow user = snapshot.users.get(i);
|
||||||
|
usersData[i] = new Object[]{user.id, user.username, user.rank, user.credits, user.roomId};
|
||||||
|
}
|
||||||
|
|
||||||
|
Object[][] roomsData = new Object[snapshot.rooms.size()][8];
|
||||||
|
for (int i = 0; i < snapshot.rooms.size(); i++) {
|
||||||
|
EmulatorStatsService.ActiveRoomRow room = snapshot.rooms.get(i);
|
||||||
|
roomsData[i] = new Object[]{
|
||||||
|
room.roomId,
|
||||||
|
room.name,
|
||||||
|
room.players,
|
||||||
|
room.items,
|
||||||
|
room.tickables,
|
||||||
|
String.format("%.2f", room.cpuMs),
|
||||||
|
room.estimatedRamKb,
|
||||||
|
room.thread
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Object[][] wiredData = new Object[snapshot.wired.size()][7];
|
||||||
|
for (int i = 0; i < snapshot.wired.size(); i++) {
|
||||||
|
EmulatorStatsService.WiredRoomRow wiredRoom = snapshot.wired.get(i);
|
||||||
|
wiredData[i] = new Object[]{
|
||||||
|
wiredRoom.roomId,
|
||||||
|
wiredRoom.averageTickMs + " ms",
|
||||||
|
wiredRoom.peakTickMs + " ms",
|
||||||
|
wiredRoom.usagePercent + "%",
|
||||||
|
wiredRoom.delayedEventsPending,
|
||||||
|
wiredRoom.overloaded ? "YES" : "NO",
|
||||||
|
wiredRoom.heavy ? "YES" : "NO"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
memLabel.setText(String.format("%d MB / %d MB", overview.memoryUsedMb, overview.memoryMaxMb));
|
||||||
|
cpuLabel.setText(String.format("%.1f %%", overview.cpuLoadPercent));
|
||||||
|
threadLabel.setText(String.valueOf(overview.activeOsThreads));
|
||||||
|
usersLabel.setText(String.valueOf(overview.connectedPlayers));
|
||||||
|
roomsLabel.setText(String.valueOf(overview.loadedRooms));
|
||||||
|
wiredLabel.setText(String.valueOf(overview.wiredTickables));
|
||||||
|
uptimeLabel.setText(EmulatorStatsService.formatDuration(overview.uptimeSeconds));
|
||||||
|
lastUpdatedLabel.setText(LocalDateTime.now().format(TIME_FORMAT));
|
||||||
|
footerStatusLabel.setText(overview.guiStatus);
|
||||||
|
memoryGraph.addValue((long) overview.memoryUsedMb * 1024L * 1024L, (long) overview.memoryMaxMb * 1024L * 1024L);
|
||||||
|
|
||||||
|
usersTableModel.setDataVector(usersData, new String[]{"ID", "Username", "Rank", "Credits", "Room ID"});
|
||||||
|
roomsTableModel.setDataVector(roomsData, new String[]{"Room ID", "Name", "Players", "Items", "Tickables", "CPU (ms)", "Est. RAM (KB)", "Thread"});
|
||||||
|
wiredTableModel.setDataVector(wiredData, new String[]{"Room ID", "Avg Tick", "Peak Tick", "Usage %", "Delayed", "Overloaded?", "Heavy?"});
|
||||||
|
usersCountLabel.setText(snapshot.users.size() + " rows");
|
||||||
|
roomsCountLabel.setText(snapshot.rooms.size() + " rows");
|
||||||
|
wiredCountLabel.setText(snapshot.wired.size() + " rows");
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Error updating dashboard metrics", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void launch() {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new EmulatorDashboard();
|
||||||
|
}
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
instance.setVisible(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MemoryGraphPanel extends JPanel {
|
||||||
|
private final LinkedList<Double> history = new LinkedList<>();
|
||||||
|
private static final int MAX_POINTS = 100;
|
||||||
|
|
||||||
|
public MemoryGraphPanel() {
|
||||||
|
setOpaque(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addValue(long used, long max) {
|
||||||
|
double percent = (double) used / (double) max;
|
||||||
|
history.addLast(percent);
|
||||||
|
if (history.size() > MAX_POINTS) {
|
||||||
|
history.removeFirst();
|
||||||
|
}
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics g) {
|
||||||
|
super.paintComponent(g);
|
||||||
|
Graphics2D g2 = (Graphics2D) g;
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
|
||||||
|
int width = getWidth();
|
||||||
|
int height = getHeight();
|
||||||
|
|
||||||
|
// Background grid and labels
|
||||||
|
g2.setFont(new Font("Segoe UI", Font.PLAIN, 10));
|
||||||
|
long maxMemRaw = Runtime.getRuntime().maxMemory();
|
||||||
|
|
||||||
|
for(int i = 0; i <= 4; i++) {
|
||||||
|
int y = i == 0 ? 15 : height * i / 4;
|
||||||
|
if (i == 4) y = height - 5;
|
||||||
|
|
||||||
|
g2.setColor(COLOR_BORDER);
|
||||||
|
g2.drawLine(0, y, width, y);
|
||||||
|
|
||||||
|
// Draw Y-axis numbers
|
||||||
|
g2.setColor(COLOR_TEXT_MUTED);
|
||||||
|
long labelVal = (long) (maxMemRaw * (1.0 - (double)i / 4.0)) / 1024 / 1024;
|
||||||
|
g2.drawString(labelVal + " MB", 5, y - 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (history.size() < 2) return;
|
||||||
|
|
||||||
|
double dx = (double) width / (MAX_POINTS - 1);
|
||||||
|
Path2D path = new Path2D.Double();
|
||||||
|
path.moveTo(0, height);
|
||||||
|
|
||||||
|
int i = MAX_POINTS - history.size();
|
||||||
|
for (Double val : history) {
|
||||||
|
double x = i * dx;
|
||||||
|
double y = height - (val * height);
|
||||||
|
if (i == MAX_POINTS - history.size()) {
|
||||||
|
path.moveTo(x, y);
|
||||||
|
} else {
|
||||||
|
path.lineTo(x, y);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw line
|
||||||
|
g2.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||||
|
g2.setColor(COLOR_PRIMARY);
|
||||||
|
g2.draw(path);
|
||||||
|
|
||||||
|
// Fill area
|
||||||
|
path.lineTo(width, height);
|
||||||
|
path.lineTo((MAX_POINTS - history.size()) * dx, height);
|
||||||
|
path.closePath();
|
||||||
|
|
||||||
|
GradientPaint fillPaint = new GradientPaint(
|
||||||
|
0, 0, new Color(COLOR_PRIMARY.getRed(), COLOR_PRIMARY.getGreen(), COLOR_PRIMARY.getBlue(), 120),
|
||||||
|
0, height, new Color(COLOR_PRIMARY.getRed(), COLOR_PRIMARY.getGreen(), COLOR_PRIMARY.getBlue(), 10)
|
||||||
|
);
|
||||||
|
g2.setPaint(fillPaint);
|
||||||
|
g2.fill(path);
|
||||||
|
|
||||||
|
Double lastValue = history.peekLast();
|
||||||
|
if (lastValue != null) {
|
||||||
|
String usageLabel = String.format("Usage %.1f%%", lastValue * 100.0);
|
||||||
|
g2.setFont(new Font("Segoe UI", Font.BOLD, 12));
|
||||||
|
FontMetrics metrics = g2.getFontMetrics();
|
||||||
|
int labelWidth = metrics.stringWidth(usageLabel) + 16;
|
||||||
|
int labelHeight = 24;
|
||||||
|
int labelX = Math.max(8, width - labelWidth - 8);
|
||||||
|
int labelY = 8;
|
||||||
|
|
||||||
|
g2.setColor(new Color(0, 0, 0, 130));
|
||||||
|
g2.fillRoundRect(labelX, labelY, labelWidth, labelHeight, 12, 12);
|
||||||
|
g2.setColor(COLOR_TEXT);
|
||||||
|
g2.drawString(usageLabel, labelX + 8, labelY + 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatDuration(long millis) {
|
||||||
|
long totalSeconds = Math.max(0L, millis / 1000L);
|
||||||
|
long hours = totalSeconds / 3600L;
|
||||||
|
long minutes = (totalSeconds % 3600L) / 60L;
|
||||||
|
long seconds = totalSeconds % 60L;
|
||||||
|
return String.format("%02dh %02dm %02ds", hours, minutes, seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,11 +6,15 @@ import com.eu.habbo.habbohotel.achievements.AchievementManager;
|
|||||||
import com.eu.habbo.habbohotel.bots.BotManager;
|
import com.eu.habbo.habbohotel.bots.BotManager;
|
||||||
import com.eu.habbo.habbohotel.campaign.calendar.CalendarManager;
|
import com.eu.habbo.habbohotel.campaign.calendar.CalendarManager;
|
||||||
import com.eu.habbo.habbohotel.catalog.CatalogManager;
|
import com.eu.habbo.habbohotel.catalog.CatalogManager;
|
||||||
|
import com.eu.habbo.habbohotel.wheel.WheelManager;
|
||||||
|
import com.eu.habbo.habbohotel.soundboard.SoundboardManager;
|
||||||
|
import com.eu.habbo.habbohotel.mentions.MentionManager;
|
||||||
import com.eu.habbo.habbohotel.commands.CommandHandler;
|
import com.eu.habbo.habbohotel.commands.CommandHandler;
|
||||||
import com.eu.habbo.habbohotel.crafting.CraftingManager;
|
import com.eu.habbo.habbohotel.crafting.CraftingManager;
|
||||||
import com.eu.habbo.habbohotel.guides.GuideManager;
|
import com.eu.habbo.habbohotel.guides.GuideManager;
|
||||||
import com.eu.habbo.habbohotel.guilds.GuildManager;
|
import com.eu.habbo.habbohotel.guilds.GuildManager;
|
||||||
import com.eu.habbo.habbohotel.hotelview.HotelViewManager;
|
import com.eu.habbo.habbohotel.hotelview.HotelViewManager;
|
||||||
|
import com.eu.habbo.habbohotel.items.FurnitureTextProvider;
|
||||||
import com.eu.habbo.habbohotel.items.ItemManager;
|
import com.eu.habbo.habbohotel.items.ItemManager;
|
||||||
import com.eu.habbo.habbohotel.modtool.ModToolManager;
|
import com.eu.habbo.habbohotel.modtool.ModToolManager;
|
||||||
import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
|
import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
|
||||||
@@ -21,6 +25,7 @@ import com.eu.habbo.habbohotel.pets.PetManager;
|
|||||||
import com.eu.habbo.habbohotel.polls.PollManager;
|
import com.eu.habbo.habbohotel.polls.PollManager;
|
||||||
import com.eu.habbo.habbohotel.rooms.RoomChatBubbleManager;
|
import com.eu.habbo.habbohotel.rooms.RoomChatBubbleManager;
|
||||||
import com.eu.habbo.habbohotel.rooms.RoomManager;
|
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.HabboManager;
|
||||||
import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeManager;
|
import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeManager;
|
||||||
import com.eu.habbo.habbohotel.users.infostand.InfostandBackgroundManager;
|
import com.eu.habbo.habbohotel.users.infostand.InfostandBackgroundManager;
|
||||||
@@ -43,6 +48,7 @@ public class GameEnvironment {
|
|||||||
private NavigatorManager navigatorManager;
|
private NavigatorManager navigatorManager;
|
||||||
private GuildManager guildManager;
|
private GuildManager guildManager;
|
||||||
private ItemManager itemManager;
|
private ItemManager itemManager;
|
||||||
|
private FurnitureTextProvider furnitureTextProvider;
|
||||||
private CatalogManager catalogManager;
|
private CatalogManager catalogManager;
|
||||||
private HotelViewManager hotelViewManager;
|
private HotelViewManager hotelViewManager;
|
||||||
private RoomManager roomManager;
|
private RoomManager roomManager;
|
||||||
@@ -60,8 +66,12 @@ public class GameEnvironment {
|
|||||||
private SubscriptionManager subscriptionManager;
|
private SubscriptionManager subscriptionManager;
|
||||||
private CalendarManager calendarManager;
|
private CalendarManager calendarManager;
|
||||||
private RoomChatBubbleManager roomChatBubbleManager;
|
private RoomChatBubbleManager roomChatBubbleManager;
|
||||||
|
private GoogleTranslateManager googleTranslateManager;
|
||||||
private CustomBadgeManager customBadgeManager;
|
private CustomBadgeManager customBadgeManager;
|
||||||
private InfostandBackgroundManager infostandBackgroundManager;
|
private InfostandBackgroundManager infostandBackgroundManager;
|
||||||
|
private WheelManager wheelManager;
|
||||||
|
private SoundboardManager soundboardManager;
|
||||||
|
private MentionManager mentionManager;
|
||||||
|
|
||||||
public void load() throws Exception {
|
public void load() throws Exception {
|
||||||
LOGGER.info("GameEnvironment -> Loading...");
|
LOGGER.info("GameEnvironment -> Loading...");
|
||||||
@@ -71,6 +81,8 @@ public class GameEnvironment {
|
|||||||
this.hotelViewManager = new HotelViewManager();
|
this.hotelViewManager = new HotelViewManager();
|
||||||
this.itemManager = new ItemManager();
|
this.itemManager = new ItemManager();
|
||||||
this.itemManager.load();
|
this.itemManager.load();
|
||||||
|
this.furnitureTextProvider = new FurnitureTextProvider();
|
||||||
|
this.furnitureTextProvider.init();
|
||||||
this.botManager = new BotManager();
|
this.botManager = new BotManager();
|
||||||
this.petManager = new PetManager();
|
this.petManager = new PetManager();
|
||||||
this.guildManager = new GuildManager();
|
this.guildManager = new GuildManager();
|
||||||
@@ -88,8 +100,12 @@ public class GameEnvironment {
|
|||||||
this.pollManager = new PollManager();
|
this.pollManager = new PollManager();
|
||||||
this.calendarManager = new CalendarManager();
|
this.calendarManager = new CalendarManager();
|
||||||
this.roomChatBubbleManager = new RoomChatBubbleManager();
|
this.roomChatBubbleManager = new RoomChatBubbleManager();
|
||||||
|
this.googleTranslateManager = new GoogleTranslateManager();
|
||||||
this.customBadgeManager = new CustomBadgeManager();
|
this.customBadgeManager = new CustomBadgeManager();
|
||||||
this.infostandBackgroundManager = new InfostandBackgroundManager();
|
this.infostandBackgroundManager = new InfostandBackgroundManager();
|
||||||
|
this.wheelManager = new WheelManager();
|
||||||
|
this.soundboardManager = new SoundboardManager();
|
||||||
|
this.mentionManager = new MentionManager();
|
||||||
|
|
||||||
this.roomManager.loadPublicRooms();
|
this.roomManager.loadPublicRooms();
|
||||||
this.navigatorManager.loadNavigator();
|
this.navigatorManager.loadNavigator();
|
||||||
@@ -127,6 +143,9 @@ public class GameEnvironment {
|
|||||||
this.hotelViewManager.dispose();
|
this.hotelViewManager.dispose();
|
||||||
this.subscriptionManager.dispose();
|
this.subscriptionManager.dispose();
|
||||||
this.calendarManager.dispose();
|
this.calendarManager.dispose();
|
||||||
|
if (this.googleTranslateManager != null) {
|
||||||
|
this.googleTranslateManager.clearCache();
|
||||||
|
}
|
||||||
LOGGER.info("GameEnvironment -> Disposed!");
|
LOGGER.info("GameEnvironment -> Disposed!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,10 +165,22 @@ public class GameEnvironment {
|
|||||||
return this.itemManager;
|
return this.itemManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FurnitureTextProvider getFurnitureTextProvider() {
|
||||||
|
return this.furnitureTextProvider;
|
||||||
|
}
|
||||||
|
|
||||||
public CatalogManager getCatalogManager() {
|
public CatalogManager getCatalogManager() {
|
||||||
return this.catalogManager;
|
return this.catalogManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WheelManager getWheelManager() {
|
||||||
|
return this.wheelManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SoundboardManager getSoundboardManager() {
|
||||||
|
return this.soundboardManager;
|
||||||
|
}
|
||||||
|
|
||||||
public HotelViewManager getHotelViewManager() {
|
public HotelViewManager getHotelViewManager() {
|
||||||
return this.hotelViewManager;
|
return this.hotelViewManager;
|
||||||
}
|
}
|
||||||
@@ -182,6 +213,10 @@ public class GameEnvironment {
|
|||||||
return this.petManager;
|
return this.petManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MentionManager getMentionManager() {
|
||||||
|
return this.mentionManager;
|
||||||
|
}
|
||||||
|
|
||||||
public AchievementManager getAchievementManager() {
|
public AchievementManager getAchievementManager() {
|
||||||
return this.achievementManager;
|
return this.achievementManager;
|
||||||
}
|
}
|
||||||
@@ -226,6 +261,10 @@ public class GameEnvironment {
|
|||||||
return roomChatBubbleManager;
|
return roomChatBubbleManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GoogleTranslateManager getGoogleTranslateManager() {
|
||||||
|
return this.googleTranslateManager;
|
||||||
|
}
|
||||||
|
|
||||||
public CustomBadgeManager getCustomBadgeManager() {
|
public CustomBadgeManager getCustomBadgeManager() {
|
||||||
return this.customBadgeManager;
|
return this.customBadgeManager;
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-32
@@ -1,7 +1,6 @@
|
|||||||
package com.eu.habbo.habbohotel.achievements;
|
package com.eu.habbo.habbohotel.achievements;
|
||||||
|
|
||||||
import com.eu.habbo.Emulator;
|
import com.eu.habbo.Emulator;
|
||||||
import com.eu.habbo.database.SqlQueries;
|
|
||||||
import com.eu.habbo.habbohotel.items.Item;
|
import com.eu.habbo.habbohotel.items.Item;
|
||||||
import com.eu.habbo.habbohotel.users.Habbo;
|
import com.eu.habbo.habbohotel.users.Habbo;
|
||||||
import com.eu.habbo.habbohotel.users.HabboBadge;
|
import com.eu.habbo.habbohotel.users.HabboBadge;
|
||||||
@@ -50,12 +49,16 @@ public class AchievementManager {
|
|||||||
if (habbo != null) {
|
if (habbo != null) {
|
||||||
progressAchievement(habbo, achievement, amount);
|
progressAchievement(habbo, achievement, amount);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
SqlQueries.update(
|
PreparedStatement statement = connection.prepareStatement("" +
|
||||||
"INSERT INTO users_achievements_queue (user_id, achievement_id, amount) VALUES (?, ?, ?) "
|
"INSERT INTO users_achievements_queue (user_id, achievement_id, amount) VALUES (?, ?, ?) " +
|
||||||
+ "ON DUPLICATE KEY UPDATE amount = amount + ?",
|
"ON DUPLICATE KEY UPDATE amount = amount + ?")) {
|
||||||
habboId, achievement.id, amount, amount);
|
statement.setInt(1, habboId);
|
||||||
} catch (SqlQueries.DataAccessException e) {
|
statement.setInt(2, achievement.id);
|
||||||
|
statement.setInt(3, amount);
|
||||||
|
statement.setInt(4, amount);
|
||||||
|
statement.execute();
|
||||||
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Caught SQL exception", e);
|
LOGGER.error("Caught SQL exception", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,9 +100,9 @@ public class AchievementManager {
|
|||||||
if (oldLevel != null && (oldLevel.level == achievement.levels.size() && currentProgress >= oldLevel.progress)) //Maximum achievement gotten.
|
if (oldLevel != null && (oldLevel.level == achievement.levels.size() && currentProgress >= oldLevel.progress)) //Maximum achievement gotten.
|
||||||
return;
|
return;
|
||||||
|
|
||||||
habbo.getHabboStats().setProgress(achievement, currentProgress + amount);
|
int newProgress = habbo.getHabboStats().incrementProgress(achievement, amount);
|
||||||
|
|
||||||
AchievementLevel newLevel = achievement.getLevelForProgress(currentProgress + amount);
|
AchievementLevel newLevel = achievement.getLevelForProgress(newProgress);
|
||||||
|
|
||||||
if (AchievementManager.TALENTTRACK_ENABLED) {
|
if (AchievementManager.TALENTTRACK_ENABLED) {
|
||||||
for (TalentTrackType type : TalentTrackType.values()) {
|
for (TalentTrackType type : TalentTrackType.values()) {
|
||||||
@@ -200,41 +203,48 @@ public class AchievementManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void createUserEntry(Habbo habbo, Achievement achievement) {
|
public static void createUserEntry(Habbo habbo, Achievement achievement) {
|
||||||
try {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_achievements (user_id, achievement_name, progress) VALUES (?, ?, ?)")) {
|
||||||
SqlQueries.update(
|
statement.setInt(1, habbo.getHabboInfo().getId());
|
||||||
"INSERT INTO users_achievements (user_id, achievement_name, progress) VALUES (?, ?, ?)",
|
statement.setString(2, achievement.name);
|
||||||
habbo.getHabboInfo().getId(), achievement.name, 1);
|
statement.setInt(3, 1);
|
||||||
} catch (SqlQueries.DataAccessException e) {
|
statement.execute();
|
||||||
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Caught SQL exception", e);
|
LOGGER.error("Caught SQL exception", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void saveAchievements(Habbo habbo) {
|
public static void saveAchievements(Habbo habbo) {
|
||||||
int userId = habbo.getHabboInfo().getId();
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users_achievements SET progress = ? WHERE achievement_name = ? AND user_id = ? LIMIT 1")) {
|
||||||
try {
|
statement.setInt(3, habbo.getHabboInfo().getId());
|
||||||
SqlQueries.batchUpdate(
|
for (Map.Entry<Achievement, Integer> map : habbo.getHabboStats().getAchievementProgress().entrySet()) {
|
||||||
"UPDATE users_achievements SET progress = ? WHERE achievement_name = ? AND user_id = ? LIMIT 1",
|
statement.setInt(1, map.getValue());
|
||||||
habbo.getHabboStats().getAchievementProgress().entrySet(),
|
statement.setString(2, map.getKey().name);
|
||||||
(ps, entry) -> {
|
statement.addBatch();
|
||||||
ps.setInt(1, entry.getValue());
|
}
|
||||||
ps.setString(2, entry.getKey().name);
|
statement.executeBatch();
|
||||||
ps.setInt(3, userId);
|
} catch (SQLException e) {
|
||||||
});
|
|
||||||
} catch (SqlQueries.DataAccessException e) {
|
|
||||||
LOGGER.error("Caught SQL exception", e);
|
LOGGER.error("Caught SQL exception", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getAchievementProgressForHabbo(int userId, Achievement achievement) {
|
public static int getAchievementProgressForHabbo(int userId, Achievement achievement) {
|
||||||
try {
|
if (achievement == null) {
|
||||||
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);
|
|
||||||
return 0;
|
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() {
|
public void reload() {
|
||||||
|
|||||||
@@ -138,18 +138,20 @@ public class Bot implements Runnable {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (this.needsUpdate) {
|
if (this.needsUpdate) {
|
||||||
|
Room localRoom = this.room;
|
||||||
|
RoomUnit localRoomUnit = this.roomUnit;
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE bots SET name = ?, motto = ?, figure = ?, gender = ?, user_id = ?, room_id = ?, x = ?, y = ?, z = ?, rot = ?, dance = ?, freeroam = ?, chat_lines = ?, chat_auto = ?, chat_random = ?, chat_delay = ?, effect = ?, bubble_id = ? WHERE id = ?")) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE bots SET name = ?, motto = ?, figure = ?, gender = ?, user_id = ?, room_id = ?, x = ?, y = ?, z = ?, rot = ?, dance = ?, freeroam = ?, chat_lines = ?, chat_auto = ?, chat_random = ?, chat_delay = ?, effect = ?, bubble_id = ? WHERE id = ?")) {
|
||||||
statement.setString(1, this.name);
|
statement.setString(1, this.name);
|
||||||
statement.setString(2, this.motto);
|
statement.setString(2, this.motto);
|
||||||
statement.setString(3, this.figure);
|
statement.setString(3, this.figure);
|
||||||
statement.setString(4, this.gender.toString());
|
statement.setString(4, this.gender.toString());
|
||||||
statement.setInt(5, this.ownerId);
|
statement.setInt(5, this.ownerId);
|
||||||
statement.setInt(6, this.room == null ? 0 : this.room.getId());
|
statement.setInt(6, localRoom == null ? 0 : localRoom.getId());
|
||||||
statement.setInt(7, this.roomUnit == null ? 0 : this.roomUnit.getX());
|
statement.setInt(7, localRoomUnit == null ? 0 : localRoomUnit.getX());
|
||||||
statement.setInt(8, this.roomUnit == null ? 0 : this.roomUnit.getY());
|
statement.setInt(8, localRoomUnit == null ? 0 : localRoomUnit.getY());
|
||||||
statement.setDouble(9, this.roomUnit == null ? 0 : this.roomUnit.getZ());
|
statement.setDouble(9, localRoomUnit == null ? 0 : localRoomUnit.getZ());
|
||||||
statement.setInt(10, this.roomUnit == null ? 0 : this.roomUnit.getBodyRotation().getValue());
|
statement.setInt(10, localRoomUnit == null ? 0 : localRoomUnit.getBodyRotation().getValue());
|
||||||
statement.setInt(11, this.roomUnit == null ? 0 : this.roomUnit.getDanceType().getType());
|
statement.setInt(11, localRoomUnit == null ? 0 : localRoomUnit.getDanceType().getType());
|
||||||
statement.setString(12, this.canWalk ? "1" : "0");
|
statement.setString(12, this.canWalk ? "1" : "0");
|
||||||
StringBuilder text = new StringBuilder();
|
StringBuilder text = new StringBuilder();
|
||||||
for (String s : this.chatLines) {
|
for (String s : this.chatLines) {
|
||||||
@@ -187,11 +189,7 @@ public class Bot implements Runnable {
|
|||||||
int timeOut = Emulator.getRandom().nextInt(20) * 2;
|
int timeOut = Emulator.getRandom().nextInt(20) * 2;
|
||||||
this.roomUnit.setWalkTimeOut((timeOut < 10 ? 5 : timeOut) + Emulator.getIntUnixTimestamp());
|
this.roomUnit.setWalkTimeOut((timeOut < 10 ? 5 : timeOut) + Emulator.getIntUnixTimestamp());
|
||||||
}
|
}
|
||||||
}/* else {
|
}
|
||||||
for (RoomTile t : this.room.getLayout().getTilesAround(this.room.getLayout().getTile(this.getRoomUnit().getX(), this.getRoomUnit().getY()))) {
|
|
||||||
WiredManager.handle(WiredTriggerType.BOT_REACHED_STF, this.roomUnit, this.room, this.room.getItemsAt(t).toArray());
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.chatLines.isEmpty() && this.chatTimeOut <= Emulator.getIntUnixTimestamp() && this.chatAuto) {
|
if (!this.chatLines.isEmpty() && this.chatTimeOut <= Emulator.getIntUnixTimestamp() && this.chatAuto) {
|
||||||
@@ -216,7 +214,7 @@ public class Bot implements Runnable {
|
|||||||
} else {
|
} else {
|
||||||
this.lastChatIndex++;
|
this.lastChatIndex++;
|
||||||
if (this.lastChatIndex >= this.chatLines.size()) {
|
if (this.lastChatIndex >= this.chatLines.size()) {
|
||||||
this.lastChatIndex = 0; // start from scratch :-3
|
this.lastChatIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +280,7 @@ public class Bot implements Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onPickUp(Habbo habbo, Room room) {
|
public void onPickUp(Habbo habbo, Room room) {
|
||||||
|
this.stopFollowingHabbo();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onUserSay(final RoomChatMessage message) {
|
public void onUserSay(final RoomChatMessage message) {
|
||||||
@@ -308,9 +306,6 @@ public class Bot implements Runnable {
|
|||||||
public void setName(String name) {
|
public void setName(String name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.needsUpdate = true;
|
this.needsUpdate = true;
|
||||||
|
|
||||||
//if(this.room != null)
|
|
||||||
//this.room.sendComposer(new ChangeNameUpdatedComposer(this.getRoomUnit(), this.getName()).compose());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMotto() {
|
public String getMotto() {
|
||||||
@@ -537,5 +532,28 @@ public class Bot implements Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final short[] DEFAULT_OWNER_ACTION_IDS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11};
|
||||||
|
|
||||||
|
public static final int ACTION_ROTATE = 11;
|
||||||
|
|
||||||
|
private static final long MIN_OWNER_ACTION_INTERVAL_MS = 200L;
|
||||||
|
|
||||||
|
private volatile long lastOwnerActionAt;
|
||||||
|
|
||||||
|
public short[] getOwnerActionIds() {
|
||||||
|
return DEFAULT_OWNER_ACTION_IDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean tryAcquireOwnerActionSlot() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (now - this.lastOwnerActionAt < MIN_OWNER_ACTION_INTERVAL_MS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.lastOwnerActionAt = now;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onPostOwnerAction(int actionId) {
|
||||||
|
// no-op default
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public class BotManager {
|
|||||||
addBotDefinition("generic", Bot.class);
|
addBotDefinition("generic", Bot.class);
|
||||||
addBotDefinition("bartender", ButlerBot.class);
|
addBotDefinition("bartender", ButlerBot.class);
|
||||||
addBotDefinition("visitor_log", VisitorBot.class);
|
addBotDefinition("visitor_log", VisitorBot.class);
|
||||||
|
addBotDefinition(FrankBot.BOT_TYPE, FrankBot.class);
|
||||||
|
|
||||||
this.reload();
|
this.reload();
|
||||||
|
|
||||||
@@ -71,13 +72,23 @@ public class BotManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Bot createBot(THashMap<String, String> data, String type) {
|
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;
|
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)) {
|
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.setString(1, data.get("name"));
|
statement.setInt(1, ownerId);
|
||||||
statement.setString(2, data.get("motto"));
|
statement.setString(2, data.get("name"));
|
||||||
statement.setString(3, data.get("figure"));
|
statement.setString(3, data.get("motto"));
|
||||||
statement.setString(4, data.get("gender").toUpperCase());
|
statement.setString(4, data.get("figure"));
|
||||||
statement.setString(5, type);
|
statement.setString(5, data.get("gender").toUpperCase());
|
||||||
|
statement.setString(6, type);
|
||||||
statement.execute();
|
statement.execute();
|
||||||
try (ResultSet set = statement.getGeneratedKeys()) {
|
try (ResultSet set = statement.getGeneratedKeys()) {
|
||||||
if (set.next()) {
|
if (set.next()) {
|
||||||
@@ -177,7 +188,11 @@ public class BotManager {
|
|||||||
if (pickedUpEvent.isCancelled())
|
if (pickedUpEvent.isCancelled())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (habbo == null || (bot.getOwnerId() == habbo.getHabboInfo().getId() || habbo.hasPermission(Permission.ACC_ANYROOMOWNER))) {
|
Room currentRoom = habbo != null ? habbo.getHabboInfo().getCurrentRoom() : null;
|
||||||
|
if (habbo == null
|
||||||
|
|| bot.getOwnerId() == habbo.getHabboInfo().getId()
|
||||||
|
|| habbo.hasPermission(Permission.ACC_ANYROOMOWNER)
|
||||||
|
|| (currentRoom != null && (currentRoom.getOwnerId() == habbo.getHabboInfo().getId() || habbo.hasPermission(Permission.ACC_PLACEFURNI)))) {
|
||||||
if (habbo != null && !habbo.hasPermission(Permission.ACC_UNLIMITED_BOTS) && habbo.getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
|
if (habbo != null && !habbo.hasPermission(Permission.ACC_UNLIMITED_BOTS) && habbo.getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
|
||||||
habbo.alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
|
habbo.alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class ButlerBot extends Bot {
|
public class ButlerBot extends Bot {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(ButlerBot.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(ButlerBot.class);
|
||||||
public static THashMap<THashSet<String>, Integer> serveItems = new THashMap<>();
|
public static THashMap<THashSet<String>, Integer> serveItems = new THashMap<>();
|
||||||
|
private static final ConcurrentHashMap<Pattern, Integer> serveItemsCompiled = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public ButlerBot(ResultSet set) throws SQLException {
|
public ButlerBot(ResultSet set) throws SQLException {
|
||||||
super(set);
|
super(set);
|
||||||
@@ -38,6 +41,7 @@ public class ButlerBot extends Bot {
|
|||||||
serveItems = new THashMap<>();
|
serveItems = new THashMap<>();
|
||||||
|
|
||||||
serveItems.clear();
|
serveItems.clear();
|
||||||
|
serveItemsCompiled.clear();
|
||||||
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM bot_serves")) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM bot_serves")) {
|
||||||
while (set.next()) {
|
while (set.next()) {
|
||||||
@@ -45,6 +49,17 @@ public class ButlerBot extends Bot {
|
|||||||
THashSet<String> ks = new THashSet<>();
|
THashSet<String> ks = new THashSet<>();
|
||||||
Collections.addAll(ks, keys);
|
Collections.addAll(ks, keys);
|
||||||
serveItems.put(ks, set.getInt("item"));
|
serveItems.put(ks, set.getInt("item"));
|
||||||
|
|
||||||
|
for (String key : keys) {
|
||||||
|
if (key != null && !key.trim().isEmpty()) {
|
||||||
|
try {
|
||||||
|
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(key.toLowerCase()) + "\\b");
|
||||||
|
serveItemsCompiled.put(pattern, set.getInt("item"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Failed to compile butler bot keyword pattern: {}", key, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Caught SQL exception", e);
|
LOGGER.error("Caught SQL exception", e);
|
||||||
@@ -53,6 +68,7 @@ public class ButlerBot extends Bot {
|
|||||||
|
|
||||||
public static void dispose() {
|
public static void dispose() {
|
||||||
serveItems.clear();
|
serveItems.clear();
|
||||||
|
serveItemsCompiled.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -66,74 +82,73 @@ public class ButlerBot extends Bot {
|
|||||||
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.commanddistance")) {
|
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.commanddistance")) {
|
||||||
|
|
||||||
if (message.getUnfilteredMessage() != null) {
|
if (message.getUnfilteredMessage() != null) {
|
||||||
for (Map.Entry<THashSet<String>, Integer> set : serveItems.entrySet()) {
|
String unfilteredLower = message.getUnfilteredMessage().toLowerCase();
|
||||||
for (String keyword : set.getKey()) {
|
for (Map.Entry<Pattern, Integer> entry : serveItemsCompiled.entrySet()) {
|
||||||
|
Pattern pattern = entry.getKey();
|
||||||
|
if (pattern.matcher(unfilteredLower).matches()) {
|
||||||
|
int itemId = entry.getValue();
|
||||||
|
String keyword = pattern.pattern().replace("\\b", "").replace("\\Q", "").replace("\\E", "");
|
||||||
|
|
||||||
// Check if the string contains a certain keyword using a regex.
|
// Enable plugins to cancel this event
|
||||||
// If keyword = tea, teapot wouldn't trigger it.
|
BotServerItemEvent serveEvent = new BotServerItemEvent(this, message.getHabbo(), itemId);
|
||||||
if (message.getUnfilteredMessage().toLowerCase().matches("\\b" + keyword + "\\b")) {
|
if (Emulator.getPluginManager().fireEvent(serveEvent).isCancelled()) {
|
||||||
|
|
||||||
// Enable plugins to cancel this event
|
|
||||||
BotServerItemEvent serveEvent = new BotServerItemEvent(this, message.getHabbo(), set.getValue());
|
|
||||||
if (Emulator.getPluginManager().fireEvent(serveEvent).isCancelled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start give handitem process
|
|
||||||
if (this.getRoomUnit().canWalk()) {
|
|
||||||
final String key = keyword;
|
|
||||||
final Bot bot = this;
|
|
||||||
|
|
||||||
// Step 1: Look at Habbo
|
|
||||||
bot.lookAt(serveEvent.habbo);
|
|
||||||
|
|
||||||
// Step 2: Prepare tasks for when the Bot (carrying the handitem) reaches the Habbo
|
|
||||||
final List<Runnable> tasks = new ArrayList<>();
|
|
||||||
tasks.add(new RoomUnitGiveHanditem(serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
|
|
||||||
tasks.add(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), 0));
|
|
||||||
|
|
||||||
tasks.add(() -> {
|
|
||||||
if(this.getRoom() != null) {
|
|
||||||
String botMessage = Emulator.getTexts()
|
|
||||||
.getValue("bots.butler.given")
|
|
||||||
.replace("%key%", key)
|
|
||||||
.replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
|
|
||||||
|
|
||||||
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), botMessage)) {
|
|
||||||
bot.talk(botMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
List<Runnable> failedReached = new ArrayList<>();
|
|
||||||
failedReached.add(() -> {
|
|
||||||
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.servedistance", 8)) {
|
|
||||||
for (Runnable task : tasks) {
|
|
||||||
task.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Give bot the handitem that it's going to give the Habbo
|
|
||||||
Emulator.getThreading().run(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
|
|
||||||
|
|
||||||
if (distanceBetweenBotAndHabbo > Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)) {
|
|
||||||
Emulator.getThreading().run(new RoomUnitWalkToRoomUnit(this.getRoomUnit(), serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), tasks, failedReached, Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)));
|
|
||||||
} else {
|
|
||||||
Emulator.getThreading().run(failedReached.get(0), 1000);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(this.getRoom() != null) {
|
|
||||||
this.getRoom().giveHandItem(serveEvent.habbo, serveEvent.itemId);
|
|
||||||
|
|
||||||
String msg = Emulator.getTexts().getValue("bots.butler.given").replace("%key%", keyword).replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
|
|
||||||
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), msg)) {
|
|
||||||
this.talk(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start give handitem process
|
||||||
|
if (this.getRoomUnit().canWalk()) {
|
||||||
|
final String key = keyword;
|
||||||
|
final Bot bot = this;
|
||||||
|
|
||||||
|
// Step 1: Look at Habbo
|
||||||
|
bot.lookAt(serveEvent.habbo);
|
||||||
|
|
||||||
|
// Step 2: Prepare tasks for when the Bot (carrying the handitem) reaches the Habbo
|
||||||
|
final List<Runnable> tasks = new ArrayList<>();
|
||||||
|
tasks.add(new RoomUnitGiveHanditem(serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
|
||||||
|
tasks.add(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), 0));
|
||||||
|
|
||||||
|
tasks.add(() -> {
|
||||||
|
if(this.getRoom() != null) {
|
||||||
|
String botMessage = Emulator.getTexts()
|
||||||
|
.getValue("bots.butler.given")
|
||||||
|
.replace("%key%", key)
|
||||||
|
.replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
|
||||||
|
|
||||||
|
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), botMessage)) {
|
||||||
|
bot.talk(botMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Runnable> failedReached = new ArrayList<>();
|
||||||
|
failedReached.add(() -> {
|
||||||
|
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.servedistance", 8)) {
|
||||||
|
for (Runnable task : tasks) {
|
||||||
|
task.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Give bot the handitem that it's going to give the Habbo
|
||||||
|
Emulator.getThreading().run(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
|
||||||
|
|
||||||
|
if (distanceBetweenBotAndHabbo > Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)) {
|
||||||
|
Emulator.getThreading().run(new RoomUnitWalkToRoomUnit(this.getRoomUnit(), serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), tasks, failedReached, Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)));
|
||||||
|
} else {
|
||||||
|
Emulator.getThreading().run(failedReached.get(0), 1000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(this.getRoom() != null) {
|
||||||
|
this.getRoom().giveHandItem(serveEvent.habbo, serveEvent.itemId);
|
||||||
|
|
||||||
|
String msg = Emulator.getTexts().getValue("bots.butler.given").replace("%key%", keyword).replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
|
||||||
|
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), msg)) {
|
||||||
|
this.talk(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,455 @@
|
|||||||
|
package com.eu.habbo.habbohotel.bots;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
|
import com.eu.habbo.habbohotel.rooms.*;
|
||||||
|
import com.eu.habbo.habbohotel.users.Habbo;
|
||||||
|
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer;
|
||||||
|
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserWhisperComposer;
|
||||||
|
import com.eu.habbo.threading.runnables.RoomUnitWalkToLocation;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class FrankBot extends ButlerBot {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(FrankBot.class);
|
||||||
|
public static final String BOT_TYPE = "frank";
|
||||||
|
public static final String PERMISSION_USE = "acc_bot_frank";
|
||||||
|
private static final String KEY_DOOR_LINES = "__door_lines";
|
||||||
|
private static final String KEY_BUSY_WHISPER = "__busy_whisper";
|
||||||
|
private static final String KEY_DOOR_TRIGGERS = "__door_triggers";
|
||||||
|
private static final List<String> DEFAULT_DOOR_LINES = List.of(
|
||||||
|
"Right this way - mind the step!",
|
||||||
|
"And out you go. Come back soon!",
|
||||||
|
"Allow me to escort you to the exit.",
|
||||||
|
"There's the door. Farewell, true believer!"
|
||||||
|
);
|
||||||
|
private static final String DEFAULT_BUSY_WHISPER =
|
||||||
|
"Sorry, I am currently busy. Please wait until I am available.";
|
||||||
|
private static final Pattern DEFAULT_DOOR_PATTERN = Pattern.compile(
|
||||||
|
"\\b(show me the door|kick me|i want to leave|let me out)\\b");
|
||||||
|
|
||||||
|
private static final ConcurrentHashMap<Pattern, List<String>> chatResponses = new ConcurrentHashMap<>();
|
||||||
|
private static volatile List<String> doorLines = DEFAULT_DOOR_LINES;
|
||||||
|
private static volatile String busyWhisper = DEFAULT_BUSY_WHISPER;
|
||||||
|
private static volatile Pattern doorTriggerPattern = DEFAULT_DOOR_PATTERN;
|
||||||
|
|
||||||
|
private static final Random RANDOM = new Random();
|
||||||
|
|
||||||
|
private static final int MAX_CHAT_KEYWORDS = 256;
|
||||||
|
private static final int MAX_DOOR_TRIGGERS = 32;
|
||||||
|
private static final int MAX_MESSAGE_LEN = 256;
|
||||||
|
private static final long BUSY_WHISPER_COOLDOWN_MS = 5000L;
|
||||||
|
|
||||||
|
private volatile RoomTile homeTile;
|
||||||
|
private volatile RoomUserRotation homeRotation;
|
||||||
|
private final AtomicBoolean busy = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean returnScheduled = new AtomicBoolean(false);
|
||||||
|
private final ConcurrentHashMap<Integer, Long> lastBusyWhisperAt = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public FrankBot(ResultSet set) throws SQLException {
|
||||||
|
super(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FrankBot(Bot bot) {
|
||||||
|
super(bot);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlace(Habbo habbo, Room room) {
|
||||||
|
super.onPlace(habbo, room);
|
||||||
|
if (this.getRoomUnit() != null) {
|
||||||
|
this.homeTile = this.getRoomUnit().getCurrentLocation();
|
||||||
|
this.homeRotation = this.getRoomUnit().getBodyRotation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final short[] FRANK_OWNER_ACTIONS = { (short) Bot.ACTION_ROTATE };
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public short[] getOwnerActionIds() {
|
||||||
|
return FRANK_OWNER_ACTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPostOwnerAction(int actionId) {
|
||||||
|
if (actionId == ACTION_ROTATE && this.getRoomUnit() != null) {
|
||||||
|
this.homeRotation = this.getRoomUnit().getBodyRotation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void initialise() {
|
||||||
|
chatResponses.clear();
|
||||||
|
doorLines = DEFAULT_DOOR_LINES;
|
||||||
|
busyWhisper = DEFAULT_BUSY_WHISPER;
|
||||||
|
doorTriggerPattern = DEFAULT_DOOR_PATTERN;
|
||||||
|
|
||||||
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
|
Statement statement = connection.createStatement();
|
||||||
|
ResultSet set = statement.executeQuery("SELECT `keys`, `responses` FROM bot_chat_responses WHERE bot_type = '" + BOT_TYPE + "'")) {
|
||||||
|
while (set.next()) {
|
||||||
|
String keysRaw = set.getString("keys");
|
||||||
|
String responsesRaw = set.getString("responses");
|
||||||
|
|
||||||
|
if (keysRaw == null || responsesRaw == null) continue;
|
||||||
|
|
||||||
|
List<String> responses = new ArrayList<>();
|
||||||
|
for (String line : responsesRaw.split("\n")) {
|
||||||
|
String trimmed = line.trim();
|
||||||
|
if (!trimmed.isEmpty()) responses.add(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responses.isEmpty()) continue;
|
||||||
|
|
||||||
|
String firstKey = keysRaw.split(";", 2)[0].trim();
|
||||||
|
if (firstKey.startsWith("__")) {
|
||||||
|
switch (firstKey) {
|
||||||
|
case KEY_DOOR_LINES:
|
||||||
|
doorLines = new CopyOnWriteArrayList<>(responses);
|
||||||
|
break;
|
||||||
|
case KEY_BUSY_WHISPER:
|
||||||
|
busyWhisper = responses.get(0);
|
||||||
|
break;
|
||||||
|
case KEY_DOOR_TRIGGERS:
|
||||||
|
doorTriggerPattern = buildDoorTriggerPattern(responses);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOGGER.warn("FrankBot: unknown system key '{}', ignored", firstKey);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> shared = new CopyOnWriteArrayList<>(responses);
|
||||||
|
|
||||||
|
for (String key : keysRaw.split(";")) {
|
||||||
|
if (chatResponses.size() >= MAX_CHAT_KEYWORDS) {
|
||||||
|
LOGGER.warn("FrankBot: chat keyword cap ({}) reached, remaining rows ignored",
|
||||||
|
MAX_CHAT_KEYWORDS);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
String k = key == null ? "" : key.trim().toLowerCase();
|
||||||
|
if (k.isEmpty()) continue;
|
||||||
|
try {
|
||||||
|
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(k) + "\\b");
|
||||||
|
chatResponses.put(pattern, shared);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Failed to compile Frank chat keyword pattern: {}", k, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOGGER.warn("FrankBot: could not load bot_chat_responses ({}). Frank will still serve items.", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
ButlerBot.initialise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void dispose() {
|
||||||
|
chatResponses.clear();
|
||||||
|
doorLines = DEFAULT_DOOR_LINES;
|
||||||
|
busyWhisper = DEFAULT_BUSY_WHISPER;
|
||||||
|
doorTriggerPattern = DEFAULT_DOOR_PATTERN;
|
||||||
|
ButlerBot.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pattern buildDoorTriggerPattern(List<String> triggers) {
|
||||||
|
StringBuilder sb = new StringBuilder("\\b(");
|
||||||
|
boolean first = true;
|
||||||
|
int count = 0;
|
||||||
|
for (String trigger : triggers) {
|
||||||
|
if (count >= MAX_DOOR_TRIGGERS) {
|
||||||
|
LOGGER.warn("FrankBot: door trigger cap ({}) reached, extra entries ignored",
|
||||||
|
MAX_DOOR_TRIGGERS);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
String t = trigger == null ? "" : trigger.trim().toLowerCase();
|
||||||
|
if (t.isEmpty()) continue;
|
||||||
|
if (!first) sb.append('|');
|
||||||
|
sb.append(Pattern.quote(t));
|
||||||
|
first = false;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
sb.append(")\\b");
|
||||||
|
|
||||||
|
if (first) return DEFAULT_DOOR_PATTERN;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Pattern.compile(sb.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("FrankBot: failed to compile door trigger pattern from {}, falling back to default", triggers, e);
|
||||||
|
return DEFAULT_DOOR_PATTERN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUserSay(final RoomChatMessage message) {
|
||||||
|
Room currentRoom = this.getRoom();
|
||||||
|
if (currentRoom == null) return;
|
||||||
|
|
||||||
|
Habbo asker = message.getHabbo();
|
||||||
|
if (asker == null || asker.getClient() == null) return;
|
||||||
|
|
||||||
|
if (this.getRoomUnit() == null) return;
|
||||||
|
|
||||||
|
String raw = message.getUnfilteredMessage();
|
||||||
|
if (raw != null && raw.length() > MAX_MESSAGE_LEN) return;
|
||||||
|
|
||||||
|
if (this.homeTile == null) {
|
||||||
|
this.homeTile = this.getRoomUnit().getCurrentLocation();
|
||||||
|
this.homeRotation = this.getRoomUnit().getBodyRotation();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.busy.get() || this.getRoomUnit().hasStatus(RoomUnitStatus.MOVE)) {
|
||||||
|
this.whisperThrottled(asker, busyWhisper);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw != null) {
|
||||||
|
double distance = this.getRoomUnit().getCurrentLocation().distance(asker.getRoomUnit().getCurrentLocation());
|
||||||
|
int commandDistance = Emulator.getConfig().getInt("hotel.bot.butler.commanddistance");
|
||||||
|
|
||||||
|
if (distance <= commandDistance) {
|
||||||
|
String lower = raw.toLowerCase();
|
||||||
|
|
||||||
|
if (doorTriggerPattern.matcher(lower).find()) {
|
||||||
|
if (!this.busy.compareAndSet(false, true)) {
|
||||||
|
this.whisperThrottled(asker, busyWhisper);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.showToTheDoor(asker);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (java.util.Map.Entry<Pattern, List<String>> entry : chatResponses.entrySet()) {
|
||||||
|
if (entry.getKey().matcher(lower).find()) {
|
||||||
|
List<String> options = entry.getValue();
|
||||||
|
if (options.isEmpty()) continue;
|
||||||
|
|
||||||
|
String reply = options.get(RANDOM.nextInt(options.size()));
|
||||||
|
this.talk(reply);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.busy.compareAndSet(false, true)) {
|
||||||
|
this.whisperThrottled(asker, busyWhisper);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.onUserSay(message);
|
||||||
|
this.schedulePostServeReturn(currentRoom.getId(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void whisperThrottled(Habbo target, String text) {
|
||||||
|
if (target == null || text == null || text.isEmpty() || this.getRoomUnit() == null) return;
|
||||||
|
int userId = target.getHabboInfo().getId();
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
Long last = lastBusyWhisperAt.get(userId);
|
||||||
|
if (last != null && (now - last) < BUSY_WHISPER_COOLDOWN_MS) return;
|
||||||
|
lastBusyWhisperAt.put(userId, now);
|
||||||
|
RoomChatMessage msg = new RoomChatMessage(text, this.getRoomUnit(), RoomChatMessageBubbles.BOT);
|
||||||
|
target.getClient().sendResponse(new RoomUserWhisperComposer(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showToTheDoor(final Habbo target) {
|
||||||
|
final Room room = this.getRoom();
|
||||||
|
if (room == null || room.getLayout() == null || target == null) {
|
||||||
|
this.busy.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final RoomTile doorTile = room.getLayout().getDoorTile();
|
||||||
|
if (doorTile == null) {
|
||||||
|
this.busy.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lookAt(target);
|
||||||
|
List<String> lines = doorLines;
|
||||||
|
String line = lines.isEmpty() ? DEFAULT_DOOR_LINES.get(RANDOM.nextInt(DEFAULT_DOOR_LINES.size()))
|
||||||
|
: lines.get(RANDOM.nextInt(lines.size()));
|
||||||
|
this.talk(line);
|
||||||
|
|
||||||
|
final int targetId = target.getHabboInfo().getId();
|
||||||
|
final int roomId = room.getId();
|
||||||
|
final AtomicBoolean fired = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
final Runnable kickThenReturn = () -> {
|
||||||
|
if (!fired.compareAndSet(false, true)) return;
|
||||||
|
Room currentRoom = this.getRoom();
|
||||||
|
if (currentRoom == null || currentRoom.getId() != roomId) {
|
||||||
|
this.busy.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Habbo stillHere = currentRoom.getHabbo(targetId);
|
||||||
|
if (stillHere != null) {
|
||||||
|
currentRoom.kickHabbo(stillHere, false);
|
||||||
|
}
|
||||||
|
this.scheduleReturnHome(targetId, roomId, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.getRoomUnit().canWalk() && !this.getRoomUnit().getCurrentLocation().equals(doorTile)) {
|
||||||
|
List<Runnable> onArrive = new ArrayList<>();
|
||||||
|
onArrive.add(kickThenReturn);
|
||||||
|
|
||||||
|
List<Runnable> onFail = new ArrayList<>();
|
||||||
|
onFail.add(() -> Emulator.getThreading().run(kickThenReturn, 1500));
|
||||||
|
|
||||||
|
this.getRoomUnit().setGoalLocation(doorTile);
|
||||||
|
Emulator.getThreading().run(
|
||||||
|
new RoomUnitWalkToLocation(this.getRoomUnit(), doorTile, room, onArrive, onFail));
|
||||||
|
} else {
|
||||||
|
Emulator.getThreading().run(kickThenReturn, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int RETURN_HOME_POLL_MS = 500;
|
||||||
|
private static final int RETURN_HOME_MAX_WAIT_MS = 8000;
|
||||||
|
private static final int POST_SERVE_POLL_MS = 750;
|
||||||
|
private static final int POST_SERVE_MAX_WAIT_MS = 30000;
|
||||||
|
|
||||||
|
private void schedulePostServeReturn(final int roomId, final int waitedMs) {
|
||||||
|
if (waitedMs == 0 && !this.returnScheduled.compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (waitedMs >= POST_SERVE_MAX_WAIT_MS) {
|
||||||
|
this.returnScheduled.set(false);
|
||||||
|
this.busy.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.homeTile == null) {
|
||||||
|
this.returnScheduled.set(false);
|
||||||
|
this.busy.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Emulator.getThreading().run(() -> {
|
||||||
|
Room r = this.getRoom();
|
||||||
|
if (r == null || r.getId() != roomId || this.getRoomUnit() == null || this.homeTile == null) {
|
||||||
|
this.returnScheduled.set(false);
|
||||||
|
this.busy.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.getRoomUnit().getCurrentLocation().equals(this.homeTile)) {
|
||||||
|
if (this.homeRotation != null && this.getRoomUnit().getBodyRotation() != this.homeRotation) {
|
||||||
|
this.getRoomUnit().setRotation(this.homeRotation);
|
||||||
|
r.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||||
|
this.persistPosition();
|
||||||
|
} else {
|
||||||
|
this.busy.set(false);
|
||||||
|
}
|
||||||
|
this.returnScheduled.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean stillWalking = this.getRoomUnit().hasStatus(RoomUnitStatus.MOVE)
|
||||||
|
|| (this.getRoomUnit().getPath() != null && !this.getRoomUnit().getPath().isEmpty());
|
||||||
|
|
||||||
|
if (stillWalking) {
|
||||||
|
this.schedulePostServeReturn(roomId, waitedMs + POST_SERVE_POLL_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.returnScheduled.set(false);
|
||||||
|
this.returnHome(-1, false);
|
||||||
|
}, POST_SERVE_POLL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleReturnHome(final int kickedHabboId, final int roomId, final int waitedMs) {
|
||||||
|
Room currentRoom = this.getRoom();
|
||||||
|
if (currentRoom == null || currentRoom.getId() != roomId) return;
|
||||||
|
|
||||||
|
boolean stillEscorting = currentRoom.getHabbo(kickedHabboId) != null;
|
||||||
|
|
||||||
|
if (!stillEscorting || waitedMs >= RETURN_HOME_MAX_WAIT_MS) {
|
||||||
|
this.returnHome(kickedHabboId, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Emulator.getThreading().run(
|
||||||
|
() -> this.scheduleReturnHome(kickedHabboId, roomId, waitedMs + RETURN_HOME_POLL_MS),
|
||||||
|
RETURN_HOME_POLL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void returnHome(int kickedHabboId, boolean alwaysTeleport) {
|
||||||
|
final Room room = this.getRoom();
|
||||||
|
if (room == null || this.homeTile == null || this.getRoomUnit() == null) {
|
||||||
|
this.busy.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Runnable teleportHome = () -> {
|
||||||
|
Room r = this.getRoom();
|
||||||
|
if (r == null || this.getRoomUnit() == null) return;
|
||||||
|
|
||||||
|
double homeZ = r.getTopHeightAt(this.homeTile.x, this.homeTile.y);
|
||||||
|
|
||||||
|
this.getRoomUnit().stopWalking();
|
||||||
|
this.getRoomUnit().setZ(homeZ);
|
||||||
|
this.getRoomUnit().setLocation(this.homeTile);
|
||||||
|
this.getRoomUnit().setPreviousLocationZ(homeZ);
|
||||||
|
if (this.homeRotation != null) {
|
||||||
|
this.getRoomUnit().setRotation(this.homeRotation);
|
||||||
|
}
|
||||||
|
this.getRoomUnit().statusUpdate(true);
|
||||||
|
r.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||||
|
this.persistPosition();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.getRoomUnit().getCurrentLocation().equals(this.homeTile)) {
|
||||||
|
if (this.homeRotation != null) {
|
||||||
|
this.getRoomUnit().setRotation(this.homeRotation);
|
||||||
|
room.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||||
|
}
|
||||||
|
this.persistPosition();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasOtherWatchers = false;
|
||||||
|
for (Habbo h : room.getCurrentHabbos().values()) {
|
||||||
|
if (h.getHabboInfo().getId() != kickedHabboId) {
|
||||||
|
hasOtherWatchers = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alwaysTeleport || !hasOtherWatchers || !this.getRoomUnit().canWalk()) {
|
||||||
|
teleportHome.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Runnable> onArrive = new ArrayList<>();
|
||||||
|
onArrive.add(() -> {
|
||||||
|
if (this.homeRotation != null && this.getRoom() != null) {
|
||||||
|
this.getRoomUnit().setRotation(this.homeRotation);
|
||||||
|
this.getRoom().sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||||
|
}
|
||||||
|
this.persistPosition();
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Runnable> onFail = new ArrayList<>();
|
||||||
|
onFail.add(teleportHome);
|
||||||
|
|
||||||
|
this.getRoomUnit().setGoalLocation(this.homeTile);
|
||||||
|
Emulator.getThreading().run(
|
||||||
|
new RoomUnitWalkToLocation(this.getRoomUnit(), this.homeTile, room, onArrive, onFail));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persistPosition() {
|
||||||
|
this.needsUpdate(true);
|
||||||
|
this.run();
|
||||||
|
this.busy.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -202,6 +202,8 @@ public class CatalogManager {
|
|||||||
public final Item ecotronItem;
|
public final Item ecotronItem;
|
||||||
public final THashMap<Integer, CatalogLimitedConfiguration> limitedNumbers;
|
public final THashMap<Integer, CatalogLimitedConfiguration> limitedNumbers;
|
||||||
private final List<Voucher> vouchers;
|
private final List<Voucher> vouchers;
|
||||||
|
public final TIntObjectMap<int[]> furnitureValues;
|
||||||
|
private volatile byte[] rareValuesPayloadCache;
|
||||||
|
|
||||||
public CatalogManager() {
|
public CatalogManager() {
|
||||||
long millis = System.currentTimeMillis();
|
long millis = System.currentTimeMillis();
|
||||||
@@ -219,6 +221,7 @@ public class CatalogManager {
|
|||||||
this.buildersClubOfferDefs = new TIntIntHashMap();
|
this.buildersClubOfferDefs = new TIntIntHashMap();
|
||||||
this.vouchers = new ArrayList<>();
|
this.vouchers = new ArrayList<>();
|
||||||
this.limitedNumbers = new THashMap<>();
|
this.limitedNumbers = new THashMap<>();
|
||||||
|
this.furnitureValues = new TIntObjectHashMap<>();
|
||||||
|
|
||||||
this.initialize();
|
this.initialize();
|
||||||
|
|
||||||
@@ -243,6 +246,76 @@ public class CatalogManager {
|
|||||||
this.loadClothing();
|
this.loadClothing();
|
||||||
this.loadRecycler();
|
this.loadRecycler();
|
||||||
this.loadGiftWrappers();
|
this.loadGiftWrappers();
|
||||||
|
this.loadFurnitureValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void loadFurnitureValues() {
|
||||||
|
this.furnitureValues.clear();
|
||||||
|
final int diamondType = Emulator.getConfig().getInt("seasonal.currency.diamond", 5);
|
||||||
|
|
||||||
|
for (CatalogPage page : this.catalogPages.valueCollection()) {
|
||||||
|
for (CatalogItem catalogItem : page.getCatalogItems().valueCollection()) {
|
||||||
|
if (catalogItem.getAmount() != 1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int credits = catalogItem.getCredits();
|
||||||
|
int points = catalogItem.getPoints();
|
||||||
|
int pointsType = catalogItem.getPointsType();
|
||||||
|
|
||||||
|
if (points <= 0 || pointsType != diamondType)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
THashSet<Item> baseItems = catalogItem.getBaseItems();
|
||||||
|
|
||||||
|
if (baseItems.size() != 1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (Item item : baseItems) {
|
||||||
|
FurnitureType type = item.getType();
|
||||||
|
|
||||||
|
if (type != FurnitureType.FLOOR && type != FurnitureType.WALL)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int spriteId = item.getSpriteId();
|
||||||
|
|
||||||
|
if (spriteId > 0 && !this.furnitureValues.containsKey(spriteId)) {
|
||||||
|
this.furnitureValues.put(spriteId, new int[]{credits, points, pointsType});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rebuildRareValuesPayloadCache();
|
||||||
|
|
||||||
|
LOGGER.info("Furniture Values -> Loaded! ({} entries)", this.furnitureValues.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebuildRareValuesPayloadCache() {
|
||||||
|
try (java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(this.furnitureValues.size() * 16 + 8);
|
||||||
|
java.io.DataOutputStream out = new java.io.DataOutputStream(baos)) {
|
||||||
|
out.writeInt(this.furnitureValues.size());
|
||||||
|
TIntObjectIterator<int[]> iterator = this.furnitureValues.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
iterator.advance();
|
||||||
|
int[] value = iterator.value();
|
||||||
|
out.writeInt(iterator.key()); // spriteId
|
||||||
|
out.writeInt(value[0]); // credits
|
||||||
|
out.writeInt(value[1]); // points
|
||||||
|
out.writeInt(value[2]); // pointsType
|
||||||
|
}
|
||||||
|
this.rareValuesPayloadCache = baos.toByteArray();
|
||||||
|
} catch (java.io.IOException e) {
|
||||||
|
LOGGER.error("Failed to build rare values payload cache", e);
|
||||||
|
this.rareValuesPayloadCache = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TIntObjectMap<int[]> getFurnitureValues() {
|
||||||
|
return this.furnitureValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getRareValuesPayloadSnapshot() {
|
||||||
|
return this.rareValuesPayloadCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void loadLimitedNumbers() {
|
private synchronized void loadLimitedNumbers() {
|
||||||
@@ -981,13 +1054,13 @@ public class CatalogManager {
|
|||||||
if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) {
|
if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) {
|
||||||
int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total");
|
int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total");
|
||||||
if (habbo.getHabboStats().totalLtds() >= ltdLimit) {
|
if (habbo.getHabboStats().totalLtds() >= ltdLimit) {
|
||||||
habbo.alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total").replace("%itemname%", item.getBaseItems().iterator().next().getFullName()).replace("%limit%", ltdLimit + ""));
|
habbo.alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total").replace("%itemname%", item.getBaseItems().iterator().next().getDisplayName()).replace("%limit%", ltdLimit + ""));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item");
|
ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item");
|
||||||
if (habbo.getHabboStats().totalLtds(item.id) >= ltdLimit) {
|
if (habbo.getHabboStats().totalLtds(item.id) >= ltdLimit) {
|
||||||
habbo.alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item").replace("%itemname%", item.getBaseItems().iterator().next().getFullName()).replace("%limit%", ltdLimit + ""));
|
habbo.alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item").replace("%itemname%", item.getBaseItems().iterator().next().getDisplayName()).replace("%limit%", ltdLimit + ""));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1046,10 +1119,19 @@ public class CatalogManager {
|
|||||||
for (Item baseItem : item.getBaseItems()) {
|
for (Item baseItem : item.getBaseItems()) {
|
||||||
for (int k = 0; k < item.getItemAmount(baseItem.getId()); k++) {
|
for (int k = 0; k < item.getItemAmount(baseItem.getId()); k++) {
|
||||||
if (baseItem.getName().startsWith("rentable_bot_") || baseItem.getName().startsWith("bot_")) {
|
if (baseItem.getName().startsWith("rentable_bot_") || baseItem.getName().startsWith("bot_")) {
|
||||||
|
String baseName = baseItem.getName();
|
||||||
String type = item.getName().replace("rentable_bot_", "");
|
String type = item.getName().replace("rentable_bot_", "");
|
||||||
type = type.replace("bot_", "");
|
type = type.replace("bot_", "");
|
||||||
type = type.replace("visitor_logger", "visitor_log");
|
type = type.replace("visitor_logger", "visitor_log");
|
||||||
|
|
||||||
|
if (("bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)
|
||||||
|
|| ("rentable_bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)) {
|
||||||
|
if (!habbo.getClient().getHabbo().hasPermission(com.eu.habbo.habbohotel.bots.FrankBot.PERMISSION_USE)) {
|
||||||
|
habbo.getClient().sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
THashMap<String, String> data = new THashMap<>();
|
THashMap<String, String> data = new THashMap<>();
|
||||||
|
|
||||||
for (String s : item.getExtradata().split(";")) {
|
for (String s : item.getExtradata().split(";")) {
|
||||||
@@ -1058,7 +1140,7 @@ public class CatalogManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Bot bot = Emulator.getGameEnvironment().getBotManager().createBot(data, type);
|
Bot bot = Emulator.getGameEnvironment().getBotManager().createBot(data, type, habbo.getHabboInfo().getId());
|
||||||
|
|
||||||
if (bot != null) {
|
if (bot != null) {
|
||||||
bot.setOwnerId(habbo.getClient().getHabbo().getHabboInfo().getId());
|
bot.setOwnerId(habbo.getClient().getHabbo().getHabboInfo().getId());
|
||||||
@@ -1165,6 +1247,11 @@ public class CatalogManager {
|
|||||||
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
|
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
|
||||||
|
|
||||||
if (guild != null && Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, habbo) != null) {
|
if (guild != null && Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, habbo) != null) {
|
||||||
|
if (baseItem.getName().equals("guild_forum") && guild.getOwnerId() != habbo.getHabboInfo().getId()) {
|
||||||
|
habbo.getClient().sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
InteractionGuildFurni habboItem = (InteractionGuildFurni) Emulator.getGameEnvironment().getItemManager().createItem(habbo.getClient().getHabbo().getHabboInfo().getId(), baseItem, limitedStack, limitedNumber, extradata);
|
InteractionGuildFurni habboItem = (InteractionGuildFurni) Emulator.getGameEnvironment().getItemManager().createItem(habbo.getClient().getHabbo().getHabboInfo().getId(), baseItem, limitedStack, limitedNumber, extradata);
|
||||||
habboItem.setExtradata("");
|
habboItem.setExtradata("");
|
||||||
habboItem.needsUpdate(true);
|
habboItem.needsUpdate(true);
|
||||||
|
|||||||
@@ -72,7 +72,11 @@ public class ClubOffer implements ISerialize {
|
|||||||
this.type = OfferType.fromDatabase(set.getString("type"));
|
this.type = OfferType.fromDatabase(set.getString("type"));
|
||||||
this.vip = this.type == OfferType.VIP;
|
this.vip = this.type == OfferType.VIP;
|
||||||
this.deal = set.getString("deal").equals("1");
|
this.deal = set.getString("deal").equals("1");
|
||||||
this.giftable = set.getString("giftable").equals("1");
|
boolean giftable = false;
|
||||||
|
try {
|
||||||
|
giftable = "1".equals(set.getString("giftable"));
|
||||||
|
} catch (SQLException ignored) {}
|
||||||
|
this.giftable = giftable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getId() {
|
public int getId() {
|
||||||
|
|||||||
+7
-3
@@ -42,14 +42,18 @@ public class RoomBundleLayout extends SingleBundle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.room == null) {
|
if (this.room == null) {
|
||||||
if (this.roomId > 0) {
|
RoomManager roomManager = Emulator.getGameEnvironment().getRoomManager();
|
||||||
this.room = Emulator.getGameEnvironment().getRoomManager().loadRoom(this.roomId);
|
if (this.roomId > 0 && roomManager != null) {
|
||||||
|
this.room = roomManager.loadRoom(this.roomId);
|
||||||
|
|
||||||
if (this.room != null)
|
if (this.room != null)
|
||||||
this.room.preventUnloading = true;
|
this.room.preventUnloading = true;
|
||||||
} else {
|
} else if (this.roomId <= 0) {
|
||||||
LOGGER.error("No room id specified for room bundle {}({})", this.getPageName(), this.getId());
|
LOGGER.error("No room id specified for room bundle {}({})", this.getPageName(), this.getId());
|
||||||
}
|
}
|
||||||
|
// roomManager can be null when CatalogManager.loadFurnitureValues() runs
|
||||||
|
// during GameEnvironment.load() before RoomManager is constructed; in that
|
||||||
|
// case skip eager room loading — the bundle resolves lazily at runtime.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.room == null) {
|
if (this.room == null) {
|
||||||
|
|||||||
+15
-4
@@ -171,8 +171,9 @@ public class MarketPlace {
|
|||||||
statement.setInt(paramIndex++, maxPrice);
|
statement.setInt(paramIndex++, maxPrice);
|
||||||
}
|
}
|
||||||
if (!search.isEmpty()) {
|
if (!search.isEmpty()) {
|
||||||
statement.setString(paramIndex++, "%" + search + "%");
|
String likeSearch = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(search) + "%";
|
||||||
statement.setString(paramIndex++, "%" + search + "%");
|
statement.setString(paramIndex++, likeSearch);
|
||||||
|
statement.setString(paramIndex++, likeSearch);
|
||||||
}
|
}
|
||||||
|
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
@@ -278,8 +279,9 @@ public class MarketPlace {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int soldTimestamp = Emulator.getIntUnixTimestamp();
|
||||||
try (PreparedStatement updateOffer = connection.prepareStatement("UPDATE marketplace_items SET state = 2, sold_timestamp = ? WHERE id = ? AND state = 1")) {
|
try (PreparedStatement updateOffer = connection.prepareStatement("UPDATE marketplace_items SET state = 2, sold_timestamp = ? WHERE id = ? AND state = 1")) {
|
||||||
updateOffer.setInt(1, Emulator.getIntUnixTimestamp());
|
updateOffer.setInt(1, soldTimestamp);
|
||||||
updateOffer.setInt(2, offerId);
|
updateOffer.setInt(2, offerId);
|
||||||
int updated = updateOffer.executeUpdate();
|
int updated = updateOffer.executeUpdate();
|
||||||
if (updated == 0) {
|
if (updated == 0) {
|
||||||
@@ -306,7 +308,11 @@ public class MarketPlace {
|
|||||||
client.sendResponse(new MarketplaceBuyErrorComposer(MarketplaceBuyErrorComposer.REFRESH, 0, offerId, price));
|
client.sendResponse(new MarketplaceBuyErrorComposer(MarketplaceBuyErrorComposer.REFRESH, 0, offerId, price));
|
||||||
|
|
||||||
if (habbo != null) {
|
if (habbo != null) {
|
||||||
habbo.getInventory().getOffer(offerId).setState(MarketPlaceState.SOLD);
|
MarketPlaceOffer offer = habbo.getInventory().getOffer(offerId);
|
||||||
|
if (offer != null) {
|
||||||
|
offer.setState(MarketPlaceState.SOLD);
|
||||||
|
offer.setSoldTimestamp(soldTimestamp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,6 +374,11 @@ public class MarketPlace {
|
|||||||
event.item.setFromGift(false);
|
event.item.setFromGift(false);
|
||||||
|
|
||||||
MarketPlaceOffer offer = new MarketPlaceOffer(event.item, event.price, client.getHabbo());
|
MarketPlaceOffer offer = new MarketPlaceOffer(event.item, event.price, client.getHabbo());
|
||||||
|
if (!offer.isPersisted()) {
|
||||||
|
LOGGER.warn("Marketplace offer insert failed for user {} item {}", client.getHabbo().getHabboInfo().getId(), event.item.getId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
client.getHabbo().getInventory().addMarketplaceOffer(offer);
|
client.getHabbo().getInventory().addMarketplaceOffer(offer);
|
||||||
client.getHabbo().getInventory().getItemsComponent().removeHabboItem(event.item);
|
client.getHabbo().getInventory().getItemsComponent().removeHabboItem(event.item);
|
||||||
item.setUserId(-1);
|
item.setUserId(-1);
|
||||||
|
|||||||
+4
@@ -98,6 +98,10 @@ public class MarketPlaceOffer implements Runnable {
|
|||||||
return this.offerId;
|
return this.offerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPersisted() {
|
||||||
|
return this.offerId > 0;
|
||||||
|
}
|
||||||
|
|
||||||
public void setOfferId(int offerId) {
|
public void setOfferId(int offerId) {
|
||||||
this.offerId = offerId;
|
this.offerId = offerId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package com.eu.habbo.habbohotel.commands;
|
|||||||
import com.eu.habbo.Emulator;
|
import com.eu.habbo.Emulator;
|
||||||
import com.eu.habbo.habbohotel.catalog.CatalogManager;
|
import com.eu.habbo.habbohotel.catalog.CatalogManager;
|
||||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
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;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
|
||||||
@@ -13,9 +11,10 @@ public class AboutCommand extends Command {
|
|||||||
public AboutCommand() {
|
public AboutCommand() {
|
||||||
super(null, new String[]{"about", "info", "online", "server"});
|
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" +
|
public static final String NITRO_INFO_SENTINEL = "[NITRO_INFO_V1]";
|
||||||
"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 REPORT_ISSUES_URL = "https://github.com/duckietm/Nitro-V3/issues";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(GameClient gameClient, String[] params) {
|
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 minute = TimeUnit.SECONDS.toMinutes(seconds) - (TimeUnit.SECONDS.toHours(seconds) * 60);
|
||||||
long second = TimeUnit.SECONDS.toSeconds(seconds) - (TimeUnit.SECONDS.toMinutes(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)) {
|
if (Emulator.getConfig().getBoolean("info.shown", true)) {
|
||||||
message += "<b>Hotel Statistics</b>\r" +
|
message.append("<b>Hotel Statistics</b>\r")
|
||||||
"- Online Users: " + Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "\r" +
|
.append("- Online Users: ").append(Emulator.getGameEnvironment().getHabboManager().getOnlineCount()).append("\r")
|
||||||
"- Active Rooms: " + Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size() + "\r" +
|
.append("- Active Rooms: ").append(Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size()).append("\r")
|
||||||
"- Shop: " + Emulator.getGameEnvironment().getCatalogManager().catalogPages.size() + " pages and " + CatalogManager.catalogItemAmount + " items. \r" +
|
.append("- Shop: ").append(Emulator.getGameEnvironment().getCatalogManager().catalogPages.size()).append(" pages and ").append(CatalogManager.catalogItemAmount).append(" items.\r")
|
||||||
"- Furni: " + Emulator.getGameEnvironment().getItemManager().getItems().size() + " item definitions" + "\r" +
|
.append("- Furni: ").append(Emulator.getGameEnvironment().getItemManager().getItems().size()).append(" item definitions\r")
|
||||||
"\n" +
|
.append("\n")
|
||||||
"<b>Server Statistics</b>\r" +
|
.append("<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" +
|
.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")
|
||||||
"- RAM Usage: " + (Emulator.getRuntime().totalMemory() - Emulator.getRuntime().freeMemory()) / (1024 * 1024) + "/" + (Emulator.getRuntime().freeMemory()) / (1024 * 1024) + "MB\r" +
|
.append("- RAM Usage: ").append((Emulator.getRuntime().totalMemory() - Emulator.getRuntime().freeMemory()) / (1024 * 1024)).append("/").append((Emulator.getRuntime().freeMemory()) / (1024 * 1024)).append("MB\r")
|
||||||
"- CPU Cores: " + Emulator.getRuntime().availableProcessors() + "\r" +
|
.append("- CPU Cores: ").append(Emulator.getRuntime().availableProcessors()).append("\r")
|
||||||
"- Total Memory: " + Emulator.getRuntime().maxMemory() / (1024 * 1024) + "MB" + "\r\n";
|
.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" +
|
gameClient.getHabbo().alert(message.toString());
|
||||||
" - The General";
|
|
||||||
gameClient.getHabbo().alert(message);
|
|
||||||
gameClient.sendResponse(new MessagesForYouComposer(Collections.singletonList(credits)));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
|
|||||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class BanCommand extends Command {
|
public class BanCommand extends Command {
|
||||||
public BanCommand() {
|
public BanCommand() {
|
||||||
super("cmd_ban", Emulator.getTexts().getValue("commands.keys.cmd_ban").split(";"));
|
super("cmd_ban", Emulator.getTexts().getValue("commands.keys.cmd_ban").split(";"));
|
||||||
@@ -72,7 +74,13 @@ public class BanCommand extends Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ModToolBan ban = Emulator.getGameEnvironment().getModToolManager().ban(target.getId(), gameClient.getHabbo(), reason.toString(), banTime, ModToolBanType.ACCOUNT, -1).get(0);
|
List<ModToolBan> bans = Emulator.getGameEnvironment().getModToolManager().ban(target.getId(), gameClient.getHabbo(), reason.toString(), banTime, ModToolBanType.ACCOUNT, -1);
|
||||||
|
if (bans == null || bans.isEmpty()) {
|
||||||
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.user_offline"), RoomChatMessageBubbles.ALERT);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModToolBan ban = bans.get(0);
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_ban.ban_issued").replace("%user%", target.getUsername()).replace("%time%", ban.expireDate - Emulator.getIntUnixTimestamp() + "").replace("%reason%", ban.reason), RoomChatMessageBubbles.ALERT);
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_ban.ban_issued").replace("%user%", target.getUsername()).replace("%time%", ban.expireDate - Emulator.getIntUnixTimestamp() + "").replace("%reason%", ban.reason), RoomChatMessageBubbles.ALERT);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.eu.habbo.habbohotel.commands;
|
|
||||||
|
|
||||||
import com.eu.habbo.Emulator;
|
|
||||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
|
||||||
import com.eu.habbo.messages.outgoing.users.UserDataComposer;
|
|
||||||
|
|
||||||
public class ChangeNameCommand extends Command {
|
|
||||||
public ChangeNameCommand() {
|
|
||||||
super("cmd_changename", Emulator.getTexts().getValue("commands.keys.cmd_changename").split(";"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
|
||||||
gameClient.getHabbo().getHabboStats().allowNameChange = !gameClient.getHabbo().getHabboStats().allowNameChange;
|
|
||||||
gameClient.sendResponse(new UserDataComposer(gameClient.getHabbo()));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -184,7 +184,6 @@ public class CommandHandler {
|
|||||||
addCommand(new BlockAlertCommand());
|
addCommand(new BlockAlertCommand());
|
||||||
addCommand(new BotsCommand());
|
addCommand(new BotsCommand());
|
||||||
addCommand(new CalendarCommand());
|
addCommand(new CalendarCommand());
|
||||||
addCommand(new ChangeNameCommand());
|
|
||||||
addCommand(new ChatTypeCommand());
|
addCommand(new ChatTypeCommand());
|
||||||
addCommand(new CommandsCommand());
|
addCommand(new CommandsCommand());
|
||||||
addCommand(new ControlCommand());
|
addCommand(new ControlCommand());
|
||||||
@@ -192,11 +191,14 @@ public class CommandHandler {
|
|||||||
addCommand(new CreditsCommand());
|
addCommand(new CreditsCommand());
|
||||||
addCommand(new DanceCommand());
|
addCommand(new DanceCommand());
|
||||||
addCommand(new DiagonalCommand());
|
addCommand(new DiagonalCommand());
|
||||||
|
addCommand(new DisableMassMentionsCommand());
|
||||||
|
addCommand(new DisableMentionsCommand());
|
||||||
addCommand(new DisconnectCommand());
|
addCommand(new DisconnectCommand());
|
||||||
addCommand(new EjectAllCommand());
|
addCommand(new EjectAllCommand());
|
||||||
addCommand(new EmptyInventoryCommand());
|
addCommand(new EmptyInventoryCommand());
|
||||||
addCommand(new EmptyBotsInventoryCommand());
|
addCommand(new EmptyBotsInventoryCommand());
|
||||||
addCommand(new EmptyPetsInventoryCommand());
|
addCommand(new EmptyPetsInventoryCommand());
|
||||||
|
addCommand(new EmuStatsCommand());
|
||||||
addCommand(new EnableCommand());
|
addCommand(new EnableCommand());
|
||||||
addCommand(new EventCommand());
|
addCommand(new EventCommand());
|
||||||
addCommand(new FacelessCommand());
|
addCommand(new FacelessCommand());
|
||||||
@@ -301,7 +303,6 @@ public class CommandHandler {
|
|||||||
addCommand(new GivePrefixCommand());
|
addCommand(new GivePrefixCommand());
|
||||||
addCommand(new ListPrefixesCommand());
|
addCommand(new ListPrefixesCommand());
|
||||||
addCommand(new RemovePrefixCommand());
|
addCommand(new RemovePrefixCommand());
|
||||||
addCommand(new PrefixBlacklistCommand());
|
|
||||||
addCommand(new WiredCommand());
|
addCommand(new WiredCommand());
|
||||||
addCommand(new TestCommand());
|
addCommand(new TestCommand());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,22 @@ public class CommandsCommand extends Command {
|
|||||||
message.append("(").append(commands.size()).append("):\r\n");
|
message.append("(").append(commands.size()).append("):\r\n");
|
||||||
|
|
||||||
for (Command c : commands) {
|
for (Command c : commands) {
|
||||||
message.append(Emulator.getTexts().getValue("commands.description." + c.permission, "commands.description." + c.permission)).append("\r");
|
String textKey = "commands.description." + c.permission;
|
||||||
|
String commandText = Emulator.getTexts().getValue(textKey, "");
|
||||||
|
String commandLine = ":" + c.keys[0];
|
||||||
|
String description = "";
|
||||||
|
|
||||||
|
if (commandText.startsWith(":")) {
|
||||||
|
commandLine = commandText;
|
||||||
|
} else if (!commandText.isEmpty() && !commandText.equals(textKey)) {
|
||||||
|
description = commandText;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.append(commandLine).append("\r");
|
||||||
|
|
||||||
|
if (!description.isEmpty()) {
|
||||||
|
message.append(description).append("\r");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gameClient.getHabbo().alert(new String[]{message.toString()});
|
gameClient.getHabbo().alert(new String[]{message.toString()});
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package com.eu.habbo.habbohotel.commands;
|
||||||
|
|
||||||
|
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||||
|
import com.eu.habbo.habbohotel.users.Habbo;
|
||||||
|
|
||||||
|
public class DisableMassMentionsCommand extends Command {
|
||||||
|
public DisableMassMentionsCommand() {
|
||||||
|
super("cmd_disablemassmentions", new String[]{"disablemassmentions", "togglemassmentions"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||||
|
if (gameClient == null) return true;
|
||||||
|
Habbo habbo = gameClient.getHabbo();
|
||||||
|
if (habbo == null || habbo.getHabboStats() == null) return true;
|
||||||
|
|
||||||
|
boolean newState = !habbo.getHabboStats().massMentionsEnabled();
|
||||||
|
habbo.getHabboStats().setMassMentionsEnabled(newState);
|
||||||
|
|
||||||
|
habbo.whisper(newState
|
||||||
|
? "Broadcast mentions (@all / @friends / @room) are now ENABLED for you."
|
||||||
|
: "Broadcast mentions (@all / @friends / @room) are now DISABLED for you. Direct @nick mentions still work.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.eu.habbo.habbohotel.commands;
|
||||||
|
|
||||||
|
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||||
|
import com.eu.habbo.habbohotel.users.Habbo;
|
||||||
|
|
||||||
|
public class DisableMentionsCommand extends Command {
|
||||||
|
public DisableMentionsCommand() {
|
||||||
|
super("cmd_disablementions", new String[]{"disablementions", "togglementions"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||||
|
if (gameClient == null) return true;
|
||||||
|
Habbo habbo = gameClient.getHabbo();
|
||||||
|
if (habbo == null || habbo.getHabboStats() == null) return true;
|
||||||
|
|
||||||
|
boolean newState = !habbo.getHabboStats().mentionsEnabled();
|
||||||
|
habbo.getHabboStats().setMentionsEnabled(newState);
|
||||||
|
|
||||||
|
habbo.whisper(newState
|
||||||
|
? "@mention notifications are now ENABLED for you."
|
||||||
|
: "@mention notifications are now DISABLED for you. You will not receive direct or broadcast mentions.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.eu.habbo.habbohotel.commands;
|
||||||
|
|
||||||
|
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||||
|
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||||
|
|
||||||
|
public class EmuStatsCommand extends Command {
|
||||||
|
public EmuStatsCommand() {
|
||||||
|
super(Permission.ACC_MODTOOL_ROOM_INFO, new String[]{"emustats"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handle(GameClient gameClient, String[] params) {
|
||||||
|
gameClient.getHabbo().whisper("Emulator stats are available in the Nitro stats window.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
|
|||||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||||
import com.eu.habbo.habbohotel.modtool.WordFilter;
|
import com.eu.habbo.habbohotel.modtool.WordFilter;
|
||||||
import com.eu.habbo.habbohotel.modtool.WordFilterWord;
|
import com.eu.habbo.habbohotel.modtool.WordFilterWord;
|
||||||
|
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -21,30 +22,44 @@ public class FilterWordCommand extends Command {
|
|||||||
@Override
|
@Override
|
||||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||||
if (params.length < 2) {
|
if (params.length < 2) {
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.missing_word"));
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.missing_word"), RoomChatMessageBubbles.ALERT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
String word = params[1];
|
String word = params[1];
|
||||||
|
|
||||||
|
// Optional trailing "prefix" keyword marks the word as prefix-only (blocks
|
||||||
|
// custom prefixes but not chat/motto/guild). Usage:
|
||||||
|
// :filterword <word> -> everywhere, default replacement
|
||||||
|
// :filterword <word> <replacement> -> everywhere
|
||||||
|
// :filterword <word> prefix -> prefix-only, default replacement
|
||||||
|
// :filterword <word> <replacement> prefix -> prefix-only
|
||||||
|
boolean prefixOnly = false;
|
||||||
String replacement = WordFilter.DEFAULT_REPLACEMENT;
|
String replacement = WordFilter.DEFAULT_REPLACEMENT;
|
||||||
if (params.length == 3) {
|
|
||||||
replacement = params[2];
|
if (params.length >= 3) {
|
||||||
|
if (params[params.length - 1].equalsIgnoreCase("prefix")) {
|
||||||
|
prefixOnly = true;
|
||||||
|
if (params.length >= 4) replacement = params[2];
|
||||||
|
} else {
|
||||||
|
replacement = params[2];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WordFilterWord wordFilterWord = new WordFilterWord(word, replacement);
|
WordFilterWord wordFilterWord = new WordFilterWord(word, replacement, prefixOnly);
|
||||||
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO wordfilter (`key`, `replacement`) VALUES (?, ?)")) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO wordfilter (`key`, `replacement`, `prefix_only`) VALUES (?, ?, ?)")) {
|
||||||
statement.setString(1, word);
|
statement.setString(1, word);
|
||||||
statement.setString(2, replacement);
|
statement.setString(2, replacement);
|
||||||
|
statement.setString(3, prefixOnly ? "1" : "0");
|
||||||
statement.execute();
|
statement.execute();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Caught SQL exception", e);
|
LOGGER.error("Caught SQL exception", e);
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.error"));
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.error"), RoomChatMessageBubbles.ALERT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_filterword.added").replace("%word%", word).replace("%replacement%", replacement));
|
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_filterword.added").replace("%word%", word).replace("%replacement%", replacement) + (prefixOnly ? " [prefix-only]" : ""), RoomChatMessageBubbles.ALERT);
|
||||||
Emulator.getGameEnvironment().getWordFilter().addWord(wordFilterWord);
|
Emulator.getGameEnvironment().getWordFilter().addWord(wordFilterWord);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
|
|||||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class IPBanCommand extends Command {
|
public class IPBanCommand extends Command {
|
||||||
public final static int TEN_YEARS = 315569260;
|
public final static int TEN_YEARS = 315569260;
|
||||||
|
|
||||||
@@ -50,12 +52,12 @@ public class IPBanCommand extends Command {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
List<?> bans = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
||||||
count++;
|
count += bans != null ? bans.size() : 0;
|
||||||
for (Habbo h : Emulator.getGameServer().getGameClientManager().getHabbosWithIP(habbo.getIpLogin())) {
|
for (Habbo h : Emulator.getGameServer().getGameClientManager().getHabbosWithIP(habbo.getIpLogin())) {
|
||||||
if (h != null) {
|
if (h != null) {
|
||||||
count++;
|
bans = Emulator.getGameEnvironment().getModToolManager().ban(h.getHabboInfo().getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
||||||
Emulator.getGameEnvironment().getModToolManager().ban(h.getHabboInfo().getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
|
count += bans != null ? bans.size() : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
|
|||||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class MachineBanCommand extends Command {
|
public class MachineBanCommand extends Command {
|
||||||
public MachineBanCommand() {
|
public MachineBanCommand() {
|
||||||
super("cmd_machine_ban", Emulator.getTexts().getValue("commands.keys.cmd_machine_ban").split(";"));
|
super("cmd_machine_ban", Emulator.getTexts().getValue("commands.keys.cmd_machine_ban").split(";"));
|
||||||
@@ -46,7 +48,8 @@ public class MachineBanCommand extends Command {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
count = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), IPBanCommand.TEN_YEARS, ModToolBanType.MACHINE, -1).size();
|
List<?> bans = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), IPBanCommand.TEN_YEARS, ModToolBanType.MACHINE, -1);
|
||||||
|
count = bans != null ? bans.size() : 0;
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -58,4 +61,4 @@ public class MachineBanCommand extends Command {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
package com.eu.habbo.habbohotel.commands;
|
|
||||||
|
|
||||||
import com.eu.habbo.Emulator;
|
|
||||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
|
||||||
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.PreparedStatement;
|
|
||||||
import java.sql.ResultSet;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
|
|
||||||
public class PrefixBlacklistCommand extends Command {
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(PrefixBlacklistCommand.class);
|
|
||||||
|
|
||||||
public PrefixBlacklistCommand() {
|
|
||||||
super("cmd_prefix_blacklist", Emulator.getTexts().getValue("commands.keys.cmd_prefix_blacklist").split(";"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
|
||||||
if (params.length < 2) {
|
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String action = params[1].toLowerCase();
|
|
||||||
|
|
||||||
if (action.equals("list")) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append(Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.header")).append("\r");
|
|
||||||
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
|
||||||
PreparedStatement statement = connection.prepareStatement("SELECT word FROM custom_prefix_blacklist ORDER BY word")) {
|
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
|
||||||
int count = 0;
|
|
||||||
while (set.next()) {
|
|
||||||
sb.append("- ").append(set.getString("word")).append("\r");
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
if (count == 0) {
|
|
||||||
sb.append(Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.empty"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
LOGGER.error("Error listing prefix blacklist", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
gameClient.getHabbo().whisper(sb.toString(), RoomChatMessageBubbles.ALERT);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.length < 3) {
|
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String word = params[2].toLowerCase().trim();
|
|
||||||
|
|
||||||
if (word.isEmpty()) {
|
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.empty_word"), RoomChatMessageBubbles.ALERT);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.equals("add")) {
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
|
||||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO custom_prefix_blacklist (word) VALUES (?)")) {
|
|
||||||
statement.setString(1, word);
|
|
||||||
statement.execute();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
LOGGER.error("Error adding prefix blacklist word", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
gameClient.getHabbo().whisper(
|
|
||||||
Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.added").replace("%word%", word),
|
|
||||||
RoomChatMessageBubbles.ALERT
|
|
||||||
);
|
|
||||||
} else if (action.equals("remove")) {
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
|
||||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM custom_prefix_blacklist WHERE word = ?")) {
|
|
||||||
statement.setString(1, word);
|
|
||||||
statement.execute();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
LOGGER.error("Error removing prefix blacklist word", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
gameClient.getHabbo().whisper(
|
|
||||||
Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.removed").replace("%word%", word),
|
|
||||||
RoomChatMessageBubbles.ALERT
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+40
-1
@@ -2,7 +2,12 @@ package com.eu.habbo.habbohotel.commands;
|
|||||||
|
|
||||||
import com.eu.habbo.Emulator;
|
import com.eu.habbo.Emulator;
|
||||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||||
|
import com.eu.habbo.habbohotel.permissions.PermissionsManager;
|
||||||
|
import com.eu.habbo.habbohotel.permissions.Rank;
|
||||||
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
|
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
|
||||||
|
import com.eu.habbo.habbohotel.users.Habbo;
|
||||||
|
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||||
|
import com.eu.habbo.messages.outgoing.users.UserPermissionsComposer;
|
||||||
|
|
||||||
public class UpdatePermissionsCommand extends Command {
|
public class UpdatePermissionsCommand extends Command {
|
||||||
public UpdatePermissionsCommand() {
|
public UpdatePermissionsCommand() {
|
||||||
@@ -13,7 +18,41 @@ public class UpdatePermissionsCommand extends Command {
|
|||||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||||
Emulator.getGameEnvironment().getPermissionsManager().reload();
|
Emulator.getGameEnvironment().getPermissionsManager().reload();
|
||||||
|
|
||||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_update_permissions"), RoomChatMessageBubbles.ALERT);
|
// PermissionsManager.reload() rebuilt the rank table — each online
|
||||||
|
// Habbo's HabboInfo still references the OLD Rank object, so
|
||||||
|
// server-side hasPermission() / wire composers would keep
|
||||||
|
// reporting stale data until relogin. Re-bind every connected
|
||||||
|
// user to the freshly-loaded Rank by id, then ship the new
|
||||||
|
// UserPermissionsComposer (which carries clubLevel,
|
||||||
|
// securityLevel, isAmbassador, rank metadata and the resolved
|
||||||
|
// permission_definitions map) so Nitro clients' React-side
|
||||||
|
// useHasPermission(key) / useUserRank() / useUserPermissions()
|
||||||
|
// consumers re-render against the updated tables without an F5.
|
||||||
|
HabboManager habboManager = Emulator.getGameEnvironment().getHabboManager();
|
||||||
|
PermissionsManager permissions = Emulator.getGameEnvironment().getPermissionsManager();
|
||||||
|
|
||||||
|
int refreshed = 0;
|
||||||
|
|
||||||
|
for (Habbo habbo : habboManager.getOnlineHabbos().values()) {
|
||||||
|
if (habbo == null || habbo.getHabboInfo() == null || habbo.getClient() == null) continue;
|
||||||
|
|
||||||
|
int currentRankId = habbo.getHabboInfo().getRank().getId();
|
||||||
|
// Defensive fallback: if the admin deleted the rank from the
|
||||||
|
// permission_ranks table between sessions, fall back to rank 1
|
||||||
|
// (Member) so the user isn't stranded with a null Rank.
|
||||||
|
Rank freshRank = permissions.rankExists(currentRankId)
|
||||||
|
? permissions.getRank(currentRankId)
|
||||||
|
: permissions.getRank(1);
|
||||||
|
|
||||||
|
habbo.getHabboInfo().setRank(freshRank);
|
||||||
|
habbo.getClient().sendResponse(new UserPermissionsComposer(habbo));
|
||||||
|
refreshed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameClient.getHabbo().whisper(
|
||||||
|
Emulator.getTexts().getValue("commands.succes.cmd_update_permissions") + " (" + refreshed + " online refreshed)",
|
||||||
|
RoomChatMessageBubbles.ALERT
|
||||||
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,13 +149,23 @@ public class GameClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
|
this.dispose(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispose(boolean allowSessionResume) {
|
||||||
try {
|
try {
|
||||||
this.channel.close();
|
this.channel.close();
|
||||||
|
|
||||||
if (this.habbo != null) {
|
if (this.habbo != null) {
|
||||||
if (this.habbo.isOnline()) {
|
// Agisci sull'Habbo SOLO se è ancora attaccato a QUESTO client. Su un
|
||||||
|
// reconnect veloce (drop Cloudflare → il client riconnette) l'Habbo può
|
||||||
|
// essere già stato riassegnato alla NUOVA connessione (session resume):
|
||||||
|
// in quel caso questo dispose della vecchia connessione NON deve
|
||||||
|
// parcheggiarlo né disconnetterlo, altrimenti ucciderebbe la sessione
|
||||||
|
// appena ripristinata (era la causa del "Bye"/kick al 2° reconnect).
|
||||||
|
if (this.habbo.getClient() == this && this.habbo.isOnline()) {
|
||||||
// Try to park the habbo in the grace period instead of immediate disconnect
|
// Try to park the habbo in the grace period instead of immediate disconnect
|
||||||
boolean parked = SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket);
|
boolean parked = allowSessionResume && SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket);
|
||||||
|
|
||||||
if (!parked) {
|
if (!parked) {
|
||||||
// No grace period configured — immediate disconnect as before
|
// No grace period configured — immediate disconnect as before
|
||||||
@@ -171,4 +181,4 @@ public class GameClient {
|
|||||||
LOGGER.error("Caught exception", e);
|
LOGGER.error("Caught exception", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,14 +43,34 @@ public class GameClientManager {
|
|||||||
|
|
||||||
|
|
||||||
public void disposeClient(GameClient client) {
|
public void disposeClient(GameClient client) {
|
||||||
this.disposeClient(client.getChannel());
|
if (client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disposeClient(client.getChannel(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forceDisposeClient(GameClient client) {
|
||||||
|
if (client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disposeClient(client.getChannel(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void disposeClient(Channel channel) {
|
private void disposeClient(Channel channel) {
|
||||||
|
this.disposeClient(channel, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disposeClient(Channel channel, boolean allowSessionResume) {
|
||||||
|
if (channel == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
GameClient client = channel.attr(GameServerAttributes.CLIENT).get();
|
GameClient client = channel.attr(GameServerAttributes.CLIENT).get();
|
||||||
|
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
client.dispose();
|
client.dispose(allowSessionResume);
|
||||||
}
|
}
|
||||||
channel.deregister();
|
channel.deregister();
|
||||||
channel.attr(GameServerAttributes.CLIENT).set(null);
|
channel.attr(GameServerAttributes.CLIENT).set(null);
|
||||||
@@ -190,4 +210,4 @@ public class GameClientManager {
|
|||||||
CFKeepAlive();
|
CFKeepAlive();
|
||||||
}, 30000);
|
}, 30000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-4
@@ -71,6 +71,15 @@ public class SessionResumeManager {
|
|||||||
}
|
}
|
||||||
}, graceSeconds * 1000);
|
}, graceSeconds * 1000);
|
||||||
|
|
||||||
|
if (future == null) {
|
||||||
|
// The scheduler refused the grace-expiry task (pool saturated or
|
||||||
|
// shutting down). Parking now would leave a GhostSession that nothing
|
||||||
|
// can ever reap (the Habbo + room refs pinned for the JVM lifetime),
|
||||||
|
// so disconnect immediately instead.
|
||||||
|
performFullDisconnect(habbo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd));
|
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd));
|
||||||
|
|
||||||
applyPausedEffect(habbo);
|
applyPausedEffect(habbo);
|
||||||
@@ -118,16 +127,32 @@ public class SessionResumeManager {
|
|||||||
LOGGER.error("[SessionResume] Error during deferred disconnect", e);
|
LOGGER.error("[SessionResume] Error during deferred disconnect", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSsoTicket(habbo.getHabboInfo().getId());
|
// NON svuotare il ticket SSO qui. Dietro Cloudflare la pagina si ricarica
|
||||||
|
// lentamente (~15s) e la grace (5s) scade prima che la nuova connessione
|
||||||
|
// arrivi: svuotando il ticket si cancellava quello NUOVO appena scritto dal
|
||||||
|
// CMS per il refresh → "non-existing SSO token" → bisognava refreshare 2 volte.
|
||||||
|
// Il ticket vive col suo TTL (auth_ticket_expires_at) e viene sovrascritto dal
|
||||||
|
// CMS al prossimo /client o azzerato al logout.
|
||||||
}
|
}
|
||||||
|
|
||||||
private void restoreSsoTicket(int userId, String ssoTicket) {
|
private void restoreSsoTicket(int userId, String ssoTicket) {
|
||||||
|
// Restore the old ticket ONLY if no fresh ticket has been written in the
|
||||||
|
// meantime. On a hard-refresh the CMS writes a NEW auth_ticket for the same
|
||||||
|
// user before this parking restore runs; without the guard we'd clobber it
|
||||||
|
// with the old ticket, so the new connection's SSO wouldn't be found and the
|
||||||
|
// client would get "session expired" on the first attempt. The guard means:
|
||||||
|
// normal reconnect (ticket cleared to '' after login) -> restore; hard-refresh
|
||||||
|
// (CMS already wrote a new ticket) -> leave the new ticket untouched.
|
||||||
try (var connection = Emulator.getDatabase().getDataSource().getConnection();
|
try (var connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) {
|
var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? AND (auth_ticket = '' OR auth_ticket IS NULL) LIMIT 1")) {
|
||||||
statement.setString(1, ssoTicket);
|
statement.setString(1, ssoTicket);
|
||||||
statement.setInt(2, userId);
|
statement.setInt(2, userId);
|
||||||
statement.execute();
|
int updated = statement.executeUpdate();
|
||||||
LOGGER.info("[SessionResume] Restored SSO ticket for user {} during grace period", userId);
|
if (updated > 0) {
|
||||||
|
LOGGER.info("[SessionResume] Restored SSO ticket for user {} during grace period", userId);
|
||||||
|
} else {
|
||||||
|
LOGGER.info("[SessionResume] Skipped SSO restore for user {} — a newer ticket is already present (likely a fresh login/hard-refresh)", userId);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error("[SessionResume] Failed to restore SSO ticket for user " + userId, e);
|
LOGGER.error("[SessionResume] Failed to restore SSO ticket for user " + userId, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,10 +208,10 @@ public abstract class Game implements Runnable {
|
|||||||
this.state = GameState.IDLE;
|
this.state = GameState.IDLE;
|
||||||
|
|
||||||
boolean gamesActive = false;
|
boolean gamesActive = false;
|
||||||
for (HabboItem timer : room.getFloorItems()) {
|
for (InteractionGameTimer timer : room.getRoomSpecialTypes().getGameTimers().values()) {
|
||||||
if (timer instanceof InteractionGameTimer) {
|
if (timer.isRunning()) {
|
||||||
if (((InteractionGameTimer) timer).isRunning())
|
gamesActive = true;
|
||||||
gamesActive = true;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,49 +6,55 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager;
|
|||||||
public class GamePlayer {
|
public class GamePlayer {
|
||||||
|
|
||||||
private final Habbo habbo;
|
private final Habbo habbo;
|
||||||
|
|
||||||
|
|
||||||
private GameTeamColors teamColor;
|
private GameTeamColors teamColor;
|
||||||
|
|
||||||
|
|
||||||
private int score;
|
private int score;
|
||||||
private int wiredScore;
|
private int wiredScore;
|
||||||
|
|
||||||
|
|
||||||
public GamePlayer(Habbo habbo, GameTeamColors teamColor) {
|
public GamePlayer(Habbo habbo, GameTeamColors teamColor) {
|
||||||
this.habbo = habbo;
|
this.habbo = habbo;
|
||||||
this.teamColor = teamColor;
|
this.teamColor = teamColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void reset() {
|
public void reset() {
|
||||||
this.score = 0;
|
this.score = 0;
|
||||||
this.wiredScore = 0;
|
this.wiredScore = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void addScore(int amount) {
|
public void addScore(int amount) {
|
||||||
addScore(amount, false);
|
addScore(amount, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void addScore(int amount, boolean isWired) {
|
public void addScore(int amount, boolean isWired) {
|
||||||
if (habbo.getHabboInfo().getGamePlayer() != null && this.habbo.getHabboInfo().getCurrentGame() != null && this.habbo.getHabboInfo().getCurrentRoom().getGame(this.habbo.getHabboInfo().getCurrentGame()).getTeamForHabbo(this.habbo) != null) {
|
com.eu.habbo.habbohotel.rooms.Room roomToTrigger = null;
|
||||||
this.score += amount;
|
com.eu.habbo.habbohotel.rooms.RoomUnit roomUnitToTrigger = null;
|
||||||
|
int currentScore = 0;
|
||||||
|
|
||||||
if (this.score < 0) this.score = 0;
|
synchronized (this) {
|
||||||
|
if (this.habbo.getHabboInfo().getGamePlayer() != null && this.habbo.getHabboInfo().getCurrentGame() != null && this.habbo.getHabboInfo().getCurrentRoom().getGame(this.habbo.getHabboInfo().getCurrentGame()).getTeamForHabbo(this.habbo) != null) {
|
||||||
|
this.score += amount;
|
||||||
|
|
||||||
if(isWired) {
|
if (this.score < 0) this.score = 0;
|
||||||
this.wiredScore += amount;
|
|
||||||
|
|
||||||
if (this.wiredScore < 0) {
|
if (isWired) {
|
||||||
this.wiredScore = 0;
|
this.wiredScore += amount;
|
||||||
|
|
||||||
|
if (this.wiredScore < 0) {
|
||||||
|
this.wiredScore = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.wiredScore > this.score) {
|
||||||
|
this.wiredScore = this.score;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.wiredScore > this.score) {
|
roomToTrigger = this.habbo.getHabboInfo().getCurrentRoom();
|
||||||
this.wiredScore = this.score;
|
roomUnitToTrigger = this.habbo.getRoomUnit();
|
||||||
}
|
currentScore = this.score;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WiredManager.triggerScoreAchieved(this.habbo.getHabboInfo().getCurrentRoom(), this.habbo.getRoomUnit(), this.score, amount);
|
if (roomToTrigger != null && roomUnitToTrigger != null) {
|
||||||
|
WiredManager.triggerScoreAchieved(roomToTrigger, roomUnitToTrigger, currentScore, amount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,12 +62,10 @@ public class GamePlayer {
|
|||||||
return this.habbo;
|
return this.habbo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public GameTeamColors getTeamColor() {
|
public GameTeamColors getTeamColor() {
|
||||||
return this.teamColor;
|
return this.teamColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public int getScore() {
|
public int getScore() {
|
||||||
return this.score;
|
return this.score;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,6 +252,25 @@ public class Guild implements Runnable {
|
|||||||
return this.readForum;
|
return this.readForum;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean canHabboReadForum(int habboId, GuildMember member, boolean staff) {
|
||||||
|
if (staff || this.getOwnerId() == habboId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.readForum) {
|
||||||
|
case EVERYONE:
|
||||||
|
return true;
|
||||||
|
case MEMBERS:
|
||||||
|
return member != null && member.getRank().type <= GuildRank.MEMBER.type;
|
||||||
|
case ADMINS:
|
||||||
|
return member != null && member.getRank().type < GuildRank.MEMBER.type;
|
||||||
|
case OWNER:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setReadForum(SettingsState readForum) {
|
public void setReadForum(SettingsState readForum) {
|
||||||
this.readForum = readForum;
|
this.readForum = readForum;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ package com.eu.habbo.habbohotel.guilds;
|
|||||||
|
|
||||||
import com.eu.habbo.Emulator;
|
import com.eu.habbo.Emulator;
|
||||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||||
|
import com.eu.habbo.habbohotel.guilds.forums.ForumThread;
|
||||||
import com.eu.habbo.habbohotel.guilds.forums.ForumView;
|
import com.eu.habbo.habbohotel.guilds.forums.ForumView;
|
||||||
import com.eu.habbo.habbohotel.items.interactions.InteractionGuildFurni;
|
import com.eu.habbo.habbohotel.items.interactions.InteractionGuildFurni;
|
||||||
import com.eu.habbo.habbohotel.rooms.Room;
|
import com.eu.habbo.habbohotel.rooms.Room;
|
||||||
import com.eu.habbo.habbohotel.users.Habbo;
|
import com.eu.habbo.habbohotel.users.Habbo;
|
||||||
import com.eu.habbo.messages.outgoing.guilds.GuildJoinErrorComposer;
|
import com.eu.habbo.messages.outgoing.guilds.GuildJoinErrorComposer;
|
||||||
|
import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer;
|
||||||
import gnu.trove.TCollections;
|
import gnu.trove.TCollections;
|
||||||
import gnu.trove.iterator.TIntObjectIterator;
|
import gnu.trove.iterator.TIntObjectIterator;
|
||||||
import gnu.trove.map.TIntObjectMap;
|
import gnu.trove.map.TIntObjectMap;
|
||||||
@@ -142,12 +144,36 @@ public class GuildManager {
|
|||||||
deleteFavourite.execute();
|
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 = ?")) {
|
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM guilds_members WHERE guild_id = ?")) {
|
||||||
statement.setInt(1, guild.getId());
|
statement.setInt(1, guild.getId());
|
||||||
statement.execute();
|
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 = ?")) {
|
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM guilds WHERE id = ?")) {
|
||||||
statement.setInt(1, guild.getId());
|
statement.setInt(1, guild.getId());
|
||||||
statement.execute();
|
statement.execute();
|
||||||
@@ -161,6 +187,10 @@ public class GuildManager {
|
|||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Caught SQL exception", e);
|
LOGGER.error("Caught SQL exception", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.guilds.remove(guild.getId());
|
||||||
|
ForumThread.clearCacheForGuild(guild.getId());
|
||||||
|
GuildForumDataComposer.invalidateUnreadCache(guild.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -391,9 +421,9 @@ public class GuildManager {
|
|||||||
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.username, users.look, guilds_members.* FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC LIMIT ?, ?")) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.username, users.look, guilds_members.* FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC LIMIT ?, ?")) {
|
||||||
statement.setInt(1, guild.getId());
|
statement.setInt(1, guild.getId());
|
||||||
statement.setString(2, "%" + query + "%");
|
statement.setString(2, "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%");
|
||||||
statement.setInt(3, page * 14);
|
statement.setInt(3, page * 14);
|
||||||
statement.setInt(4, (page * 14) + 14);
|
statement.setInt(4, 14);
|
||||||
|
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
while (set.next()) {
|
while (set.next()) {
|
||||||
|
|||||||
@@ -101,21 +101,27 @@ public class ForumThread implements Runnable, ISerialize {
|
|||||||
if (statement.executeUpdate() < 1)
|
if (statement.executeUpdate() < 1)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
ResultSet set = statement.getGeneratedKeys();
|
try (ResultSet set = statement.getGeneratedKeys()) {
|
||||||
if (set.next()) {
|
if (set.next()) {
|
||||||
int threadId = set.getInt(1);
|
int threadId = set.getInt(1);
|
||||||
createdThread = new ForumThread(threadId, guild.getId(), opener.getHabboInfo().getId(), subject, 0, timestamp, timestamp, ForumThreadState.OPEN, false, false, 0, null);
|
createdThread = new ForumThread(threadId, guild.getId(), opener.getHabboInfo().getId(), subject, 0, timestamp, timestamp, ForumThreadState.OPEN, false, false, 0, null);
|
||||||
cacheThread(createdThread);
|
cacheThread(createdThread);
|
||||||
|
}
|
||||||
ForumThreadComment comment = ForumThreadComment.create(createdThread, opener, message);
|
|
||||||
createdThread.addComment(comment);
|
|
||||||
|
|
||||||
Emulator.getPluginManager().fireEvent(new GuildForumThreadCreated(createdThread));
|
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Caught SQL exception", e);
|
LOGGER.error("Caught SQL exception", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ForumThreadComment.create() opens its OWN connection; do it after the
|
||||||
|
// thread's connection has been released to avoid holding two pooled
|
||||||
|
// connections simultaneously per forum-thread creation.
|
||||||
|
if (createdThread != null) {
|
||||||
|
ForumThreadComment comment = ForumThreadComment.create(createdThread, opener, message);
|
||||||
|
createdThread.addComment(comment);
|
||||||
|
|
||||||
|
Emulator.getPluginManager().fireEvent(new GuildForumThreadCreated(createdThread));
|
||||||
|
}
|
||||||
|
|
||||||
return createdThread;
|
return createdThread;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -98,12 +98,13 @@ public class ForumThreadComment implements Runnable, ISerialize {
|
|||||||
if (statement.executeUpdate() < 1)
|
if (statement.executeUpdate() < 1)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
ResultSet set = statement.getGeneratedKeys();
|
try (ResultSet set = statement.getGeneratedKeys()) {
|
||||||
if (set.next()) {
|
if (set.next()) {
|
||||||
int commentId = set.getInt(1);
|
int commentId = set.getInt(1);
|
||||||
createdComment = new ForumThreadComment(commentId, thread.getThreadId(), poster.getHabboInfo().getId(), message, timestamp, ForumThreadState.OPEN, 0);
|
createdComment = new ForumThreadComment(commentId, thread.getThreadId(), poster.getHabboInfo().getId(), message, timestamp, ForumThreadState.OPEN, 0);
|
||||||
|
|
||||||
Emulator.getPluginManager().fireEvent(new GuildForumThreadCommentCreated(createdComment));
|
Emulator.getPluginManager().fireEvent(new GuildForumThreadCommentCreated(createdComment));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Caught SQL exception", e);
|
LOGGER.error("Caught SQL exception", e);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One parsed furnidata entry. {@code classname} is the raw furnidata classname
|
||||||
|
* (may carry a {@code *N} colour-variant suffix); the provider keys on the base.
|
||||||
|
*/
|
||||||
|
public record FurnidataEntry(int id, String classname, FurnitureType type, String name, String description) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One process-wide lock serializing every furnidata reindex and every editor-driven
|
||||||
|
* furnidata write, so an editor write never races the file watcher's reindex and the
|
||||||
|
* volatile index is never observed mid-swap by two writers.
|
||||||
|
*/
|
||||||
|
public final class FurnidataLock {
|
||||||
|
public static final ReentrantLock LOCK = new ReentrantLock();
|
||||||
|
private FurnidataLock() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Neutral furnidata reader. Supports a single JSON/JSON5 file or a split-tier
|
||||||
|
* directory ({@code core/custom/seasonal} with {@code manifest.json(5)}).
|
||||||
|
* Never throws: any IO/parse error yields an empty list (the caller decides the
|
||||||
|
* fallback). All resolved paths are guarded against escaping the base dir.
|
||||||
|
*/
|
||||||
|
public class FurnidataReader {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(FurnidataReader.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");
|
||||||
|
|
||||||
|
private final Path source;
|
||||||
|
private final long maxBytes;
|
||||||
|
|
||||||
|
public FurnidataReader(Path source, long maxBytes) {
|
||||||
|
this.source = source;
|
||||||
|
this.maxBytes = maxBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FurnidataEntry> read() {
|
||||||
|
List<FurnidataEntry> out = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
if (this.source == null || !Files.exists(this.source)) return out;
|
||||||
|
|
||||||
|
if (Files.isDirectory(this.source)) {
|
||||||
|
readSplitDir(this.source, out);
|
||||||
|
} else {
|
||||||
|
String content = readJson5Capped(this.source);
|
||||||
|
if (content != null) {
|
||||||
|
parseRoot(JsonParser.parseString(content).getAsJsonObject(), out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnidataReader failed to read {} — returning empty", this.source, e);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readSplitDir(Path base, List<FurnidataEntry> out) {
|
||||||
|
List<String> tiers = readManifestList(base, "tiers", DEFAULT_TIERS);
|
||||||
|
Path baseNorm = base.toAbsolutePath().normalize();
|
||||||
|
|
||||||
|
for (String tier : tiers) {
|
||||||
|
Path tierDir = base.resolve(tier);
|
||||||
|
if (!isInside(baseNorm, tierDir) || !Files.isDirectory(tierDir)) continue;
|
||||||
|
|
||||||
|
for (String fileName : readManifestList(tierDir, "files", List.of())) {
|
||||||
|
Path file = tierDir.resolve(fileName);
|
||||||
|
if (!isInside(baseNorm, file)) {
|
||||||
|
LOGGER.warn("FurnidataReader: ignoring out-of-base file {}", file);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Files.exists(file)) continue;
|
||||||
|
try {
|
||||||
|
String content = readJson5Capped(file);
|
||||||
|
if (content != null) parseRoot(JsonParser.parseString(content).getAsJsonObject(), out);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnidataReader: failed to parse {}", file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> readManifestList(Path dir, String key, List<String> fallback) {
|
||||||
|
for (String name : MANIFEST_NAMES) {
|
||||||
|
Path m = dir.resolve(name);
|
||||||
|
if (!Files.exists(m)) continue;
|
||||||
|
try {
|
||||||
|
String raw = readJson5Capped(m);
|
||||||
|
if (raw == null) continue;
|
||||||
|
JsonObject obj = JsonParser.parseString(raw).getAsJsonObject();
|
||||||
|
if (obj.has(key) && obj.get(key).isJsonArray()) {
|
||||||
|
List<String> list = new ArrayList<>();
|
||||||
|
for (JsonElement el : obj.getAsJsonArray(key)) list.add(el.getAsString());
|
||||||
|
if (!list.isEmpty()) return list;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnidataReader: bad manifest {}", m, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseRoot(JsonObject root, List<FurnidataEntry> out) {
|
||||||
|
for (String section : SECTIONS) {
|
||||||
|
if (!root.has(section)) continue;
|
||||||
|
JsonObject sectionObj = root.getAsJsonObject(section);
|
||||||
|
if (!sectionObj.has("furnitype")) continue;
|
||||||
|
FurnitureType type = section.equals("roomitemtypes") ? FurnitureType.FLOOR : FurnitureType.WALL;
|
||||||
|
JsonArray types = sectionObj.getAsJsonArray("furnitype");
|
||||||
|
for (JsonElement el : types) {
|
||||||
|
JsonObject o = el.getAsJsonObject();
|
||||||
|
if (!o.has("id") || o.get("id").isJsonNull() || !o.has("classname") || o.get("classname").isJsonNull()) continue;
|
||||||
|
out.add(new FurnidataEntry(
|
||||||
|
o.get("id").getAsInt(),
|
||||||
|
o.get("classname").getAsString(),
|
||||||
|
type,
|
||||||
|
(o.has("name") && !o.get("name").isJsonNull()) ? o.get("name").getAsString() : "",
|
||||||
|
(o.has("description") && !o.get("description").isJsonNull()) ? o.get("description").getAsString() : ""
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the JSON5-stripped content, or null if the file exceeds the byte cap. */
|
||||||
|
private String readJson5Capped(Path path) throws Exception {
|
||||||
|
long size = Files.size(path);
|
||||||
|
if (size > this.maxBytes) {
|
||||||
|
LOGGER.warn("FurnidataReader: {} is {} bytes, over cap {} — refusing", path, size, this.maxBytes);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return stripJson5(Files.readString(path, StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isInside(Path baseNorm, Path candidate) {
|
||||||
|
return candidate.toAbsolutePath().normalize().startsWith(baseNorm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip // and block comments and trailing commas so Gson can parse JSON5.
|
||||||
|
* Known limitation: the trailing-comma pass is a regex over the whole output,
|
||||||
|
* so a string value literally containing ",[whitespace]}" or ",[whitespace]]"
|
||||||
|
* would be altered. Real Habbo furnidata names/descriptions do not contain
|
||||||
|
* that pattern; values are additionally sanitized downstream before use.
|
||||||
|
*/
|
||||||
|
static String stripJson5(String content) {
|
||||||
|
if (content == null || content.isEmpty()) return content;
|
||||||
|
StringBuilder out = new StringBuilder(content.length());
|
||||||
|
int i = 0, len = content.length();
|
||||||
|
boolean inString = false, escape = false;
|
||||||
|
char stringChar = 0;
|
||||||
|
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) break; i = eol; continue; }
|
||||||
|
if (next == '*') { int end = content.indexOf("*/", i + 2); if (end < 0) break; i = end + 2; continue; }
|
||||||
|
}
|
||||||
|
out.append(c);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return out.toString().replaceAll(",(\\s*[}\\]])", "$1");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
public final class FurnidataSourceResolver {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(FurnidataSourceResolver.class);
|
||||||
|
|
||||||
|
public enum Status {
|
||||||
|
RESOLVED,
|
||||||
|
SOURCE_MISSING,
|
||||||
|
CONFIG_MISSING,
|
||||||
|
UNRESOLVED_PLACEHOLDER,
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Source(Path path, boolean directory, Status status, String message) {
|
||||||
|
public boolean ok() {
|
||||||
|
return this.status == Status.RESOLVED && this.path != null && Files.exists(this.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private FurnidataSourceResolver() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Source resolve() {
|
||||||
|
try {
|
||||||
|
String override = Emulator.getConfig().getValue("items.furnidata.path", "");
|
||||||
|
if (!override.isEmpty()) {
|
||||||
|
Path p = Paths.get(override);
|
||||||
|
if (Files.exists(p)) return new Source(p, Files.isDirectory(p), Status.RESOLVED, "items.furnidata.path");
|
||||||
|
return new Source(p, Files.isDirectory(p), Status.SOURCE_MISSING, "items.furnidata.path does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
String rendererConfigPath = Emulator.getConfig().getValue("furni.editor.renderer.config.path", "");
|
||||||
|
String assetBasePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||||
|
|
||||||
|
if (!rendererConfigPath.isEmpty()) {
|
||||||
|
Source fromRenderer = resolveFromRendererConfig(Paths.get(rendererConfigPath), assetBasePath.isEmpty() ? null : Paths.get(assetBasePath));
|
||||||
|
if (fromRenderer.ok() || fromRenderer.status() == Status.UNRESOLVED_PLACEHOLDER) return fromRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
Source fallback = resolveFromAssetBase(assetBasePath);
|
||||||
|
if (fallback != null) return fallback;
|
||||||
|
|
||||||
|
return new Source(null, false, Status.CONFIG_MISSING, "No furnidata source config found");
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnidataSourceResolver failed", e);
|
||||||
|
return new Source(null, false, Status.ERROR, e.getMessage() != null ? e.getMessage() : "Resolver error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Source resolveFromRendererConfig(Path rendererConfig, Path assetBase) {
|
||||||
|
try {
|
||||||
|
if (rendererConfig == null || !Files.exists(rendererConfig)) {
|
||||||
|
return new Source(rendererConfig, false, Status.SOURCE_MISSING, "renderer-config path does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
String raw = Files.readString(rendererConfig, StandardCharsets.UTF_8);
|
||||||
|
JsonObject rendererObj = JsonParser.parseString(FurnidataReader.stripJson5(raw)).getAsJsonObject();
|
||||||
|
String furniUrl = expandRendererUrl(rendererObj, "furnidata.url");
|
||||||
|
|
||||||
|
if (furniUrl.isBlank()) return new Source(null, false, Status.CONFIG_MISSING, "furnidata.url is missing");
|
||||||
|
if (hasUnresolvedPathPlaceholder(furniUrl)) return new Source(null, false, Status.UNRESOLVED_PLACEHOLDER, furniUrl);
|
||||||
|
|
||||||
|
Source source = toLocalSource(assetBase, furniUrl);
|
||||||
|
if (source == null) return new Source(null, false, Status.CONFIG_MISSING, "furni.editor.asset.base.path is missing");
|
||||||
|
if (!Files.exists(source.path())) return new Source(source.path(), source.directory(), Status.SOURCE_MISSING, "Resolved source does not exist");
|
||||||
|
|
||||||
|
return source;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new Source(null, false, Status.ERROR, e.getMessage() != null ? e.getMessage() : "renderer-config parse failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Source resolveFromAssetBase(String assetBasePath) {
|
||||||
|
if (assetBasePath == null || assetBasePath.isEmpty()) return null;
|
||||||
|
|
||||||
|
Path dir = Paths.get(assetBasePath);
|
||||||
|
Path split = dir.resolve("furnidata");
|
||||||
|
if (Files.isDirectory(split)) return new Source(split, true, Status.RESOLVED, "asset base split furnidata");
|
||||||
|
|
||||||
|
Path legacy = dir.resolve("FurnitureData.json");
|
||||||
|
if (Files.exists(legacy)) return new Source(legacy, false, Status.RESOLVED, "asset base FurnitureData.json");
|
||||||
|
|
||||||
|
return new Source(dir, true, Status.SOURCE_MISSING, "No furnidata or FurnitureData.json under asset base");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String expandRendererUrl(JsonObject rendererObj, String key) {
|
||||||
|
if (rendererObj == null || !rendererObj.has(key)) return "";
|
||||||
|
|
||||||
|
String value = rendererObj.get(key).getAsString();
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
int start = value.indexOf("${");
|
||||||
|
if (start < 0) break;
|
||||||
|
|
||||||
|
int end = value.indexOf('}', start + 2);
|
||||||
|
if (end < 0) break;
|
||||||
|
|
||||||
|
String placeholder = value.substring(start + 2, end);
|
||||||
|
if (!rendererObj.has(placeholder)) break;
|
||||||
|
|
||||||
|
value = value.substring(0, start) + rendererObj.get(placeholder).getAsString() + value.substring(end + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Source toLocalSource(Path assetBase, String furniUrl) {
|
||||||
|
if (furniUrl == null || furniUrl.isBlank()) return null;
|
||||||
|
|
||||||
|
String cleanUrl = stripQueryAndFragment(furniUrl);
|
||||||
|
boolean splitMode = cleanUrl.endsWith("/");
|
||||||
|
|
||||||
|
if (!cleanUrl.startsWith("http")) {
|
||||||
|
Path local = Paths.get(cleanUrl);
|
||||||
|
return new Source(local, splitMode || Files.isDirectory(local), Status.RESOLVED, "local furnidata.url");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assetBase == null) return null;
|
||||||
|
|
||||||
|
String urlPath;
|
||||||
|
try {
|
||||||
|
urlPath = URI.create(cleanUrl).getPath();
|
||||||
|
} catch (Exception e) {
|
||||||
|
int scheme = cleanUrl.indexOf("://");
|
||||||
|
int pathStart = scheme >= 0 ? cleanUrl.indexOf('/', scheme + 3) : -1;
|
||||||
|
urlPath = pathStart >= 0 ? cleanUrl.substring(pathStart) : cleanUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = urlPath.replace('\\', '/');
|
||||||
|
String baseName = assetBase.getFileName() != null ? assetBase.getFileName().toString() : "";
|
||||||
|
String marker = "/" + baseName + "/";
|
||||||
|
int markerIndex = baseName.isEmpty() ? -1 : normalized.indexOf(marker);
|
||||||
|
|
||||||
|
Path candidate;
|
||||||
|
if (markerIndex >= 0) {
|
||||||
|
candidate = assetBase.resolve(normalized.substring(markerIndex + marker.length()));
|
||||||
|
} else if (splitMode) {
|
||||||
|
String trimmed = normalized.endsWith("/") ? normalized.substring(0, normalized.length() - 1) : normalized;
|
||||||
|
candidate = assetBase.resolve(trimmed.substring(trimmed.lastIndexOf('/') + 1));
|
||||||
|
} else {
|
||||||
|
candidate = assetBase.resolve(normalized.substring(normalized.lastIndexOf('/') + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Source(candidate, splitMode || Files.isDirectory(candidate), Status.RESOLVED, "renderer-config furnidata.url");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasUnresolvedPathPlaceholder(String value) {
|
||||||
|
if (value == null) return false;
|
||||||
|
return stripQueryAndFragment(value).contains("${");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stripQueryAndFragment(String value) {
|
||||||
|
String out = value;
|
||||||
|
int q = out.indexOf('?');
|
||||||
|
if (q >= 0) out = out.substring(0, q);
|
||||||
|
int h = out.indexOf('#');
|
||||||
|
if (h >= 0) out = out.substring(0, h);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
|
import com.eu.habbo.habbohotel.users.Habbo;
|
||||||
|
import com.eu.habbo.messages.outgoing.furniture.FurnitureDataReloadComposer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.ClosedWatchServiceException;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardWatchEventKinds;
|
||||||
|
import java.nio.file.WatchKey;
|
||||||
|
import java.nio.file.WatchService;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watches the furnidata source on a single daemon thread. On change (debounced),
|
||||||
|
* re-indexes via the provider and broadcasts only the delta — or a compact
|
||||||
|
* reload-hint when the delta exceeds the cap. A minimum interval throttles bursts.
|
||||||
|
* For the split-tier directory layout, the base dir AND its immediate
|
||||||
|
* subdirectories are registered. Never throws out of the loop.
|
||||||
|
*/
|
||||||
|
public class FurnidataWatcher {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(FurnidataWatcher.class);
|
||||||
|
|
||||||
|
private final FurnitureTextProvider provider;
|
||||||
|
private final Path watchDir;
|
||||||
|
private final boolean sourceIsDir;
|
||||||
|
private final long maxBytes;
|
||||||
|
private final long debounceMs;
|
||||||
|
private final long minIntervalMs;
|
||||||
|
private final int deltaCap;
|
||||||
|
|
||||||
|
private volatile boolean running = false;
|
||||||
|
private volatile WatchService ws;
|
||||||
|
private long lastBroadcast = 0L;
|
||||||
|
|
||||||
|
public FurnidataWatcher(FurnitureTextProvider provider, Path source, long maxBytes) {
|
||||||
|
this.provider = provider;
|
||||||
|
this.sourceIsDir = Files.isDirectory(source);
|
||||||
|
this.watchDir = this.sourceIsDir ? source : source.getParent();
|
||||||
|
this.maxBytes = maxBytes;
|
||||||
|
this.debounceMs = Long.parseLong(Emulator.getConfig().getValue("items.furnidata.watch.debounce.ms", "750"));
|
||||||
|
this.minIntervalMs = Long.parseLong(Emulator.getConfig().getValue("items.furnidata.watch.min.interval.ms", "5000"));
|
||||||
|
this.deltaCap = Integer.parseInt(Emulator.getConfig().getValue("items.furnidata.delta.cap", "500"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
if (this.running || this.watchDir == null) return;
|
||||||
|
this.running = true;
|
||||||
|
Thread t = new Thread(this::run, "FurnidataWatcher");
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
this.running = false;
|
||||||
|
WatchService local = this.ws;
|
||||||
|
if (local != null) {
|
||||||
|
try { local.close(); } catch (IOException ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void run() {
|
||||||
|
try {
|
||||||
|
this.ws = FileSystems.getDefault().newWatchService();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.warn("FurnidataWatcher: could not create WatchService", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (WatchService service = this.ws) {
|
||||||
|
registerDirs(service);
|
||||||
|
while (this.running) {
|
||||||
|
WatchKey key = service.take();
|
||||||
|
key.pollEvents();
|
||||||
|
Thread.sleep(this.debounceMs);
|
||||||
|
key.pollEvents();
|
||||||
|
if (!key.reset()) {
|
||||||
|
LOGGER.warn("FurnidataWatcher: watch key invalidated (directory removed?) — stopping");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
onChange();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnidataWatcher: onChange failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} catch (ClosedWatchServiceException ignored) {
|
||||||
|
// stop() closed the service — normal shutdown
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnidataWatcher stopped", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Register the base dir, plus one level of subdirectories for the split-tier layout. */
|
||||||
|
private void registerDirs(WatchService service) throws IOException {
|
||||||
|
this.watchDir.register(service, StandardWatchEventKinds.ENTRY_MODIFY,
|
||||||
|
StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
|
||||||
|
if (this.sourceIsDir) {
|
||||||
|
try (DirectoryStream<Path> ds = Files.newDirectoryStream(this.watchDir)) {
|
||||||
|
for (Path child : ds) {
|
||||||
|
if (Files.isDirectory(child)) {
|
||||||
|
child.register(service, StandardWatchEventKinds.ENTRY_MODIFY,
|
||||||
|
StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onChange() throws InterruptedException {
|
||||||
|
// Re-index under the shared furnidata lock so the watcher and editor
|
||||||
|
// writes never swap the index concurrently. The lock is released before
|
||||||
|
// the throttle/broadcast below so a slow broadcast can't stall editor saves.
|
||||||
|
List<FurnidataEntry> delta;
|
||||||
|
FurnidataLock.LOCK.lock();
|
||||||
|
try {
|
||||||
|
Path source = this.provider.getSource();
|
||||||
|
if (source == null) return;
|
||||||
|
delta = this.provider.reindex(new FurnidataReader(source, this.maxBytes).read());
|
||||||
|
} finally {
|
||||||
|
FurnidataLock.LOCK.unlock();
|
||||||
|
}
|
||||||
|
if (delta.isEmpty()) return;
|
||||||
|
|
||||||
|
// Min-interval throttle: the index has already been swapped, so we must
|
||||||
|
// not drop this delta (the next reindex would diff against the updated
|
||||||
|
// index and never re-emit it). Instead, defer the broadcast until the
|
||||||
|
// interval elapses. Running on a dedicated daemon thread, sleeping is
|
||||||
|
// safe; file events arriving meanwhile coalesce into the next cycle.
|
||||||
|
long sinceLast = System.currentTimeMillis() - this.lastBroadcast;
|
||||||
|
if (sinceLast < this.minIntervalMs) {
|
||||||
|
Thread.sleep(this.minIntervalMs - sinceLast);
|
||||||
|
}
|
||||||
|
this.lastBroadcast = System.currentTimeMillis();
|
||||||
|
|
||||||
|
FurnitureDataReloadComposer composer = (delta.size() > this.deltaCap)
|
||||||
|
? new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_RELOAD_HINT, List.of())
|
||||||
|
: new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_DELTA, delta);
|
||||||
|
|
||||||
|
broadcast(composer);
|
||||||
|
LOGGER.info("FurnidataWatcher: broadcast {} ({} entries)",
|
||||||
|
delta.size() > this.deltaCap ? "reload-hint" : "delta", delta.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcast(FurnitureDataReloadComposer composer) {
|
||||||
|
for (Habbo habbo : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().values()) {
|
||||||
|
if (habbo.getClient() != null) {
|
||||||
|
habbo.getClient().sendResponse(composer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comment-preserving, atomic, backed-up writer for furnidata name/description, keyed by
|
||||||
|
* classname. Supports single-file and split-tier (writes the tier that currently resolves
|
||||||
|
* the classname). Edit-only: refuses classnames absent from the furnidata.
|
||||||
|
*/
|
||||||
|
public class FurnidataWriter {
|
||||||
|
|
||||||
|
/** Default tier names in override order (later = higher priority, wins on conflict). */
|
||||||
|
private static final List<String> DEFAULT_TIERS = Arrays.asList("core", "custom", "seasonal");
|
||||||
|
|
||||||
|
/** Manifest filenames tried in order (json5 first, plain json second). */
|
||||||
|
private static final List<String> MANIFEST_NAMES = Arrays.asList("manifest.json5", "manifest.json");
|
||||||
|
|
||||||
|
private final Path source; // file (single) or base dir (split-tier)
|
||||||
|
private final boolean directory; // true => split-tier
|
||||||
|
private final long maxBytes;
|
||||||
|
private final int backupKeep;
|
||||||
|
|
||||||
|
public FurnidataWriter(Path source, boolean directory, long maxBytes, int backupKeep) {
|
||||||
|
this.source = source;
|
||||||
|
this.directory = directory;
|
||||||
|
this.maxBytes = maxBytes;
|
||||||
|
this.backupKeep = Math.max(1, backupKeep);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return true if an entry for classname was found and written. */
|
||||||
|
public boolean write(String classname, String name, String description) throws IOException {
|
||||||
|
String cn = classname == null ? "" : classname.trim().toLowerCase(java.util.Locale.ROOT);
|
||||||
|
if (cn.isEmpty()) return false;
|
||||||
|
String safeName = FurnitureTextProvider.sanitize(name);
|
||||||
|
String safeDesc = FurnitureTextProvider.sanitize(description);
|
||||||
|
|
||||||
|
Path target = locateFile(cn);
|
||||||
|
if (target == null) return false;
|
||||||
|
|
||||||
|
String raw = Files.readString(target, StandardCharsets.UTF_8);
|
||||||
|
String edited = replaceEntryFields(raw, cn, safeName, safeDesc);
|
||||||
|
if (edited == null || edited.equals(raw)) {
|
||||||
|
// classname not present in this file, or no change
|
||||||
|
return edited != null && !edited.equals(raw);
|
||||||
|
}
|
||||||
|
backup(target);
|
||||||
|
atomicWrite(target, edited);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** For single-file just returns the file; for split-tier, the tier file that contains cn. */
|
||||||
|
private Path locateFile(String cn) throws IOException {
|
||||||
|
if (!directory) {
|
||||||
|
// confirm existence via the reader (size-guarded, parses the same way)
|
||||||
|
return containsClassname(source, cn) ? source : null;
|
||||||
|
}
|
||||||
|
// split-tier: iterate tiers in OVERRIDE order (later tiers win); pick the last containing cn
|
||||||
|
Path winner = null;
|
||||||
|
for (Path tierFile : splitTierFilesInOrder()) {
|
||||||
|
if (containsClassname(tierFile, cn)) winner = tierFile;
|
||||||
|
}
|
||||||
|
return winner;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsClassname(Path file, String cn) {
|
||||||
|
for (FurnidataEntry e : new FurnidataReader(file, maxBytes).read()) {
|
||||||
|
if (e.classname() != null && e.classname().trim().toLowerCase(java.util.Locale.ROOT).equals(cn)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the "name" and "description" string values inside the JSON object that holds
|
||||||
|
* "classname": "<cn>". Preserves everything else (comments, ordering, formatting).
|
||||||
|
* Handles double- and single-quoted JSON5 keys/values. Returns null if cn not found.
|
||||||
|
*/
|
||||||
|
static String replaceEntryFields(String raw, String cn, String name, String description) {
|
||||||
|
// find the classname value occurrence (case-insensitive on the value)
|
||||||
|
Pattern classProp = Pattern.compile(
|
||||||
|
"([\"'])classname\\1\\s*:\\s*([\"'])((?:\\\\.|(?!\\2).)*)\\2", Pattern.CASE_INSENSITIVE);
|
||||||
|
Matcher m = classProp.matcher(raw);
|
||||||
|
int objStart = -1, objEnd = -1;
|
||||||
|
while (m.find()) {
|
||||||
|
String val = m.group(3).trim().toLowerCase(java.util.Locale.ROOT);
|
||||||
|
if (!val.equals(cn)) continue;
|
||||||
|
// expand to the enclosing { ... }
|
||||||
|
objStart = lastUnbalancedBrace(raw, m.start());
|
||||||
|
objEnd = matchingClose(raw, objStart);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (objStart < 0 || objEnd < 0) return null;
|
||||||
|
String obj = raw.substring(objStart, objEnd + 1);
|
||||||
|
String newObj = replaceField(obj, "name", name);
|
||||||
|
newObj = replaceField(newObj, "description", description);
|
||||||
|
return raw.substring(0, objStart) + newObj + raw.substring(objEnd + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String replaceField(String obj, String field, String value) {
|
||||||
|
Pattern p = Pattern.compile(
|
||||||
|
"(([\"'])" + Pattern.quote(field) + "\\2\\s*:\\s*)([\"'])((?:\\\\.|(?!\\3).)*)\\3");
|
||||||
|
Matcher m = p.matcher(obj);
|
||||||
|
if (!m.find()) return obj; // field absent → leave object as-is
|
||||||
|
String replacement = m.group(1) + '"' + jsonEscape(value) + '"';
|
||||||
|
return obj.substring(0, m.start()) + replacement + obj.substring(m.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int lastUnbalancedBrace(String s, int from) {
|
||||||
|
int depth = 0;
|
||||||
|
for (int i = from; i >= 0; i--) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if (c == '}') depth++;
|
||||||
|
else if (c == '{') { if (depth == 0) return i; depth--; }
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int matchingClose(String s, int open) {
|
||||||
|
int depth = 0; boolean inStr = false; char q = 0;
|
||||||
|
for (int i = open; i < s.length(); i++) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if (inStr) { if (c == '\\') { i++; } else if (c == q) inStr = false; continue; }
|
||||||
|
if (c == '"' || c == '\'') { inStr = true; q = c; }
|
||||||
|
else if (c == '{') depth++;
|
||||||
|
else if (c == '}') { depth--; if (depth == 0) return i; }
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String jsonEscape(String v) {
|
||||||
|
StringBuilder b = new StringBuilder(v.length() + 8);
|
||||||
|
for (int i = 0; i < v.length(); i++) {
|
||||||
|
char c = v.charAt(i);
|
||||||
|
if (c == '"' || c == '\\') b.append('\\').append(c);
|
||||||
|
else b.append(c);
|
||||||
|
}
|
||||||
|
return b.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerate every data file reachable from the split-tier base directory, in
|
||||||
|
* override order (core → custom → seasonal, or the order declared in the top-level
|
||||||
|
* {@code manifest.json(5)}). Within each tier the per-tier manifest's {@code files}
|
||||||
|
* array determines the file order.
|
||||||
|
*
|
||||||
|
* <p>All resolved paths are checked against the normalised base directory via
|
||||||
|
* {@link #safeResolve}: any entry that would escape the base is silently skipped.
|
||||||
|
*
|
||||||
|
* @return ordered list of existing, in-bounds data files (earliest tier first).
|
||||||
|
*/
|
||||||
|
private List<Path> splitTierFilesInOrder() throws IOException {
|
||||||
|
Path base = source.toAbsolutePath().normalize();
|
||||||
|
List<String> tiers = manifestList(base, "tiers", DEFAULT_TIERS);
|
||||||
|
List<Path> result = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String tier : tiers) {
|
||||||
|
Path tierDir = safeResolve(base, tier);
|
||||||
|
if (tierDir == null || !Files.isDirectory(tierDir)) continue;
|
||||||
|
|
||||||
|
for (String fileName : manifestList(tierDir, "files", List.of())) {
|
||||||
|
Path file = safeResolve(base, tierDir.resolve(fileName).toString());
|
||||||
|
if (file == null || !Files.isRegularFile(file)) continue;
|
||||||
|
result.add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve {@code entry} relative to {@code base} and verify the result stays
|
||||||
|
* inside {@code base} (path-traversal guard).
|
||||||
|
*
|
||||||
|
* @param base the normalised absolute base directory.
|
||||||
|
* @param entry a path string (may be relative or absolute, may contain {@code ..}).
|
||||||
|
* @return the normalised absolute path if it is inside {@code base}; {@code null} otherwise.
|
||||||
|
*/
|
||||||
|
private static Path safeResolve(Path base, String entry) {
|
||||||
|
try {
|
||||||
|
Path resolved = base.resolve(entry).toAbsolutePath().normalize();
|
||||||
|
return resolved.startsWith(base) ? resolved : null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the {@code key} string-array from the first manifest file found in {@code dir}
|
||||||
|
* ({@code manifest.json5} then {@code manifest.json}). Falls back to {@code fallback}
|
||||||
|
* if no manifest exists or the key is absent/empty.
|
||||||
|
*/
|
||||||
|
private List<String> manifestList(Path dir, String key, List<String> fallback) {
|
||||||
|
for (String name : MANIFEST_NAMES) {
|
||||||
|
Path m = dir.resolve(name);
|
||||||
|
if (!Files.exists(m)) continue;
|
||||||
|
try {
|
||||||
|
String stripped = FurnidataReader.stripJson5(
|
||||||
|
Files.readString(m, StandardCharsets.UTF_8));
|
||||||
|
com.google.gson.JsonObject obj =
|
||||||
|
com.google.gson.JsonParser.parseString(stripped).getAsJsonObject();
|
||||||
|
if (obj.has(key) && obj.get(key).isJsonArray()) {
|
||||||
|
List<String> list = new ArrayList<>();
|
||||||
|
for (com.google.gson.JsonElement el : obj.getAsJsonArray(key))
|
||||||
|
list.add(el.getAsString());
|
||||||
|
if (!list.isEmpty()) return list;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// bad manifest → fall through to next candidate / fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void backup(Path target) throws IOException {
|
||||||
|
Path bak = target.resolveSibling(target.getFileName() + ".bak." + System.nanoTime());
|
||||||
|
Files.copy(target, bak, StandardCopyOption.COPY_ATTRIBUTES);
|
||||||
|
pruneBackups(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pruneBackups(Path target) throws IOException {
|
||||||
|
String prefix = target.getFileName() + ".bak.";
|
||||||
|
try (var stream = Files.list(target.getParent())) {
|
||||||
|
List<Path> baks = stream.filter(p -> p.getFileName().toString().startsWith(prefix))
|
||||||
|
.sorted(Comparator.comparingLong(p -> backupStamp(p))).toList();
|
||||||
|
for (int i = 0; i < baks.size() - backupKeep; i++) Files.deleteIfExists(baks.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long backupStamp(Path p) {
|
||||||
|
String s = p.getFileName().toString();
|
||||||
|
try { return Long.parseLong(s.substring(s.lastIndexOf('.') + 1)); } catch (Exception e) { return 0L; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void atomicWrite(Path target, String content) throws IOException {
|
||||||
|
Path tmp = target.resolveSibling(target.getFileName() + ".tmp." + System.nanoTime());
|
||||||
|
Files.writeString(tmp, content, StandardCharsets.UTF_8);
|
||||||
|
try {
|
||||||
|
Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
} catch (AtomicMoveNotSupportedException e) {
|
||||||
|
Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Restore the most recent backup of the (single-file) target. @return true if restored. */
|
||||||
|
public boolean revertLastBackup() throws IOException {
|
||||||
|
if (directory) return revertSplitTier();
|
||||||
|
return revertFile(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean revertFile(Path target) throws IOException {
|
||||||
|
String prefix = target.getFileName() + ".bak.";
|
||||||
|
try (var stream = Files.list(target.getParent())) {
|
||||||
|
Path latest = stream.filter(p -> p.getFileName().toString().startsWith(prefix))
|
||||||
|
.max(Comparator.comparingLong(FurnidataWriter::backupStamp)).orElse(null);
|
||||||
|
if (latest == null) return false;
|
||||||
|
atomicWrite(target, Files.readString(latest, StandardCharsets.UTF_8));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean revertSplitTier() throws IOException {
|
||||||
|
boolean any = false;
|
||||||
|
for (Path f : splitTierFilesInOrder()) any |= revertFile(f);
|
||||||
|
return any;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package com.eu.habbo.habbohotel.items;
|
||||||
|
|
||||||
|
import com.eu.habbo.Emulator;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory index of furnidata display names, keyed by the lowercased base
|
||||||
|
* classname (the {@code *N} colour-variant suffix is stripped). Read lazily by
|
||||||
|
* {@link Item#getDisplayName()}. Names are sanitized at index time.
|
||||||
|
*
|
||||||
|
* Thread-safety: the index is held behind a {@code volatile} reference; readers
|
||||||
|
* never block; {@link #reindex(List)} builds a fresh map and swaps it atomically.
|
||||||
|
*/
|
||||||
|
public class FurnitureTextProvider {
|
||||||
|
|
||||||
|
private static final int MAX_LEN = 256;
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(FurnitureTextProvider.class);
|
||||||
|
private static final long DEFAULT_MAX_BYTES = 64L * 1024 * 1024;
|
||||||
|
|
||||||
|
private final boolean enabled;
|
||||||
|
private volatile Map<String, FurniText> index = Map.of();
|
||||||
|
private volatile Path source;
|
||||||
|
private FurnidataWatcher watcher;
|
||||||
|
|
||||||
|
public FurnitureTextProvider(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Production constructor: reads the enable toggle from config. */
|
||||||
|
public FurnitureTextProvider() {
|
||||||
|
this(Boolean.parseBoolean(Emulator.getConfig().getValue("items.furnidata.names.enabled", "true")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve the furnidata source from config and build the initial index. Never throws. */
|
||||||
|
public void init() {
|
||||||
|
try {
|
||||||
|
this.source = resolveSource();
|
||||||
|
if (this.source == null) {
|
||||||
|
LOGGER.warn("FurnitureTextProvider: no furnidata source resolved - names fall back to public_name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reindex(new FurnidataReader(this.source, DEFAULT_MAX_BYTES).read());
|
||||||
|
LOGGER.info("FurnitureTextProvider: indexed {} furnidata names from {}", this.index.size(), this.source);
|
||||||
|
|
||||||
|
if (Boolean.parseBoolean(Emulator.getConfig().getValue("items.furnidata.watch.enabled", "true"))) {
|
||||||
|
if (this.watcher != null) this.watcher.stop();
|
||||||
|
this.watcher = new FurnidataWatcher(this, this.source, DEFAULT_MAX_BYTES);
|
||||||
|
this.watcher.start();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnitureTextProvider.init failed — names fall back to public_name", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getSource() {
|
||||||
|
return this.source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns {@code true} when the resolved source is a directory (split-tier layout). */
|
||||||
|
public boolean isSourceDirectory() {
|
||||||
|
return this.source != null && Files.isDirectory(this.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the byte cap used when reading furnidata files. */
|
||||||
|
public long getMaxBytes() {
|
||||||
|
return Long.parseLong(com.eu.habbo.Emulator.getConfig().getValue("items.furnidata.max.bytes", String.valueOf(DEFAULT_MAX_BYTES)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-reads the furnidata from the current source and reindexes atomically.
|
||||||
|
* Returns the delta list (new/changed entries) from {@link #reindex(List)}.
|
||||||
|
* Never throws — returns an empty list when the source is unavailable.
|
||||||
|
*/
|
||||||
|
public java.util.List<FurnidataEntry> reindexFromSource() {
|
||||||
|
try {
|
||||||
|
if (this.source == null) return java.util.List.of();
|
||||||
|
return reindex(new FurnidataReader(this.source, DEFAULT_MAX_BYTES).read());
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("FurnitureTextProvider.reindexFromSource failed", e);
|
||||||
|
return java.util.List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path resolveSource() {
|
||||||
|
FurnidataSourceResolver.Source source = FurnidataSourceResolver.resolve();
|
||||||
|
if (source.ok()) return source.path();
|
||||||
|
LOGGER.warn("FurnitureTextProvider: no furnidata source resolved ({}) - {}", source.status(), source.message());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a fresh sanitized index, swap it in atomically, and return the
|
||||||
|
* changed/added entries (sanitized) as the delta versus the previous index.
|
||||||
|
*/
|
||||||
|
public java.util.List<FurnidataEntry> reindex(java.util.List<FurnidataEntry> entries) {
|
||||||
|
Map<String, FurniText> next = new HashMap<>(Math.max(16, entries.size() * 2));
|
||||||
|
for (FurnidataEntry e : entries) {
|
||||||
|
String key = baseKey(e.classname());
|
||||||
|
if (key == null) continue;
|
||||||
|
next.put(key, new FurniText(e.id(), e.type(), sanitize(e.name()), sanitize(e.description())));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, FurniText> prev = this.index;
|
||||||
|
java.util.List<FurnidataEntry> delta = new java.util.ArrayList<>();
|
||||||
|
for (Map.Entry<String, FurniText> en : next.entrySet()) {
|
||||||
|
FurniText cur = en.getValue();
|
||||||
|
FurniText old = prev.get(en.getKey());
|
||||||
|
if (old == null || !old.name().equals(cur.name()) || !old.description().equals(cur.description())) {
|
||||||
|
delta.add(new FurnidataEntry(cur.id(), en.getKey(), cur.type(), cur.name(), cur.description()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.index = next; // atomic reference swap
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the sanitized display name for a DB classname, or null if absent/disabled. */
|
||||||
|
public String getName(String classname) {
|
||||||
|
if (!this.enabled) return null;
|
||||||
|
String key = baseKey(classname);
|
||||||
|
if (key == null) return null;
|
||||||
|
FurniText t = this.index.get(key);
|
||||||
|
return (t != null) ? t.name() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String baseKey(String classname) {
|
||||||
|
if (classname == null) return null;
|
||||||
|
int star = classname.indexOf('*');
|
||||||
|
String base = (star >= 0) ? classname.substring(0, star) : classname;
|
||||||
|
base = base.trim().toLowerCase(Locale.ROOT);
|
||||||
|
return base.isEmpty() ? null : base;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cap length, strip control chars/newlines, neutralize % (placeholder-injection safe).
|
||||||
|
* The 256 cap is in Java {@code char} units (UTF-16 code units), which is acceptable for
|
||||||
|
* furni names (controlled, predominantly ASCII source). Lone/astral surrogates are not
|
||||||
|
* specially handled.
|
||||||
|
*/
|
||||||
|
public static String sanitize(String value) {
|
||||||
|
if (value == null) return "";
|
||||||
|
StringBuilder sb = new StringBuilder(Math.min(value.length(), MAX_LEN));
|
||||||
|
for (int i = 0; i < value.length() && sb.length() < MAX_LEN; i++) {
|
||||||
|
char c = value.charAt(i);
|
||||||
|
if (c == '%') { sb.append('%'); continue; } // fullwidth percent — not a placeholder token
|
||||||
|
if (c == '\n' || c == '\r' || Character.isISOControl(c)) continue;
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all lowercased base classnames whose furnidata display name contains
|
||||||
|
* {@code query} (case-insensitive, substring). Results are capped at 200 to
|
||||||
|
* bound SQL IN-clause size. Returns an empty list when query is null/blank.
|
||||||
|
*/
|
||||||
|
public java.util.List<String> findClassnamesByName(String query) {
|
||||||
|
java.util.List<String> out = new java.util.ArrayList<>();
|
||||||
|
if (query == null) return out;
|
||||||
|
String q = query.trim().toLowerCase(Locale.ROOT);
|
||||||
|
if (q.isEmpty()) return out;
|
||||||
|
Map<String, FurniText> idx = this.index; // local ref (volatile)
|
||||||
|
for (Map.Entry<String, FurniText> e : idx.entrySet()) {
|
||||||
|
FurniText t = e.getValue();
|
||||||
|
if (t != null && t.name() != null && t.name().toLowerCase(Locale.ROOT).contains(q)) {
|
||||||
|
out.add(e.getKey()); // key is the lowercased base classname
|
||||||
|
if (out.size() >= 200) break; // bound IN-clause size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record FurniText(int id, FurnitureType type, String name, String description) {}
|
||||||
|
}
|
||||||
@@ -48,6 +48,12 @@ public class Item implements ISerialize {
|
|||||||
return item.getName().toLowerCase().startsWith("a0 pet");
|
return item.getName().toLowerCase().startsWith("a0 pet");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isBot(Item item) {
|
||||||
|
if (item == null) return false;
|
||||||
|
String name = item.getName();
|
||||||
|
return name != null && (name.startsWith("bot_") || name.startsWith("rentable_bot_"));
|
||||||
|
}
|
||||||
|
|
||||||
public static double getCurrentHeight(HabboItem item) {
|
public static double getCurrentHeight(HabboItem item) {
|
||||||
if (item instanceof InteractionMultiHeight && item.getBaseItem().getMultiHeights().length > 0) {
|
if (item instanceof InteractionMultiHeight && item.getBaseItem().getMultiHeights().length > 0) {
|
||||||
if (item.getExtradata().isEmpty()) {
|
if (item.getExtradata().isEmpty()) {
|
||||||
@@ -117,7 +123,7 @@ public class Item implements ISerialize {
|
|||||||
|
|
||||||
if (!set.getString("vending_ids").isEmpty()) {
|
if (!set.getString("vending_ids").isEmpty()) {
|
||||||
this.vendingItems = new TIntArrayList();
|
this.vendingItems = new TIntArrayList();
|
||||||
String[] vendingIds = set.getString("vending_ids").replace(";", ",").split(",");
|
String[] vendingIds = set.getString("vending_ids").replace(";", ",").replace(".", ",").split(",");
|
||||||
for (String s : vendingIds) {
|
for (String s : vendingIds) {
|
||||||
this.vendingItems.add(Integer.parseInt(s.replace(" ", "")));
|
this.vendingItems.add(Integer.parseInt(s.replace(" ", "")));
|
||||||
}
|
}
|
||||||
@@ -161,6 +167,20 @@ public class Item implements ISerialize {
|
|||||||
return this.fullName;
|
return this.fullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display name for user-facing/log output, sourced from furnidata (by classname).
|
||||||
|
* Falls back to the DB public_name when furnidata has no entry or names are disabled.
|
||||||
|
* Never returns null.
|
||||||
|
*/
|
||||||
|
public String getDisplayName() {
|
||||||
|
FurnitureTextProvider provider = (Emulator.getGameEnvironment() != null)
|
||||||
|
? Emulator.getGameEnvironment().getFurnitureTextProvider()
|
||||||
|
: null;
|
||||||
|
String name = (provider != null) ? provider.getName(this.name) : null;
|
||||||
|
if (name != null && !name.isBlank()) return name;
|
||||||
|
return (this.fullName != null) ? this.fullName : "";
|
||||||
|
}
|
||||||
|
|
||||||
public FurnitureType getType() {
|
public FurnitureType getType() {
|
||||||
return this.type;
|
return this.type;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -13,11 +13,13 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class InteractionGift extends HabboItem {
|
public class InteractionGift extends HabboItem {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(InteractionGift.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(InteractionGift.class);
|
||||||
|
|
||||||
public boolean explode = false;
|
public boolean explode = false;
|
||||||
|
private final AtomicBoolean opening = new AtomicBoolean(false);
|
||||||
private int[] itemId;
|
private int[] itemId;
|
||||||
private int colorId = 0;
|
private int colorId = 0;
|
||||||
private int ribbonId = 0;
|
private int ribbonId = 0;
|
||||||
@@ -46,6 +48,15 @@ public class InteractionGift extends HabboItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claims the right to open this gift, returning true exactly once. Guards
|
||||||
|
* against two near-simultaneous OpenRecycleBox packets both scheduling an
|
||||||
|
* (async, delayed) OpenGift before the wrapper is removed from the room.
|
||||||
|
*/
|
||||||
|
public boolean tryStartOpening() {
|
||||||
|
return this.opening.compareAndSet(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serializeExtradata(ServerMessage serverMessage) {
|
public void serializeExtradata(ServerMessage serverMessage) {
|
||||||
//serverMessage.appendInt(this.colorId * 1000 + this.ribbonId);
|
//serverMessage.appendInt(this.colorId * 1000 + this.ribbonId);
|
||||||
|
|||||||
+2
-3
@@ -65,9 +65,8 @@ public class InteractionMultiHeight extends HabboItem {
|
|||||||
if (this.getBaseItem().getMultiHeights().length > 0) {
|
if (this.getBaseItem().getMultiHeights().length > 0) {
|
||||||
this.setExtradata("" + (Integer.parseInt(this.getExtradata()) + 1) % (this.getBaseItem().getMultiHeights().length));
|
this.setExtradata("" + (Integer.parseInt(this.getExtradata()) + 1) % (this.getBaseItem().getMultiHeights().length));
|
||||||
this.needsUpdate(true);
|
this.needsUpdate(true);
|
||||||
room.updateTiles(room.getLayout().getTilesAt(room.getLayout().getTile(this.getX(), this.getY()), this.getBaseItem().getWidth(), this.getBaseItem().getLength(), this.getRotation()));
|
room.updateItem(this);
|
||||||
room.updateItemState(this);
|
this.updateUnitsOnItem(room);
|
||||||
//room.sendComposer(new UpdateStackHeightComposer(this.getX(), this.getY(), this.getBaseItem().getMultiHeights()[Integer.valueOf(this.getExtradata())] * 256.0D).compose());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -89,7 +89,9 @@ public class InteractionOneWayGate extends HabboItem {
|
|||||||
Emulator.getThreading().run(new RoomUnitWalkToLocation(unit, tile, room, onFail, onFail));
|
Emulator.getThreading().run(new RoomUnitWalkToLocation(unit, tile, room, onFail, onFail));
|
||||||
|
|
||||||
Emulator.getThreading().run(() -> {
|
Emulator.getThreading().run(() -> {
|
||||||
WiredManager.triggerUserWalksOn(room, unit, this);
|
if (room.isLoaded()) {
|
||||||
|
WiredManager.triggerUserWalksOn(room, unit, this);
|
||||||
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+4
@@ -29,6 +29,10 @@ public class InteractionRoomAds extends InteractionCustomValues {
|
|||||||
{
|
{
|
||||||
this.put("offsetZ", "0");
|
this.put("offsetZ", "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
this.put("scale", "100");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public InteractionRoomAds(ResultSet set, Item baseItem) throws SQLException {
|
public InteractionRoomAds(ResultSet set, Item baseItem) throws SQLException {
|
||||||
|
|||||||
+38
-16
@@ -18,6 +18,7 @@ import java.sql.SQLException;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base abstract class for all wired furniture items (triggers, effects, conditions, extras).
|
* Base abstract class for all wired furniture items (triggers, effects, conditions, extras).
|
||||||
@@ -61,7 +62,11 @@ public abstract class InteractionWired extends InteractionDefault {
|
|||||||
*/
|
*/
|
||||||
private static final long CACHE_EXPIRY_MS = 5 * 60 * 1000;
|
private static final long CACHE_EXPIRY_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
private long cooldown;
|
private volatile long cooldown;
|
||||||
|
// Ensures one box is processed by a single thread at a time, so the
|
||||||
|
// cooldown check-and-set in WiredHandler can't double-fire when a packet
|
||||||
|
// thread and the room cycle thread trigger the same box concurrently.
|
||||||
|
private final AtomicBoolean processing = new AtomicBoolean(false);
|
||||||
private final ConcurrentHashMap<Long, Long> userExecutionCache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<Long, Long> userExecutionCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
InteractionWired(ResultSet set, Item baseItem) throws SQLException {
|
InteractionWired(ResultSet set, Item baseItem) throws SQLException {
|
||||||
@@ -93,23 +98,24 @@ public abstract class InteractionWired extends InteractionDefault {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (this.needsUpdate()) {
|
if (this.needsUpdate()) {
|
||||||
String wiredData = this.getWiredData();
|
String wiredDataRaw = this.getWiredData();
|
||||||
|
final String wiredData = (wiredDataRaw == null) ? "" : wiredDataRaw;
|
||||||
|
final int currentRoomId = this.getRoomId();
|
||||||
|
final int currentId = this.getId();
|
||||||
|
|
||||||
if (wiredData == null) {
|
Emulator.getThreading().run(() -> {
|
||||||
wiredData = "";
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE items SET wired_data = ? WHERE id = ?")) {
|
||||||
}
|
if (currentRoomId != 0) {
|
||||||
|
statement.setString(1, wiredData);
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE items SET wired_data = ? WHERE id = ?")) {
|
} else {
|
||||||
if (this.getRoomId() != 0) {
|
statement.setString(1, "");
|
||||||
statement.setString(1, wiredData);
|
}
|
||||||
} else {
|
statement.setInt(2, currentId);
|
||||||
statement.setString(1, "");
|
statement.execute();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOGGER.error("Caught SQL exception", e);
|
||||||
}
|
}
|
||||||
statement.setInt(2, this.getId());
|
});
|
||||||
statement.execute();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
LOGGER.error("Caught SQL exception", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
super.run();
|
super.run();
|
||||||
}
|
}
|
||||||
@@ -148,6 +154,15 @@ public abstract class InteractionWired extends InteractionDefault {
|
|||||||
this.cooldown = newMillis;
|
this.cooldown = newMillis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Claims exclusive processing of this box; returns false if another thread is already in it. */
|
||||||
|
public boolean tryBeginProcessing() {
|
||||||
|
return this.processing.compareAndSet(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void endProcessing() {
|
||||||
|
this.processing.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean allowWiredResetState() {
|
public boolean allowWiredResetState() {
|
||||||
return false;
|
return false;
|
||||||
@@ -216,6 +231,9 @@ public abstract class InteractionWired extends InteractionDefault {
|
|||||||
public static WiredSettings readSettings(ClientMessage packet, boolean isEffect)
|
public static WiredSettings readSettings(ClientMessage packet, boolean isEffect)
|
||||||
{
|
{
|
||||||
int intParamCount = packet.readInt();
|
int intParamCount = packet.readInt();
|
||||||
|
if (intParamCount < 0 || intParamCount > 100) {
|
||||||
|
throw new IllegalArgumentException("Invalid intParamCount: " + intParamCount);
|
||||||
|
}
|
||||||
int[] intParams = new int[intParamCount];
|
int[] intParams = new int[intParamCount];
|
||||||
|
|
||||||
for(int i = 0; i < intParamCount; i++)
|
for(int i = 0; i < intParamCount; i++)
|
||||||
@@ -226,6 +244,10 @@ public abstract class InteractionWired extends InteractionDefault {
|
|||||||
String stringParam = packet.readString();
|
String stringParam = packet.readString();
|
||||||
|
|
||||||
int itemCount = packet.readInt();
|
int itemCount = packet.readInt();
|
||||||
|
int selectionLimit = Emulator.getConfig() != null ? Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5) : 5;
|
||||||
|
if (itemCount < 0 || itemCount > selectionLimit * 20) {
|
||||||
|
throw new IllegalArgumentException("Invalid itemCount: " + itemCount + " exceeds maximum allowed limit");
|
||||||
|
}
|
||||||
int[] itemIds = new int[itemCount];
|
int[] itemIds = new int[itemCount];
|
||||||
|
|
||||||
for(int i = 0; i < itemCount; i++)
|
for(int i = 0; i < itemCount; i++)
|
||||||
|
|||||||
+20
-11
@@ -154,6 +154,7 @@ public class InteractionGameTimer extends HabboItem {
|
|||||||
@Override
|
@Override
|
||||||
public void onPickUp(Room room) {
|
public void onPickUp(Room room) {
|
||||||
this.endGame(room);
|
this.endGame(room);
|
||||||
|
this.threadActive = false;
|
||||||
|
|
||||||
this.timeNow = this.getInitialTimeValue();
|
this.timeNow = this.getInitialTimeValue();
|
||||||
this.setExtradata(this.timeNow + "\t" + this.baseTime);
|
this.setExtradata(this.timeNow + "\t" + this.baseTime);
|
||||||
@@ -220,8 +221,7 @@ public class InteractionGameTimer extends HabboItem {
|
|||||||
room.updateItem(this);
|
room.updateItem(this);
|
||||||
WiredManager.triggerGameStarts(room);
|
WiredManager.triggerGameStarts(room);
|
||||||
|
|
||||||
if (!this.threadActive) {
|
if (this.tryActivateTimerThread()) {
|
||||||
this.threadActive = true;
|
|
||||||
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
|
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
|
||||||
}
|
}
|
||||||
} else if (client != null) {
|
} else if (client != null) {
|
||||||
@@ -243,8 +243,7 @@ public class InteractionGameTimer extends HabboItem {
|
|||||||
} else {
|
} else {
|
||||||
this.unpause(room);
|
this.unpause(room);
|
||||||
|
|
||||||
if (!this.threadActive) {
|
if (this.tryActivateTimerThread()) {
|
||||||
this.threadActive = true;
|
|
||||||
this.scheduleTimerRunnable(this.getTimerResumeDelayMs());
|
this.scheduleTimerRunnable(this.getTimerResumeDelayMs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,8 +256,7 @@ public class InteractionGameTimer extends HabboItem {
|
|||||||
this.createNewGame(room);
|
this.createNewGame(room);
|
||||||
WiredManager.triggerGameStarts(room);
|
WiredManager.triggerGameStarts(room);
|
||||||
|
|
||||||
if (!this.threadActive) {
|
if (this.tryActivateTimerThread()) {
|
||||||
this.threadActive = true;
|
|
||||||
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
|
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,8 +295,7 @@ public class InteractionGameTimer extends HabboItem {
|
|||||||
}
|
}
|
||||||
this.createNewGame(room);
|
this.createNewGame(room);
|
||||||
WiredManager.triggerGameStarts(room);
|
WiredManager.triggerGameStarts(room);
|
||||||
if (!threadActive) {
|
if (this.tryActivateTimerThread()) {
|
||||||
threadActive = true;
|
|
||||||
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
|
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,8 +318,7 @@ public class InteractionGameTimer extends HabboItem {
|
|||||||
this.isPaused = false;
|
this.isPaused = false;
|
||||||
this.unpause(room);
|
this.unpause(room);
|
||||||
|
|
||||||
if (!this.threadActive) {
|
if (this.tryActivateTimerThread()) {
|
||||||
this.threadActive = true;
|
|
||||||
this.scheduleTimerRunnable(this.getTimerResumeDelayMs());
|
this.scheduleTimerRunnable(this.getTimerResumeDelayMs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,7 +402,9 @@ public class InteractionGameTimer extends HabboItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setThreadActive(boolean threadActive) {
|
public void setThreadActive(boolean threadActive) {
|
||||||
this.threadActive = threadActive;
|
synchronized (this) {
|
||||||
|
this.threadActive = threadActive;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPaused() {
|
public boolean isPaused() {
|
||||||
@@ -428,4 +426,15 @@ public class InteractionGameTimer extends HabboItem {
|
|||||||
public int getBaseTime() {
|
public int getBaseTime() {
|
||||||
return this.baseTime;
|
return this.baseTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean tryActivateTimerThread() {
|
||||||
|
synchronized (this) {
|
||||||
|
if (this.threadActive) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.threadActive = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -190,6 +190,14 @@ public class InteractionPetBreedingNest extends HabboItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void breed(Habbo habbo, String name, int petOneId, int petTwoId) {
|
public void breed(Habbo habbo, String name, int petOneId, int petTwoId) {
|
||||||
|
// Guard before the destructive delete below: a crafted packet can call
|
||||||
|
// this on a nest that isn't full, which would delete the nest furni and
|
||||||
|
// then NPE on petOne/petTwo in the async runnable (losing the furni).
|
||||||
|
if (habbo == null || this.petOne == null || this.petTwo == null
|
||||||
|
|| habbo.getHabboInfo().getCurrentRoom() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Emulator.getThreading().run(new QueryDeleteHabboItem(this.getId()));
|
Emulator.getThreading().run(new QueryDeleteHabboItem(this.getId()));
|
||||||
|
|
||||||
this.setExtradata("2");
|
this.setExtradata("2");
|
||||||
|
|||||||
+8
-2
@@ -65,8 +65,14 @@ public class WiredConditionHabboCount extends InteractionWiredCondition {
|
|||||||
} else {
|
} else {
|
||||||
String[] data = wiredData.split(":");
|
String[] data = wiredData.split(":");
|
||||||
|
|
||||||
this.lowerLimit = Integer.parseInt(data[0]);
|
if (data.length >= 2) {
|
||||||
this.upperLimit = Integer.parseInt(data[1]);
|
try {
|
||||||
|
this.lowerLimit = Integer.parseInt(data[0].trim());
|
||||||
|
this.upperLimit = Integer.parseInt(data[1].trim());
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// malformed legacy data — keep the constructed defaults
|
||||||
|
}
|
||||||
|
}
|
||||||
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-11
@@ -263,22 +263,29 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
|
|||||||
} else {
|
} else {
|
||||||
String[] data = wiredData.split(":");
|
String[] data = wiredData.split(":");
|
||||||
|
|
||||||
int itemCount = Integer.parseInt(data[0]);
|
if (data.length >= 5) {
|
||||||
|
try {
|
||||||
|
int itemCount = Integer.parseInt(data[0]);
|
||||||
|
|
||||||
String[] items = data[1].split(";");
|
String[] items = data[1].split(";");
|
||||||
|
|
||||||
for (int i = 0; i < itemCount; i++) {
|
for (int i = 0; i < itemCount && i < items.length; i++) {
|
||||||
String[] stuff = items[i].split("-");
|
String[] stuff = items[i].split("-");
|
||||||
|
|
||||||
if (stuff.length >= 6)
|
if (stuff.length >= 6)
|
||||||
this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]), Double.parseDouble(stuff[5])));
|
this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]), Double.parseDouble(stuff[5])));
|
||||||
else if (stuff.length >= 5)
|
else if (stuff.length >= 5)
|
||||||
this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4])));
|
this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4])));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = data[2].equals("1");
|
||||||
|
this.direction = data[3].equals("1");
|
||||||
|
this.position = data[4].equals("1");
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// malformed legacy data — keep whatever was parsed plus defaults
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = data[2].equals("1");
|
|
||||||
this.direction = data[3].equals("1");
|
|
||||||
this.position = data[4].equals("1");
|
|
||||||
this.altitude = false;
|
this.altitude = false;
|
||||||
this.furniSource = this.settings.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
this.furniSource = this.settings.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
|
||||||
this.quantifier = QUANTIFIER_ALL;
|
this.quantifier = QUANTIFIER_ALL;
|
||||||
|
|||||||
+8
-2
@@ -64,8 +64,14 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition {
|
|||||||
this.userSource = data.userSource;
|
this.userSource = data.userSource;
|
||||||
} else {
|
} else {
|
||||||
String[] data = wiredData.split(":");
|
String[] data = wiredData.split(":");
|
||||||
this.lowerLimit = Integer.parseInt(data[0]);
|
if (data.length >= 2) {
|
||||||
this.upperLimit = Integer.parseInt(data[1]);
|
try {
|
||||||
|
this.lowerLimit = Integer.parseInt(data[0].trim());
|
||||||
|
this.upperLimit = Integer.parseInt(data[1].trim());
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// malformed legacy data — keep the constructed defaults
|
||||||
|
}
|
||||||
|
}
|
||||||
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+158
-9
@@ -1,5 +1,6 @@
|
|||||||
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
|
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.Item;
|
||||||
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition;
|
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition;
|
||||||
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
|
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.wired.core.WiredSourceUtil;
|
||||||
import com.eu.habbo.habbohotel.users.HabboItem;
|
import com.eu.habbo.habbohotel.users.HabboItem;
|
||||||
import com.eu.habbo.messages.ServerMessage;
|
import com.eu.habbo.messages.ServerMessage;
|
||||||
|
import gnu.trove.set.hash.THashSet;
|
||||||
|
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
|
public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
|
||||||
private static final int COMPARISON_LESS_THAN = 0;
|
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_USERS = 0;
|
||||||
private static final int SOURCE_GROUP_FURNI = 1;
|
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;
|
public static final WiredConditionType type = WiredConditionType.SLC_QUANTITY;
|
||||||
|
|
||||||
|
private final THashSet<HabboItem> items;
|
||||||
private int comparison = COMPARISON_EQUAL;
|
private int comparison = COMPARISON_EQUAL;
|
||||||
private int quantity = 0;
|
private int quantity = 0;
|
||||||
private int sourceGroup = SOURCE_GROUP_USERS;
|
private int sourceGroup = SOURCE_GROUP_USERS;
|
||||||
@@ -33,10 +43,12 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
|
|||||||
|
|
||||||
public WiredConditionSelectionQuantity(ResultSet set, Item baseItem) throws SQLException {
|
public WiredConditionSelectionQuantity(ResultSet set, Item baseItem) throws SQLException {
|
||||||
super(set, baseItem);
|
super(set, baseItem);
|
||||||
|
this.items = new THashSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public WiredConditionSelectionQuantity(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
|
public WiredConditionSelectionQuantity(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
|
||||||
super(id, userId, item, extradata, limitedStack, limitedSells);
|
super(id, userId, item, extradata, limitedStack, limitedSells);
|
||||||
|
this.items = new THashSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -46,9 +58,18 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serializeWiredData(ServerMessage message, Room room) {
|
public void serializeWiredData(ServerMessage message, Room room) {
|
||||||
message.appendBoolean(false);
|
this.refresh(room);
|
||||||
message.appendInt(5);
|
|
||||||
message.appendInt(0);
|
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.getBaseItem().getSpriteId());
|
||||||
message.appendInt(this.getId());
|
message.appendInt(this.getId());
|
||||||
message.appendString("");
|
message.appendString("");
|
||||||
@@ -69,8 +90,36 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
|
|||||||
|
|
||||||
this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL;
|
this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL;
|
||||||
this.quantity = (params.length > 1) ? this.normalizeQuantity(params[1]) : 0;
|
this.quantity = (params.length > 1) ? this.normalizeQuantity(params[1]) : 0;
|
||||||
this.sourceGroup = (params.length > 2) ? this.normalizeSourceGroup(params[2]) : SOURCE_GROUP_USERS;
|
this.items.clear();
|
||||||
this.sourceType = (params.length > 3) ? this.normalizeSourceType(this.sourceGroup, params[3]) : WiredSourceUtil.SOURCE_TRIGGER;
|
|
||||||
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -97,11 +146,14 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getWiredData() {
|
public String getWiredData() {
|
||||||
|
this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()));
|
||||||
|
|
||||||
return WiredManager.getGson().toJson(new JsonData(
|
return WiredManager.getGson().toJson(new JsonData(
|
||||||
this.comparison,
|
this.comparison,
|
||||||
this.quantity,
|
this.quantity,
|
||||||
this.sourceGroup,
|
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.quantity = this.normalizeQuantity(data.quantity);
|
||||||
this.sourceGroup = this.normalizeSourceGroup(data.sourceGroup);
|
this.sourceGroup = this.normalizeSourceGroup(data.sourceGroup);
|
||||||
this.sourceType = this.normalizeSourceType(this.sourceGroup, data.sourceType);
|
this.sourceType = this.normalizeSourceType(this.sourceGroup, data.sourceType);
|
||||||
|
this.loadSelectedItems(data.itemIds, room);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +203,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPickUp() {
|
public void onPickUp() {
|
||||||
|
this.items.clear();
|
||||||
this.comparison = COMPARISON_EQUAL;
|
this.comparison = COMPARISON_EQUAL;
|
||||||
this.quantity = 0;
|
this.quantity = 0;
|
||||||
this.sourceGroup = SOURCE_GROUP_USERS;
|
this.sourceGroup = SOURCE_GROUP_USERS;
|
||||||
@@ -158,7 +212,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
|
|||||||
|
|
||||||
private int resolveCount(WiredContext ctx) {
|
private int resolveCount(WiredContext ctx) {
|
||||||
if (this.sourceGroup == SOURCE_GROUP_FURNI) {
|
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();
|
return items.size();
|
||||||
}
|
}
|
||||||
@@ -188,10 +242,18 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
|
|||||||
|
|
||||||
private int normalizeSourceType(int group, int value) {
|
private int normalizeSourceType(int group, int value) {
|
||||||
if (group == SOURCE_GROUP_USERS) {
|
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) {
|
switch (value) {
|
||||||
|
case WiredSourceUtil.SOURCE_SELECTED:
|
||||||
case WiredSourceUtil.SOURCE_SELECTOR:
|
case WiredSourceUtil.SOURCE_SELECTOR:
|
||||||
case WiredSourceUtil.SOURCE_SIGNAL:
|
case WiredSourceUtil.SOURCE_SIGNAL:
|
||||||
case WiredSourceUtil.SOURCE_TRIGGER:
|
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 {
|
static class JsonData {
|
||||||
int comparison;
|
int comparison;
|
||||||
int quantity;
|
int quantity;
|
||||||
int sourceGroup;
|
int sourceGroup;
|
||||||
int sourceType;
|
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.comparison = comparison;
|
||||||
this.quantity = quantity;
|
this.quantity = quantity;
|
||||||
this.sourceGroup = sourceGroup;
|
this.sourceGroup = sourceGroup;
|
||||||
this.sourceType = sourceType;
|
this.sourceType = sourceType;
|
||||||
|
this.itemIds = itemIds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -82,7 +82,7 @@ public class WiredEffectBotTalk extends InteractionWiredEffect {
|
|||||||
|
|
||||||
this.setDelay(delay);
|
this.setDelay(delay);
|
||||||
this.botName = data[0].substring(0, Math.min(data[0].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
|
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.mode = mode;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect {
|
|||||||
throw new WiredSaveException("Delay too long");
|
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.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.mode = mode;
|
||||||
this.setDelay(delay);
|
this.setDelay(delay);
|
||||||
|
|
||||||
|
|||||||
+66
-20
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
|
|||||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||||
import com.eu.habbo.habbohotel.items.Item;
|
import com.eu.habbo.habbohotel.items.Item;
|
||||||
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect;
|
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.games.InteractionGameUpCounter;
|
||||||
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
|
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
|
||||||
import com.eu.habbo.habbohotel.rooms.Room;
|
import com.eu.habbo.habbohotel.rooms.Room;
|
||||||
@@ -60,29 +61,74 @@ public class WiredEffectControlClock extends InteractionWiredEffect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (HabboItem item : effectiveItems) {
|
for (HabboItem item : effectiveItems) {
|
||||||
if (!(item instanceof InteractionGameUpCounter)) {
|
if (!(item instanceof InteractionGameTimer)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
InteractionGameUpCounter counter = (InteractionGameUpCounter) item;
|
if (item instanceof InteractionGameUpCounter) {
|
||||||
|
this.controlUpCounter((InteractionGameUpCounter) item, room);
|
||||||
switch (this.action) {
|
continue;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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));
|
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");
|
throw new WiredSaveException("wiredfurni.error.require_counter_furni");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+33
-34
@@ -53,26 +53,37 @@ public class WiredEffectFurniToFurni extends InteractionWiredEffect {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
HabboItem moveItem = this.resolveLastMoveItem(ctx);
|
List<HabboItem> moveItems = this.resolveMoveItems(ctx);
|
||||||
HabboItem targetItem = this.resolveLastTargetItem(ctx);
|
List<HabboItem> targetItems = this.resolveTargetItems(ctx);
|
||||||
|
|
||||||
if (moveItem == null || targetItem == null || moveItem.getId() == targetItem.getId()) {
|
if (moveItems.isEmpty() || targetItems.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RoomTile targetTile = room.getLayout().getTile(targetItem.getX(), targetItem.getY());
|
int targetIndex = 0;
|
||||||
if (targetTile == null) {
|
for (HabboItem moveItem : moveItems) {
|
||||||
return;
|
if (moveItem == null) {
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), null, false, ctx);
|
HabboItem targetItem = targetItems.get(targetIndex % targetItems.size());
|
||||||
if (error == FurnitureMovementError.NONE) {
|
targetIndex++;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), targetItem.getZ(), null, false, ctx);
|
if (targetItem == null || moveItem.getId() == targetItem.getId()) {
|
||||||
if (error == FurnitureMovementError.NONE) {
|
continue;
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
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;
|
return COOLDOWN_MOVEMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HabboItem resolveLastMoveItem(WiredContext ctx) {
|
private List<HabboItem> resolveMoveItems(WiredContext ctx) {
|
||||||
return this.resolveLastItem(ctx, this.moveSource, this.moveItems);
|
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;
|
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) {
|
if (source == WiredSourceUtil.SOURCE_SELECTED) {
|
||||||
this.validateItems(items);
|
this.validateItems(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<HabboItem> resolvedItems = WiredSourceUtil.resolveItems(ctx, source, items);
|
return WiredSourceUtil.resolveItems(ctx, source, items).stream()
|
||||||
|
.filter(item -> item != null && ctx.room().getHabboItem(item.getId()) != null)
|
||||||
if (resolvedItems.isEmpty()) {
|
.collect(Collectors.toList());
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int index = resolvedItems.size() - 1; index >= 0; index--) {
|
|
||||||
HabboItem item = resolvedItems.get(index);
|
|
||||||
|
|
||||||
if (item != null) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HabboItem> parseItems(String data, Room room) throws WiredSaveException {
|
private List<HabboItem> parseItems(String data, Room room) throws WiredSaveException {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user