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
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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,477 @@
|
||||
-- ============================================================
|
||||
-- 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`;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `wired_emulator_settings` (
|
||||
`key` VARCHAR(255) NOT NULL,
|
||||
`value` TEXT NOT NULL,
|
||||
`comment` TEXT NULL DEFAULT '',
|
||||
PRIMARY KEY (`key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
|
||||
('crypto.ws.enabled', '0'),
|
||||
('crypto.ws.signing.enabled', '0'),
|
||||
('crypto.ws.signing.public_key', ''),
|
||||
('crypto.ws.signing.private_key', ''),
|
||||
('login.access.jwt.secret', ''),
|
||||
('login.remember.duration.days', '30'),
|
||||
('login.remember.rotate.interval.minutes', '15'),
|
||||
('login.remember.jwt.secret', ''),
|
||||
('login.turnstile.enabled', '0'),
|
||||
('login.turnstile.sitekey', ''),
|
||||
('login.turnstile.secretkey', ''),
|
||||
('login.ratelimit.enabled', '1'),
|
||||
('login.ratelimit.max_attempts', '5'),
|
||||
('login.ratelimit.window_sec', '60'),
|
||||
('login.ratelimit.lockout_sec', '120'),
|
||||
('login.register.enabled', '1'),
|
||||
('register.max_per_ip', '5'),
|
||||
('register.default.look', 'hr-100-7.hd-180-1.ch-210-66.lg-270-82.sh-290-80'),
|
||||
('register.default.motto', 'I love Habbo!'),
|
||||
('password.reset.url', 'http://localhost/reset-password'),
|
||||
('smtp.provider', 'own'),
|
||||
('smtp.host', 'localhost'),
|
||||
('smtp.port', '587'),
|
||||
('smtp.username', ''),
|
||||
('smtp.password', ''),
|
||||
('smtp.from_address', 'no-reply@example.com'),
|
||||
('smtp.from_name', 'Habbo Hotel'),
|
||||
('smtp.use_tls', '1'),
|
||||
('smtp.use_ssl', '0'),
|
||||
('new_user_credits', '0'),
|
||||
('new_user_duckets', '0'),
|
||||
('new_user_diamonds', '0')
|
||||
ON DUPLICATE KEY UPDATE `value` = `value`;
|
||||
|
||||
INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||
('wired.engine.enabled', '1', 'Compatibility flag. The runtime uses the new wired engine.'),
|
||||
('wired.engine.exclusive', '1', 'Compatibility flag. The runtime uses exclusive wired engine execution.'),
|
||||
('wired.engine.maxStepsPerStack', '100', 'Maximum internal processing steps allowed for a single wired stack execution.'),
|
||||
('wired.engine.debug', '0', 'Enable verbose debug logging for the wired engine.'),
|
||||
('wired.custom.enabled', '0', 'Enable custom legacy wired compatibility behavior.'),
|
||||
('hotel.wired.furni.selection.count', '5', 'Maximum number of furni that a wired box can store or select.'),
|
||||
('hotel.wired.max_delay', '20', 'Maximum delay value accepted by wired effects that support delayed execution.'),
|
||||
('hotel.wired.message.max_length', '512', 'Maximum length of wired message text fields.'),
|
||||
('wired.effect.teleport.delay', '500', 'Delay in milliseconds used by wired teleport movement.'),
|
||||
('wired.place.under', '0', 'Allow placing wired furniture underneath other items when room rules permit it.'),
|
||||
('wired.tick.interval.ms', '50', 'Global wired tick interval in milliseconds.'),
|
||||
('wired.tick.resolution', '100', 'Legacy wired tick resolution value.'),
|
||||
('wired.tick.debug', '0', 'Enable verbose logging for the wired tick service.'),
|
||||
('wired.tick.thread.priority', '6', 'Java thread priority used by the wired tick service.'),
|
||||
('wired.highscores.displaycount', '25', 'Maximum number of wired highscore entries shown to users.'),
|
||||
('wired.abuse.max.recursion.depth', '10', 'Maximum recursive wired depth before execution is stopped.'),
|
||||
('wired.abuse.max.events.per.window', '100', 'Maximum identical wired events allowed inside the abuse rate-limit window.'),
|
||||
('wired.abuse.rate.limit.window.ms', '10000', 'Wired abuse rate-limit window in milliseconds.'),
|
||||
('wired.abuse.ban.duration.ms', '600000', 'Temporary wired ban duration after abuse detection.'),
|
||||
('wired.monitor.usage.window.ms', '1000', 'Rolling window size for wired usage monitoring.'),
|
||||
('wired.monitor.usage.limit', '1000', 'Maximum wired usage budget in one monitor window.'),
|
||||
('wired.monitor.delayed.events.limit', '100', 'Maximum delayed wired events queued in one room.'),
|
||||
('wired.monitor.overload.average.ms', '50', 'Average execution time threshold for overload tracking.'),
|
||||
('wired.monitor.overload.peak.ms', '150', 'Peak execution time threshold for overload tracking.'),
|
||||
('wired.monitor.overload.consecutive.windows', '2', 'Consecutive overloaded windows required before logging overload.'),
|
||||
('wired.monitor.heavy.usage.percent', '70', 'Usage percentage threshold for heavy-room tracking.'),
|
||||
('wired.monitor.heavy.consecutive.windows', '5', 'Consecutive windows above heavy usage threshold.'),
|
||||
('wired.monitor.heavy.delayed.percent', '60', 'Delayed queue percentage threshold for heavy-room tracking.')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`value` = VALUES(`value`),
|
||||
`comment` = VALUES(`comment`);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Login API, room templates, remember-me, and news
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `password_resets` (
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`token` VARCHAR(128) NOT NULL,
|
||||
`expires_at` TIMESTAMP NOT NULL,
|
||||
`created_ip` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`user_id`),
|
||||
UNIQUE KEY `idx_password_resets_token` (`token`),
|
||||
CONSTRAINT `fk_password_resets_user`
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users_remember_families` (
|
||||
`family_id` CHAR(36) NOT NULL,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`current_version` INT(11) NOT NULL DEFAULT 1,
|
||||
`created_at` INT(11) NOT NULL,
|
||||
`expires_at` INT(11) NOT NULL,
|
||||
`revoked` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`last_ip` VARCHAR(45) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`family_id`),
|
||||
KEY `idx_users_remember_families_user_id` (`user_id`),
|
||||
KEY `idx_users_remember_families_expires_at` (`expires_at`),
|
||||
CONSTRAINT `fk_users_remember_families_user`
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_templates` (
|
||||
`template_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`description` VARCHAR(256) NOT NULL DEFAULT '',
|
||||
`thumbnail` VARCHAR(512) NOT NULL DEFAULT '',
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
`enabled` ENUM('0','1') NOT NULL DEFAULT '1',
|
||||
`name` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`room_description` VARCHAR(250) NOT NULL DEFAULT '',
|
||||
`model` VARCHAR(100) NOT NULL,
|
||||
`password` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`state` ENUM('open','locked','password','invisible') NOT NULL DEFAULT 'open',
|
||||
`users_max` INT(11) NOT NULL DEFAULT 25,
|
||||
`category` INT(11) NOT NULL DEFAULT 0,
|
||||
`paper_floor` VARCHAR(50) NOT NULL DEFAULT '0.0',
|
||||
`paper_wall` VARCHAR(50) NOT NULL DEFAULT '0.0',
|
||||
`paper_landscape` VARCHAR(50) NOT NULL DEFAULT '0.0',
|
||||
`thickness_wall` INT(11) NOT NULL DEFAULT 0,
|
||||
`thickness_floor` INT(11) NOT NULL DEFAULT 0,
|
||||
`moodlight_data` VARCHAR(2048) NOT NULL DEFAULT '',
|
||||
`override_model` ENUM('0','1') NOT NULL DEFAULT '0',
|
||||
`trade_mode` INT(2) NOT NULL DEFAULT 2,
|
||||
`heightmap` MEDIUMTEXT NOT NULL,
|
||||
`door_x` INT(11) NOT NULL DEFAULT 0,
|
||||
`door_y` INT(11) NOT NULL DEFAULT 0,
|
||||
`door_dir` INT(4) NOT NULL DEFAULT 2,
|
||||
PRIMARY KEY (`template_id`),
|
||||
KEY `idx_room_templates_enabled_sort` (`enabled`, `sort_order`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_templates_items` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`template_id` INT(11) NOT NULL,
|
||||
`item_id` INT(11) UNSIGNED NOT NULL,
|
||||
`wall_pos` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
`x` INT(11) NOT NULL DEFAULT 0,
|
||||
`y` INT(11) NOT NULL DEFAULT 0,
|
||||
`z` DOUBLE(10,6) NOT NULL DEFAULT 0.000000,
|
||||
`rot` INT(11) NOT NULL DEFAULT 0,
|
||||
`extra_data` VARCHAR(2096) NOT NULL DEFAULT '',
|
||||
`wired_data` VARCHAR(4096) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_room_templates_items_template_id` (`template_id`),
|
||||
KEY `idx_room_templates_items_item_id` (`item_id`),
|
||||
CONSTRAINT `fk_room_templates_items_template`
|
||||
FOREIGN KEY (`template_id`) REFERENCES `room_templates` (`template_id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_room_templates_items_item_base`
|
||||
FOREIGN KEY (`item_id`) REFERENCES `items_base` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ui_news` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(150) NOT NULL,
|
||||
`body` TEXT NOT NULL,
|
||||
`image` MEDIUMTEXT DEFAULT NULL,
|
||||
`link_text` VARCHAR(80) NOT NULL DEFAULT '',
|
||||
`link_url` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_ui_news_enabled_sort` (`enabled`, `sort_order`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
INSERT INTO `ui_news` (`title`, `body`, `image`, `link_text`, `link_url`, `enabled`, `sort_order`)
|
||||
SELECT 'Welcome to the Hotel!', 'Catch up on the latest events, updates and competitions happening right now in the hotel.', '', '', '', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `ui_news`);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Wired runtime data
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_wired_settings` (
|
||||
`room_id` INT(11) NOT NULL,
|
||||
`inspect_mask` INT(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can open and inspect Wired in the room. 1=everyone, 2=users with rights, 4=group members, 8=group admins.',
|
||||
`modify_mask` INT(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can modify Wired in the room. 2=users with rights, 4=group members, 8=group admins.',
|
||||
PRIMARY KEY (`room_id`),
|
||||
CONSTRAINT `fk_room_wired_settings_room`
|
||||
FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_wired_variables` (
|
||||
`room_id` INT(11) NOT NULL,
|
||||
`variable_item_id` INT(11) NOT NULL,
|
||||
`value` INT(11) DEFAULT NULL,
|
||||
`created_at` INT(11) NOT NULL DEFAULT 0,
|
||||
`updated_at` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`room_id`, `variable_item_id`),
|
||||
KEY `idx_room_wired_variables_room_item` (`room_id`, `variable_item_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_user_wired_variables` (
|
||||
`room_id` INT(11) NOT NULL,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`variable_item_id` INT(11) NOT NULL,
|
||||
`value` INT(11) DEFAULT NULL,
|
||||
`created_at` INT(11) NOT NULL DEFAULT 0,
|
||||
`updated_at` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`room_id`, `user_id`, `variable_item_id`),
|
||||
KEY `idx_room_user_wired_variables_room_item` (`room_id`, `variable_item_id`),
|
||||
KEY `idx_room_user_wired_variables_user` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_furni_wired_variables` (
|
||||
`room_id` INT(11) NOT NULL,
|
||||
`furni_id` INT(11) NOT NULL,
|
||||
`variable_item_id` INT(11) NOT NULL,
|
||||
`value` INT(11) DEFAULT NULL,
|
||||
`created_at` INT(11) NOT NULL DEFAULT 0,
|
||||
`updated_at` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`room_id`, `furni_id`, `variable_item_id`),
|
||||
KEY `idx_room_furni_wired_variables_room_item` (`room_id`, `variable_item_id`),
|
||||
KEY `idx_room_furni_wired_variables_furni` (`furni_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- User customization: prefixes, nick icons, profile backgrounds
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
ALTER TABLE `users`
|
||||
ADD COLUMN IF NOT EXISTS `background_id` INT(11) NOT NULL DEFAULT 0 AFTER `machine_id`,
|
||||
ADD COLUMN IF NOT EXISTS `background_stand_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_id`,
|
||||
ADD COLUMN IF NOT EXISTS `background_overlay_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_stand_id`,
|
||||
ADD COLUMN IF NOT EXISTS `background_card_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_overlay_id`;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `infostand_backgrounds` (
|
||||
`id` INT(11) NOT NULL,
|
||||
`category` ENUM('background','stand','overlay','card') NOT NULL,
|
||||
`min_rank` INT(11) NOT NULL DEFAULT 0,
|
||||
`is_hc_only` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`is_ambassador_only` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`, `category`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO `infostand_backgrounds` (`id`, `category`, `min_rank`, `is_hc_only`, `is_ambassador_only`) VALUES
|
||||
(0, 'background', 0, 0, 0),
|
||||
(0, 'stand', 0, 0, 0),
|
||||
(0, 'overlay', 0, 0, 0),
|
||||
(0, 'card', 0, 0, 0);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `user_prefixes` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`text` VARCHAR(50) NOT NULL,
|
||||
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
|
||||
`icon` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`effect` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`font` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`catalog_prefix_id` INT(11) NOT NULL DEFAULT 0,
|
||||
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`points` INT(11) NOT NULL DEFAULT 0,
|
||||
`points_type` INT(11) NOT NULL DEFAULT 0,
|
||||
`is_custom` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`active` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_prefixes_user_id` (`user_id`),
|
||||
KEY `idx_user_prefixes_user_active` (`user_id`, `active`),
|
||||
CONSTRAINT `fk_user_prefixes_user`
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_prefixes_catalog` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`text` VARCHAR(50) NOT NULL,
|
||||
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
|
||||
`icon` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`effect` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`font` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`points` INT(11) NOT NULL DEFAULT 0,
|
||||
`points_type` INT(11) NOT NULL DEFAULT 0,
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `user_visual_settings` (
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`display_order` VARCHAR(50) NOT NULL DEFAULT 'icon-prefix-name',
|
||||
PRIMARY KEY (`user_id`),
|
||||
CONSTRAINT `fk_user_visual_settings_user`
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
|
||||
`key_name` VARCHAR(100) NOT NULL,
|
||||
`value` VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (`key_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`word` VARCHAR(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_custom_prefix_blacklist_word` (`word`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES
|
||||
('max_length', '15'),
|
||||
('min_rank_to_buy', '1'),
|
||||
('price_credits', '5'),
|
||||
('price_points', '0'),
|
||||
('points_type', '0'),
|
||||
('font_price_credits', '10'),
|
||||
('font_price_points', '0'),
|
||||
('font_points_type', '0');
|
||||
|
||||
INSERT IGNORE INTO `custom_prefixes_catalog`
|
||||
(`id`, `display_name`, `text`, `color`, `icon`, `effect`, `font`, `points`, `points_type`, `enabled`, `sort_order`)
|
||||
VALUES
|
||||
(1, 'VIP', 'VIP', '#FFD700', '', 'glow', '', 10, 0, 1, 1),
|
||||
(2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2),
|
||||
(3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_nick_icons_catalog` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`icon_key` VARCHAR(50) NOT NULL,
|
||||
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`points` INT(11) NOT NULL DEFAULT 0,
|
||||
`points_type` INT(11) NOT NULL DEFAULT 0,
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_custom_nick_icons_catalog_icon_key` (`icon_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `user_nick_icons` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`icon_key` VARCHAR(50) NOT NULL,
|
||||
`active` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_user_nick_icons_user_icon` (`user_id`, `icon_key`),
|
||||
KEY `idx_user_nick_icons_user_id` (`user_id`),
|
||||
KEY `idx_user_nick_icons_user_active` (`user_id`, `active`),
|
||||
CONSTRAINT `fk_user_nick_icons_user`
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO `custom_nick_icons_catalog` (`icon_key`, `display_name`, `points`, `points_type`, `enabled`, `sort_order`) VALUES
|
||||
('1', 'Icon 1', 10, 0, 1, 1),
|
||||
('2', 'Icon 2', 10, 0, 1, 2),
|
||||
('3', 'Icon 3', 10, 0, 1, 3),
|
||||
('4', 'Icon 4', 10, 0, 1, 4),
|
||||
('5', 'Icon 5', 10, 0, 1, 5),
|
||||
('6', 'Icon 6', 10, 0, 1, 6);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Custom badge maker
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users_custom_badge_settings` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`badge_path` VARCHAR(255) NOT NULL DEFAULT '/var/www/gamedata/c_images/album1584',
|
||||
`badge_url` VARCHAR(255) NOT NULL DEFAULT '/gamedata/c_images/album1584',
|
||||
`price_badge` INT(11) NOT NULL DEFAULT 0,
|
||||
`currency_type` INT(11) NOT NULL DEFAULT -1,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
INSERT INTO `users_custom_badge_settings` (`id`, `badge_path`, `badge_url`, `price_badge`, `currency_type`)
|
||||
SELECT 1, '/var/www/gamedata/c_images/album1584', '/gamedata/c_images/album1584', 50, 5
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `users_custom_badge_settings` WHERE `id` = 1);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `user_custom_badge` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`badge_id` VARCHAR(64) NOT NULL,
|
||||
`badge_name` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`badge_description` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`date_created` INT(11) NOT NULL DEFAULT 0,
|
||||
`date_edit` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_user_custom_badge_badge_id` (`badge_id`),
|
||||
KEY `idx_user_custom_badge_user_id` (`user_id`),
|
||||
CONSTRAINT `fk_user_custom_badge_user`
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- UI/catalog compatibility values used by the current client
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
INSERT INTO `chat_bubbles` (`type`, `name`, `permission`, `overridable`, `triggers_talking_furniture`) VALUES
|
||||
(200, 'SHOW_MESSAGE_RED', '', 1, 0),
|
||||
(201, 'SHOW_MESSAGE_GREEN', '', 1, 0),
|
||||
(202, 'SHOW_MESSAGE_BLUE', '', 1, 0),
|
||||
(210, 'SHOW_MESSAGE_ALERT', '', 1, 0),
|
||||
(211, 'SHOW_MESSAGE_INFO', '', 1, 0),
|
||||
(212, 'SHOW_MESSAGE_WARNING', '', 1, 0),
|
||||
(220, 'SHOW_MESSAGE_WRONG', '', 1, 0),
|
||||
(221, 'SHOW_MESSAGE_WRONG_CIRCLED', '', 1, 0),
|
||||
(222, 'SHOW_MESSAGE_CORRECT', '', 1, 0),
|
||||
(223, 'SHOW_MESSAGE_CORRECT_CIRCLED', '', 1, 0),
|
||||
(224, 'SHOW_MESSAGE_QUESTION', '', 1, 0),
|
||||
(225, 'SHOW_MESSAGE_QUESTION_CIRCLED', '', 1, 0),
|
||||
(226, 'SHOW_MESSAGE_ARROW_UP', '', 1, 0),
|
||||
(227, 'SHOW_MESSAGE_ARROW_UP_CIRCLED', '', 1, 0),
|
||||
(228, 'SHOW_MESSAGE_ARROW_DOWN', '', 1, 0),
|
||||
(229, 'SHOW_MESSAGE_ARROW_DOWN_CIRCLED', '', 1, 0),
|
||||
(250, 'SHOW_MESSAGE_SKULL', '', 1, 0),
|
||||
(251, 'SHOW_MESSAGE_SKULL_ALT', '', 1, 0),
|
||||
(252, 'SHOW_MESSAGE_MAGNIFIER', '', 1, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`name` = VALUES(`name`),
|
||||
`permission` = VALUES(`permission`),
|
||||
`overridable` = VALUES(`overridable`),
|
||||
`triggers_talking_furniture` = VALUES(`triggers_talking_furniture`);
|
||||
|
||||
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
|
||||
('commands.keys.cmd_setroom_template', 'setroom_template;set_room_template'),
|
||||
('commands.succes.cmd_setroom_template.verify', 'Copy the current room "%roomname%" to room_templates? Type :setroom_template %generic.yes% to confirm.'),
|
||||
('commands.succes.cmd_setroom_template', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'),
|
||||
('commands.error.cmd_setroom_template', 'Could not save room as template. Check the server log for details.'),
|
||||
('commands.error.cmd_setroom_template.no_room', 'You must be inside a room to use this command.'),
|
||||
('commands.keys.cmd_give_prefix', 'giveprefix'),
|
||||
('commands.keys.cmd_list_prefixes', 'listprefixes'),
|
||||
('commands.keys.cmd_remove_prefix', 'removeprefix'),
|
||||
('commands.keys.cmd_prefix_blacklist', 'prefixblacklist'),
|
||||
('wiredfurni.badgereceived.body', 'You have just received a new Badge! Check your Inventory!'),
|
||||
('wiredfurni.badgereceived.title', 'Badge received!');
|
||||
|
||||
-- Optional permission metadata for normalized permission schemas.
|
||||
-- Actual rank values still belong in the permissions/permission_ranks setup.
|
||||
CREATE TABLE IF NOT EXISTS `permission_definitions` (
|
||||
`permission_key` VARCHAR(64) NOT NULL,
|
||||
`max_value` TINYINT(3) UNSIGNED NOT NULL DEFAULT 1,
|
||||
`comment` TEXT NOT NULL,
|
||||
PRIMARY KEY (`permission_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`) VALUES
|
||||
('cmd_setroom_template', 1, 'Allows using :setroom_template to copy a room into the login room-template table.'),
|
||||
('cmd_give_prefix', 1, 'Allows granting custom prefixes to users.'),
|
||||
('cmd_list_prefixes', 1, 'Allows listing custom prefixes assigned to users.'),
|
||||
('cmd_remove_prefix', 1, 'Allows removing custom prefixes from users.'),
|
||||
('cmd_prefix_blacklist', 1, 'Allows managing the custom prefix blacklist.')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`max_value` = VALUES(`max_value`),
|
||||
`comment` = VALUES(`comment`);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Explicitly obsolete table from older remember-me attempts.
|
||||
-- The current Java uses users_remember_families only.
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
DROP TABLE IF EXISTS `users_remember_tokens`;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- ============================================================
|
||||
-- Fix: acc_supporttool wrongly granted to VIP / wrongly denied to Super Mod
|
||||
-- ============================================================
|
||||
-- The default permission_definitions seed shipped acc_supporttool
|
||||
-- with rank pattern (0, 1, 1, 1, 1, 0, 1) — i.e. rank_2 (VIP) and
|
||||
-- rank_3 (X, junior helper) had ALLOWED, while rank_6 (Super Mod)
|
||||
-- did NOT. That's two bugs:
|
||||
--
|
||||
-- * VIP users see the ModTools button on the toolbar and can
|
||||
-- open Room/User info windows. The actual sanction endpoints
|
||||
-- still gate on ACC_SUPPORTTOOL server-side so they can't
|
||||
-- actually moderate, but the UI exposure is wrong and lets a
|
||||
-- VIP request user info / room info / chatlogs they have no
|
||||
-- business reading.
|
||||
-- * Super Mod is denied the tool entirely, which is obviously
|
||||
-- unintended given the rank name.
|
||||
--
|
||||
-- Intended pattern: only Support (4) and up — (0, 0, 0, 1, 1, 1, 1).
|
||||
--
|
||||
-- Run on existing deployments to align with the corrected default
|
||||
-- seed in `Default Database/FullDatabase.sql`. Idempotent.
|
||||
|
||||
UPDATE `permission_definitions`
|
||||
SET `rank_1` = 0,
|
||||
`rank_2` = 0,
|
||||
`rank_3` = 0,
|
||||
`rank_4` = 1,
|
||||
`rank_5` = 1,
|
||||
`rank_6` = 1,
|
||||
`rank_7` = 1
|
||||
WHERE `permission_key` = 'acc_supporttool';
|
||||
@@ -1,989 +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';
|
||||
+6
@@ -121,6 +121,12 @@ INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
|
||||
INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
|
||||
('camera.extradata', '{"t":"%timestamp%","u":"%id%","m":"","s":"%room_id%","w":"%url%"}');
|
||||
|
||||
INSERT INTO emulator_settings (`key`, `value`) VALUES
|
||||
('session.reconnect.grace.seconds', '5');
|
||||
|
||||
INSERT INTO emulator_settings (`key`, `value`) VALUES
|
||||
('session.reconnect.effect.id', '188');
|
||||
|
||||
-- Camera emulator texts
|
||||
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
|
||||
('camera.permission', 'You do not have permission to use the camera.');
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
|
||||
('furni.editor.renderer.config.path', '/var/www/Gamedata/config/renderer-config.json'),
|
||||
('furni.editor.asset.base.path', '/var/www/Gamedata/furniture/nitro-assets/');
|
||||
('furni.editor.renderer.config.path', '/var/www/Nitro-V3/dist/configuration/renderer-config.json'),
|
||||
('furni.editor.asset.base.path', '/var/www/gamedata/furniture/');
|
||||
|
||||
ALTER TABLE permissions
|
||||
ADD COLUMN `acc_catalogfurni` ENUM('0','1') NOT NULL DEFAULT '0' AFTER `acc_catalog_ids`;
|
||||
+162
-163
@@ -1,15 +1,31 @@
|
||||
-- Normalizes the legacy `permissions` table into:
|
||||
-- 1. `permission_ranks` -> one row per rank with rank metadata.
|
||||
-- 2. `permission_definitions` -> one row per permission key with comments and one `rank_<id>` column per rank.
|
||||
-- 2. `permission_definitions` -> one row per permission key with comments
|
||||
-- and one `rank_<id>` column per rank.
|
||||
--
|
||||
-- This migration keeps the old `permissions` table untouched so the emulator can safely fall back to it.
|
||||
-- It also cleans up the older experimental normalized objects if they were already created.
|
||||
-- This version uses NO stored procedures and NO DELIMITER directives, so it
|
||||
-- works in any MySQL/MariaDB client (HeidiSQL, DBeaver, mysql CLI, phpMyAdmin)
|
||||
-- regardless of how that client handles delimiters.
|
||||
--
|
||||
-- It builds two large dynamic SQL strings via GROUP_CONCAT and executes each
|
||||
-- with PREPARE / EXECUTE. That replaces both stored procedures from the
|
||||
-- original migration.
|
||||
|
||||
SET SESSION group_concat_max_len = 1048576;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- Clean up older experimental objects from previous attempts.
|
||||
-- --------------------------------------------------------------------------
|
||||
DROP VIEW IF EXISTS `permissions_matrix_view`;
|
||||
DROP PROCEDURE IF EXISTS `refresh_permissions_matrix_view`;
|
||||
DROP PROCEDURE IF EXISTS `refresh_permission_definition_rank_columns`;
|
||||
DROP PROCEDURE IF EXISTS `refresh_permission_definition_values`;
|
||||
DROP TABLE IF EXISTS `permission_rank_values`;
|
||||
DROP TABLE IF EXISTS `permission_nodes`;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- Target tables.
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `permission_ranks` (
|
||||
`id` int(11) NOT NULL,
|
||||
`rank_name` varchar(25) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
|
||||
@@ -42,41 +58,30 @@ ALTER TABLE `permission_definitions`
|
||||
DROP COLUMN IF EXISTS `value_type`,
|
||||
DROP COLUMN IF EXISTS `sort_order`;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- Make sure the legacy `permissions` table has the rank-metadata columns
|
||||
-- the migration reads from it.
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE `permissions`
|
||||
ADD COLUMN IF NOT EXISTS `hidden_rank` tinyint(1) NOT NULL DEFAULT 0 AFTER `rank_name`,
|
||||
ADD COLUMN IF NOT EXISTS `job_description` varchar(255) NOT NULL DEFAULT 'Here to help' AFTER `badge`,
|
||||
ADD COLUMN IF NOT EXISTS `staff_color` varchar(8) NOT NULL DEFAULT '#327fa8' AFTER `job_description`,
|
||||
ADD COLUMN IF NOT EXISTS `staff_background` varchar(255) NOT NULL DEFAULT 'staff-bg.png' AFTER `staff_color`;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- Copy rank metadata into `permission_ranks`.
|
||||
-- --------------------------------------------------------------------------
|
||||
INSERT INTO `permission_ranks` (
|
||||
`id`,
|
||||
`rank_name`,
|
||||
`hidden_rank`,
|
||||
`badge`,
|
||||
`job_description`,
|
||||
`staff_color`,
|
||||
`staff_background`,
|
||||
`level`,
|
||||
`room_effect`,
|
||||
`log_commands`,
|
||||
`prefix`,
|
||||
`prefix_color`,
|
||||
`auto_credits_amount`,
|
||||
`auto_pixels_amount`,
|
||||
`auto_gotw_amount`,
|
||||
`auto_points_amount`
|
||||
`id`, `rank_name`, `hidden_rank`, `badge`, `job_description`,
|
||||
`staff_color`, `staff_background`, `level`, `room_effect`, `log_commands`,
|
||||
`prefix`, `prefix_color`,
|
||||
`auto_credits_amount`, `auto_pixels_amount`, `auto_gotw_amount`, `auto_points_amount`
|
||||
)
|
||||
SELECT
|
||||
`id`,
|
||||
`rank_name`,
|
||||
`hidden_rank`,
|
||||
`badge`,
|
||||
`job_description`,
|
||||
`staff_color`,
|
||||
`staff_background`,
|
||||
`level`,
|
||||
`room_effect`,
|
||||
`log_commands`,
|
||||
`prefix`,
|
||||
`prefix_color`,
|
||||
`auto_credits_amount`,
|
||||
`auto_pixels_amount`,
|
||||
`auto_gotw_amount`,
|
||||
`auto_points_amount`
|
||||
`id`, `rank_name`, `hidden_rank`, `badge`, `job_description`,
|
||||
`staff_color`, `staff_background`, `level`, `room_effect`, `log_commands`,
|
||||
`prefix`, `prefix_color`,
|
||||
`auto_credits_amount`, `auto_pixels_amount`, `auto_gotw_amount`, `auto_points_amount`
|
||||
FROM `permissions`
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`rank_name` = VALUES(`rank_name`),
|
||||
@@ -95,55 +100,30 @@ ON DUPLICATE KEY UPDATE
|
||||
`auto_gotw_amount` = VALUES(`auto_gotw_amount`),
|
||||
`auto_points_amount` = VALUES(`auto_points_amount`);
|
||||
|
||||
DROP PROCEDURE IF EXISTS `refresh_permission_definition_rank_columns`;
|
||||
-- --------------------------------------------------------------------------
|
||||
-- Add a `rank_<id>` column to `permission_definitions` for every rank,
|
||||
-- in one dynamic ALTER TABLE statement.
|
||||
-- (Replaces the refresh_permission_definition_rank_columns procedure.)
|
||||
-- --------------------------------------------------------------------------
|
||||
SET @add_rank_columns_sql = NULL;
|
||||
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE `refresh_permission_definition_rank_columns`()
|
||||
BEGIN
|
||||
DECLARE done INT DEFAULT 0;
|
||||
DECLARE current_rank_id INT;
|
||||
DECLARE current_column_name VARCHAR(32);
|
||||
DECLARE column_exists INT DEFAULT 0;
|
||||
DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC;
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
|
||||
SELECT GROUP_CONCAT(
|
||||
CONCAT('ADD COLUMN IF NOT EXISTS `rank_', `id`, '` tinyint(3) unsigned NOT NULL DEFAULT 0')
|
||||
ORDER BY `id` ASC
|
||||
SEPARATOR ', '
|
||||
)
|
||||
INTO @add_rank_columns_sql
|
||||
FROM `permission_ranks`;
|
||||
|
||||
OPEN rank_cursor;
|
||||
SET @add_rank_columns_sql = CONCAT('ALTER TABLE `permission_definitions` ', @add_rank_columns_sql);
|
||||
|
||||
rank_loop: LOOP
|
||||
FETCH rank_cursor INTO current_rank_id;
|
||||
|
||||
IF done = 1 THEN
|
||||
LEAVE rank_loop;
|
||||
END IF;
|
||||
|
||||
SET current_column_name = CONCAT('rank_', current_rank_id);
|
||||
|
||||
SELECT COUNT(*)
|
||||
INTO column_exists
|
||||
FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = DATABASE()
|
||||
AND `table_name` = 'permission_definitions'
|
||||
AND `column_name` = current_column_name;
|
||||
|
||||
IF column_exists = 0 THEN
|
||||
SET @alter_permissions_column_sql = CONCAT(
|
||||
'ALTER TABLE `permission_definitions` ADD COLUMN `',
|
||||
current_column_name,
|
||||
'` tinyint(3) unsigned NOT NULL DEFAULT 0'
|
||||
);
|
||||
|
||||
PREPARE alter_permissions_column_stmt FROM @alter_permissions_column_sql;
|
||||
EXECUTE alter_permissions_column_stmt;
|
||||
DEALLOCATE PREPARE alter_permissions_column_stmt;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
CLOSE rank_cursor;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
CALL `refresh_permission_definition_rank_columns`();
|
||||
PREPARE add_rank_columns_stmt FROM @add_rank_columns_sql;
|
||||
EXECUTE add_rank_columns_stmt;
|
||||
DEALLOCATE PREPARE add_rank_columns_stmt;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- Seed `permission_definitions` from the columns of the legacy table.
|
||||
-- --------------------------------------------------------------------------
|
||||
INSERT INTO `permission_definitions` (
|
||||
`permission_key`,
|
||||
`max_value`,
|
||||
@@ -187,27 +167,18 @@ FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = DATABASE()
|
||||
AND `table_name` = 'permissions'
|
||||
AND `column_name` NOT IN (
|
||||
'id',
|
||||
'rank_name',
|
||||
'hidden_rank',
|
||||
'badge',
|
||||
'job_description',
|
||||
'staff_color',
|
||||
'staff_background',
|
||||
'level',
|
||||
'room_effect',
|
||||
'log_commands',
|
||||
'prefix',
|
||||
'prefix_color',
|
||||
'auto_credits_amount',
|
||||
'auto_pixels_amount',
|
||||
'auto_gotw_amount',
|
||||
'auto_points_amount'
|
||||
'id', 'rank_name', 'hidden_rank', 'badge', 'job_description',
|
||||
'staff_color', 'staff_background', 'level', 'room_effect', 'log_commands',
|
||||
'prefix', 'prefix_color',
|
||||
'auto_credits_amount', 'auto_pixels_amount', 'auto_gotw_amount', 'auto_points_amount'
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`max_value` = VALUES(`max_value`),
|
||||
`comment` = VALUES(`comment`);
|
||||
`comment` = VALUES(`comment`);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- Override generated comments with curated text where we have it.
|
||||
-- --------------------------------------------------------------------------
|
||||
DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`;
|
||||
|
||||
CREATE TEMPORARY TABLE `tmp_permission_comments` (
|
||||
@@ -421,79 +392,107 @@ SET pd.`comment` = tc.`comment`;
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`;
|
||||
|
||||
DROP PROCEDURE IF EXISTS `refresh_permission_definition_values`;
|
||||
-- --------------------------------------------------------------------------
|
||||
-- Copy values from the wide `permissions` table into each `rank_<id>` column
|
||||
-- of `permission_definitions`, one rank at a time, via dynamic SQL.
|
||||
-- (Replaces the refresh_permission_definition_values procedure.)
|
||||
--
|
||||
-- Strategy: build a single UPDATE per rank that joins `permission_definitions`
|
||||
-- against a derived table whose rows are (permission_key, value) for that
|
||||
-- rank — one row per permission column in `permissions`.
|
||||
-- --------------------------------------------------------------------------
|
||||
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE `refresh_permission_definition_values`()
|
||||
BEGIN
|
||||
DECLARE done INT DEFAULT 0;
|
||||
DECLARE current_rank_id INT;
|
||||
DECLARE current_column_name VARCHAR(32);
|
||||
DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC;
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
|
||||
-- We need to loop over rank ids without a stored procedure. We do that by
|
||||
-- selecting all rank ids into a comma-separated string and then iterating
|
||||
-- with substring math using a CTE-driven counter. Simpler: build one giant
|
||||
-- UPDATE per rank by hand using GROUP_CONCAT and then EXECUTE each in turn.
|
||||
--
|
||||
-- To avoid a procedural loop entirely, we instead emit a *single* UPDATE
|
||||
-- that uses CASE expressions to set every `rank_<id>` column from a single
|
||||
-- derived table containing all ranks' values. This is one PREPARE / EXECUTE.
|
||||
|
||||
OPEN rank_cursor;
|
||||
SET @permission_columns_sql = NULL;
|
||||
|
||||
rank_loop: LOOP
|
||||
FETCH rank_cursor INTO current_rank_id;
|
||||
-- All the permission columns from the legacy table, comma separated and quoted.
|
||||
SELECT GROUP_CONCAT(
|
||||
CONCAT('`', REPLACE(`column_name`, '`', '``'), '`')
|
||||
ORDER BY `ordinal_position`
|
||||
SEPARATOR ', '
|
||||
)
|
||||
INTO @permission_columns_sql
|
||||
FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = DATABASE()
|
||||
AND `table_name` = 'permissions'
|
||||
AND `column_name` NOT IN (
|
||||
'id', 'rank_name', 'hidden_rank', 'badge', 'job_description',
|
||||
'staff_color', 'staff_background', 'level', 'room_effect', 'log_commands',
|
||||
'prefix', 'prefix_color',
|
||||
'auto_credits_amount', 'auto_pixels_amount', 'auto_gotw_amount', 'auto_points_amount'
|
||||
);
|
||||
|
||||
IF done = 1 THEN
|
||||
LEAVE rank_loop;
|
||||
END IF;
|
||||
-- Build the UNPIVOT body: one "SELECT id, 'col' AS k, `col` AS v FROM permissions UNION ALL ..." per column.
|
||||
SET @unpivot_sql = NULL;
|
||||
|
||||
SET current_column_name = CONCAT('rank_', current_rank_id);
|
||||
SELECT GROUP_CONCAT(
|
||||
CONCAT(
|
||||
'SELECT `id` AS rank_id, ''',
|
||||
REPLACE(`column_name`, '''', ''''''),
|
||||
''' AS permission_key, CAST(COALESCE(`',
|
||||
REPLACE(`column_name`, '`', '``'),
|
||||
'`, ''0'') AS UNSIGNED) AS permission_value FROM `permissions`'
|
||||
)
|
||||
ORDER BY `ordinal_position`
|
||||
SEPARATOR ' UNION ALL '
|
||||
)
|
||||
INTO @unpivot_sql
|
||||
FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = DATABASE()
|
||||
AND `table_name` = 'permissions'
|
||||
AND `column_name` NOT IN (
|
||||
'id', 'rank_name', 'hidden_rank', 'badge', 'job_description',
|
||||
'staff_color', 'staff_background', 'level', 'room_effect', 'log_commands',
|
||||
'prefix', 'prefix_color',
|
||||
'auto_credits_amount', 'auto_pixels_amount', 'auto_gotw_amount', 'auto_points_amount'
|
||||
);
|
||||
|
||||
SELECT GROUP_CONCAT(
|
||||
CONCAT(
|
||||
'SELECT ''',
|
||||
REPLACE(`column_name`, '''', ''''''),
|
||||
''' AS permission_key, CAST(COALESCE(`',
|
||||
REPLACE(`column_name`, '`', '``'),
|
||||
'`, ''0'') AS UNSIGNED) AS permission_value FROM `permissions` WHERE `id` = ',
|
||||
current_rank_id
|
||||
)
|
||||
ORDER BY `ordinal_position`
|
||||
SEPARATOR ' UNION ALL '
|
||||
) INTO @permission_rank_source_sql
|
||||
FROM `information_schema`.`columns`
|
||||
WHERE `table_schema` = DATABASE()
|
||||
AND `table_name` = 'permissions'
|
||||
AND `column_name` NOT IN (
|
||||
'id',
|
||||
'rank_name',
|
||||
'hidden_rank',
|
||||
'badge',
|
||||
'job_description',
|
||||
'staff_color',
|
||||
'staff_background',
|
||||
'level',
|
||||
'room_effect',
|
||||
'log_commands',
|
||||
'prefix',
|
||||
'prefix_color',
|
||||
'auto_credits_amount',
|
||||
'auto_pixels_amount',
|
||||
'auto_gotw_amount',
|
||||
'auto_points_amount'
|
||||
);
|
||||
-- Build the SET clause: `rank_<id>` = MAX(CASE WHEN rank_id = <id> THEN permission_value END) for each rank.
|
||||
SET @set_clause_sql = NULL;
|
||||
|
||||
SET @permission_rank_update_sql = CONCAT(
|
||||
'UPDATE `permission_definitions` pd ',
|
||||
'INNER JOIN (',
|
||||
@permission_rank_source_sql,
|
||||
') src ON src.permission_key = pd.permission_key ',
|
||||
'SET pd.`',
|
||||
current_column_name,
|
||||
'` = src.permission_value'
|
||||
);
|
||||
SELECT GROUP_CONCAT(
|
||||
CONCAT(
|
||||
'pd.`rank_', `id`,
|
||||
'` = COALESCE(src.`rank_', `id`, '`, pd.`rank_', `id`, '`)'
|
||||
)
|
||||
ORDER BY `id` ASC
|
||||
SEPARATOR ', '
|
||||
)
|
||||
INTO @set_clause_sql
|
||||
FROM `permission_ranks`;
|
||||
|
||||
PREPARE permission_rank_update_stmt FROM @permission_rank_update_sql;
|
||||
EXECUTE permission_rank_update_stmt;
|
||||
DEALLOCATE PREPARE permission_rank_update_stmt;
|
||||
END LOOP;
|
||||
-- Pivot subquery: one row per permission_key, one column per rank_<id>.
|
||||
SET @pivot_sql = NULL;
|
||||
|
||||
CLOSE rank_cursor;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
SELECT GROUP_CONCAT(
|
||||
CONCAT(
|
||||
'MAX(CASE WHEN rank_id = ', `id`,
|
||||
' THEN permission_value END) AS `rank_', `id`, '`'
|
||||
)
|
||||
ORDER BY `id` ASC
|
||||
SEPARATOR ', '
|
||||
)
|
||||
INTO @pivot_sql
|
||||
FROM `permission_ranks`;
|
||||
|
||||
CALL `refresh_permission_definition_values`();
|
||||
SET @final_update_sql = CONCAT(
|
||||
'UPDATE `permission_definitions` pd ',
|
||||
'INNER JOIN ( ',
|
||||
'SELECT permission_key, ', @pivot_sql, ' ',
|
||||
'FROM ( ', @unpivot_sql, ' ) u ',
|
||||
'GROUP BY permission_key ',
|
||||
') src ON src.permission_key = pd.`permission_key` ',
|
||||
'SET ', @set_clause_sql
|
||||
);
|
||||
|
||||
PREPARE final_update_stmt FROM @final_update_sql;
|
||||
EXECUTE final_update_stmt;
|
||||
DEALLOCATE PREPARE final_update_stmt;
|
||||
@@ -0,0 +1,500 @@
|
||||
UPDATE emulator_settings SET `value` = '1' WHERE (`key` = 'wired.engine.enabled');
|
||||
UPDATE emulator_settings SET `value` = '1' WHERE (`key` = 'wired.engine.exclusive');
|
||||
|
||||
ALTER TABLE emulator_settings
|
||||
ADD COLUMN IF NOT EXISTS `comment` VARCHAR(255) NOT NULL AFTER `value`;
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `catalog_items_bc` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`item_ids` varchar(666) NOT NULL,
|
||||
`page_id` int(11) NOT NULL,
|
||||
`catalog_name` varchar(100) NOT NULL,
|
||||
`order_number` int(11) NOT NULL DEFAULT 1,
|
||||
`extradata` varchar(500) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `catalog_pages_bc` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`parent_id` int(11) NOT NULL DEFAULT -1,
|
||||
`caption` varchar(128) NOT NULL,
|
||||
`page_layout` enum(
|
||||
'default_3x3','club_buy','club_gift','frontpage','spaces','recycler',
|
||||
'recycler_info','recycler_prizes','trophies','plasto','marketplace',
|
||||
'marketplace_own_items','spaces_new','soundmachine','guilds','guild_furni',
|
||||
'info_duckets','info_rentables','info_pets','roomads','single_bundle',
|
||||
'sold_ltd_items','badge_display','bots','pets','pets2','pets3',
|
||||
'productpage1','room_bundle','recent_purchases',
|
||||
'default_3x3_color_grouping','guild_forum','vip_buy','info_loyalty',
|
||||
'loyalty_vip_buy','collectibles','petcustomization','frontpage_featured'
|
||||
) NOT NULL DEFAULT 'default_3x3',
|
||||
`icon_color` int(11) NOT NULL DEFAULT 1,
|
||||
`icon_image` int(11) NOT NULL DEFAULT 1,
|
||||
`order_num` int(11) NOT NULL DEFAULT 1,
|
||||
`visible` enum('0','1') NOT NULL DEFAULT '1',
|
||||
`enabled` enum('0','1') NOT NULL DEFAULT '1',
|
||||
`page_headline` varchar(1024) NOT NULL DEFAULT '',
|
||||
`page_teaser` varchar(64) NOT NULL DEFAULT '',
|
||||
`page_special` varchar(2048) DEFAULT '' COMMENT 'Gold Bubble: catalog_special_txtbg1 // Speech Bubble: catalog_special_txtbg2 // Place normal text in page_text_teaser',
|
||||
`page_text1` text DEFAULT NULL,
|
||||
`page_text2` text DEFAULT NULL,
|
||||
`page_text_details` text DEFAULT NULL,
|
||||
`page_text_teaser` text DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
ALTER TABLE `catalog_club_offers`
|
||||
MODIFY COLUMN `type` ENUM('HC','VIP','BUILDERS_CLUB','BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC';
|
||||
|
||||
ALTER TABLE `catalog_pages`
|
||||
MODIFY COLUMN `page_layout` ENUM(
|
||||
'default_3x3',
|
||||
'club_buy',
|
||||
'club_gift',
|
||||
'frontpage',
|
||||
'spaces',
|
||||
'recycler',
|
||||
'recycler_info',
|
||||
'recycler_prizes',
|
||||
'trophies',
|
||||
'plasto',
|
||||
'marketplace',
|
||||
'marketplace_own_items',
|
||||
'spaces_new',
|
||||
'soundmachine',
|
||||
'guilds',
|
||||
'guild_furni',
|
||||
'info_duckets',
|
||||
'info_rentables',
|
||||
'info_pets',
|
||||
'roomads',
|
||||
'single_bundle',
|
||||
'sold_ltd_items',
|
||||
'badge_display',
|
||||
'bots',
|
||||
'pets',
|
||||
'pets2',
|
||||
'pets3',
|
||||
'productpage1',
|
||||
'room_bundle',
|
||||
'recent_purchases',
|
||||
'default_3x3_color_grouping',
|
||||
'guild_forum',
|
||||
'vip_buy',
|
||||
'info_loyalty',
|
||||
'loyalty_vip_buy',
|
||||
'collectibles',
|
||||
'petcustomization',
|
||||
'frontpage_featured',
|
||||
'builders_club_frontpage',
|
||||
'builders_club_addons',
|
||||
'builders_club_loyalty'
|
||||
) NOT NULL DEFAULT 'default_3x3';
|
||||
|
||||
ALTER TABLE `catalog_pages`
|
||||
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL','BUILDER','BOTH') NOT NULL DEFAULT 'NORMAL'
|
||||
AFTER `club_only`;
|
||||
|
||||
ALTER TABLE `catalog_pages_bc`
|
||||
MODIFY COLUMN `page_layout` ENUM(
|
||||
'default_3x3',
|
||||
'club_buy',
|
||||
'club_gift',
|
||||
'frontpage',
|
||||
'spaces',
|
||||
'recycler',
|
||||
'recycler_info',
|
||||
'recycler_prizes',
|
||||
'trophies',
|
||||
'plasto',
|
||||
'marketplace',
|
||||
'marketplace_own_items',
|
||||
'spaces_new',
|
||||
'soundmachine',
|
||||
'guilds',
|
||||
'guild_furni',
|
||||
'info_duckets',
|
||||
'info_rentables',
|
||||
'info_pets',
|
||||
'roomads',
|
||||
'single_bundle',
|
||||
'sold_ltd_items',
|
||||
'badge_display',
|
||||
'bots',
|
||||
'pets',
|
||||
'pets2',
|
||||
'pets3',
|
||||
'productpage1',
|
||||
'room_bundle',
|
||||
'recent_purchases',
|
||||
'default_3x3_color_grouping',
|
||||
'guild_forum',
|
||||
'vip_buy',
|
||||
'info_loyalty',
|
||||
'loyalty_vip_buy',
|
||||
'collectibles',
|
||||
'petcustomization',
|
||||
'frontpage_featured',
|
||||
'builders_club_frontpage',
|
||||
'builders_club_addons',
|
||||
'builders_club_loyalty'
|
||||
) NOT NULL DEFAULT 'default_3x3';
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'users_settings'
|
||||
AND COLUMN_NAME = 'builders_club_bonus_furni'
|
||||
);
|
||||
SET @sql := IF(@col_exists = 0,
|
||||
'ALTER TABLE `users_settings` ADD COLUMN `builders_club_bonus_furni` INT NOT NULL DEFAULT 0;',
|
||||
'SELECT "exists";'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `wired_emulator_settings` (
|
||||
`key` varchar(191) NOT NULL,
|
||||
`value` text NOT NULL,
|
||||
`comment` text NOT NULL,
|
||||
PRIMARY KEY (`key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
|
||||
|
||||
INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`)
|
||||
SELECT 'wired.engine.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.enabled' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.'
|
||||
UNION ALL
|
||||
SELECT 'wired.engine.exclusive', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.exclusive' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.'
|
||||
UNION ALL
|
||||
SELECT 'wired.engine.maxStepsPerStack', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.maxStepsPerStack' LIMIT 1), '100'), 'Maximum amount of internal processing steps allowed for a single wired stack execution.'
|
||||
UNION ALL
|
||||
SELECT 'wired.engine.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.debug' LIMIT 1), '0'), 'Enable verbose debug logging for the new wired engine.'
|
||||
UNION ALL
|
||||
SELECT 'wired.custom.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.custom.enabled' LIMIT 1), '0'), 'Enable custom legacy wired behaviour such as user-based cooldown exceptions and compatibility logic.'
|
||||
UNION ALL
|
||||
SELECT 'hotel.wired.furni.selection.count', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.furni.selection.count' LIMIT 1), '5'), 'Maximum number of furni that a wired box can store or select.'
|
||||
UNION ALL
|
||||
SELECT 'hotel.wired.max_delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.max_delay' LIMIT 1), '20'), 'Maximum delay value accepted by wired effects that support delayed execution.'
|
||||
UNION ALL
|
||||
SELECT 'hotel.wired.message.max_length', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.message.max_length' LIMIT 1), '100'), 'Maximum length of text fields used by wired messages and bot text effects.'
|
||||
UNION ALL
|
||||
SELECT 'wired.effect.teleport.delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.effect.teleport.delay' LIMIT 1), '500'), 'Delay in milliseconds used by wired teleport movement.'
|
||||
UNION ALL
|
||||
SELECT 'wired.place.under', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.place.under' LIMIT 1), '0'), 'Allow placing wired furniture underneath other items when room rules permit it.'
|
||||
UNION ALL
|
||||
SELECT 'wired.tick.interval.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.interval.ms' LIMIT 1), '50'), 'Global wired tick interval in milliseconds used by repeaters and other tick-driven wired items.'
|
||||
UNION ALL
|
||||
SELECT 'wired.tick.resolution', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.resolution' LIMIT 1), '100'), 'Legacy wired tick resolution value kept for compatibility with older wired timing setups.'
|
||||
UNION ALL
|
||||
SELECT 'wired.tick.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.debug' LIMIT 1), '0'), 'Enable verbose logging for the wired tick service.'
|
||||
UNION ALL
|
||||
SELECT 'wired.tick.thread.priority', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.thread.priority' LIMIT 1), '6'), 'Java thread priority used by the wired tick service.'
|
||||
UNION ALL
|
||||
SELECT 'wired.highscores.displaycount', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.highscores.displaycount' LIMIT 1), '25'), 'Maximum number of wired highscore entries shown to users when a highscore is displayed.'
|
||||
UNION ALL
|
||||
SELECT 'wired.abuse.max.recursion.depth', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.recursion.depth' LIMIT 1), '10'), 'Maximum recursive wired depth allowed before execution is stopped.'
|
||||
UNION ALL
|
||||
SELECT 'wired.abuse.max.events.per.window', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.events.per.window' LIMIT 1), '100'), 'Maximum amount of identical wired events allowed inside the abuse rate-limit window before a room ban is applied.'
|
||||
UNION ALL
|
||||
SELECT 'wired.abuse.rate.limit.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.rate.limit.window.ms' LIMIT 1), '10000'), 'Time window in milliseconds used by the wired abuse rate limiter.'
|
||||
UNION ALL
|
||||
SELECT 'wired.abuse.ban.duration.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.ban.duration.ms' LIMIT 1), '600000'), 'Duration in milliseconds of the temporary wired ban after abuse detection.'
|
||||
UNION ALL
|
||||
SELECT 'wired.monitor.usage.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.window.ms' LIMIT 1), '1000'), 'Rolling window size in milliseconds used to calculate wired usage in the :wired monitor.'
|
||||
UNION ALL
|
||||
SELECT 'wired.monitor.usage.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.limit' LIMIT 1), '1000'), 'Maximum wired usage budget allowed in one monitor window before EXECUTION_CAP is raised.'
|
||||
UNION ALL
|
||||
SELECT 'wired.monitor.delayed.events.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.delayed.events.limit' LIMIT 1), '100'), 'Maximum number of delayed wired events that can be queued in one room at the same time.'
|
||||
UNION ALL
|
||||
SELECT 'wired.monitor.overload.average.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.average.ms' LIMIT 1), '50'), 'Average execution time threshold in milliseconds that starts overload tracking.'
|
||||
UNION ALL
|
||||
SELECT 'wired.monitor.overload.peak.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.peak.ms' LIMIT 1), '150'), 'Peak single execution time threshold in milliseconds that starts overload tracking.'
|
||||
UNION ALL
|
||||
SELECT 'wired.monitor.overload.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.consecutive.windows' LIMIT 1), '2'), 'Number of consecutive overloaded monitor windows required before logging EXECUTOR_OVERLOAD.'
|
||||
UNION ALL
|
||||
SELECT 'wired.monitor.heavy.usage.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.usage.percent' LIMIT 1), '70'), 'Usage percentage threshold that contributes to marking a room as heavy in the :wired monitor.'
|
||||
UNION ALL
|
||||
SELECT 'wired.monitor.heavy.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.consecutive.windows' LIMIT 1), '5'), 'Number of consecutive windows above the heavy usage threshold required before the room is marked as heavy.'
|
||||
UNION ALL
|
||||
SELECT 'wired.monitor.heavy.delayed.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.delayed.percent' LIMIT 1), '60'), 'Delayed queue percentage threshold that also contributes to the heavy-room calculation.'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`value` = VALUES(`value`),
|
||||
`comment` = VALUES(`comment`);
|
||||
|
||||
DELETE FROM `emulator_settings`
|
||||
WHERE `key` IN (
|
||||
'wired.engine.enabled',
|
||||
'wired.engine.exclusive',
|
||||
'wired.engine.maxStepsPerStack',
|
||||
'wired.engine.debug',
|
||||
'wired.custom.enabled',
|
||||
'hotel.wired.furni.selection.count',
|
||||
'hotel.wired.max_delay',
|
||||
'hotel.wired.message.max_length',
|
||||
'wired.effect.teleport.delay',
|
||||
'wired.place.under',
|
||||
'wired.tick.interval.ms',
|
||||
'wired.tick.resolution',
|
||||
'wired.tick.debug',
|
||||
'wired.tick.thread.priority',
|
||||
'wired.highscores.displaycount',
|
||||
'wired.abuse.max.recursion.depth',
|
||||
'wired.abuse.max.events.per.window',
|
||||
'wired.abuse.rate.limit.window.ms',
|
||||
'wired.abuse.ban.duration.ms',
|
||||
'wired.monitor.usage.window.ms',
|
||||
'wired.monitor.usage.limit',
|
||||
'wired.monitor.delayed.events.limit',
|
||||
'wired.monitor.overload.average.ms',
|
||||
'wired.monitor.overload.peak.ms',
|
||||
'wired.monitor.overload.consecutive.windows',
|
||||
'wired.monitor.heavy.usage.percent',
|
||||
'wired.monitor.heavy.consecutive.windows',
|
||||
'wired.monitor.heavy.delayed.percent'
|
||||
);
|
||||
|
||||
UPDATE `emulator_settings` SET `comment` = 'Allow whispering while a user stands inside a mute area.' WHERE `key` = 'room.chat.mutearea.allow_whisper';
|
||||
UPDATE `emulator_settings` SET `comment` = 'HTML or text format used for room chat prefixes.' WHERE `key` = 'room.chat.prefix.format';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Badge code displayed on promoted rooms.' WHERE `key` = 'room.promotion.badge';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Image used by Rosie bubble notifications.' WHERE `key` = 'rosie.bubble.image.url';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Currency type used by Rosie when buying a room or room package.' WHERE `key` = 'rosie.buyroom.currency.type';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `runtime.threads`.' WHERE `key` = 'runtime.threads';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.private.chats`.' WHERE `key` = 'save.private.chats';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.room.chats`.' WHERE `key` = 'save.room.chats';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Expose moderation tickets to the scripter or automation tooling.' WHERE `key` = 'scripter.modtool.tickets';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for diamonds.' WHERE `key` = 'seasonal.currency.diamond';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for duckets.' WHERE `key` = 'seasonal.currency.ducket';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated display names for seasonal currency types.' WHERE `key` = 'seasonal.currency.names';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for pixels.' WHERE `key` = 'seasonal.currency.pixel';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for shells.' WHERE `key` = 'seasonal.currency.shell';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Primary seasonal currency type ID.' WHERE `key` = 'seasonal.primary.type';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated list of currency type IDs treated as seasonal currencies.' WHERE `key` = 'seasonal.types';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Achievement code granted for the HC subscription tier.' WHERE `key` = 'subscriptions.hc.achievement';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Number of days before expiry when HC discount offers become available.' WHERE `key` = 'subscriptions.hc.discount.days_before_end';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Enable discounted HC renewal offers.' WHERE `key` = 'subscriptions.hc.discount.enabled';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Reset tracked credits spent when the HC subscription expires.' WHERE `key` = 'subscriptions.hc.payday.creditsspent_reset_on_expire';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Currency rewarded by the HC payday system.' WHERE `key` = 'subscriptions.hc.payday.currency';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Enable the HC payday reward system.' WHERE `key` = 'subscriptions.hc.payday.enabled';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Date interval used between HC payday reward runs.' WHERE `key` = 'subscriptions.hc.payday.interval';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Next scheduled execution date for HC payday rewards.' WHERE `key` = 'subscriptions.hc.payday.next_date';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Percentage of eligible spending returned by HC payday.' WHERE `key` = 'subscriptions.hc.payday.percentage';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated streak thresholds and rewards for HC payday.' WHERE `key` = 'subscriptions.hc.payday.streak';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Enable the subscription background scheduler.' WHERE `key` = 'subscriptions.scheduler.enabled';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Interval in minutes between subscription scheduler runs.' WHERE `key` = 'subscriptions.scheduler.interval';
|
||||
UPDATE `emulator_settings` SET `comment` = 'Compatibility marker used by the custom team wired implementation. Do not remove.' WHERE `key` = 'team.wired.update.rc-1';
|
||||
UPDATE `emulator_settings` SET `comment` = 'API key used by the YouTube integration.' WHERE `key` = 'youtube.apikey';
|
||||
|
||||
-- =============================================================
|
||||
-- Permissions normalization is handled by 005_normalize_permissions_schema.sql
|
||||
-- (Removed from this file to avoid DELIMITER issues in HeidiSQL.)
|
||||
-- =============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_wired_settings` (
|
||||
`room_id` int(11) NOT NULL,
|
||||
`inspect_mask` int(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can open and inspect Wired in the room. 1=everyone, 2=users with rights, 4=group members, 8=group admins.',
|
||||
`modify_mask` int(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can modify Wired in the room. 2=users with rights, 4=group members, 8=group admins.',
|
||||
PRIMARY KEY (`room_id`),
|
||||
CONSTRAINT `fk_room_wired_settings_room_id` FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_user_wired_variables` (
|
||||
`room_id` int(11) NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`variable_item_id` int(11) NOT NULL,
|
||||
`value` int(11) DEFAULT NULL,
|
||||
`created_at` int(11) NOT NULL DEFAULT 0,
|
||||
`updated_at` int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`room_id`, `user_id`, `variable_item_id`),
|
||||
KEY `idx_room_user_wired_variables_room_item` (`room_id`, `variable_item_id`),
|
||||
KEY `idx_room_user_wired_variables_user` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_furni_wired_variables` (
|
||||
`room_id` int(11) NOT NULL,
|
||||
`furni_id` int(11) NOT NULL,
|
||||
`variable_item_id` int(11) NOT NULL,
|
||||
`value` int(11) DEFAULT NULL,
|
||||
`created_at` int(11) NOT NULL DEFAULT 0,
|
||||
`updated_at` int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`room_id`, `furni_id`, `variable_item_id`),
|
||||
KEY `idx_room_furni_wired_variables_room_item` (`room_id`, `variable_item_id`),
|
||||
KEY `idx_room_furni_wired_variables_furni` (`furni_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_wired_variables` (
|
||||
`room_id` int(11) NOT NULL,
|
||||
`variable_item_id` int(11) NOT NULL,
|
||||
`value` int(11) NOT NULL DEFAULT 0,
|
||||
`created_at` int(11) NOT NULL DEFAULT 0,
|
||||
`updated_at` int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`room_id`, `variable_item_id`),
|
||||
KEY `idx_room_wired_variables_room_item` (`room_id`, `variable_item_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
ALTER TABLE `room_user_wired_variables`
|
||||
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
|
||||
|
||||
ALTER TABLE `room_user_wired_variables`
|
||||
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
|
||||
|
||||
UPDATE `room_user_wired_variables`
|
||||
SET
|
||||
`created_at` = IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()),
|
||||
`updated_at` = IF(`updated_at` > 0, `updated_at`, IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()));
|
||||
|
||||
ALTER TABLE `room_furni_wired_variables`
|
||||
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
|
||||
|
||||
ALTER TABLE `room_furni_wired_variables`
|
||||
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
|
||||
|
||||
UPDATE `room_furni_wired_variables`
|
||||
SET
|
||||
`created_at` = IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()),
|
||||
`updated_at` = IF(`updated_at` > 0, `updated_at`, IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()));
|
||||
|
||||
ALTER TABLE `room_wired_variables`
|
||||
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
|
||||
|
||||
ALTER TABLE `room_wired_variables`
|
||||
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
|
||||
|
||||
UPDATE `room_wired_variables`
|
||||
SET
|
||||
`created_at` = 0,
|
||||
`updated_at` = IF(`updated_at` > 0, `updated_at`, UNIX_TIMESTAMP());
|
||||
|
||||
INSERT INTO `chat_bubbles` (`type`, `name`, `permission`, `overridable`, `triggers_talking_furniture`) VALUES
|
||||
(200, 'SHOW_MESSAGE_RED', '', 1, 0),
|
||||
(201, 'SHOW_MESSAGE_GREEN', '', 1, 0),
|
||||
(202, 'SHOW_MESSAGE_BLUE', '', 1, 0),
|
||||
(210, 'SHOW_MESSAGE_ALERT', '', 1, 0),
|
||||
(211, 'SHOW_MESSAGE_INFO', '', 1, 0),
|
||||
(212, 'SHOW_MESSAGE_WARNING', '', 1, 0),
|
||||
(220, 'SHOW_MESSAGE_WRONG', '', 1, 0),
|
||||
(221, 'SHOW_MESSAGE_WRONG_CIRCLED', '', 1, 0),
|
||||
(222, 'SHOW_MESSAGE_CORRECT', '', 1, 0),
|
||||
(223, 'SHOW_MESSAGE_CORRECT_CIRCLED', '', 1, 0),
|
||||
(224, 'SHOW_MESSAGE_QUESTION', '', 1, 0),
|
||||
(225, 'SHOW_MESSAGE_QUESTION_CIRCLED', '', 1, 0),
|
||||
(226, 'SHOW_MESSAGE_ARROW_UP', '', 1, 0),
|
||||
(227, 'SHOW_MESSAGE_ARROW_UP_CIRCLED', '', 1, 0),
|
||||
(228, 'SHOW_MESSAGE_ARROW_DOWN', '', 1, 0),
|
||||
(229, 'SHOW_MESSAGE_ARROW_DOWN_CIRCLED', '', 1, 0),
|
||||
(250, 'SHOW_MESSAGE_SKULL', '', 1, 0),
|
||||
(251, 'SHOW_MESSAGE_SKULL_ALT', '', 1, 0),
|
||||
(252, 'SHOW_MESSAGE_MAGNIFIER', '', 1, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`name` = VALUES(`name`),
|
||||
`permission` = VALUES(`permission`),
|
||||
`overridable` = VALUES(`overridable`),
|
||||
`triggers_talking_furniture` = VALUES(`triggers_talking_furniture`);
|
||||
|
||||
ALTER TABLE `catalog_club_offers`
|
||||
MODIFY COLUMN `type` ENUM('HC', 'VIP', 'BUILDERS_CLUB', 'BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC';
|
||||
|
||||
ALTER TABLE `catalog_pages`
|
||||
MODIFY COLUMN `page_layout` ENUM(
|
||||
'default_3x3',
|
||||
'club_buy',
|
||||
'club_gift',
|
||||
'frontpage',
|
||||
'spaces',
|
||||
'recycler',
|
||||
'recycler_info',
|
||||
'recycler_prizes',
|
||||
'trophies',
|
||||
'plasto',
|
||||
'marketplace',
|
||||
'marketplace_own_items',
|
||||
'spaces_new',
|
||||
'soundmachine',
|
||||
'guilds',
|
||||
'guild_furni',
|
||||
'info_duckets',
|
||||
'info_rentables',
|
||||
'info_pets',
|
||||
'roomads',
|
||||
'single_bundle',
|
||||
'sold_ltd_items',
|
||||
'badge_display',
|
||||
'bots',
|
||||
'pets',
|
||||
'pets2',
|
||||
'pets3',
|
||||
'productpage1',
|
||||
'room_bundle',
|
||||
'recent_purchases',
|
||||
'default_3x3_color_grouping',
|
||||
'guild_forum',
|
||||
'vip_buy',
|
||||
'info_loyalty',
|
||||
'loyalty_vip_buy',
|
||||
'collectibles',
|
||||
'petcustomization',
|
||||
'frontpage_featured',
|
||||
'builders_club_frontpage',
|
||||
'builders_club_addons',
|
||||
'builders_club_loyalty'
|
||||
) NOT NULL DEFAULT 'default_3x3';
|
||||
|
||||
ALTER TABLE `catalog_pages`
|
||||
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL', 'BUILDER', 'BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `club_only`;
|
||||
|
||||
ALTER TABLE `rooms`
|
||||
ADD COLUMN IF NOT EXISTS `builders_club_trial_locked` TINYINT(1) NOT NULL DEFAULT 0 AFTER `allow_underpass`,
|
||||
ADD COLUMN IF NOT EXISTS `builders_club_original_state` VARCHAR(16) NOT NULL DEFAULT 'open' AFTER `builders_club_trial_locked`;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `builders_club_items` (
|
||||
`item_id` INT(11) NOT NULL,
|
||||
`user_id` INT(11) NOT NULL,
|
||||
`room_id` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`item_id`),
|
||||
KEY `idx_builders_club_items_user_id` (`user_id`),
|
||||
KEY `idx_builders_club_items_room_id` (`room_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
|
||||
|
||||
ALTER TABLE `catalog_pages_bc`
|
||||
MODIFY COLUMN `page_layout` ENUM(
|
||||
'default_3x3',
|
||||
'club_buy',
|
||||
'club_gift',
|
||||
'frontpage',
|
||||
'spaces',
|
||||
'recycler',
|
||||
'recycler_info',
|
||||
'recycler_prizes',
|
||||
'trophies',
|
||||
'plasto',
|
||||
'marketplace',
|
||||
'marketplace_own_items',
|
||||
'spaces_new',
|
||||
'soundmachine',
|
||||
'guilds',
|
||||
'guild_furni',
|
||||
'info_duckets',
|
||||
'info_rentables',
|
||||
'info_pets',
|
||||
'roomads',
|
||||
'single_bundle',
|
||||
'sold_ltd_items',
|
||||
'badge_display',
|
||||
'bots',
|
||||
'pets',
|
||||
'pets2',
|
||||
'pets3',
|
||||
'productpage1',
|
||||
'room_bundle',
|
||||
'recent_purchases',
|
||||
'default_3x3_color_grouping',
|
||||
'guild_forum',
|
||||
'vip_buy',
|
||||
'info_loyalty',
|
||||
'loyalty_vip_buy',
|
||||
'collectibles',
|
||||
'petcustomization',
|
||||
'frontpage_featured',
|
||||
'builders_club_frontpage',
|
||||
'builders_club_addons',
|
||||
'builders_club_loyalty'
|
||||
) NOT NULL DEFAULT 'default_3x3';
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
ALTER TABLE users
|
||||
ADD COLUMN `last_username_change` INT(11) NOT NULL;
|
||||
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');
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.eu.habbo</groupId>
|
||||
<artifactId>Habbo</artifactId>
|
||||
<version>4.1.15</version>
|
||||
<version>4.2.13</version>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
@@ -50,6 +50,7 @@ public class RoomUserPetComposer extends MessageComposer {
|
||||
this.response.appendString("");
|
||||
this.response.appendString("unknown");
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
return this.response;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ package com.eu.habbo.habbohotel.commands;
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.catalog.CatalogManager;
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.MessagesForYouComposer;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
@@ -13,9 +11,10 @@ public class AboutCommand extends Command {
|
||||
public AboutCommand() {
|
||||
super(null, new String[]{"about", "info", "online", "server"});
|
||||
}
|
||||
public static String credits = "Arcturus Morningstar is an opensource project based on Arcturus By TheGeneral \n" +
|
||||
"The Following people have all contributed to this emulator:\n" +
|
||||
"TheGeneral\n Beny\n Alejandro\n Capheus\n Skeletor\n Harmonic\n Mike\n Remco\n zGrav \n Quadral \n Harmony\n Swirny\n ArpyAge\n Mikkel\n Rodolfo\n Rasmus\n Kitt Mustang\n Snaiker\n nttzx\n necmi\n Dome\n Jose Flores\n Cam\n Oliver\n Narzo\n Tenshie\n MartenM\n Ridge\n SenpaiDipper\n Snaiker\n Thijmen\n DuckieTM\n simoleo89\n Medievalshell\n Lorenzo (the wired master)";
|
||||
|
||||
public static final String NITRO_INFO_SENTINEL = "[NITRO_INFO_V1]";
|
||||
public static final String REPORT_ISSUES_URL = "https://github.com/duckietm/Nitro-V3/issues";
|
||||
|
||||
@Override
|
||||
public boolean handle(GameClient gameClient, String[] params) {
|
||||
|
||||
@@ -27,28 +26,31 @@ public class AboutCommand extends Command {
|
||||
long minute = TimeUnit.SECONDS.toMinutes(seconds) - (TimeUnit.SECONDS.toHours(seconds) * 60);
|
||||
long second = TimeUnit.SECONDS.toSeconds(seconds) - (TimeUnit.SECONDS.toMinutes(seconds) * 60);
|
||||
|
||||
String message = "<b>" + Emulator.version + "</b>\r\n";
|
||||
StringBuilder message = new StringBuilder();
|
||||
message.append(NITRO_INFO_SENTINEL).append("\r");
|
||||
message.append("<b>").append(Emulator.version).append("</b>\r\n");
|
||||
|
||||
if (Emulator.getConfig().getBoolean("info.shown", true)) {
|
||||
message += "<b>Hotel Statistics</b>\r" +
|
||||
"- Online Users: " + Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "\r" +
|
||||
"- Active Rooms: " + Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size() + "\r" +
|
||||
"- Shop: " + Emulator.getGameEnvironment().getCatalogManager().catalogPages.size() + " pages and " + CatalogManager.catalogItemAmount + " items. \r" +
|
||||
"- Furni: " + Emulator.getGameEnvironment().getItemManager().getItems().size() + " item definitions" + "\r" +
|
||||
"\n" +
|
||||
"<b>Server Statistics</b>\r" +
|
||||
"- Uptime: " + day + (day > 1 ? " days, " : " day, ") + hours + (hours > 1 ? " hours, " : " hour, ") + minute + (minute > 1 ? " minutes, " : " minute, ") + second + (second > 1 ? " seconds!" : " second!") + "\r" +
|
||||
"- RAM Usage: " + (Emulator.getRuntime().totalMemory() - Emulator.getRuntime().freeMemory()) / (1024 * 1024) + "/" + (Emulator.getRuntime().freeMemory()) / (1024 * 1024) + "MB\r" +
|
||||
"- CPU Cores: " + Emulator.getRuntime().availableProcessors() + "\r" +
|
||||
"- Total Memory: " + Emulator.getRuntime().maxMemory() / (1024 * 1024) + "MB" + "\r\n";
|
||||
message.append("<b>Hotel Statistics</b>\r")
|
||||
.append("- Online Users: ").append(Emulator.getGameEnvironment().getHabboManager().getOnlineCount()).append("\r")
|
||||
.append("- Active Rooms: ").append(Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size()).append("\r")
|
||||
.append("- Shop: ").append(Emulator.getGameEnvironment().getCatalogManager().catalogPages.size()).append(" pages and ").append(CatalogManager.catalogItemAmount).append(" items.\r")
|
||||
.append("- Furni: ").append(Emulator.getGameEnvironment().getItemManager().getItems().size()).append(" item definitions\r")
|
||||
.append("\n")
|
||||
.append("<b>Server Statistics</b>\r")
|
||||
.append("- Uptime: ").append(day).append(day == 1 ? " day, " : " days, ").append(hours).append(hours == 1 ? " hour, " : " hours, ").append(minute).append(minute == 1 ? " minute, " : " minutes, ").append(second).append(second == 1 ? " second!" : " seconds!").append("\r")
|
||||
.append("- RAM Usage: ").append((Emulator.getRuntime().totalMemory() - Emulator.getRuntime().freeMemory()) / (1024 * 1024)).append("/").append((Emulator.getRuntime().freeMemory()) / (1024 * 1024)).append("MB\r")
|
||||
.append("- CPU Cores: ").append(Emulator.getRuntime().availableProcessors()).append("\r")
|
||||
.append("- Total Memory: ").append(Emulator.getRuntime().maxMemory() / (1024 * 1024)).append("MB\r\n");
|
||||
}
|
||||
|
||||
message += "\r" +
|
||||
message.append("<b>Credits</b>\r")
|
||||
.append("- The General\r")
|
||||
.append("- Krews Team\r")
|
||||
.append("- DuckieTM, simoleo89, Medievalshell, Lorenzo (the wired master), Remco\r\n")
|
||||
.append("Report issues at: ").append(REPORT_ISSUES_URL);
|
||||
|
||||
"<b>Thanks for using Arcturus. Report issues on the forums. http://arcturus.wf \r\r" +
|
||||
" - The General";
|
||||
gameClient.getHabbo().alert(message);
|
||||
gameClient.sendResponse(new MessagesForYouComposer(Collections.singletonList(credits)));
|
||||
gameClient.getHabbo().alert(message.toString());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+40
-1
@@ -2,7 +2,12 @@ package com.eu.habbo.habbohotel.commands;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.habbohotel.permissions.PermissionsManager;
|
||||
import com.eu.habbo.habbohotel.permissions.Rank;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||
import com.eu.habbo.messages.outgoing.users.UserPermissionsComposer;
|
||||
|
||||
public class UpdatePermissionsCommand extends Command {
|
||||
public UpdatePermissionsCommand() {
|
||||
@@ -13,7 +18,41 @@ public class UpdatePermissionsCommand extends Command {
|
||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||
Emulator.getGameEnvironment().getPermissionsManager().reload();
|
||||
|
||||
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_update_permissions"), RoomChatMessageBubbles.ALERT);
|
||||
// PermissionsManager.reload() rebuilt the rank table — each online
|
||||
// Habbo's HabboInfo still references the OLD Rank object, so
|
||||
// server-side hasPermission() / wire composers would keep
|
||||
// reporting stale data until relogin. Re-bind every connected
|
||||
// user to the freshly-loaded Rank by id, then ship the new
|
||||
// UserPermissionsComposer (which carries clubLevel,
|
||||
// securityLevel, isAmbassador, rank metadata and the resolved
|
||||
// permission_definitions map) so Nitro clients' React-side
|
||||
// useHasPermission(key) / useUserRank() / useUserPermissions()
|
||||
// consumers re-render against the updated tables without an F5.
|
||||
HabboManager habboManager = Emulator.getGameEnvironment().getHabboManager();
|
||||
PermissionsManager permissions = Emulator.getGameEnvironment().getPermissionsManager();
|
||||
|
||||
int refreshed = 0;
|
||||
|
||||
for (Habbo habbo : habboManager.getOnlineHabbos().values()) {
|
||||
if (habbo == null || habbo.getHabboInfo() == null || habbo.getClient() == null) continue;
|
||||
|
||||
int currentRankId = habbo.getHabboInfo().getRank().getId();
|
||||
// Defensive fallback: if the admin deleted the rank from the
|
||||
// permission_ranks table between sessions, fall back to rank 1
|
||||
// (Member) so the user isn't stranded with a null Rank.
|
||||
Rank freshRank = permissions.rankExists(currentRankId)
|
||||
? permissions.getRank(currentRankId)
|
||||
: permissions.getRank(1);
|
||||
|
||||
habbo.getHabboInfo().setRank(freshRank);
|
||||
habbo.getClient().sendResponse(new UserPermissionsComposer(habbo));
|
||||
refreshed++;
|
||||
}
|
||||
|
||||
gameClient.getHabbo().whisper(
|
||||
Emulator.getTexts().getValue("commands.succes.cmd_update_permissions") + " (" + refreshed + " online refreshed)",
|
||||
RoomChatMessageBubbles.ALERT
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -651,6 +651,10 @@ public class ModToolManager {
|
||||
sender.getClient().sendResponse(new ModToolIssueHandledComposer(ModToolIssueHandledComposer.ABUSIVE));
|
||||
}
|
||||
|
||||
// Reporter (the user who opened the CFH) gets their abusive
|
||||
// counter bumped — the legacy stat shown in the User Info table.
|
||||
bumpUserSettingCounter(issue.senderId, "cfh_abusive");
|
||||
|
||||
this.updateTicketToMods(issue);
|
||||
|
||||
this.removeTicket(issue);
|
||||
@@ -737,4 +741,38 @@ public class ModToolManager {
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments a single integer counter on `users_settings` for the
|
||||
* given user. Used by the moderation sanction handlers to bump the
|
||||
* legacy counters that `ModToolUserInfoComposer` surfaces (cfh_warnings,
|
||||
* cfh_bans, cfh_abusive, tradelock_amount) — historically these were
|
||||
* only ever incremented by the CFH submission path, so a user could
|
||||
* accumulate any number of bans/mutes without the User Info table
|
||||
* reflecting it.
|
||||
*
|
||||
* Restricted to a whitelisted column name to keep the dynamic SQL
|
||||
* safe; the caller passes a Permission-style constant.
|
||||
*/
|
||||
public static void bumpUserSettingCounter(int userId, String column) {
|
||||
switch (column) {
|
||||
case "cfh_warnings":
|
||||
case "cfh_bans":
|
||||
case "cfh_abusive":
|
||||
case "tradelock_amount":
|
||||
break;
|
||||
default:
|
||||
LOGGER.warn("Refusing to bump unrecognized user_settings column: {}", column);
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE users_settings SET " + column + " = " + column + " + 1 WHERE user_id = ?")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception bumping {} for user {}", column, userId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ public class HabboInfo implements Runnable {
|
||||
private int InfostandStand;
|
||||
private int InfostandOverlay;
|
||||
private int InfostandCardBg;
|
||||
private int InfostandBorder;
|
||||
private int loadingRoom;
|
||||
private Room currentRoom;
|
||||
private String roomEntryMethod = "door";
|
||||
@@ -93,6 +94,11 @@ public class HabboInfo implements Runnable {
|
||||
this.InfostandStand = set.getInt("background_stand_id");
|
||||
this.InfostandOverlay = set.getInt("background_overlay_id");
|
||||
this.InfostandCardBg = set.getInt("background_card_id");
|
||||
try {
|
||||
this.InfostandBorder = set.getInt("background_border_id");
|
||||
} catch (SQLException ignored) {
|
||||
this.InfostandBorder = 0;
|
||||
}
|
||||
this.currentRoom = null;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
@@ -300,6 +306,15 @@ public class HabboInfo implements Runnable {
|
||||
public void setInfostandCardBg(int infostandCardBg) {
|
||||
InfostandCardBg = infostandCardBg;
|
||||
}
|
||||
|
||||
public int getInfostandBorder() {
|
||||
return InfostandBorder;
|
||||
}
|
||||
|
||||
public void setInfostandBorder(int infostandBorder) {
|
||||
InfostandBorder = infostandBorder;
|
||||
}
|
||||
|
||||
public Rank getRank() {
|
||||
return this.rank;
|
||||
}
|
||||
@@ -587,7 +602,7 @@ public class HabboInfo implements Runnable {
|
||||
|
||||
try {
|
||||
SqlQueries.update(
|
||||
"UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ?, background_card_id = ? WHERE id = ?",
|
||||
"UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ?, background_card_id = ?, background_border_id = ? WHERE id = ?",
|
||||
this.motto,
|
||||
this.online ? "1" : "0",
|
||||
this.look,
|
||||
@@ -604,6 +619,7 @@ public class HabboInfo implements Runnable {
|
||||
this.InfostandStand,
|
||||
this.InfostandOverlay,
|
||||
this.InfostandCardBg,
|
||||
this.InfostandBorder,
|
||||
this.id);
|
||||
} catch (SqlQueries.DataAccessException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
|
||||
@@ -95,7 +95,7 @@ public class HabboManager {
|
||||
int userId = 0;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) {
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
|
||||
statement.setString(1, sso);
|
||||
try (ResultSet s = statement.executeQuery()) {
|
||||
if (s.next()) {
|
||||
@@ -121,7 +121,7 @@ public class HabboManager {
|
||||
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE auth_ticket = ? LIMIT 1")) {
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
|
||||
statement.setString(1, sso);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.next()) {
|
||||
|
||||
+5
-3
@@ -24,7 +24,8 @@ public class InfostandBackgroundManager {
|
||||
BACKGROUND("background"),
|
||||
STAND("stand"),
|
||||
OVERLAY("overlay"),
|
||||
CARD("card");
|
||||
CARD("card"),
|
||||
BORDER("border");
|
||||
|
||||
public final String dbValue;
|
||||
|
||||
@@ -89,11 +90,12 @@ public class InfostandBackgroundManager {
|
||||
this.enforce = loaded > 0;
|
||||
|
||||
if (this.enforce) {
|
||||
LOGGER.info("InfostandBackgroundManager -> Loaded {} backgrounds, {} stands, {} overlays, {} cards from infostand_backgrounds.",
|
||||
LOGGER.info("InfostandBackgroundManager -> Loaded {} backgrounds, {} stands, {} overlays, {} cards, {} borders from infostand_backgrounds.",
|
||||
this.entries.get(Category.BACKGROUND).size(),
|
||||
this.entries.get(Category.STAND).size(),
|
||||
this.entries.get(Category.OVERLAY).size(),
|
||||
this.entries.get(Category.CARD).size());
|
||||
this.entries.get(Category.CARD).size(),
|
||||
this.entries.get(Category.BORDER).size());
|
||||
} else {
|
||||
LOGGER.info("InfostandBackgroundManager -> infostand_backgrounds is empty, server-side validation disabled (only range clamp will apply).");
|
||||
}
|
||||
|
||||
+271
-61
@@ -13,107 +13,317 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Manages reading and writing of FurnitureData.json entries.
|
||||
* Resolves the file path from emulator config keys.
|
||||
* Manages reading and writing of FurnitureData entries.
|
||||
*
|
||||
* Accepts both legacy single-file layouts (FurnitureData.json) and the split
|
||||
* directory layout introduced by the split-aware loader on the Nitro V3 side:
|
||||
*
|
||||
* <base>/
|
||||
* manifest.json5 OPTIONAL { "tiers": ["core", "custom", "seasonal"] }
|
||||
* core/manifest.json5 REQUIRED { "files": ["floor-001.json5", ...] }
|
||||
* core/*.json5
|
||||
* custom/manifest.json5 OPTIONAL
|
||||
* seasonal/manifest.json5 OPTIONAL
|
||||
*
|
||||
* The path is resolved from the emulator config:
|
||||
*
|
||||
* furni.editor.renderer.config.path -> renderer-config.json (read for the
|
||||
* furnidata.url value)
|
||||
* furni.editor.asset.base.path -> filesystem base used to derive the
|
||||
* local path from an http(s) URL
|
||||
*/
|
||||
public class FurniDataManager {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FurniDataManager.class);
|
||||
|
||||
private static final List<String> DEFAULT_TIERS = Arrays.asList("core", "custom", "seasonal");
|
||||
private static final List<String> MANIFEST_NAMES = Arrays.asList("manifest.json5", "manifest.json");
|
||||
private static final List<String> SECTIONS = Arrays.asList("roomitemtypes", "wallitemtypes");
|
||||
|
||||
/**
|
||||
* Get the JSON string for a specific item from FurnitureData.json.
|
||||
* Get the JSON string for a specific item.
|
||||
* Returns "{}" if not found or on error.
|
||||
*/
|
||||
public static String getItemJson(int itemId) {
|
||||
try {
|
||||
Path furniDataPath = resolveFurniDataPath();
|
||||
if (furniDataPath == null || !Files.exists(furniDataPath)) {
|
||||
return "{}";
|
||||
ResolvedSource source = resolveSource();
|
||||
if (source == null) return "{}";
|
||||
|
||||
if (source.directory) {
|
||||
return findItemInSplitDir(source.path, itemId);
|
||||
}
|
||||
|
||||
String content = Files.readString(furniDataPath, StandardCharsets.UTF_8);
|
||||
JsonObject root = JsonParser.parseString(content).getAsJsonObject();
|
||||
if (!Files.exists(source.path)) return "{}";
|
||||
|
||||
// Search in both "roomitemtypes" and "wallitemtypes"
|
||||
for (String section : new String[]{"roomitemtypes", "wallitemtypes"}) {
|
||||
if (!root.has(section)) continue;
|
||||
JsonObject sectionObj = root.getAsJsonObject(section);
|
||||
if (!sectionObj.has("furnitype")) continue;
|
||||
JsonArray types = sectionObj.getAsJsonArray("furnitype");
|
||||
|
||||
for (JsonElement el : types) {
|
||||
JsonObject obj = el.getAsJsonObject();
|
||||
if (obj.has("id") && obj.get("id").getAsInt() == itemId) {
|
||||
return obj.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
String content = readJson5(source.path);
|
||||
return findItemInRoot(JsonParser.parseString(content).getAsJsonObject(), itemId);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to read FurnitureData.json for item " + itemId, e);
|
||||
LOGGER.warn("Failed to read FurnitureData for item " + itemId, e);
|
||||
}
|
||||
|
||||
return "{}";
|
||||
}
|
||||
|
||||
private static String findItemInRoot(JsonObject root, int itemId) {
|
||||
for (String section : SECTIONS) {
|
||||
if (!root.has(section)) continue;
|
||||
JsonObject sectionObj = root.getAsJsonObject(section);
|
||||
if (!sectionObj.has("furnitype")) continue;
|
||||
JsonArray types = sectionObj.getAsJsonArray("furnitype");
|
||||
|
||||
for (JsonElement el : types) {
|
||||
JsonObject obj = el.getAsJsonObject();
|
||||
if (obj.has("id") && obj.get("id").getAsInt() == itemId) {
|
||||
return obj.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the path to FurnitureData.json from emulator config.
|
||||
* Walk the split directory layout looking for an item by id.
|
||||
* Later tiers (custom, then seasonal) override earlier ones.
|
||||
*/
|
||||
private static Path resolveFurniDataPath() {
|
||||
private static String findItemInSplitDir(Path baseDir, int itemId) {
|
||||
if (!Files.isDirectory(baseDir)) return "{}";
|
||||
|
||||
List<String> tiers = readTiersManifest(baseDir);
|
||||
String found = null;
|
||||
|
||||
for (String tier : tiers) {
|
||||
Path tierDir = baseDir.resolve(tier);
|
||||
if (!Files.isDirectory(tierDir)) continue;
|
||||
|
||||
List<String> files = readFilesManifest(tierDir);
|
||||
for (String fileName : files) {
|
||||
Path file = tierDir.resolve(fileName);
|
||||
if (!Files.exists(file)) continue;
|
||||
|
||||
try {
|
||||
String content = readJson5(file);
|
||||
JsonObject obj = JsonParser.parseString(content).getAsJsonObject();
|
||||
String match = findItemInRoot(obj, itemId);
|
||||
if (match != null) found = match;
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to parse split gamedata file " + file, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return found != null ? found : "{}";
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<String> readTiersManifest(Path baseDir) {
|
||||
Path manifest = firstExisting(baseDir, MANIFEST_NAMES);
|
||||
if (manifest == null) return DEFAULT_TIERS;
|
||||
|
||||
try {
|
||||
String content = readJson5(manifest);
|
||||
JsonObject obj = JsonParser.parseString(content).getAsJsonObject();
|
||||
if (obj.has("tiers") && obj.get("tiers").isJsonArray()) {
|
||||
JsonArray arr = obj.getAsJsonArray("tiers");
|
||||
List<String> out = new java.util.ArrayList<>();
|
||||
for (JsonElement el : arr) out.add(el.getAsString());
|
||||
if (!out.isEmpty()) return out;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to read root manifest " + manifest + ", falling back to default tiers", e);
|
||||
}
|
||||
return DEFAULT_TIERS;
|
||||
}
|
||||
|
||||
private static List<String> readFilesManifest(Path tierDir) {
|
||||
Path manifest = firstExisting(tierDir, MANIFEST_NAMES);
|
||||
if (manifest == null) return java.util.Collections.emptyList();
|
||||
|
||||
try {
|
||||
String content = readJson5(manifest);
|
||||
JsonObject obj = JsonParser.parseString(content).getAsJsonObject();
|
||||
if (obj.has("files") && obj.get("files").isJsonArray()) {
|
||||
JsonArray arr = obj.getAsJsonArray("files");
|
||||
List<String> out = new java.util.ArrayList<>();
|
||||
for (JsonElement el : arr) out.add(el.getAsString());
|
||||
return out;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to read tier manifest " + manifest, e);
|
||||
}
|
||||
return java.util.Collections.emptyList();
|
||||
}
|
||||
|
||||
private static Path firstExisting(Path dir, List<String> names) {
|
||||
for (String name : names) {
|
||||
Path p = dir.resolve(name);
|
||||
if (Files.exists(p)) return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a JSON or JSON5 file. Strips line and block comments and trailing
|
||||
* commas so Gson can parse the result. String contents are preserved
|
||||
* verbatim; comments embedded inside strings are not removed.
|
||||
*/
|
||||
private static String readJson5(Path path) throws IOException {
|
||||
String raw = Files.readString(path, StandardCharsets.UTF_8);
|
||||
return stripJson5(raw);
|
||||
}
|
||||
|
||||
static String stripJson5(String content) {
|
||||
if (content == null || content.isEmpty()) return content;
|
||||
|
||||
StringBuilder out = new StringBuilder(content.length());
|
||||
int i = 0;
|
||||
int len = content.length();
|
||||
boolean inString = false;
|
||||
char stringChar = 0;
|
||||
boolean escape = false;
|
||||
|
||||
while (i < len) {
|
||||
char c = content.charAt(i);
|
||||
|
||||
if (inString) {
|
||||
out.append(c);
|
||||
if (escape) {
|
||||
escape = false;
|
||||
} else if (c == '\\') {
|
||||
escape = true;
|
||||
} else if (c == stringChar) {
|
||||
inString = false;
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"' || c == '\'') {
|
||||
inString = true;
|
||||
stringChar = c;
|
||||
out.append(c);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '/' && i + 1 < len) {
|
||||
char next = content.charAt(i + 1);
|
||||
if (next == '/') {
|
||||
int eol = content.indexOf('\n', i + 2);
|
||||
if (eol < 0) { i = len; break; }
|
||||
i = eol;
|
||||
continue;
|
||||
}
|
||||
if (next == '*') {
|
||||
int end = content.indexOf("*/", i + 2);
|
||||
if (end < 0) { i = len; break; }
|
||||
i = end + 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
out.append(c);
|
||||
i++;
|
||||
}
|
||||
|
||||
String stripped = out.toString();
|
||||
// Remove trailing commas before } or ]
|
||||
stripped = stripped.replaceAll(",(\\s*[}\\]])", "$1");
|
||||
return stripped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the resolved location of the furnidata source: either a single
|
||||
* file or a directory in split-layout mode.
|
||||
*/
|
||||
private static class ResolvedSource {
|
||||
final Path path;
|
||||
final boolean directory;
|
||||
|
||||
ResolvedSource(Path path, boolean directory) {
|
||||
this.path = path;
|
||||
this.directory = directory;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the location of the furnidata source. Returns null if no
|
||||
* candidate can be found.
|
||||
*/
|
||||
private static ResolvedSource resolveSource() {
|
||||
try {
|
||||
String configPath = Emulator.getConfig().getValue("furni.editor.renderer.config.path", "");
|
||||
|
||||
if (configPath.isEmpty()) {
|
||||
// Fallback: try common locations
|
||||
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||
if (!basePath.isEmpty()) {
|
||||
Path candidate = Paths.get(basePath, "FurnitureData.json");
|
||||
if (Files.exists(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
Path fallback = fallbackToBasePath();
|
||||
return fallback != null ? new ResolvedSource(fallback, Files.isDirectory(fallback)) : null;
|
||||
}
|
||||
|
||||
// Read the renderer config to find the furnidata URL/path
|
||||
Path rendererConfig = Paths.get(configPath);
|
||||
if (!Files.exists(rendererConfig)) return null;
|
||||
|
||||
String rendererContent = Files.readString(rendererConfig, StandardCharsets.UTF_8);
|
||||
String rendererContent = readJson5(rendererConfig);
|
||||
JsonObject rendererObj = JsonParser.parseString(rendererContent).getAsJsonObject();
|
||||
|
||||
if (rendererObj.has("furnidata.url")) {
|
||||
String furniUrl = rendererObj.get("furnidata.url").getAsString();
|
||||
if (!rendererObj.has("furnidata.url")) return null;
|
||||
|
||||
// Skip unresolved placeholders like ${gamedata.url}
|
||||
if (furniUrl.contains("${")) {
|
||||
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||
if (!basePath.isEmpty()) {
|
||||
Path candidate = Paths.get(basePath, "FurnitureData.json");
|
||||
if (Files.exists(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
String furniUrl = rendererObj.get("furnidata.url").getAsString();
|
||||
|
||||
// Strip query string (?v=1 etc.)
|
||||
String cleanUrl = furniUrl.contains("?") ? furniUrl.substring(0, furniUrl.indexOf('?')) : furniUrl;
|
||||
|
||||
// If it's a local file path (not http), use it directly
|
||||
if (!cleanUrl.startsWith("http")) {
|
||||
return Paths.get(cleanUrl);
|
||||
}
|
||||
|
||||
// For http URLs, try to derive local path from base path
|
||||
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||
if (!basePath.isEmpty()) {
|
||||
// Extract filename from URL (without query string)
|
||||
String filename = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1);
|
||||
return Paths.get(basePath, filename);
|
||||
}
|
||||
if (furniUrl.contains("${")) {
|
||||
Path fallback = fallbackToBasePath();
|
||||
return fallback != null ? new ResolvedSource(fallback, Files.isDirectory(fallback)) : null;
|
||||
}
|
||||
|
||||
// Strip query string and fragment (e.g. ?v=123 or #anchor)
|
||||
String cleanUrl = furniUrl;
|
||||
int q = cleanUrl.indexOf('?');
|
||||
if (q >= 0) cleanUrl = cleanUrl.substring(0, q);
|
||||
int h = cleanUrl.indexOf('#');
|
||||
if (h >= 0) cleanUrl = cleanUrl.substring(0, h);
|
||||
|
||||
boolean splitMode = cleanUrl.endsWith("/");
|
||||
|
||||
// Local file path (not http) — return as-is, the caller will check
|
||||
// whether it points at a file or a directory.
|
||||
if (!cleanUrl.startsWith("http")) {
|
||||
Path local = Paths.get(cleanUrl);
|
||||
return new ResolvedSource(local, splitMode || Files.isDirectory(local));
|
||||
}
|
||||
|
||||
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||
if (basePath.isEmpty()) return null;
|
||||
|
||||
if (splitMode) {
|
||||
// Derive the directory name from the URL: take the last non-empty
|
||||
// segment before the trailing slash. e.g. https://x/y/furnidata/ -> "furnidata"
|
||||
String trimmed = cleanUrl.endsWith("/") ? cleanUrl.substring(0, cleanUrl.length() - 1) : cleanUrl;
|
||||
String dirName = trimmed.substring(trimmed.lastIndexOf('/') + 1);
|
||||
Path candidate = Paths.get(basePath, dirName);
|
||||
return new ResolvedSource(candidate, true);
|
||||
}
|
||||
|
||||
String filename = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1);
|
||||
Path candidate = Paths.get(basePath, filename);
|
||||
return new ResolvedSource(candidate, false);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to resolve FurnitureData.json path", e);
|
||||
LOGGER.warn("Failed to resolve FurnitureData source", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Path fallbackToBasePath() {
|
||||
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||
if (basePath.isEmpty()) return null;
|
||||
Path dir = Paths.get(basePath);
|
||||
// Prefer the split layout if it exists, then the legacy file.
|
||||
Path splitCandidate = dir.resolve("furnidata");
|
||||
if (Files.isDirectory(splitCandidate)) return splitCandidate;
|
||||
Path legacy = dir.resolve("FurnitureData.json");
|
||||
if (Files.exists(legacy)) return legacy;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ public class SecureLoginEvent extends MessageHandler {
|
||||
// First, look up the user ID to check for ghost sessions
|
||||
int lookupUserId = 0;
|
||||
try (java.sql.Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
||||
java.sql.PreparedStatement stmt = conn.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) {
|
||||
java.sql.PreparedStatement stmt = conn.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
|
||||
stmt.setString(1, sso);
|
||||
try (java.sql.ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
|
||||
+3
@@ -1,6 +1,7 @@
|
||||
package com.eu.habbo.messages.incoming.modtool;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolManager;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
@@ -47,6 +48,8 @@ public class ModToolSanctionAlertEvent extends MessageHandler {
|
||||
} else {
|
||||
habbo.alert(message);
|
||||
}
|
||||
|
||||
ModToolManager.bumpUserSettingCounter(userId, "cfh_warnings");
|
||||
} else {
|
||||
this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name"))));
|
||||
}
|
||||
|
||||
+2
@@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.modtool;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolBanType;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolManager;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
|
||||
import com.eu.habbo.habbohotel.modtool.ScripterManager;
|
||||
@@ -73,6 +74,7 @@ public class ModToolSanctionBanEvent extends MessageHandler {
|
||||
Emulator.getGameEnvironment().getModToolManager().ban(userId, this.client.getHabbo(), message, duration, ModToolBanType.ACCOUNT, cfhTopic);
|
||||
}
|
||||
|
||||
ModToolManager.bumpUserSettingCounter(userId, "cfh_bans");
|
||||
} else {
|
||||
ScripterManager.scripterDetected(this.client, Emulator.getTexts().getValue("scripter.warning.modtools.ban").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
|
||||
}
|
||||
|
||||
+3
@@ -1,6 +1,7 @@
|
||||
package com.eu.habbo.messages.incoming.modtool;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolManager;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolSanctionLevelItem;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
|
||||
@@ -59,6 +60,8 @@ public class ModToolSanctionMuteEvent extends MessageHandler {
|
||||
habbo.alert(message);
|
||||
this.client.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_mute.muted").replace("%user%", habbo.getHabboInfo().getUsername()));
|
||||
}
|
||||
|
||||
ModToolManager.bumpUserSettingCounter(userId, "cfh_warnings");
|
||||
} else {
|
||||
this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name"))));
|
||||
}
|
||||
|
||||
+3
@@ -1,6 +1,7 @@
|
||||
package com.eu.habbo.messages.incoming.modtool;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolManager;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
@@ -49,6 +50,8 @@ public class ModToolSanctionTradeLockEvent extends MessageHandler {
|
||||
habbo.getHabboStats().setAllowTrade(false);
|
||||
habbo.alert(message);
|
||||
}
|
||||
|
||||
ModToolManager.bumpUserSettingCounter(userId, "tradelock_amount");
|
||||
} else {
|
||||
this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name"))));
|
||||
}
|
||||
|
||||
+5
-1
@@ -37,6 +37,7 @@ public class ChangeInfostandBgEvent extends MessageHandler {
|
||||
int requestedStand = sanitize(this.packet.readInt());
|
||||
int requestedOverlay = sanitize(this.packet.readInt());
|
||||
int requestedCard = this.packet.bytesAvailable() >= 4 ? sanitize(this.packet.readInt()) : 0;
|
||||
int requestedBorder = this.packet.bytesAvailable() >= 4 ? sanitize(this.packet.readInt()) : 0;
|
||||
|
||||
InfostandBackgroundManager manager = Emulator.getGameEnvironment() != null ? Emulator.getGameEnvironment().getInfostandBackgroundManager() : null;
|
||||
|
||||
@@ -44,11 +45,13 @@ public class ChangeInfostandBgEvent extends MessageHandler {
|
||||
int backgroundStand = resolve(manager, habbo, Category.STAND, requestedStand, info.getInfostandStand());
|
||||
int backgroundOverlay = resolve(manager, habbo, Category.OVERLAY, requestedOverlay, info.getInfostandOverlay());
|
||||
int backgroundCard = resolve(manager, habbo, Category.CARD, requestedCard, info.getInfostandCardBg());
|
||||
int backgroundBorder = resolve(manager, habbo, Category.BORDER, requestedBorder, info.getInfostandBorder());
|
||||
|
||||
if (info.getInfostandBg() == backgroundImage
|
||||
&& info.getInfostandStand() == backgroundStand
|
||||
&& info.getInfostandOverlay() == backgroundOverlay
|
||||
&& info.getInfostandCardBg() == backgroundCard) {
|
||||
&& info.getInfostandCardBg() == backgroundCard
|
||||
&& info.getInfostandBorder() == backgroundBorder) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,6 +59,7 @@ public class ChangeInfostandBgEvent extends MessageHandler {
|
||||
info.setInfostandStand(backgroundStand);
|
||||
info.setInfostandOverlay(backgroundOverlay);
|
||||
info.setInfostandCardBg(backgroundCard);
|
||||
info.setInfostandBorder(backgroundBorder);
|
||||
info.run();
|
||||
|
||||
if (info.getCurrentRoom() != null) {
|
||||
|
||||
+100
-21
@@ -12,10 +12,13 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.*;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
public class ModToolUserInfoComposer extends MessageComposer {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ModToolUserInfoComposer.class);
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
|
||||
private final ResultSet set;
|
||||
private final boolean hideMail;
|
||||
@@ -29,37 +32,30 @@ public class ModToolUserInfoComposer extends MessageComposer {
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.ModToolUserInfoComposer);
|
||||
try {
|
||||
int totalBans = 0;
|
||||
int userId = this.set.getInt("user_id");
|
||||
String machineId = this.set.getString("machine_id");
|
||||
int now = Emulator.getIntUnixTimestamp();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) AS amount FROM bans WHERE user_id = ?")) {
|
||||
statement.setInt(1, this.set.getInt("user_id"));
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.next()) {
|
||||
totalBans = set.getInt("amount");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
int totalBans = countBansForUser(userId);
|
||||
int lastPurchaseTimestamp = fetchLastPurchaseTimestamp(userId);
|
||||
int tradeLockExpiryTimestamp = fetchActiveTradeLockExpiry(userId, now);
|
||||
int identityRelatedBanCount = countIdentityRelatedBans(userId, machineId);
|
||||
|
||||
this.response.appendInt(this.set.getInt("user_id"));
|
||||
this.response.appendInt(userId);
|
||||
this.response.appendString(this.set.getString("username"));
|
||||
this.response.appendString(this.set.getString("look"));
|
||||
this.response.appendInt((Emulator.getIntUnixTimestamp() - this.set.getInt("account_created")) / 60);
|
||||
this.response.appendInt((this.set.getInt("online") == 1 ? 0 : Emulator.getIntUnixTimestamp() - this.set.getInt("last_online")) / 60);
|
||||
this.response.appendInt((now - this.set.getInt("account_created")) / 60);
|
||||
this.response.appendInt((this.set.getInt("online") == 1 ? 0 : now - this.set.getInt("last_online")) / 60);
|
||||
this.response.appendBoolean(this.set.getInt("online") == 1);
|
||||
this.response.appendInt(this.set.getInt("cfh_send"));
|
||||
this.response.appendInt(this.set.getInt("cfh_abusive"));
|
||||
this.response.appendInt(this.set.getInt("cfh_warnings"));
|
||||
this.response.appendInt(totalBans); // Number of bans
|
||||
this.response.appendInt(this.set.getInt("tradelock_amount"));
|
||||
this.response.appendString(""); //Trading lock expiry timestamp
|
||||
this.response.appendString(""); //Last Purchase Timestamp
|
||||
this.response.appendInt(this.set.getInt("user_id")); //Personal Identification #
|
||||
this.response.appendInt(0); // Number of account bans
|
||||
this.response.appendString(formatUnixTimestamp(tradeLockExpiryTimestamp)); // Trading lock expiry timestamp
|
||||
this.response.appendString(formatUnixTimestamp(lastPurchaseTimestamp)); // Last Purchase Timestamp
|
||||
this.response.appendInt(userId); //Personal Identification #
|
||||
this.response.appendInt(identityRelatedBanCount); // Number of account bans on the same machine_id
|
||||
this.response.appendString(this.hideMail ? "" : this.set.getString("mail"));
|
||||
this.response.appendString("Rank (" + this.set.getInt("rank_id") + "): " + this.set.getString("rank_name")); //user_class_txt
|
||||
|
||||
@@ -90,4 +86,87 @@ public class ModToolUserInfoComposer extends MessageComposer {
|
||||
public ResultSet getSet() {
|
||||
return set;
|
||||
}
|
||||
|
||||
private static int countBansForUser(int userId) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) AS amount FROM bans WHERE user_id = ?")) {
|
||||
statement.setInt(1, userId);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.next()) return set.getInt("amount");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Most recent purchase timestamp from logs_shop_purchases for this
|
||||
* user. Returns 0 when the user has never bought anything (in which
|
||||
* case the wire field stays empty and the client shows the empty
|
||||
* placeholder).
|
||||
*/
|
||||
private static int fetchLastPurchaseTimestamp(int userId) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT MAX(`timestamp`) AS ts FROM logs_shop_purchases WHERE user_id = ?")) {
|
||||
statement.setInt(1, userId);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.next()) return set.getInt("ts");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Latest active trade-lock expiry from the sanctions table. Only
|
||||
* locks expiring in the future are considered — past entries don't
|
||||
* count. Returns 0 when no active lock exists.
|
||||
*/
|
||||
private static int fetchActiveTradeLockExpiry(int userId, int now) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT MAX(trade_locked_until) AS expiry FROM sanctions WHERE habbo_id = ? AND trade_locked_until > ?")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setInt(2, now);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.next()) return set.getInt("expiry");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count of OTHER user accounts that have been banned from the same
|
||||
* machine_id as this user. An empty machine_id (default '') is
|
||||
* ignored — never matches anything by definition. Self is excluded
|
||||
* because the user's own bans are already counted under banCount.
|
||||
*/
|
||||
private static int countIdentityRelatedBans(int userId, String machineId) {
|
||||
if (machineId == null || machineId.isEmpty()) return 0;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT COUNT(DISTINCT user_id) AS amount FROM bans WHERE machine_id = ? AND user_id != ?")) {
|
||||
statement.setString(1, machineId);
|
||||
statement.setInt(2, userId);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.next()) return set.getInt("amount");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire format for date fields is `yyyy-MM-dd HH:mm`. A 0 timestamp
|
||||
* is rendered as an empty string so the client falls back to its
|
||||
* empty-state placeholder.
|
||||
*/
|
||||
private static String formatUnixTimestamp(int timestamp) {
|
||||
if (timestamp <= 0) return "";
|
||||
return DATE_FORMAT.format(new Date(timestamp * 1000L));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ public class RoomPetComposer extends MessageComposer implements TIntObjectProced
|
||||
this.response.appendString("");
|
||||
this.response.appendString("unknown");
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ public class RoomUserDataComposer extends MessageComposer {
|
||||
this.response.appendString(customizationData.prefixEffect);
|
||||
this.response.appendString(customizationData.prefixFont);
|
||||
this.response.appendString(customizationData.displayOrder);
|
||||
this.response.appendInt(this.habbo.getHabboInfo().getInfostandBorder());
|
||||
return this.response;
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -78,6 +78,7 @@ public class RoomUsersComposer extends MessageComposer {
|
||||
this.response.appendString(customizationData.displayOrder);
|
||||
this.response.appendString(this.habbo.getHabboInfo().getRoomEntryMethod());
|
||||
this.response.appendInt(this.habbo.getHabboInfo().getRoomEntryTeleportId());
|
||||
this.response.appendInt(this.habbo.getHabboInfo().getInfostandBorder());
|
||||
} else if (this.habbos != null) {
|
||||
this.response.appendInt(this.habbos.size());
|
||||
for (Habbo habbo : this.habbos) {
|
||||
@@ -120,6 +121,7 @@ public class RoomUsersComposer extends MessageComposer {
|
||||
this.response.appendString(customizationData.displayOrder);
|
||||
this.response.appendString(habbo.getHabboInfo().getRoomEntryMethod());
|
||||
this.response.appendInt(habbo.getHabboInfo().getRoomEntryTeleportId());
|
||||
this.response.appendInt(habbo.getHabboInfo().getInfostandBorder());
|
||||
}
|
||||
}
|
||||
} else if (this.bot != null) {
|
||||
@@ -154,6 +156,7 @@ public class RoomUsersComposer extends MessageComposer {
|
||||
this.response.appendShort(9);
|
||||
this.response.appendString("unknown");
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
} else if (this.bots != null) {
|
||||
this.response.appendInt(this.bots.size());
|
||||
for (Bot bot : this.bots) {
|
||||
@@ -187,6 +190,7 @@ public class RoomUsersComposer extends MessageComposer {
|
||||
this.response.appendShort(9);
|
||||
this.response.appendString("unknown");
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
}
|
||||
}
|
||||
return this.response;
|
||||
|
||||
+106
-1
@@ -1,11 +1,57 @@
|
||||
package com.eu.habbo.messages.outgoing.users;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.permissions.PermissionSetting;
|
||||
import com.eu.habbo.habbohotel.permissions.Rank;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
import com.eu.habbo.plugin.HabboPlugin;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Sends the full per-user permission state to the connected client.
|
||||
*
|
||||
* Wire layout (each trailing block is guarded by `bytesAvailable` on
|
||||
* the client so older Nitro builds keep parsing the prefix and stop):
|
||||
*
|
||||
* int clubLevel
|
||||
* int rank.level // mapped to securityLevel on the client
|
||||
* bool isAmbassador // legacy ACC_AMBASSADOR flag
|
||||
* --- rank metadata (Arcturus ≥ 4.2.10) ---
|
||||
* int rank.id
|
||||
* string rank.name // permission_ranks.rank_name
|
||||
* string rank.badge
|
||||
* string rank.prefix
|
||||
* string rank.prefixColor
|
||||
* --- resolved permission map (Arcturus ≥ 4.2.10) ---
|
||||
* int count
|
||||
* loop: string permission_key + int value // 1 = ALLOWED, 2 = ROOM_OWNER
|
||||
*
|
||||
* The map is the union of:
|
||||
* • rank entries with `PermissionSetting != DISALLOWED` — same data
|
||||
* `Rank.hasPermission(key, isRoomOwner)` reads server-side.
|
||||
* • plugin grants — for each key the rank doesn't allow, every
|
||||
* installed `HabboPlugin.hasPermission(habbo, key)` is consulted;
|
||||
* if any plugin grants it, the key lands on the wire with value 1
|
||||
* (plugins don't have a ROOM_OWNER concept).
|
||||
*
|
||||
* The React-side `useHasPermission(key)` / `useUserPermissions()`
|
||||
* consumers read the map directly so UI gates follow the same
|
||||
* semantics as `PermissionsManager.hasPermission(habbo, key)`
|
||||
* server-side — including plugin-granted permissions, which were
|
||||
* invisible to the client before this commit.
|
||||
*
|
||||
* Two send points:
|
||||
* 1. End of `SecureLoginEvent` — client receives the full state once.
|
||||
* 2. Inside `HabboManager.setRank` — runtime promote/demote refresh.
|
||||
* 3. Inside `UpdatePermissionsCommand` — broadcast after
|
||||
* `:update_permissions` reloads the tables at runtime.
|
||||
*/
|
||||
public class UserPermissionsComposer extends MessageComposer {
|
||||
private final int clubLevel;
|
||||
|
||||
@@ -20,11 +66,70 @@ public class UserPermissionsComposer extends MessageComposer {
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.UserPermissionsComposer);
|
||||
this.response.appendInt(this.clubLevel);
|
||||
this.response.appendInt(this.habbo.getHabboInfo().getRank().getLevel());
|
||||
|
||||
Rank rank = this.habbo.getHabboInfo().getRank();
|
||||
|
||||
this.response.appendInt(rank.getLevel());
|
||||
this.response.appendBoolean(this.habbo.hasPermission(Permission.ACC_AMBASSADOR));
|
||||
|
||||
// Rank metadata
|
||||
this.response.appendInt(rank.getId());
|
||||
this.response.appendString(rank.getName());
|
||||
this.response.appendString(rank.getBadge());
|
||||
this.response.appendString(rank.getPrefix());
|
||||
this.response.appendString(rank.getPrefixColor());
|
||||
|
||||
// Build the resolved permission map. Walk rank.getPermissions()
|
||||
// (Rank.permissions has every row from permission_definitions
|
||||
// because PermissionsManager.loadPermissionsNormalized() calls
|
||||
// rank.setPermission(key, …) for every key, including DISALLOWED
|
||||
// ones) and emit the final value per key:
|
||||
// ALLOWED → 1
|
||||
// ROOM_OWNER → 2
|
||||
// DISALLOWED + plugin yes → 1
|
||||
// DISALLOWED + plugin no → omit
|
||||
//
|
||||
// LinkedHashMap preserves the alphabetical order that the rank
|
||||
// table was populated with, which is helpful for snapshotting
|
||||
// and grep'ing wire dumps.
|
||||
Map<String, Permission> rankPermissions = rank.getPermissions();
|
||||
Map<String, Integer> resolved = new LinkedHashMap<>(rankPermissions.size());
|
||||
|
||||
for (Map.Entry<String, Permission> entry : rankPermissions.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Permission rankPerm = entry.getValue();
|
||||
|
||||
if (rankPerm.setting == PermissionSetting.ALLOWED) {
|
||||
resolved.put(key, 1);
|
||||
} else if (rankPerm.setting == PermissionSetting.ROOM_OWNER) {
|
||||
resolved.put(key, 2);
|
||||
} else if (this.anyPluginGrants(key)) {
|
||||
resolved.put(key, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Plugins may also grant CUSTOM keys that aren't in
|
||||
// permission_definitions — rare but legal. There's no enumeration
|
||||
// API on HabboPlugin to discover them, so they stay invisible
|
||||
// here. Document the limitation rather than over-engineer.
|
||||
|
||||
this.response.appendInt(resolved.size());
|
||||
|
||||
for (Map.Entry<String, Integer> entry : resolved.entrySet()) {
|
||||
this.response.appendString(entry.getKey());
|
||||
this.response.appendInt(entry.getValue());
|
||||
}
|
||||
|
||||
return this.response;
|
||||
}
|
||||
|
||||
private boolean anyPluginGrants(String key) {
|
||||
for (HabboPlugin plugin : Emulator.getPluginManager().getPlugins()) {
|
||||
if (plugin.hasPermission(this.habbo, key)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getClubLevel() {
|
||||
return clubLevel;
|
||||
}
|
||||
|
||||
+2
@@ -5,6 +5,7 @@ import com.eu.habbo.messages.PacketManager;
|
||||
import com.eu.habbo.networking.gameserver.auth.AuthHttpHandler;
|
||||
import com.eu.habbo.networking.gameserver.auth.NitroSecureApiHandler;
|
||||
import com.eu.habbo.networking.gameserver.auth.NitroSecureAssetHandler;
|
||||
import com.eu.habbo.networking.gameserver.badges.BadgeLeaderboardHttpHandler;
|
||||
import com.eu.habbo.networking.gameserver.badges.BadgeHttpHandler;
|
||||
import com.eu.habbo.networking.gameserver.codec.WebSocketCodec;
|
||||
import com.eu.habbo.networking.gameserver.crypto.WsHandshakeHandler;
|
||||
@@ -60,6 +61,7 @@ public class WebSocketChannelInitializer extends ChannelInitializer<SocketChanne
|
||||
ch.pipeline().addLast("nitroSecureApiHandler", new NitroSecureApiHandler());
|
||||
ch.pipeline().addLast("authHttpHandler", new AuthHttpHandler());
|
||||
ch.pipeline().addLast("badgeHttpHandler", new BadgeHttpHandler());
|
||||
ch.pipeline().addLast("badgeLeaderboardHttpHandler", new BadgeLeaderboardHttpHandler());
|
||||
ch.pipeline().addLast("wsProtocolHandler", new WebSocketServerProtocolHandler(this.wsConfig));
|
||||
ch.pipeline().addLast("wsFrameAggregator", new WebSocketFrameAggregator(MAX_FRAME_SIZE));
|
||||
ch.pipeline().addLast("wsCodec", new WebSocketCodec());
|
||||
|
||||
+2
-2
@@ -50,7 +50,7 @@ final class SessionEndpoints {
|
||||
|
||||
if (ssoTicket != null && !ssoTicket.isEmpty()) {
|
||||
try (PreparedStatement lookup = conn.prepareStatement(
|
||||
"SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) {
|
||||
"SELECT id FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
|
||||
lookup.setString(1, ssoTicket);
|
||||
try (ResultSet rs = lookup.executeQuery()) {
|
||||
if (rs.next()) userId = rs.getInt("id");
|
||||
@@ -134,7 +134,7 @@ final class SessionEndpoints {
|
||||
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement lookup = conn.prepareStatement(
|
||||
"SELECT id, username FROM users WHERE auth_ticket = ? LIMIT 1")) {
|
||||
"SELECT id, username FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
|
||||
lookup.setString(1, ssoTicket);
|
||||
try (ResultSet rs = lookup.executeQuery()) {
|
||||
if (!rs.next()) {
|
||||
|
||||
+458
@@ -0,0 +1,458 @@
|
||||
package com.eu.habbo.networking.gameserver.badges;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.networking.gameserver.auth.AccessTokenService;
|
||||
import com.eu.habbo.networking.gameserver.auth.CorsOriginGate;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.*;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.*;
|
||||
|
||||
public class BadgeLeaderboardHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(BadgeLeaderboardHttpHandler.class);
|
||||
|
||||
private static final String BASE_PATH = "/api/badges/leaderboard";
|
||||
private static final long CACHE_TTL_MS = 15_000L;
|
||||
private static final int MAX_BOARD_USERS = 100;
|
||||
|
||||
private static volatile Snapshot cache = null;
|
||||
|
||||
private static final class Snapshot {
|
||||
final List<UserBadgeAggregate> badgeUsers;
|
||||
final List<UserAchievementAggregate> achievementUsers;
|
||||
final JsonArray badgeStats;
|
||||
final long expiresAt;
|
||||
|
||||
Snapshot(List<UserBadgeAggregate> badgeUsers, List<UserAchievementAggregate> achievementUsers, JsonArray badgeStats, long expiresAt) {
|
||||
this.badgeUsers = badgeUsers;
|
||||
this.achievementUsers = achievementUsers;
|
||||
this.badgeStats = badgeStats;
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class UserBadgeAggregate {
|
||||
final int userId;
|
||||
final String username;
|
||||
final String figure;
|
||||
final int totalBadges;
|
||||
final EnumMap<Rarity, Integer> counts;
|
||||
|
||||
UserBadgeAggregate(int userId, String username, String figure, int totalBadges, EnumMap<Rarity, Integer> counts) {
|
||||
this.userId = userId;
|
||||
this.username = username;
|
||||
this.figure = figure;
|
||||
this.totalBadges = totalBadges;
|
||||
this.counts = counts;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class UserAchievementAggregate {
|
||||
final int userId;
|
||||
final String username;
|
||||
final String figure;
|
||||
final int achievementScore;
|
||||
|
||||
UserAchievementAggregate(int userId, String username, String figure, int achievementScore) {
|
||||
this.userId = userId;
|
||||
this.username = username;
|
||||
this.figure = figure;
|
||||
this.achievementScore = achievementScore;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ViewerProfile {
|
||||
final int userId;
|
||||
final String username;
|
||||
final String figure;
|
||||
|
||||
ViewerProfile(int userId, String username, String figure) {
|
||||
this.userId = userId;
|
||||
this.username = username;
|
||||
this.figure = figure;
|
||||
}
|
||||
}
|
||||
|
||||
private enum Rarity {
|
||||
COMMON("common"),
|
||||
RARE("rare"),
|
||||
EPIC("epic"),
|
||||
LEGENDARY("legendary"),
|
||||
MYTHICAL("mythical"),
|
||||
UNIQUE("unique");
|
||||
|
||||
final String key;
|
||||
|
||||
Rarity(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
if (!(msg instanceof FullHttpRequest req)) {
|
||||
super.channelRead(ctx, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
String path = new QueryStringDecoder(req.uri()).path();
|
||||
if (!path.equals(BASE_PATH) && !path.startsWith(BASE_PATH + "/")) {
|
||||
super.channelRead(ctx, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
handle(ctx, req);
|
||||
} finally {
|
||||
ReferenceCountUtil.release(req);
|
||||
}
|
||||
}
|
||||
|
||||
private void handle(ChannelHandlerContext ctx, FullHttpRequest req) {
|
||||
if (req.method() == HttpMethod.OPTIONS) {
|
||||
sendCors(ctx, req);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) {
|
||||
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, error("Use GET."));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Snapshot snapshot = loadSnapshot();
|
||||
int viewerUserId = authenticateOptional(req);
|
||||
ViewerProfile viewerProfile = loadViewerProfile(viewerUserId);
|
||||
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("viewerUserId", viewerUserId);
|
||||
payload.add("badgeStats", cloneArray(snapshot.badgeStats));
|
||||
payload.add("thresholds", buildThresholdsPayload());
|
||||
|
||||
JsonObject boards = new JsonObject();
|
||||
boards.add("totalBadges", buildBadgeBoard(snapshot.badgeUsers, viewerUserId, viewerProfile, null));
|
||||
boards.add("achievementLevel", buildAchievementBoard(snapshot.achievementUsers, viewerUserId, viewerProfile));
|
||||
|
||||
JsonObject rarityBoards = new JsonObject();
|
||||
for (Rarity rarity : Rarity.values()) {
|
||||
rarityBoards.add(rarity.key, buildBadgeBoard(snapshot.badgeUsers, viewerUserId, viewerProfile, rarity));
|
||||
}
|
||||
|
||||
boards.add("rarity", rarityBoards);
|
||||
payload.add("leaderboards", boards);
|
||||
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, payload);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[badges/leaderboard] unexpected error", e);
|
||||
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, error("Server error."));
|
||||
}
|
||||
}
|
||||
|
||||
private Snapshot loadSnapshot() throws Exception {
|
||||
long now = System.currentTimeMillis();
|
||||
Snapshot current = cache;
|
||||
if (current != null && current.expiresAt >= now) return current;
|
||||
|
||||
synchronized (BadgeLeaderboardHttpHandler.class) {
|
||||
current = cache;
|
||||
if (current != null && current.expiresAt >= now) return current;
|
||||
|
||||
JsonArray badgeStats = new JsonArray();
|
||||
List<UserBadgeAggregate> badgeUsers = new ArrayList<>();
|
||||
List<UserAchievementAggregate> achievementUsers = new ArrayList<>();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
loadBadgeStats(connection, badgeStats);
|
||||
loadBadgeUsers(connection, badgeUsers);
|
||||
loadAchievementUsers(connection, achievementUsers);
|
||||
}
|
||||
|
||||
Snapshot built = new Snapshot(badgeUsers, achievementUsers, badgeStats, now + CACHE_TTL_MS);
|
||||
cache = built;
|
||||
return built;
|
||||
}
|
||||
}
|
||||
|
||||
private void loadBadgeStats(Connection connection, JsonArray badgeStats) throws Exception {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT badge_code, COUNT(DISTINCT user_id) AS owner_count " +
|
||||
"FROM users_badges GROUP BY badge_code ORDER BY owner_count ASC, badge_code ASC")) {
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
String badgeCode = set.getString("badge_code");
|
||||
int ownerCount = set.getInt("owner_count");
|
||||
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.addProperty("badgeCode", badgeCode);
|
||||
entry.addProperty("ownerCount", ownerCount);
|
||||
entry.addProperty("rarity", classify(ownerCount).key);
|
||||
badgeStats.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadBadgeUsers(Connection connection, List<UserBadgeAggregate> badgeUsers) throws Exception {
|
||||
String sql =
|
||||
"SELECT u.id AS user_id, u.username, u.look, " +
|
||||
"COUNT(DISTINCT ub.badge_code) AS total_badges, " +
|
||||
"COUNT(DISTINCT CASE WHEN counts.owner_count > 50 THEN ub.badge_code END) AS common_count, " +
|
||||
"COUNT(DISTINCT CASE WHEN counts.owner_count > 10 AND counts.owner_count <= 50 THEN ub.badge_code END) AS rare_count, " +
|
||||
"COUNT(DISTINCT CASE WHEN counts.owner_count > 6 AND counts.owner_count <= 10 THEN ub.badge_code END) AS epic_count, " +
|
||||
"COUNT(DISTINCT CASE WHEN counts.owner_count > 3 AND counts.owner_count <= 6 THEN ub.badge_code END) AS legendary_count, " +
|
||||
"COUNT(DISTINCT CASE WHEN counts.owner_count > 1 AND counts.owner_count <= 3 THEN ub.badge_code END) AS mythical_count, " +
|
||||
"COUNT(DISTINCT CASE WHEN counts.owner_count = 1 THEN ub.badge_code END) AS unique_count " +
|
||||
"FROM users_badges ub " +
|
||||
"INNER JOIN users u ON u.id = ub.user_id " +
|
||||
"INNER JOIN (SELECT badge_code, COUNT(DISTINCT user_id) AS owner_count FROM users_badges GROUP BY badge_code) counts ON counts.badge_code = ub.badge_code " +
|
||||
"GROUP BY u.id, u.username, u.look";
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
EnumMap<Rarity, Integer> counts = new EnumMap<>(Rarity.class);
|
||||
counts.put(Rarity.COMMON, set.getInt("common_count"));
|
||||
counts.put(Rarity.RARE, set.getInt("rare_count"));
|
||||
counts.put(Rarity.EPIC, set.getInt("epic_count"));
|
||||
counts.put(Rarity.LEGENDARY, set.getInt("legendary_count"));
|
||||
counts.put(Rarity.MYTHICAL, set.getInt("mythical_count"));
|
||||
counts.put(Rarity.UNIQUE, set.getInt("unique_count"));
|
||||
|
||||
badgeUsers.add(new UserBadgeAggregate(
|
||||
set.getInt("user_id"),
|
||||
safe(set.getString("username")),
|
||||
safe(set.getString("look")),
|
||||
set.getInt("total_badges"),
|
||||
counts
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadAchievementUsers(Connection connection, List<UserAchievementAggregate> achievementUsers) throws Exception {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT u.id AS user_id, u.username, u.look, COALESCE(us.achievement_score, 0) AS achievement_score " +
|
||||
"FROM users u INNER JOIN users_settings us ON us.user_id = u.id " +
|
||||
"WHERE COALESCE(us.achievement_score, 0) > 0")) {
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
achievementUsers.add(new UserAchievementAggregate(
|
||||
set.getInt("user_id"),
|
||||
safe(set.getString("username")),
|
||||
safe(set.getString("look")),
|
||||
set.getInt("achievement_score")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ViewerProfile loadViewerProfile(int viewerUserId) throws Exception {
|
||||
if (viewerUserId <= 0) return null;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT id, username, look FROM users WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, viewerUserId);
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (!set.next()) return null;
|
||||
|
||||
return new ViewerProfile(
|
||||
set.getInt("id"),
|
||||
safe(set.getString("username")),
|
||||
safe(set.getString("look"))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private JsonObject buildBadgeBoard(List<UserBadgeAggregate> users, int viewerUserId, ViewerProfile viewerProfile, Rarity rarity) {
|
||||
List<JsonObject> ranked = new ArrayList<>();
|
||||
|
||||
for (UserBadgeAggregate user : users) {
|
||||
int score = (rarity == null) ? user.totalBadges : user.counts.getOrDefault(rarity, 0);
|
||||
if (score <= 0) continue;
|
||||
ranked.add(toEntry(user.userId, user.username, user.figure, score));
|
||||
}
|
||||
|
||||
ranked.sort((a, b) -> {
|
||||
int scoreCompare = Integer.compare(b.get("score").getAsInt(), a.get("score").getAsInt());
|
||||
if (scoreCompare != 0) return scoreCompare;
|
||||
return Integer.compare(a.get("userId").getAsInt(), b.get("userId").getAsInt());
|
||||
});
|
||||
|
||||
return finalizeBoard(ranked, viewerUserId, viewerProfile);
|
||||
}
|
||||
|
||||
private JsonObject buildAchievementBoard(List<UserAchievementAggregate> users, int viewerUserId, ViewerProfile viewerProfile) {
|
||||
List<JsonObject> ranked = new ArrayList<>();
|
||||
|
||||
for (UserAchievementAggregate user : users) {
|
||||
if (user.achievementScore <= 0) continue;
|
||||
ranked.add(toEntry(user.userId, user.username, user.figure, user.achievementScore));
|
||||
}
|
||||
|
||||
ranked.sort((a, b) -> {
|
||||
int scoreCompare = Integer.compare(b.get("score").getAsInt(), a.get("score").getAsInt());
|
||||
if (scoreCompare != 0) return scoreCompare;
|
||||
return Integer.compare(a.get("userId").getAsInt(), b.get("userId").getAsInt());
|
||||
});
|
||||
|
||||
return finalizeBoard(ranked, viewerUserId, viewerProfile);
|
||||
}
|
||||
|
||||
private JsonObject finalizeBoard(List<JsonObject> ranked, int viewerUserId, ViewerProfile viewerProfile) {
|
||||
JsonArray entries = new JsonArray();
|
||||
JsonObject viewerEntry = null;
|
||||
|
||||
int cappedSize = Math.min(ranked.size(), MAX_BOARD_USERS);
|
||||
|
||||
for (int index = 0; index < cappedSize; index++) {
|
||||
JsonObject entry = ranked.get(index).deepCopy();
|
||||
int rank = index + 1;
|
||||
entry.addProperty("rank", rank);
|
||||
|
||||
entries.add(entry);
|
||||
if (viewerUserId > 0 && entry.get("userId").getAsInt() == viewerUserId) viewerEntry = entry;
|
||||
}
|
||||
|
||||
if (viewerEntry == null && viewerUserId > 0) {
|
||||
for (int index = 0; index < ranked.size(); index++) {
|
||||
JsonObject entry = ranked.get(index);
|
||||
|
||||
if (entry.get("userId").getAsInt() != viewerUserId) continue;
|
||||
|
||||
viewerEntry = entry.deepCopy();
|
||||
viewerEntry.addProperty("rank", index + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (viewerEntry == null && viewerProfile != null) {
|
||||
viewerEntry = toEntry(viewerProfile.userId, viewerProfile.username, viewerProfile.figure, 0);
|
||||
viewerEntry.addProperty("rank", 0);
|
||||
}
|
||||
|
||||
JsonObject board = new JsonObject();
|
||||
board.add("entries", entries);
|
||||
board.addProperty("totalPlayers", cappedSize);
|
||||
board.add("viewerEntry", viewerEntry != null ? viewerEntry : new JsonObject());
|
||||
return board;
|
||||
}
|
||||
|
||||
private JsonObject toEntry(int userId, String username, String figure, int score) {
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.addProperty("userId", userId);
|
||||
entry.addProperty("username", username);
|
||||
entry.addProperty("figure", figure);
|
||||
entry.addProperty("score", score);
|
||||
return entry;
|
||||
}
|
||||
|
||||
private JsonObject buildThresholdsPayload() {
|
||||
JsonObject thresholds = new JsonObject();
|
||||
thresholds.addProperty("commonMinOwners", 51);
|
||||
thresholds.addProperty("rareMinOwners", 11);
|
||||
thresholds.addProperty("epicMinOwners", 7);
|
||||
thresholds.addProperty("legendaryMinOwners", 4);
|
||||
thresholds.addProperty("mythicalMinOwners", 2);
|
||||
thresholds.addProperty("uniqueOwners", 1);
|
||||
return thresholds;
|
||||
}
|
||||
|
||||
private static Rarity classify(int ownerCount) {
|
||||
if (ownerCount > 50) return Rarity.COMMON;
|
||||
if (ownerCount > 10) return Rarity.RARE;
|
||||
if (ownerCount > 6) return Rarity.EPIC;
|
||||
if (ownerCount > 3) return Rarity.LEGENDARY;
|
||||
if (ownerCount > 1) return Rarity.MYTHICAL;
|
||||
if (ownerCount > 0) return Rarity.UNIQUE;
|
||||
return Rarity.COMMON;
|
||||
}
|
||||
|
||||
private static int authenticateOptional(FullHttpRequest req) {
|
||||
String header = req.headers().get(HttpHeaderNames.AUTHORIZATION);
|
||||
if (header == null || header.isEmpty()) return 0;
|
||||
|
||||
String token = header.startsWith("Bearer ") ? header.substring(7).trim() : header.trim();
|
||||
return AccessTokenService.verify(token);
|
||||
}
|
||||
|
||||
private static JsonArray cloneArray(JsonArray source) {
|
||||
JsonArray copy = new JsonArray();
|
||||
source.forEach(element -> copy.add(element.deepCopy()));
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static String safe(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private static JsonObject error(String message) {
|
||||
JsonObject obj = new JsonObject();
|
||||
obj.addProperty("error", message);
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static void sendJson(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, JsonObject body) {
|
||||
byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8);
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes));
|
||||
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8");
|
||||
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-store, no-cache, must-revalidate");
|
||||
applyCors(req, response);
|
||||
boolean keepAlive = isKeepAlive(req);
|
||||
if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
|
||||
var future = ctx.writeAndFlush(response);
|
||||
if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
|
||||
private static void sendCors(ChannelHandlerContext ctx, FullHttpRequest req) {
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT);
|
||||
applyCors(req, response);
|
||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
|
||||
private static void applyCors(FullHttpRequest req, FullHttpResponse response) {
|
||||
String origin = req.headers().get(HttpHeaderNames.ORIGIN);
|
||||
|
||||
if (origin != null && !origin.isEmpty() && CorsOriginGate.isAllowed(req)) {
|
||||
response.headers().set("Access-Control-Allow-Origin", origin);
|
||||
response.headers().set("Access-Control-Allow-Credentials", "true");
|
||||
}
|
||||
|
||||
response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
||||
|
||||
String requestedHeaders = req.headers().get("Access-Control-Request-Headers");
|
||||
if (requestedHeaders != null && !requestedHeaders.isEmpty()) {
|
||||
response.headers().set("Access-Control-Allow-Headers", requestedHeaders);
|
||||
} else {
|
||||
response.headers().set("Access-Control-Allow-Headers",
|
||||
"Authorization, Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api");
|
||||
}
|
||||
|
||||
response.headers().set("Vary", "Origin, Access-Control-Request-Headers, Access-Control-Request-Method");
|
||||
response.headers().set("Access-Control-Max-Age", "600");
|
||||
response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp");
|
||||
}
|
||||
|
||||
private static boolean isKeepAlive(FullHttpRequest req) {
|
||||
String connection = req.headers().get(HttpHeaderNames.CONNECTION);
|
||||
return connection == null || !"close".equalsIgnoreCase(connection);
|
||||
}
|
||||
}
|
||||
+35
-6
@@ -8,10 +8,13 @@ import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.*;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
public class WebSocketHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketHttpHandler.class);
|
||||
private static final String ORIGIN_HEADER = "Origin";
|
||||
|
||||
@Override
|
||||
@@ -27,6 +30,12 @@ public class WebSocketHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
|
||||
private boolean handleHttpRequest(ChannelHandlerContext ctx, HttpMessage req) {
|
||||
captureForwardedIp(ctx, req);
|
||||
|
||||
if (!isWebSocketUpgrade(req)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String origin = "error";
|
||||
|
||||
try {
|
||||
@@ -38,27 +47,47 @@ public class WebSocketHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
String whitelist = Emulator.getConfig().getValue("ws.whitelist", "localhost");
|
||||
if (!isWhitelisted(origin, whitelist.split(","))) {
|
||||
LOGGER.warn("WebSocket upgrade rejected — origin '{}' not in ws.whitelist='{}'",
|
||||
req.headers().get(ORIGIN_HEADER), whitelist);
|
||||
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(
|
||||
HttpVersion.HTTP_1_1,
|
||||
HttpResponseStatus.FORBIDDEN,
|
||||
Unpooled.wrappedBuffer("Origin forbidden".getBytes())
|
||||
);
|
||||
response.headers().set("Vary", "Origin");
|
||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||
return false;
|
||||
}
|
||||
|
||||
String ipHeader = Emulator.getConfig().getValue("ws.ip.header", "");
|
||||
if (!ipHeader.isEmpty() && req.headers().contains(ipHeader)) {
|
||||
String ip = req.headers().get(ipHeader);
|
||||
ctx.channel().attr(GameServerAttributes.WS_IP).set(ip);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void captureForwardedIp(ChannelHandlerContext ctx, HttpMessage req) {
|
||||
String ipHeader = Emulator.getConfig().getValue("ws.ip.header", "");
|
||||
if (!ipHeader.isEmpty() && req.headers().contains(ipHeader)) {
|
||||
String ip = req.headers().get(ipHeader);
|
||||
ctx.channel().attr(GameServerAttributes.WS_IP).set(ip);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isWebSocketUpgrade(HttpMessage req) {
|
||||
String upgrade = req.headers().get(HttpHeaderNames.UPGRADE);
|
||||
if (upgrade == null || !"websocket".equalsIgnoreCase(upgrade)) return false;
|
||||
|
||||
String connection = req.headers().get(HttpHeaderNames.CONNECTION);
|
||||
if (connection == null) return false;
|
||||
|
||||
for (String token : connection.split(",")) {
|
||||
if ("upgrade".equalsIgnoreCase(token.trim())) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String getDomainNameFromUrl(String url) throws Exception {
|
||||
URI uri = new URI(url);
|
||||
String domain = uri.getHost();
|
||||
if (domain == null) return "error";
|
||||
return domain.startsWith("www.") ? domain.substring(4) : domain;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,16 +27,6 @@ rcon.host=127.0.0.1
|
||||
rcon.port=3001
|
||||
rcon.allowed=127.0.0.1;127.0.0.2
|
||||
|
||||
#WebSocket Configuration (for Nitro)
|
||||
#Set ws.enabled to true to enable WebSocket connections.
|
||||
ws.enabled=false
|
||||
ws.host=0.0.0.0
|
||||
ws.port=2096
|
||||
#Comma-separated whitelist of allowed origins. Supports wildcards: *.example.com, * (allow all)
|
||||
ws.whitelist=localhost
|
||||
#Header name for real client IP when behind a proxy (e.g., X-Forwarded-For, CF-Connecting-IP). Leave empty if not using a proxy.
|
||||
ws.ip.header=
|
||||
|
||||
# Databse configuration
|
||||
db.pool.connection_timeout_ms = 10000
|
||||
db.pool.idle_timeout_ms = 600000
|
||||
@@ -69,3 +59,12 @@ login.remember.jwt.secret=
|
||||
|
||||
# Login news API.
|
||||
login.news.limit=5
|
||||
|
||||
|
||||
#WebSocket Configuration (for Nitro)
|
||||
#Please adjust this setting in the Database !!!!
|
||||
### ws.enabled=false
|
||||
### ws.host=0.0.0.0
|
||||
### ws.port=2096
|
||||
### ws.whitelist=localhost #Comma-separated whitelist of allowed origins. Supports wildcards: *.example.com, * (allow all)
|
||||
### ws.ip.header=X-Forwarded-For #Header name for real client IP when behind a proxy (e.g., X-Forwarded-For, CF-Connecting-IP). Leave empty if not using a proxy.
|
||||
|
||||
Reference in New Issue
Block a user