Merge latest duckie main with UI login

This commit is contained in:
Lorenzune
2026-04-21 11:44:19 +02:00
63 changed files with 2087 additions and 501 deletions
+78
View File
@@ -0,0 +1,78 @@
name: Build & Release JAR
on:
push:
branches: [main]
permissions:
contents: write
jobs:
build-release:
runs-on: ubuntu-latest
defaults:
run:
working-directory: Emulator
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven
- name: Bump patch version
id: bump
run: |
CURRENT=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
echo "Current version: $CURRENT"
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
NEW="$MAJOR.$MINOR.$((PATCH + 1))"
echo "New version: $NEW"
mvn -B versions:set -DnewVersion="$NEW" -DgenerateBackupPoms=false
echo "new_version=$NEW" >> "$GITHUB_OUTPUT"
- name: Build JAR
run: mvn -B clean package -DskipTests
- name: Commit bumped pom.xml
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add pom.xml
git commit -m "🆙 Bump version to ${{ steps.bump.outputs.new_version }} [skip ci]"
git push origin HEAD:main
- name: Generate release notes from latest commit
working-directory: ${{ github.workspace }}
env:
NEW_TAG: v${{ steps.bump.outputs.new_version }}
TRIGGER_SHA: ${{ github.sha }}
run: |
PREV_TAG=$(git tag --sort=-creatordate | head -n1 || true)
{
echo "## What's Changed"
echo ""
git log -1 --pretty=format:"### %s%n%n%b" "$TRIGGER_SHA"
echo ""
echo ""
if [ -n "$PREV_TAG" ]; then
echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${NEW_TAG}"
fi
} > release_notes.md
cat release_notes.md
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.bump.outputs.new_version }}
name: Release v${{ steps.bump.outputs.new_version }}
body_path: release_notes.md
files: Emulator/target/Habbo-${{ steps.bump.outputs.new_version }}-jar-with-dependencies.jar
@@ -80,7 +80,6 @@ END//
DELIMITER ;
CALL `_add_fk_if_missing`('rooms', 'fk_rooms_owner', 'owner_id', 'users', 'id', 'CASCADE');
CALL `_add_fk_if_missing`('items', 'fk_items_user', 'user_id', 'users', 'id', 'CASCADE');
CALL `_add_fk_if_missing`('catalog_items', 'fk_catitems_page', 'page_id', 'catalog_pages', 'id', 'CASCADE');
CALL `_add_fk_if_missing`('guilds', 'fk_guilds_user', 'user_id', 'users', 'id', 'CASCADE');
@@ -1 +0,0 @@
INSERT INTO emulator_settings (`key`, `value`) VALUES ('wired.tick.workers', '6');
-71
View File
@@ -1,71 +0,0 @@
ALTER TABLE `users` DROP KEY IF EXISTS `auth_ticket`;
ALTER TABLE `users`
MODIFY `auth_ticket` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS `idx_users_auth_ticket` ON `users` (`auth_ticket`);
CREATE INDEX IF NOT EXISTS `idx_rel_user_room` ON `room_enter_log` (`user_id`, `room_id`);
CREATE INDEX IF NOT EXISTS `idx_lhcp_user_claimed` ON `logs_hc_payday` (`user_id`, `claimed`);
CREATE UNIQUE INDEX IF NOT EXISTS `uniq_room_votes_user_room` ON `room_votes` (`user_id`, `room_id`);
ALTER TABLE `room_votes` DROP KEY IF EXISTS `user_id`;
CREATE INDEX IF NOT EXISTS `idx_rgs_room_ts` ON `room_game_scores` (`room_id`, `game_start_timestamp`);
CREATE INDEX IF NOT EXISTS `idx_rgs_user` ON `room_game_scores` (`user_id`);
CREATE UNIQUE INDEX IF NOT EXISTS `uniq_crc_user_campaign_reward`
ON `calendar_rewards_claimed` (`user_id`, `campaign_id`, `reward_id`);
ALTER TABLE `calendar_rewards_claimed` DROP KEY IF EXISTS `idx_cal_claimed_user_id`;
ALTER TABLE `emulator_settings`
ENGINE = InnoDB,
CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `gift_wrappers_new`;
CREATE TABLE `gift_wrappers_new` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`sprite_id` int(11) NOT NULL,
`item_id` int(11) NOT NULL,
`type` enum('gift','wrapper') NOT NULL DEFAULT 'wrapper',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `gift_wrappers_new` (`id`, `sprite_id`, `item_id`, `type`)
SELECT `id`, `sprite_id`, `item_id`, `type` FROM `gift_wrappers`;
DROP TABLE `gift_wrappers`;
RENAME TABLE `gift_wrappers_new` TO `gift_wrappers`;
DROP TABLE IF EXISTS `pet_actions_new`;
CREATE TABLE `pet_actions_new` (
`pet_type` int(2) NOT NULL AUTO_INCREMENT,
`pet_name` varchar(32) NOT NULL,
`offspring_type` int(3) NOT NULL DEFAULT -1,
`happy_actions` varchar(100) NOT NULL DEFAULT '',
`tired_actions` varchar(100) NOT NULL DEFAULT '',
`random_actions` varchar(100) NOT NULL DEFAULT '',
`can_swim` enum('1','0') DEFAULT '0',
PRIMARY KEY (`pet_type`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `pet_actions_new`
(`pet_type`, `pet_name`, `offspring_type`, `happy_actions`, `tired_actions`, `random_actions`, `can_swim`)
SELECT `pet_type`, `pet_name`, `offspring_type`, `happy_actions`, `tired_actions`, `random_actions`, `can_swim`
FROM `pet_actions`;
DROP TABLE `pet_actions`;
RENAME TABLE `pet_actions_new` TO `pet_actions`;
DROP TABLE IF EXISTS `pet_commands_data_new`;
CREATE TABLE `pet_commands_data_new` (
`command_id` int(3) NOT NULL,
`text` varchar(25) NOT NULL,
`required_level` int(2) NOT NULL,
`reward_xp` int(3) NOT NULL DEFAULT 5,
`cost_happiness` int(11) NOT NULL DEFAULT 0,
`cost_energy` int(3) NOT NULL DEFAULT 0,
PRIMARY KEY (`command_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `pet_commands_data_new`
(`command_id`, `text`, `required_level`, `reward_xp`, `cost_happiness`, `cost_energy`)
SELECT `command_id`, `text`, `required_level`, `reward_xp`, `cost_happiness`, `cost_energy`
FROM `pet_commands_data`;
DROP TABLE `pet_commands_data`;
RENAME TABLE `pet_commands_data_new` TO `pet_commands_data`;
ALTER TABLE `calendar_rewards`
MODIFY `product_name` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
MODIFY `custom_image` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
MODIFY `badge` VARCHAR(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
MODIFY `subscription_type` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '';
@@ -1,33 +0,0 @@
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;
@@ -1,32 +0,0 @@
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());
@@ -0,0 +1,2 @@
INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
('wired.tick.workers', '6', '');
@@ -1,25 +0,0 @@
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`);
@@ -1,5 +0,0 @@
ALTER TABLE `catalog_club_offers`
MODIFY COLUMN `type` ENUM('HC', 'VIP', 'BUILDERS_CLUB', 'BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC';
ALTER TABLE `users_settings`
ADD COLUMN `builders_club_bonus_furni` INT NOT NULL DEFAULT 0 AFTER `hc_gifts_claimed`;
+989
View File
@@ -0,0 +1,989 @@
UPDATE emulator_settings SET `value` = '1' WHERE (`key` = 'wired.engine.enabled');
UPDATE emulator_settings SET `value` = '1' WHERE (`key` = 'wired.engine.exclusive');
ALTER TABLE emulator_settings
ADD COLUMN IF NOT EXISTS `comment` VARCHAR(255) NOT NULL AFTER `value`;
CREATE TABLE IF NOT EXISTS `catalog_items_bc` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`item_ids` varchar(666) NOT NULL,
`page_id` int(11) NOT NULL,
`catalog_name` varchar(100) NOT NULL,
`order_number` int(11) NOT NULL DEFAULT 1,
`extradata` varchar(500) NOT NULL DEFAULT '',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `catalog_pages_bc` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`parent_id` int(11) NOT NULL DEFAULT -1,
`caption` varchar(128) NOT NULL,
`page_layout` enum(
'default_3x3','club_buy','club_gift','frontpage','spaces','recycler',
'recycler_info','recycler_prizes','trophies','plasto','marketplace',
'marketplace_own_items','spaces_new','soundmachine','guilds','guild_furni',
'info_duckets','info_rentables','info_pets','roomads','single_bundle',
'sold_ltd_items','badge_display','bots','pets','pets2','pets3',
'productpage1','room_bundle','recent_purchases',
'default_3x3_color_grouping','guild_forum','vip_buy','info_loyalty',
'loyalty_vip_buy','collectibles','petcustomization','frontpage_featured'
) NOT NULL DEFAULT 'default_3x3',
`icon_color` int(11) NOT NULL DEFAULT 1,
`icon_image` int(11) NOT NULL DEFAULT 1,
`order_num` int(11) NOT NULL DEFAULT 1,
`visible` enum('0','1') NOT NULL DEFAULT '1',
`enabled` enum('0','1') NOT NULL DEFAULT '1',
`page_headline` varchar(1024) NOT NULL DEFAULT '',
`page_teaser` varchar(64) NOT NULL DEFAULT '',
`page_special` varchar(2048) DEFAULT '' COMMENT 'Gold Bubble: catalog_special_txtbg1 // Speech Bubble: catalog_special_txtbg2 // Place normal text in page_text_teaser',
`page_text1` text DEFAULT NULL,
`page_text2` text DEFAULT NULL,
`page_text_details` text DEFAULT NULL,
`page_text_teaser` text DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci ROW_FORMAT=DYNAMIC;
ALTER TABLE `catalog_club_offers`
MODIFY COLUMN `type` ENUM('HC','VIP','BUILDERS_CLUB','BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC';
ALTER TABLE `catalog_pages`
MODIFY COLUMN `page_layout` ENUM(
'default_3x3',
'club_buy',
'club_gift',
'frontpage',
'spaces',
'recycler',
'recycler_info',
'recycler_prizes',
'trophies',
'plasto',
'marketplace',
'marketplace_own_items',
'spaces_new',
'soundmachine',
'guilds',
'guild_furni',
'info_duckets',
'info_rentables',
'info_pets',
'roomads',
'single_bundle',
'sold_ltd_items',
'badge_display',
'bots',
'pets',
'pets2',
'pets3',
'productpage1',
'room_bundle',
'recent_purchases',
'default_3x3_color_grouping',
'guild_forum',
'vip_buy',
'info_loyalty',
'loyalty_vip_buy',
'collectibles',
'petcustomization',
'frontpage_featured',
'builders_club_frontpage',
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
ALTER TABLE `catalog_pages`
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL','BUILDER','BOTH') NOT NULL DEFAULT 'NORMAL'
AFTER `club_only`;
ALTER TABLE `catalog_pages_bc`
MODIFY COLUMN `page_layout` ENUM(
'default_3x3',
'club_buy',
'club_gift',
'frontpage',
'spaces',
'recycler',
'recycler_info',
'recycler_prizes',
'trophies',
'plasto',
'marketplace',
'marketplace_own_items',
'spaces_new',
'soundmachine',
'guilds',
'guild_furni',
'info_duckets',
'info_rentables',
'info_pets',
'roomads',
'single_bundle',
'sold_ltd_items',
'badge_display',
'bots',
'pets',
'pets2',
'pets3',
'productpage1',
'room_bundle',
'recent_purchases',
'default_3x3_color_grouping',
'guild_forum',
'vip_buy',
'info_loyalty',
'loyalty_vip_buy',
'collectibles',
'petcustomization',
'frontpage_featured',
'builders_club_frontpage',
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
SET @col_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'users_settings'
AND COLUMN_NAME = 'builders_club_bonus_furni'
);
SET @sql := IF(@col_exists = 0,
'ALTER TABLE `users_settings` ADD COLUMN `builders_club_bonus_furni` INT NOT NULL DEFAULT 0;',
'SELECT "exists";'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS `wired_emulator_settings` (
`key` varchar(191) NOT NULL,
`value` text NOT NULL,
`comment` text NOT NULL,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`)
SELECT 'wired.engine.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.enabled' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.'
UNION ALL
SELECT 'wired.engine.exclusive', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.exclusive' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.'
UNION ALL
SELECT 'wired.engine.maxStepsPerStack', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.maxStepsPerStack' LIMIT 1), '100'), 'Maximum amount of internal processing steps allowed for a single wired stack execution.'
UNION ALL
SELECT 'wired.engine.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.debug' LIMIT 1), '0'), 'Enable verbose debug logging for the new wired engine.'
UNION ALL
SELECT 'wired.custom.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.custom.enabled' LIMIT 1), '0'), 'Enable custom legacy wired behaviour such as user-based cooldown exceptions and compatibility logic.'
UNION ALL
SELECT 'hotel.wired.furni.selection.count', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.furni.selection.count' LIMIT 1), '5'), 'Maximum number of furni that a wired box can store or select.'
UNION ALL
SELECT 'hotel.wired.max_delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.max_delay' LIMIT 1), '20'), 'Maximum delay value accepted by wired effects that support delayed execution.'
UNION ALL
SELECT 'hotel.wired.message.max_length', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.message.max_length' LIMIT 1), '100'), 'Maximum length of text fields used by wired messages and bot text effects.'
UNION ALL
SELECT 'wired.effect.teleport.delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.effect.teleport.delay' LIMIT 1), '500'), 'Delay in milliseconds used by wired teleport movement.'
UNION ALL
SELECT 'wired.place.under', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.place.under' LIMIT 1), '0'), 'Allow placing wired furniture underneath other items when room rules permit it.'
UNION ALL
SELECT 'wired.tick.interval.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.interval.ms' LIMIT 1), '50'), 'Global wired tick interval in milliseconds used by repeaters and other tick-driven wired items.'
UNION ALL
SELECT 'wired.tick.resolution', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.resolution' LIMIT 1), '100'), 'Legacy wired tick resolution value kept for compatibility with older wired timing setups.'
UNION ALL
SELECT 'wired.tick.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.debug' LIMIT 1), '0'), 'Enable verbose logging for the wired tick service.'
UNION ALL
SELECT 'wired.tick.thread.priority', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.thread.priority' LIMIT 1), '6'), 'Java thread priority used by the wired tick service.'
UNION ALL
SELECT 'wired.highscores.displaycount', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.highscores.displaycount' LIMIT 1), '25'), 'Maximum number of wired highscore entries shown to users when a highscore is displayed.'
UNION ALL
SELECT 'wired.abuse.max.recursion.depth', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.recursion.depth' LIMIT 1), '10'), 'Maximum recursive wired depth allowed before execution is stopped.'
UNION ALL
SELECT 'wired.abuse.max.events.per.window', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.events.per.window' LIMIT 1), '100'), 'Maximum amount of identical wired events allowed inside the abuse rate-limit window before a room ban is applied.'
UNION ALL
SELECT 'wired.abuse.rate.limit.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.rate.limit.window.ms' LIMIT 1), '10000'), 'Time window in milliseconds used by the wired abuse rate limiter.'
UNION ALL
SELECT 'wired.abuse.ban.duration.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.ban.duration.ms' LIMIT 1), '600000'), 'Duration in milliseconds of the temporary wired ban after abuse detection.'
UNION ALL
SELECT 'wired.monitor.usage.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.window.ms' LIMIT 1), '1000'), 'Rolling window size in milliseconds used to calculate wired usage in the :wired monitor.'
UNION ALL
SELECT 'wired.monitor.usage.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.limit' LIMIT 1), '1000'), 'Maximum wired usage budget allowed in one monitor window before EXECUTION_CAP is raised.'
UNION ALL
SELECT 'wired.monitor.delayed.events.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.delayed.events.limit' LIMIT 1), '100'), 'Maximum number of delayed wired events that can be queued in one room at the same time.'
UNION ALL
SELECT 'wired.monitor.overload.average.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.average.ms' LIMIT 1), '50'), 'Average execution time threshold in milliseconds that starts overload tracking.'
UNION ALL
SELECT 'wired.monitor.overload.peak.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.peak.ms' LIMIT 1), '150'), 'Peak single execution time threshold in milliseconds that starts overload tracking.'
UNION ALL
SELECT 'wired.monitor.overload.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.consecutive.windows' LIMIT 1), '2'), 'Number of consecutive overloaded monitor windows required before logging EXECUTOR_OVERLOAD.'
UNION ALL
SELECT 'wired.monitor.heavy.usage.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.usage.percent' LIMIT 1), '70'), 'Usage percentage threshold that contributes to marking a room as heavy in the :wired monitor.'
UNION ALL
SELECT 'wired.monitor.heavy.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.consecutive.windows' LIMIT 1), '5'), 'Number of consecutive windows above the heavy usage threshold required before the room is marked as heavy.'
UNION ALL
SELECT 'wired.monitor.heavy.delayed.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.delayed.percent' LIMIT 1), '60'), 'Delayed queue percentage threshold that also contributes to the heavy-room calculation.'
ON DUPLICATE KEY UPDATE
`value` = VALUES(`value`),
`comment` = VALUES(`comment`);
DELETE FROM `emulator_settings`
WHERE `key` IN (
'wired.engine.enabled',
'wired.engine.exclusive',
'wired.engine.maxStepsPerStack',
'wired.engine.debug',
'wired.custom.enabled',
'hotel.wired.furni.selection.count',
'hotel.wired.max_delay',
'hotel.wired.message.max_length',
'wired.effect.teleport.delay',
'wired.place.under',
'wired.tick.interval.ms',
'wired.tick.resolution',
'wired.tick.debug',
'wired.tick.thread.priority',
'wired.highscores.displaycount',
'wired.abuse.max.recursion.depth',
'wired.abuse.max.events.per.window',
'wired.abuse.rate.limit.window.ms',
'wired.abuse.ban.duration.ms',
'wired.monitor.usage.window.ms',
'wired.monitor.usage.limit',
'wired.monitor.delayed.events.limit',
'wired.monitor.overload.average.ms',
'wired.monitor.overload.peak.ms',
'wired.monitor.overload.consecutive.windows',
'wired.monitor.heavy.usage.percent',
'wired.monitor.heavy.consecutive.windows',
'wired.monitor.heavy.delayed.percent'
);
UPDATE `emulator_settings` SET `comment` = 'Allow whispering while a user stands inside a mute area.' WHERE `key` = 'room.chat.mutearea.allow_whisper';
UPDATE `emulator_settings` SET `comment` = 'HTML or text format used for room chat prefixes.' WHERE `key` = 'room.chat.prefix.format';
UPDATE `emulator_settings` SET `comment` = 'Badge code displayed on promoted rooms.' WHERE `key` = 'room.promotion.badge';
UPDATE `emulator_settings` SET `comment` = 'Image used by Rosie bubble notifications.' WHERE `key` = 'rosie.bubble.image.url';
UPDATE `emulator_settings` SET `comment` = 'Currency type used by Rosie when buying a room or room package.' WHERE `key` = 'rosie.buyroom.currency.type';
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `runtime.threads`.' WHERE `key` = 'runtime.threads';
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.private.chats`.' WHERE `key` = 'save.private.chats';
UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.room.chats`.' WHERE `key` = 'save.room.chats';
UPDATE `emulator_settings` SET `comment` = 'Expose moderation tickets to the scripter or automation tooling.' WHERE `key` = 'scripter.modtool.tickets';
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for diamonds.' WHERE `key` = 'seasonal.currency.diamond';
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for duckets.' WHERE `key` = 'seasonal.currency.ducket';
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated display names for seasonal currency types.' WHERE `key` = 'seasonal.currency.names';
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for pixels.' WHERE `key` = 'seasonal.currency.pixel';
UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for shells.' WHERE `key` = 'seasonal.currency.shell';
UPDATE `emulator_settings` SET `comment` = 'Primary seasonal currency type ID.' WHERE `key` = 'seasonal.primary.type';
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated list of currency type IDs treated as seasonal currencies.' WHERE `key` = 'seasonal.types';
UPDATE `emulator_settings` SET `comment` = 'Achievement code granted for the HC subscription tier.' WHERE `key` = 'subscriptions.hc.achievement';
UPDATE `emulator_settings` SET `comment` = 'Number of days before expiry when HC discount offers become available.' WHERE `key` = 'subscriptions.hc.discount.days_before_end';
UPDATE `emulator_settings` SET `comment` = 'Enable discounted HC renewal offers.' WHERE `key` = 'subscriptions.hc.discount.enabled';
UPDATE `emulator_settings` SET `comment` = 'Reset tracked credits spent when the HC subscription expires.' WHERE `key` = 'subscriptions.hc.payday.creditsspent_reset_on_expire';
UPDATE `emulator_settings` SET `comment` = 'Currency rewarded by the HC payday system.' WHERE `key` = 'subscriptions.hc.payday.currency';
UPDATE `emulator_settings` SET `comment` = 'Enable the HC payday reward system.' WHERE `key` = 'subscriptions.hc.payday.enabled';
UPDATE `emulator_settings` SET `comment` = 'Date interval used between HC payday reward runs.' WHERE `key` = 'subscriptions.hc.payday.interval';
UPDATE `emulator_settings` SET `comment` = 'Next scheduled execution date for HC payday rewards.' WHERE `key` = 'subscriptions.hc.payday.next_date';
UPDATE `emulator_settings` SET `comment` = 'Percentage of eligible spending returned by HC payday.' WHERE `key` = 'subscriptions.hc.payday.percentage';
UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated streak thresholds and rewards for HC payday.' WHERE `key` = 'subscriptions.hc.payday.streak';
UPDATE `emulator_settings` SET `comment` = 'Enable the subscription background scheduler.' WHERE `key` = 'subscriptions.scheduler.enabled';
UPDATE `emulator_settings` SET `comment` = 'Interval in minutes between subscription scheduler runs.' WHERE `key` = 'subscriptions.scheduler.interval';
UPDATE `emulator_settings` SET `comment` = 'Compatibility marker used by the custom team wired implementation. Do not remove.' WHERE `key` = 'team.wired.update.rc-1';
UPDATE `emulator_settings` SET `comment` = 'API key used by the YouTube integration.' WHERE `key` = 'youtube.apikey';
DROP VIEW IF EXISTS `permissions_matrix_view`;
DROP PROCEDURE IF EXISTS `refresh_permissions_matrix_view`;
DROP TABLE IF EXISTS `permission_rank_values`;
DROP TABLE IF EXISTS `permission_nodes`;
CREATE TABLE IF NOT EXISTS `permission_ranks` (
`id` int(11) NOT NULL,
`rank_name` varchar(25) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
`hidden_rank` tinyint(1) NOT NULL DEFAULT 0,
`badge` varchar(12) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '',
`job_description` varchar(255) NOT NULL DEFAULT 'Here to help',
`staff_color` varchar(8) NOT NULL DEFAULT '#327fa8',
`staff_background` varchar(255) NOT NULL DEFAULT 'staff-bg.png',
`level` int(11) NOT NULL DEFAULT 1,
`room_effect` int(11) NOT NULL DEFAULT 0,
`log_commands` enum('0','1') CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '0',
`prefix` varchar(5) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '',
`prefix_color` varchar(7) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '',
`auto_credits_amount` int(11) DEFAULT 0,
`auto_pixels_amount` int(11) DEFAULT 0,
`auto_gotw_amount` int(11) DEFAULT 0,
`auto_points_amount` int(11) DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `permission_definitions` (
`permission_key` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
`max_value` tinyint(3) unsigned NOT NULL DEFAULT 1,
`comment` text NOT NULL,
PRIMARY KEY (`permission_key`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci ROW_FORMAT=DYNAMIC;
ALTER TABLE `permission_definitions`
DROP COLUMN IF EXISTS `category`,
DROP COLUMN IF EXISTS `value_type`,
DROP COLUMN IF EXISTS `sort_order`;
INSERT INTO `permission_ranks` (
`id`,
`rank_name`,
`hidden_rank`,
`badge`,
`job_description`,
`staff_color`,
`staff_background`,
`level`,
`room_effect`,
`log_commands`,
`prefix`,
`prefix_color`,
`auto_credits_amount`,
`auto_pixels_amount`,
`auto_gotw_amount`,
`auto_points_amount`
)
SELECT
`id`,
`rank_name`,
`hidden_rank`,
`badge`,
`job_description`,
`staff_color`,
`staff_background`,
`level`,
`room_effect`,
`log_commands`,
`prefix`,
`prefix_color`,
`auto_credits_amount`,
`auto_pixels_amount`,
`auto_gotw_amount`,
`auto_points_amount`
FROM `permissions`
ON DUPLICATE KEY UPDATE
`rank_name` = VALUES(`rank_name`),
`hidden_rank` = VALUES(`hidden_rank`),
`badge` = VALUES(`badge`),
`job_description` = VALUES(`job_description`),
`staff_color` = VALUES(`staff_color`),
`staff_background` = VALUES(`staff_background`),
`level` = VALUES(`level`),
`room_effect` = VALUES(`room_effect`),
`log_commands` = VALUES(`log_commands`),
`prefix` = VALUES(`prefix`),
`prefix_color` = VALUES(`prefix_color`),
`auto_credits_amount` = VALUES(`auto_credits_amount`),
`auto_pixels_amount` = VALUES(`auto_pixels_amount`),
`auto_gotw_amount` = VALUES(`auto_gotw_amount`),
`auto_points_amount` = VALUES(`auto_points_amount`);
DROP PROCEDURE IF EXISTS `refresh_permission_definition_rank_columns`;
DELIMITER $$
CREATE PROCEDURE `refresh_permission_definition_rank_columns`()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE current_rank_id INT;
DECLARE current_column_name VARCHAR(32);
DECLARE column_exists INT DEFAULT 0;
DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN rank_cursor;
rank_loop: LOOP
FETCH rank_cursor INTO current_rank_id;
IF done = 1 THEN
LEAVE rank_loop;
END IF;
SET current_column_name = CONCAT('rank_', current_rank_id);
SELECT COUNT(*)
INTO column_exists
FROM `information_schema`.`columns`
WHERE `table_schema` = DATABASE()
AND `table_name` = 'permission_definitions'
AND `column_name` = current_column_name;
IF column_exists = 0 THEN
SET @alter_permissions_column_sql = CONCAT(
'ALTER TABLE `permission_definitions` ADD COLUMN `',
current_column_name,
'` tinyint(3) unsigned NOT NULL DEFAULT 0'
);
PREPARE alter_permissions_column_stmt FROM @alter_permissions_column_sql;
EXECUTE alter_permissions_column_stmt;
DEALLOCATE PREPARE alter_permissions_column_stmt;
END IF;
END LOOP;
CLOSE rank_cursor;
END$$
DELIMITER ;
CALL `refresh_permission_definition_rank_columns`();
INSERT INTO `permission_definitions` (
`permission_key`,
`max_value`,
`comment`
)
SELECT
`column_name` AS `permission_key`,
CASE
WHEN `column_type` LIKE '%''2''%' THEN 2
ELSE 1
END AS `max_value`,
CASE
WHEN COALESCE(`column_comment`, '') <> '' THEN `column_comment`
WHEN `column_name` LIKE 'cmd\_%' AND `column_type` LIKE '%''2''%' THEN CONCAT(
'Controls access to the :',
REPLACE(SUBSTRING(`column_name`, 5), '_', ' '),
' command. Values: 0 = disabled, 1 = allowed, 2 = allowed only when room-owner rights may be used.'
)
WHEN `column_name` LIKE 'cmd\_%' THEN CONCAT(
'Controls access to the :',
REPLACE(SUBSTRING(`column_name`, 5), '_', ' '),
' command. Values: 0 = disabled, 1 = allowed.'
)
WHEN `column_name` LIKE 'acc\_%' AND `column_type` LIKE '%''2''%' THEN CONCAT(
'Controls the ',
REPLACE(SUBSTRING(`column_name`, 5), '_', ' '),
' capability for this rank. Values: 0 = disabled, 1 = enabled, 2 = enabled only when room-owner rights may be used.'
)
WHEN `column_name` LIKE 'acc\_%' THEN CONCAT(
'Controls the ',
REPLACE(SUBSTRING(`column_name`, 5), '_', ' '),
' capability for this rank. Values: 0 = disabled, 1 = enabled.'
)
ELSE CONCAT(
'Legacy permission-related value migrated from the old permissions table for ',
`column_name`,
'.'
)
END AS `comment`
FROM `information_schema`.`columns`
WHERE `table_schema` = DATABASE()
AND `table_name` = 'permissions'
AND `column_name` NOT IN (
'id',
'rank_name',
'hidden_rank',
'badge',
'job_description',
'staff_color',
'staff_background',
'level',
'room_effect',
'log_commands',
'prefix',
'prefix_color',
'auto_credits_amount',
'auto_pixels_amount',
'auto_gotw_amount',
'auto_points_amount'
)
ON DUPLICATE KEY UPDATE
`max_value` = VALUES(`max_value`),
`comment` = VALUES(`comment`);
DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`;
CREATE TEMPORARY TABLE `tmp_permission_comments` (
`permission_key` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
`comment` text NOT NULL,
PRIMARY KEY (`permission_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci;
INSERT INTO `tmp_permission_comments` (`permission_key`, `comment`) VALUES
('cmd_about', 'Allows using :about to display emulator, revision, or hotel information exposed by the command.'),
('cmd_alert', 'Allows using :alert to send a hotel alert popup to a specific user.'),
('cmd_allow_trading', 'Allows using the trading-toggle command to enable or disable trading for a target user.'),
('cmd_badge', 'Allows granting a badge code to a target user through a command.'),
('cmd_ban', 'Allows banning users from the hotel.'),
('cmd_blockalert', 'Allows sending the block-alert style moderation message.'),
('cmd_bots', 'Allows using :bots to list the bots currently placed in the room.'),
('cmd_bundle', 'Allows using :bundle / :roombundle to create a catalog room-bundle offer for the current room.'),
('cmd_calendar', 'Allows using the hotel calendar command and any calendar actions wired to that command entry.'),
('cmd_changename', 'Allows forcing a user-name change through the change-name command flow.'),
('cmd_chatcolor', 'Allows changing the active chat bubble color through the chat-color command.'),
('cmd_commands', 'Allows using :commands to list the command keys available to the current user.'),
('cmd_connect_camera', 'Allows using the command that links the in-room camera feature to the current room session.'),
('cmd_control', 'Allows using :control to take over another in-room user and stop controlling them later.'),
('cmd_coords', 'Allows using :coords to inspect room coordinates for tiles, users, or furniture.'),
('cmd_credits', 'Allows giving or removing credits from a user through the staff currency command.'),
('cmd_subscription', 'Allows granting or editing subscription time through the subscription command.'),
('cmd_danceall', 'Allows forcing every Habbo currently in the room to dance.'),
('cmd_diagonal', 'Allows toggling diagonal walking for the current room.'),
('cmd_disconnect', 'Allows disconnecting a user from the hotel immediately.'),
('cmd_duckets', 'Allows giving or removing duckets from a user through the staff currency command.'),
('cmd_ejectall', 'Allows ejecting all users from the current room.'),
('cmd_empty', 'Allows clearing the current user furniture inventory through the empty-inventory command.'),
('cmd_empty_bots', 'Allows clearing the current user bot inventory through the empty-bots command.'),
('cmd_empty_pets', 'Allows clearing the current user pet inventory through the empty-pets command.'),
('cmd_enable', 'Allows applying an avatar effect to yourself, or to another user when acc_enable_others is also granted.'),
('cmd_event', 'Allows marking the current room as an event room through the event command.'),
('cmd_faceless', 'Allows toggling the faceless avatar visual state on the executing room unit.'),
('cmd_fastwalk', 'Allows toggling fast-walk mode for yourself or another in-room user.'),
('cmd_filterword', 'Allows adding or removing entries from the configured word filter through command usage.'),
('cmd_freeze', 'Allows freezing a target user in place.'),
('cmd_freeze_bots', 'Allows freezing bots that are placed in the room.'),
('cmd_gift', 'Allows sending a gift to a target user through the gift command.'),
('cmd_give_rank', 'Allows setting another user rank through the give-rank command.'),
('cmd_ha', 'Allows sending a hotel-wide alert.'),
('acc_can_stalk', 'Allows following users even when they have disabled stalking.'),
('cmd_hal', 'Allows sending a hotel-wide alert with a clickable link or extended content.'),
('cmd_invisible', 'Allows toggling invisible staff mode.'),
('cmd_ip_ban', 'Allows banning a user by IP address.'),
('cmd_machine_ban', 'Allows banning a user by machine identifier.'),
('cmd_hand_item', 'Allows spawning or changing the hand item currently held by a user.'),
('cmd_happyhour', 'Allows starting or stopping the happy-hour event flow exposed by the happyhour command.'),
('cmd_hidewired', 'Allows toggling whether wired furniture is visually hidden in the current room.'),
('cmd_kickall', 'Allows kicking every user from the current room.'),
('cmd_softkick', 'Allows soft-kicking a user back to the hotel view without a full sanction.'),
('cmd_massbadge', 'Allows giving the same badge to many users at once.'),
('cmd_roombadge', 'Allows setting or overriding the room badge shown to users.'),
('cmd_masscredits', 'Allows giving credits to many users at once through the mass-credits command.'),
('cmd_massduckets', 'Allows giving duckets to many users at once through the mass-duckets command.'),
('cmd_massgift', 'Allows sending the same gift to many users at once.'),
('cmd_masspoints', 'Allows giving activity points to many users at once through the mass-points command.'),
('cmd_moonwalk', 'Allows toggling the moonwalk avatar effect for yourself while you are inside a room.'),
('cmd_mimic', 'Allows copying another user appearance or presence state through the mimic command.'),
('cmd_multi', 'Allows executing multiple chat commands from the special sticky/post-it scripting payload.'),
('cmd_mute', 'Allows muting a target user.'),
('cmd_pet_info', 'Allows opening the detailed pet-information view for a pet.'),
('cmd_pickall', 'Allows picking up every furniture item from the current room.'),
('cmd_plugins', 'Legacy key for the :plugins command, which currently lists loaded plugins without enforcing this dedicated permission node in code.'),
('cmd_points', 'Allows giving or removing activity points from a user through the points command.'),
('cmd_promote_offer', 'Allows using :promoteoffer to list active target offers or switch the globally promoted target offer.'),
('cmd_pull', 'Allows pulling a nearby user onto the tile directly in front of you.'),
('cmd_push', 'Allows pushing the user standing in front of you one tile farther in the direction you are facing.'),
('cmd_redeem', 'Allows redeeming redeemable inventory items through the redeem command flow.'),
('cmd_reload_room', 'Allows unloading and reloading the current room, then forwarding the occupants back into the fresh room instance.'),
('cmd_roomalert', 'Allows sending the same alert message to everyone in the current room.'),
('cmd_roomcredits', 'Allows giving credits to every Habbo currently in the room.'),
('cmd_roomeffect', 'Allows applying the same avatar effect id to every Habbo currently in the room.'),
('cmd_roomgift', 'Allows sending the same gift to every Habbo currently in the room.'),
('cmd_roomitem', 'Allows setting the same hand-item id for every Habbo in the room; using 0 clears the hand item.'),
('cmd_roommute', 'Allows muting every Habbo currently in the room.'),
('cmd_roompixels', 'Allows giving duckets or pixels to every Habbo currently in the room.'),
('cmd_roompoints', 'Allows giving activity points to every Habbo currently in the room.'),
('cmd_say', 'Allows forcing another online user to say a custom message in their current room.'),
('cmd_say_all', 'Allows making everyone in the room say a message.'),
('cmd_setmax', 'Allows using :setmax to change the maximum user capacity of the current room.'),
('cmd_set_poll', 'Allows using :setpoll to attach or remove a poll on the current room.'),
('cmd_setpublic', 'Allows using :setpublic to change the room public/private visibility state.'),
('cmd_setspeed', 'Allows using :setspeed to change the room walking speed setting.'),
('cmd_shout', 'Allows forcing another online user to shout a custom message in their current room.'),
('cmd_shout_all', 'Allows making everyone in the room shout a message.'),
('cmd_shutdown', 'Allows using the shutdown command to stop the emulator process.'),
('cmd_sitdown', 'Allows forcing users to sit down through the sitdown command.'),
('cmd_staffalert', 'Allows sending an alert that is visible only to staff members.'),
('cmd_staffonline', 'Allows viewing the current list of online staff members.'),
('cmd_summon', 'Allows summoning a target user into the room where the staff member currently is.'),
('cmd_summonrank', 'Allows summoning all online users of a given rank into the current room.'),
('cmd_super_ban', 'Allows issuing the strongest ban command variant exposed by the super-ban command.'),
('cmd_stalk', 'Allows following another user to their room.'),
('cmd_superpull', 'Allows pulling a user to the tile in front of you without the short-range reach check used by :pull.'),
('cmd_take_badge', 'Allows removing a badge code from a target user.'),
('cmd_talk', 'Allows using the legacy :talk command to make another user speak a command-provided message.'),
('cmd_teleport', 'Allows toggling the room-unit teleport mode used by the :teleport command.'),
('cmd_trash', 'Allows deleting or trashing furniture/items through the trash command flow.'),
('cmd_transform', 'Allows transforming your room unit into a chosen pet type, race, and color.'),
('cmd_unban', 'Allows removing active bans.'),
('cmd_unload', 'Allows disposing the current room instance immediately through :unload / :crash.'),
('cmd_unmute', 'Allows removing an active mute from a target user.'),
('cmd_update_achievements', 'Allows using :update_achievements to reload achievements configuration.'),
('cmd_update_bots', 'Allows using :update_bots to reload bot data and bot configuration.'),
('cmd_update_catalogue', 'Allows using :update_catalogue to reload catalogue pages and offers.'),
('cmd_update_config', 'Allows using :update_config to reload emulator configuration settings.'),
('cmd_update_guildparts', 'Allows using :update_guildparts to reload guild badge parts and guild configuration.'),
('cmd_update_hotel_view', 'Allows using :update_hotel_view to reload hotel-view assets or settings.'),
('cmd_update_items', 'Allows using :update_items to reload item data and furniture definitions.'),
('cmd_update_navigator', 'Allows using :update_navigator to reload navigator configuration and listings.'),
('cmd_update_permissions', 'Allows using :update_permissions to reload ranks and permissions from the database.'),
('cmd_update_pet_data', 'Allows using :update_pet_data to reload pet types and pet races.'),
('cmd_update_plugins', 'Allows using :update_plugins to reload plugin data or plugin metadata.'),
('cmd_update_polls', 'Allows using :update_polls to reload poll and questionnaire data.'),
('cmd_update_texts', 'Allows using :update_texts to reload external texts and localizations.'),
('cmd_update_wordfilter', 'Allows using :update_wordfilter to reload the word-filter list.'),
('cmd_userinfo', 'Allows opening the detailed user-information view used by staff tools.'),
('cmd_word_quiz', 'Allows starting a room word-quiz event with a custom question and optional duration.'),
('cmd_warp', 'Allows instantly warping your room unit to a target tile.'),
('acc_anychatcolor', 'Allows selecting any chat bubble color, including normally restricted colors.'),
('acc_anyroomowner', 'Treats the rank as room owner for owner-only checks such as room settings, wired saving, rights management, floorplan editing, and similar room-owner gates.'),
('acc_empty_others', 'Allows :empty, :empty_bots, and :empty_pets to target another user inventory instead of only your own.'),
('acc_enable_others', 'Allows :enable to apply avatar effects to another user instead of only to yourself.'),
('acc_see_whispers', 'Allows seeing whispers sent between other users in the room.'),
('acc_see_tentchat', 'Allows seeing tent chat or similar hidden chat channels that are normally not visible to everyone.'),
('acc_superwired', 'Allows saving advanced wired data without the normal wordfilter and reward payload restrictions applied to regular users.'),
('acc_supporttool', 'Allows opening and using the support/moderation tool interface.'),
('acc_unkickable', 'Prevents the user from being kicked by normal moderation or room commands.'),
('acc_guildgate', 'Allows bypassing guild gate access restrictions.'),
('acc_moverotate', 'Allows moving, rotating, and saving wired furniture without the usual room-owner restriction checks.'),
('acc_placefurni', 'Allows placing furniture, opening :wired, and passing room-right checks that normally require owner or controller rights.'),
('acc_unlimited_bots', 'Removes both the bot inventory cap and the per-room bot placement cap for this rank.'),
('acc_unlimited_pets', 'Removes both the pet inventory cap and the per-room pet placement cap for this rank.'),
('acc_hide_ip', 'Hides the user IP address in staff tools and other staff-facing views.'),
('acc_hide_mail', 'Hides the user email address in moderation tools and staff views.'),
('acc_not_mimiced', 'Prevents other users from mimicking this account.'),
('acc_chat_no_flood', 'Exempts the user from flood protection limits.'),
('acc_staff_chat', 'Allows accessing staff-only chat channels and staff broadcasts.'),
('acc_staff_pick', 'Allows using staff item pick-up actions that bypass normal room ownership restrictions.'),
('acc_enteranyroom', 'Allows entering rooms regardless of door mode, bans, or normal access restrictions.'),
('acc_fullrooms', 'Allows entering rooms even when they are at maximum user capacity.'),
('acc_infinite_credits', 'Prevents credits from being consumed when a command or purchase checks credit balance.'),
('acc_infinite_pixels', 'Prevents duckets or pixels from being consumed when the balance is checked.'),
('acc_infinite_points', 'Prevents activity points from being consumed when the balance is checked.'),
('acc_ambassador', 'Marks the rank as an ambassador for ambassador-only tools and visuals.'),
('acc_debug', 'Allows using debug-only features, commands, or internal tooling.'),
('acc_chat_no_limit', 'Lets the user hear and be heard regardless of room hearing distance limits.'),
('acc_chat_no_filter', 'Bypasses the word filter for chat and staff-generated messages.'),
('acc_nomute', 'Prevents the user from being muted by normal mute checks.'),
('acc_guild_admin', 'Allows bypassing guild admin restrictions when managing guilds.'),
('acc_catalog_ids', 'Allows seeing internal catalogue page ids, offer ids, or related technical catalogue identifiers.'),
('acc_modtool_ticket_q', 'Allows seeing and handling the moderation ticket queue.'),
('acc_modtool_user_logs', 'Allows reading user chat logs in the moderation tool.'),
('acc_modtool_user_alert', 'Allows sending moderation alerts or cautions to users.'),
('acc_modtool_user_kick', 'Allows kicking users from the moderation tool.'),
('acc_modtool_user_ban', 'Allows banning users from the moderation tool.'),
('acc_modtool_room_info', 'Allows viewing room information in the moderation tool.'),
('acc_modtool_room_logs', 'Allows viewing room chat logs in the moderation tool.'),
('acc_trade_anywhere', 'Allows starting trades outside the normal trade-enabled areas.'),
('acc_update_notifications', 'Allows receiving update notifications emitted by the emulator.'),
('acc_helper_use_guide_tool', 'Allows opening the helper guide tool.'),
('acc_helper_give_guide_tours', 'Allows accepting and handling guide tour requests.'),
('acc_helper_judge_chat_reviews', 'Allows reviewing helper or chat review tickets.'),
('acc_floorplan_editor', 'Allows opening and saving the floorplan editor.'),
('acc_camera', 'Allows using the in-room camera feature and related camera UI actions.'),
('acc_ads_background', 'Allows editing room advertisement backgrounds.'),
('cmd_wordquiz', 'Legacy alias of cmd_word_quiz for starting a room word-quiz event.'),
('acc_room_staff_tags', 'Shows staff tags or markers above the user while inside rooms.'),
('acc_infinite_friends', 'Removes the normal friend-list size limit.'),
('acc_mimic_unredeemed', 'Allows mimicking looks even when they contain unreleased or restricted clothing.'),
('cmd_update_youtube_playlists', 'Allows reloading YouTube playlist configuration for furniture integrations.'),
('cmd_add_youtube_playlist', 'Allows adding a new YouTube playlist entry.'),
('acc_mention', 'Allows using mention-related chat features beyond the normal rank restriction.'),
('cmd_setstate', 'Legacy room-editor permission for :setstate / :ss, used to change the selected furni state or extradata value.'),
('cmd_buildheight', 'Legacy room-editor permission for :buildheight / :bh, used to change the room build-height override.'),
('cmd_setrotation', 'Legacy room-editor permission for :setrotation / :rot, used to change the rotation of the selected furni.'),
('cmd_sellroom', 'Allows putting the current room up for sale through the sell-room command.'),
('cmd_buyroom', 'Allows purchasing a room that is marked as for sale through the buy-room command.'),
('cmd_pay', 'Allows transferring currency to another user through the pay command.'),
('cmd_kill', 'Allows using the kill command effect exposed by the current command set.'),
('cmd_hoverboard', 'Allows toggling the hoverboard effect or hoverboard movement mode.'),
('cmd_kiss', 'Allows using the kiss interaction command on another user.'),
('cmd_hug', 'Allows using the hug interaction command on another user.'),
('cmd_welcome', 'Allows triggering the welcome command behavior defined by the current command set.'),
('cmd_disable_effects', 'Allows disabling active avatar effects through the disable-effects command.'),
('cmd_brb', 'Allows toggling the be-right-back status command.'),
('cmd_nuke', 'Allows using the nuke command exposed by the current command set.'),
('cmd_slime', 'Allows applying the slime command/effect exposed by the current command set.'),
('cmd_explain', 'Allows using the explain command to send the predefined explanation/help flow to users.'),
('cmd_closedice', 'Legacy essentials permission for :closedice, used to close dice items in the room or all dice at once.'),
('acc_closedice_room', 'Legacy companion permission used by older closed-dice room checks.'),
('cmd_set', 'Legacy essentials permission for :set / :changefurni, the generic furni editing command documented by :set info.'),
('cmd_furnidata', 'Allows viewing technical furnidata information in-game for selected furniture.'),
('kiss_cmd', 'Legacy alias used for the kiss command permission.'),
('acc_calendar_force', 'Allows claiming calendar rewards even when the normal day-difference timing check would block the claim.'),
('cmd_update_calendar', 'Allows using :update_calendar to reload calendar definitions and rewards.'),
('cmd_update_all', 'Allows using :update_all to reload all supported runtime data sets in one command.'),
('cms_dance', 'Legacy CMS-side permission kept for website integrations; no direct in-emulator command handler was found in the current tree.'),
('acc_catalogfurni', 'Allows using catalogue administration features related to furniture pages and offers.'),
('acc_unignorable', 'Prevents the account from being ignored by other users through the ignore system.'),
('cmd_update_chat_bubbles', 'Allows using :update_chat_bubbles to reload chat-bubble definitions and assets.'),
('cmd_calendar_staff', 'Allows the staff-only actions exposed by the calendar command flow.');
UPDATE `permission_definitions` pd
INNER JOIN `tmp_permission_comments` tc ON tc.`permission_key` = pd.`permission_key`
SET pd.`comment` = tc.`comment`;
DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`;
DROP PROCEDURE IF EXISTS `refresh_permission_definition_values`;
DELIMITER $$
CREATE PROCEDURE `refresh_permission_definition_values`()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE current_rank_id INT;
DECLARE current_column_name VARCHAR(32);
DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN rank_cursor;
rank_loop: LOOP
FETCH rank_cursor INTO current_rank_id;
IF done = 1 THEN
LEAVE rank_loop;
END IF;
SET current_column_name = CONCAT('rank_', current_rank_id);
SELECT GROUP_CONCAT(
CONCAT(
'SELECT ''',
REPLACE(`column_name`, '''', ''''''),
''' AS permission_key, CAST(COALESCE(`',
REPLACE(`column_name`, '`', '``'),
'`, ''0'') AS UNSIGNED) AS permission_value FROM `permissions` WHERE `id` = ',
current_rank_id
)
ORDER BY `ordinal_position`
SEPARATOR ' UNION ALL '
) INTO @permission_rank_source_sql
FROM `information_schema`.`columns`
WHERE `table_schema` = DATABASE()
AND `table_name` = 'permissions'
AND `column_name` NOT IN (
'id',
'rank_name',
'hidden_rank',
'badge',
'job_description',
'staff_color',
'staff_background',
'level',
'room_effect',
'log_commands',
'prefix',
'prefix_color',
'auto_credits_amount',
'auto_pixels_amount',
'auto_gotw_amount',
'auto_points_amount'
);
SET @permission_rank_update_sql = CONCAT(
'UPDATE `permission_definitions` pd ',
'INNER JOIN (',
@permission_rank_source_sql,
') src ON src.permission_key = pd.permission_key ',
'SET pd.`',
current_column_name,
'` = src.permission_value'
);
PREPARE permission_rank_update_stmt FROM @permission_rank_update_sql;
EXECUTE permission_rank_update_stmt;
DEALLOCATE PREPARE permission_rank_update_stmt;
END LOOP;
CLOSE rank_cursor;
END$$
DELIMITER ;
CALL `refresh_permission_definition_values`();
CREATE TABLE IF NOT EXISTS `room_wired_settings` (
`room_id` int(11) NOT NULL,
`inspect_mask` int(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can open and inspect Wired in the room. 1=everyone, 2=users with rights, 4=group members, 8=group admins.',
`modify_mask` int(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can modify Wired in the room. 2=users with rights, 4=group members, 8=group admins.',
PRIMARY KEY (`room_id`),
CONSTRAINT `fk_room_wired_settings_room_id` FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `room_user_wired_variables` (
`room_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`variable_item_id` int(11) NOT NULL,
`value` int(11) DEFAULT NULL,
`created_at` int(11) NOT NULL DEFAULT 0,
`updated_at` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`room_id`, `user_id`, `variable_item_id`),
KEY `idx_room_user_wired_variables_room_item` (`room_id`, `variable_item_id`),
KEY `idx_room_user_wired_variables_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `room_furni_wired_variables` (
`room_id` int(11) NOT NULL,
`furni_id` int(11) NOT NULL,
`variable_item_id` int(11) NOT NULL,
`value` int(11) DEFAULT NULL,
`created_at` int(11) NOT NULL DEFAULT 0,
`updated_at` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`room_id`, `furni_id`, `variable_item_id`),
KEY `idx_room_furni_wired_variables_room_item` (`room_id`, `variable_item_id`),
KEY `idx_room_furni_wired_variables_furni` (`furni_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `room_wired_variables` (
`room_id` int(11) NOT NULL,
`variable_item_id` int(11) NOT NULL,
`value` int(11) NOT NULL DEFAULT 0,
`created_at` int(11) NOT NULL DEFAULT 0,
`updated_at` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`room_id`, `variable_item_id`),
KEY `idx_room_wired_variables_room_item` (`room_id`, `variable_item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
ALTER TABLE `room_user_wired_variables`
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
ALTER TABLE `room_user_wired_variables`
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
UPDATE `room_user_wired_variables`
SET
`created_at` = IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()),
`updated_at` = IF(`updated_at` > 0, `updated_at`, IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()));
ALTER TABLE `room_furni_wired_variables`
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
ALTER TABLE `room_furni_wired_variables`
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
UPDATE `room_furni_wired_variables`
SET
`created_at` = IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()),
`updated_at` = IF(`updated_at` > 0, `updated_at`, IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()));
ALTER TABLE `room_wired_variables`
ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`;
ALTER TABLE `room_wired_variables`
ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`;
UPDATE `room_wired_variables`
SET
`created_at` = 0,
`updated_at` = IF(`updated_at` > 0, `updated_at`, UNIX_TIMESTAMP());
INSERT INTO `chat_bubbles` (`type`, `name`, `permission`, `overridable`, `triggers_talking_furniture`) VALUES
(200, 'SHOW_MESSAGE_RED', '', 1, 0),
(201, 'SHOW_MESSAGE_GREEN', '', 1, 0),
(202, 'SHOW_MESSAGE_BLUE', '', 1, 0),
(210, 'SHOW_MESSAGE_ALERT', '', 1, 0),
(211, 'SHOW_MESSAGE_INFO', '', 1, 0),
(212, 'SHOW_MESSAGE_WARNING', '', 1, 0),
(220, 'SHOW_MESSAGE_WRONG', '', 1, 0),
(221, 'SHOW_MESSAGE_WRONG_CIRCLED', '', 1, 0),
(222, 'SHOW_MESSAGE_CORRECT', '', 1, 0),
(223, 'SHOW_MESSAGE_CORRECT_CIRCLED', '', 1, 0),
(224, 'SHOW_MESSAGE_QUESTION', '', 1, 0),
(225, 'SHOW_MESSAGE_QUESTION_CIRCLED', '', 1, 0),
(226, 'SHOW_MESSAGE_ARROW_UP', '', 1, 0),
(227, 'SHOW_MESSAGE_ARROW_UP_CIRCLED', '', 1, 0),
(228, 'SHOW_MESSAGE_ARROW_DOWN', '', 1, 0),
(229, 'SHOW_MESSAGE_ARROW_DOWN_CIRCLED', '', 1, 0),
(250, 'SHOW_MESSAGE_SKULL', '', 1, 0),
(251, 'SHOW_MESSAGE_SKULL_ALT', '', 1, 0),
(252, 'SHOW_MESSAGE_MAGNIFIER', '', 1, 0)
ON DUPLICATE KEY UPDATE
`name` = VALUES(`name`),
`permission` = VALUES(`permission`),
`overridable` = VALUES(`overridable`),
`triggers_talking_furniture` = VALUES(`triggers_talking_furniture`);
ALTER TABLE `catalog_club_offers`
MODIFY COLUMN `type` ENUM('HC', 'VIP', 'BUILDERS_CLUB', 'BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC';
ALTER TABLE `catalog_pages`
MODIFY COLUMN `page_layout` ENUM(
'default_3x3',
'club_buy',
'club_gift',
'frontpage',
'spaces',
'recycler',
'recycler_info',
'recycler_prizes',
'trophies',
'plasto',
'marketplace',
'marketplace_own_items',
'spaces_new',
'soundmachine',
'guilds',
'guild_furni',
'info_duckets',
'info_rentables',
'info_pets',
'roomads',
'single_bundle',
'sold_ltd_items',
'badge_display',
'bots',
'pets',
'pets2',
'pets3',
'productpage1',
'room_bundle',
'recent_purchases',
'default_3x3_color_grouping',
'guild_forum',
'vip_buy',
'info_loyalty',
'loyalty_vip_buy',
'collectibles',
'petcustomization',
'frontpage_featured',
'builders_club_frontpage',
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
ALTER TABLE `catalog_pages`
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL', 'BUILDER', 'BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `club_only`;
ALTER TABLE `rooms`
ADD COLUMN IF NOT EXISTS `builders_club_trial_locked` TINYINT(1) NOT NULL DEFAULT 0 AFTER `allow_underpass`,
ADD COLUMN IF NOT EXISTS `builders_club_original_state` VARCHAR(16) NOT NULL DEFAULT 'open' AFTER `builders_club_trial_locked`;
CREATE TABLE IF NOT EXISTS `builders_club_items` (
`item_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`room_id` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`item_id`),
KEY `idx_builders_club_items_user_id` (`user_id`),
KEY `idx_builders_club_items_room_id` (`room_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
ALTER TABLE `catalog_pages_bc`
MODIFY COLUMN `page_layout` ENUM(
'default_3x3',
'club_buy',
'club_gift',
'frontpage',
'spaces',
'recycler',
'recycler_info',
'recycler_prizes',
'trophies',
'plasto',
'marketplace',
'marketplace_own_items',
'spaces_new',
'soundmachine',
'guilds',
'guild_furni',
'info_duckets',
'info_rentables',
'info_pets',
'roomads',
'single_bundle',
'sold_ltd_items',
'badge_display',
'bots',
'pets',
'pets2',
'pets3',
'productpage1',
'room_bundle',
'recent_purchases',
'default_3x3_color_grouping',
'guild_forum',
'vip_buy',
'info_loyalty',
'loyalty_vip_buy',
'collectibles',
'petcustomization',
'frontpage_featured',
'builders_club_frontpage',
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
@@ -1,47 +0,0 @@
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 `catalog_mode` ENUM('NORMAL', 'BUILDER', 'BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `club_only`;
+38
View File
@@ -0,0 +1,38 @@
ALTER TABLE emulator_settings
CHANGE COLUMN `comment` `comment` TEXT NULL DEFAULT '' ;
CREATE TABLE IF NOT EXISTS `password_resets` (
`user_id` INT NOT NULL PRIMARY KEY,
`token` VARCHAR(128) NOT NULL,
`expires_at` TIMESTAMP NOT NULL,
`created_ip` VARCHAR(64) NOT NULL DEFAULT '',
UNIQUE KEY `idx_token` (`token`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
('login.turnstile.enabled', '0'),
('login.turnstile.sitekey', ''),
('login.turnstile.secretkey', ''),
('login.ratelimit.enabled', '1'),
('login.ratelimit.max_attempts','5'),
('login.ratelimit.window_sec', '60'),
('login.ratelimit.lockout_sec', '120'),
('login.register.enabled', '1'),
('register.max_per_ip', '5'),
('register.default.look', 'hr-100-7.hd-180-1.ch-210-66.lg-270-82.sh-290-80'),
('register.default.motto', 'I love Habbo!'),
('password.reset.url', 'http://localhost/reset-password'),
('smtp.provider', 'own'),
('smtp.host', 'localhost'),
('smtp.port', '587'),
('smtp.username', ''),
('smtp.password', ''),
('smtp.from_address', 'no-reply@example.com'),
('smtp.from_name', 'Habbo Hotel'),
('smtp.use_tls', '1'),
('smtp.use_ssl', '0')
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);
@@ -1,12 +0,0 @@
ALTER TABLE `rooms`
ADD COLUMN `builders_club_trial_locked` TINYINT(1) NOT NULL DEFAULT 0 AFTER `allow_underpass`,
ADD COLUMN `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;
@@ -1,44 +0,0 @@
ALTER TABLE `catalog_pages_bc`
MODIFY COLUMN `page_layout` ENUM(
'default_3x3',
'club_buy',
'club_gift',
'frontpage',
'spaces',
'recycler',
'recycler_info',
'recycler_prizes',
'trophies',
'plasto',
'marketplace',
'marketplace_own_items',
'spaces_new',
'soundmachine',
'guilds',
'guild_furni',
'info_duckets',
'info_rentables',
'info_pets',
'roomads',
'single_bundle',
'sold_ltd_items',
'badge_display',
'bots',
'pets',
'pets2',
'pets3',
'productpage1',
'room_bundle',
'recent_purchases',
'default_3x3_color_grouping',
'guild_forum',
'vip_buy',
'info_loyalty',
'loyalty_vip_buy',
'collectibles',
'petcustomization',
'frontpage_featured',
'builders_club_frontpage',
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
@@ -1,171 +0,0 @@
-- Sample seed for Builders Club catalog pages/items
-- Safe to run multiple times: it recreates the demo BC pages and their demo BC offers.
-- After import, publish/reload the catalog.
SET @bc_demo_root_caption := 'BC Demo Root';
SET @bc_demo_page_caption := 'BC Demo Furni';
DELETE FROM `catalog_items_bc`
WHERE `page_id` IN (
SELECT `id`
FROM (
SELECT `id`
FROM `catalog_pages_bc`
WHERE `caption` IN (@bc_demo_root_caption, @bc_demo_page_caption)
) AS `bc_pages_to_clear`
);
DELETE FROM `catalog_pages_bc`
WHERE `caption` IN (@bc_demo_root_caption, @bc_demo_page_caption);
INSERT INTO `catalog_pages_bc`
(
`parent_id`,
`caption`,
`page_layout`,
`icon_color`,
`icon_image`,
`order_num`,
`visible`,
`enabled`,
`page_headline`,
`page_teaser`,
`page_special`,
`page_text1`,
`page_text2`,
`page_text_details`,
`page_text_teaser`
)
VALUES
(
-1,
@bc_demo_root_caption,
'default_3x3',
1,
28,
999,
'1',
'1',
'',
'',
'',
'Builders Club Demo',
'Pagina demo creata da seed SQL',
'Root demo per il catalogo Builders Club.',
''
);
SET @bc_demo_root_id := LAST_INSERT_ID();
INSERT INTO `catalog_pages_bc`
(
`parent_id`,
`caption`,
`page_layout`,
`icon_color`,
`icon_image`,
`order_num`,
`visible`,
`enabled`,
`page_headline`,
`page_teaser`,
`page_special`,
`page_text1`,
`page_text2`,
`page_text_details`,
`page_text_teaser`
)
VALUES
(
@bc_demo_root_id,
@bc_demo_page_caption,
'default_3x3',
1,
28,
1,
'1',
'1',
'',
'',
'',
'Builders Club Furni',
'Furni demo',
'Questa pagina duplica alcuni furni del catalogo normale dentro al Builders Club.',
''
);
SET @bc_demo_page_id := LAST_INSERT_ID();
-- Source page from normal catalog: tries to use the "base" furni line.
SET @source_normal_page_id := (
SELECT `id`
FROM `catalog_pages`
WHERE `caption_save` = 'base'
ORDER BY `id`
LIMIT 1
);
-- Copy only safe single-placeable demo furni:
-- - one single numeric item id
-- - floor furni only
-- - size 1x1
-- - default interaction only
-- This avoids copying bundles, bots, effects, teleports, wall items and large furni that can fail placement during BC testing.
INSERT INTO `catalog_items_bc`
(
`item_ids`,
`page_id`,
`catalog_name`,
`order_number`,
`extradata`
)
SELECT
`ci`.`item_ids`,
@bc_demo_page_id,
CONCAT(`ci`.`catalog_name`, '_bc_demo'),
`ci`.`order_number`,
`ci`.`extradata`
FROM `catalog_items` `ci`
INNER JOIN `items_base` `ib`
ON `ib`.`id` = CAST(`ci`.`item_ids` AS UNSIGNED)
WHERE `ci`.`page_id` = @source_normal_page_id
AND `ci`.`item_ids` REGEXP '^[0-9]+$'
AND `ib`.`type` = 's'
AND `ib`.`width` = 1
AND `ib`.`length` = 1
AND `ib`.`interaction_type` = 'default'
ORDER BY `ci`.`order_number`, `ci`.`id`
LIMIT 6;
-- Fallback: if page "base" is missing or empty, duplicate any 6 safe 1x1 floor offers.
INSERT INTO `catalog_items_bc`
(
`item_ids`,
`page_id`,
`catalog_name`,
`order_number`,
`extradata`
)
SELECT
`fallback_ci`.`item_ids`,
@bc_demo_page_id,
CONCAT(`fallback_ci`.`catalog_name`, '_bc_demo'),
`fallback_ci`.`id`,
`fallback_ci`.`extradata`
FROM `catalog_items` `fallback_ci`
INNER JOIN `items_base` `fallback_ib`
ON `fallback_ib`.`id` = CAST(`fallback_ci`.`item_ids` AS UNSIGNED)
WHERE NOT EXISTS (
SELECT 1
FROM `catalog_items_bc`
WHERE `page_id` = @bc_demo_page_id
)
AND `fallback_ci`.`item_ids` REGEXP '^[0-9]+$'
AND `fallback_ib`.`type` = 's'
AND `fallback_ib`.`width` = 1
AND `fallback_ib`.`length` = 1
AND `fallback_ib`.`interaction_type` = 'default'
ORDER BY `fallback_ci`.`id`
LIMIT 6;
SELECT @bc_demo_root_id AS `bc_root_page_id`, @bc_demo_page_id AS `bc_furni_page_id`;
@@ -0,0 +1,11 @@
UPDATE items_base SET customparams = 'is_invisible' WHERE public_name like 'tile_stackmagic%';
UPDATE items_base SET customparams = 'is_invisible' WHERE public_name like 'tile_walkmagic%';
UPDATE items_base SET customparams = 'is_invisible' WHERE public_name like 'room_invisible_block%';
UPDATE items_base SET customparams = 'is_invisible' WHERE public_name = 'room_invisible_sit_tile';
UPDATE items_base SET customparams = 'is_invisible' WHERE public_name = 'room_invisible_click_tile';
UPDATE `items_base` SET `interaction_type` = 'wf_conf_invis_control' WHERE `public_name` = 'conf_invis_control';
UPDATE `items_base` SET `interaction_type` = 'wf_conf_handitem_block' WHERE `public_name` = 'conf_handitem_block';
UPDATE `items_base` SET `interaction_type` = 'wf_conf_wired_disable' WHERE `public_name` = 'conf_wired_disable';
UPDATE `items_base` SET `interaction_type` = 'wf_conf_queue_speed' WHERE `public_name` = 'conf_queue_speed';
UPDATE `items_base` SET `interaction_type` = 'wf_conf_area_hide' WHERE `public_name` = 'conf_area_hide';
+21 -5
View File
@@ -6,12 +6,12 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.1.1</version>
<version>4.1.5</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
</properties>
<build>
@@ -19,7 +19,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<version>3.15.0</version>
<configuration>
<source>19</source>
<target>19</target>
@@ -162,5 +162,21 @@
<artifactId>joda-time</artifactId>
<version>2.13.0</version>
</dependency>
<!-- jBCrypt — used by the built-in /api/auth/* HTTP login handler
to verify Laravel-style $2y$ BCrypt hashes from users.password -->
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>
<!-- Jakarta Mail — used by the built-in forgot-password endpoint
when smtp.* keys are configured in emulator_settings -->
<dependency>
<groupId>org.eclipse.angus</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.3</version>
</dependency>
</dependencies>
</project>
</project>
@@ -338,6 +338,8 @@ public class CatalogItem implements ISerialize, Runnable, Comparable<CatalogItem
message.appendBoolean(haveOffer(this));
message.appendBoolean(false); //unknown
message.appendString(this.name + ".png");
message.appendString(this.itemId == null ? "" : this.itemId);
message.appendBoolean(this.haveOffer);
}
@Override
@@ -1136,7 +1136,7 @@ public class CatalogManager {
if (extradata.length() > Emulator.getConfig().getInt("hotel.trophies.length.max", 300)) {
extradata = extradata.substring(0, Emulator.getConfig().getInt("hotel.trophies.length.max", 300));
}
extradata = habbo.getClient().getHabbo().getHabboInfo().getUsername() + (char) 9 + Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "-" + (Calendar.getInstance().get(Calendar.MONTH) + 1) + "-" + Calendar.getInstance().get(Calendar.YEAR) + (char) 9 + Emulator.getGameEnvironment().getWordFilter().filter(extradata.replace(((char) 9) + "", ""), habbo);
}
@@ -1205,7 +1205,7 @@ public class CatalogManager {
if (badgeFound && item.getBaseItems().size() == 1) {
habbo.getClient().sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.ALREADY_HAVE_BADGE));
return;
return;
}
UserCatalogItemPurchasedEvent purchasedEvent = new UserCatalogItemPurchasedEvent(habbo, item, itemsList, totalCredits, totalPoints, badges);
@@ -74,8 +74,9 @@ public abstract class CatalogPage implements Comparable<CatalogPage>, ISerialize
this.textDetails = set.getString("page_text_details");
this.textTeaser = set.getString("page_text_teaser");
if (!set.getString("includes").isEmpty()) {
for (String id : set.getString("includes").split(";")) {
String includes = set.getString("includes");
if (includes != null && !includes.isEmpty()) {
for (String id : includes.split(";")) {
try {
this.included.add(Integer.valueOf(id));
} catch (Exception e) {
@@ -15,7 +15,7 @@ public class AboutCommand extends Command {
}
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 Lorenzune";
"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)";
@Override
public boolean handle(GameClient gameClient, String[] params) {
@@ -177,6 +177,10 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition {
int count = settings.getFurniIds().length;
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false;
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
this.items.clear();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -164,6 +164,10 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition {
this.all = (params.length > 0) && (params[0] == 1);
this.furniSource = (params.length > 1) ? params[1] : ((params.length > 0 && params[0] > 1) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER);
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
this.items.clear();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -240,6 +240,10 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition {
this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL;
}
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId());
if (room == null) {
return false;
@@ -171,6 +171,10 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition {
return false;
}
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
this.items.clear();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -177,6 +177,10 @@ public class WiredConditionNotFurniHaveFurni extends InteractionWiredCondition {
int count = settings.getFurniIds().length;
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false;
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
this.items.clear();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -163,6 +163,10 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition {
this.all = (params.length > 0) && (params[0] == 1);
this.furniSource = (params.length > 1) ? params[1] : ((params.length > 0 && params[0] > 1) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER);
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
this.items.clear();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -186,6 +186,10 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition {
this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER;
this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL;
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
this.items.clear();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -133,6 +133,10 @@ public class WiredEffectBotTeleport extends InteractionWiredEffect {
throw new WiredSaveException("Too many furni selected");
}
if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
List<HabboItem> newItems = new ArrayList<>();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -85,6 +85,10 @@ public class WiredEffectBotWalkToFurni extends InteractionWiredEffect {
throw new WiredSaveException("Too many furni selected");
}
if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
List<HabboItem> newItems = new ArrayList<>();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -290,6 +290,10 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect {
throw new WiredSaveException("Too many furni selected");
}
if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
THashMap<HabboItem, WiredChangeDirectionSetting> newItems = new THashMap<>();
for (int i = 0; i < itemsCount; i++) {
@@ -263,6 +263,10 @@ public class WiredEffectMoveFurniAway extends InteractionWiredEffect {
throw new WiredSaveException("Too many furni selected");
}
if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
List<HabboItem> newItems = new ArrayList<>();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -59,6 +59,11 @@ public class WiredEffectMoveFurniTo extends InteractionWiredEffect {
this.furniSource = settings.getIntParams()[2];
int count = settings.getFurniIds().length;
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
for (int i = 0; i < count; i++) {
this.items.add(room.getHabboItem(settings.getFurniIds()[i]));
@@ -415,6 +415,10 @@ public class WiredEffectMoveFurniTowards extends InteractionWiredEffect {
throw new WiredSaveException("Too many furni selected");
}
if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
List<HabboItem> newItems = new ArrayList<>();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -261,6 +261,10 @@ public class WiredEffectMoveRotateFurni extends InteractionWiredEffect implement
int count = settings.getFurniIds().length;
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5)) return false;
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
this.items.clear();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
for (int i = 0; i < count; i++) {
@@ -7,16 +7,11 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
import com.eu.habbo.habbohotel.pets.RideablePet;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomTile;
import com.eu.habbo.habbohotel.rooms.RoomTileState;
import com.eu.habbo.habbohotel.rooms.RoomUnit;
import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
import com.eu.habbo.habbohotel.rooms.RoomUnitType;
import com.eu.habbo.habbohotel.rooms.*;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.wired.core.WiredContext;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.habbohotel.wired.WiredEffectType;
import com.eu.habbo.habbohotel.wired.core.WiredContext;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import com.eu.habbo.messages.ServerMessage;
@@ -189,6 +184,10 @@ public class WiredEffectTeleport extends InteractionWiredEffect {
throw new WiredSaveException("Too many furni selected");
}
if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
List<HabboItem> newItems = new ArrayList<>();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -169,6 +169,10 @@ public class WiredEffectToggleFurni extends InteractionWiredEffect {
throw new WiredSaveException("Too many furni selected");
}
if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
List<HabboItem> newItems = new ArrayList<>();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
for (int i = 0; i < itemsCount; i++) {
@@ -153,6 +153,10 @@ public class WiredEffectToggleRandom extends InteractionWiredEffect {
throw new WiredSaveException("Too many furni selected");
}
if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
List<HabboItem> newItems = new ArrayList<>();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -101,6 +101,10 @@ public class WiredEffectTriggerStacks extends InteractionWiredEffect {
throw new WiredSaveException("Too many furni selected");
}
if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
List<HabboItem> newItems = new ArrayList<>();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -275,6 +275,10 @@ public abstract class WiredEffectUserFurniBase extends InteractionWiredEffect {
throw new WiredSaveException("Too many furni selected");
}
if (settings.getFurniIds().length > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId());
if (room == null) {
throw new WiredSaveException("Room not found");
@@ -199,6 +199,10 @@ public class WiredEffectUserToFurni extends WiredEffectUserFurniBase {
throw new WiredSaveException("Too many furni selected");
}
if (settings.getFurniIds().length > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId());
if (room == null) {
throw new WiredSaveException("Room not found");
@@ -92,6 +92,10 @@ public class WiredEffectFurniOnFurni extends InteractionWiredEffect {
return false;
}
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
this.items.clear();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -79,6 +79,10 @@ public class WiredEffectUsersOnFurni extends InteractionWiredEffect {
return false;
}
if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) {
this.furniSource = WiredSourceUtil.SOURCE_SELECTED;
}
this.items.clear();
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
@@ -31,7 +31,7 @@ public class WiredTriggerReceiveSignal extends InteractionWiredTrigger {
private static final long ACTIVATION_PULSE_MS = 300L;
private static final String ANTENNA_INTERACTION = "antenna";
private static final String REQUIRE_ANTENNA_ERROR = "Puoi selezionare solo furni antenna.";
private static final String REQUIRE_ANTENNA_ERROR = "You can only select antenna furni.";
private int channel = 0; // signal channel (0-based)
private THashSet<HabboItem> items;
@@ -194,7 +194,6 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
private volatile boolean wiredSettingsLoaded;
private int wiredInspectMask = WIRED_ACCESS_DEFAULT_INSPECT_MASK;
private int wiredModifyMask = WIRED_ACCESS_DEFAULT_MODIFY_MASK;
private boolean youtubeEnabled = false;
private String youtubeCurrentVideo = "";
private String youtubeSenderName = "";
@@ -12,11 +12,11 @@ import com.eu.habbo.habbohotel.users.DanceType;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboGender;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.habbohotel.wired.WiredUserActionType;
import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper;
import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper;
import com.eu.habbo.habbohotel.wired.WiredUserActionType;
import com.eu.habbo.messages.outgoing.generic.alerts.GenericErrorMessagesComposer;
import com.eu.habbo.messages.outgoing.inventory.AddPetComposer;
import com.eu.habbo.messages.outgoing.rooms.pets.RoomPetComposer;
@@ -1264,9 +1264,14 @@ public class RoomUnitManager {
if (habbo == null || habbo.getRoomUnit() == null) {
return;
}
boolean wasIdle = habbo.getRoomUnit().isIdle();
habbo.getRoomUnit().resetIdleTimer();
this.room.sendComposer(new RoomUnitIdleComposer(habbo.getRoomUnit()).compose());
WiredManager.triggerUserUnidles(this.room, habbo.getRoomUnit());
if (wasIdle) {
this.room.sendComposer(new RoomUnitIdleComposer(habbo.getRoomUnit()).compose());
WiredManager.triggerUserUnidles(this.room, habbo.getRoomUnit());
}
}
/**
@@ -1448,11 +1453,6 @@ public class RoomUnitManager {
}
}
// ==================== DISPOSAL ====================
/**
* Disposes the unit manager.
*/
public void dispose() {
this.currentHabbos.clear();
this.currentBots.clear();
@@ -292,6 +292,8 @@ public class PacketManager {
this.registerHandler(Incoming.CatalogAdminMoveOfferEvent, CatalogAdminMoveOfferEvent.class);
this.registerHandler(Incoming.CatalogAdminMovePageEvent, CatalogAdminMovePageEvent.class);
this.registerHandler(Incoming.CatalogAdminPublishEvent, CatalogAdminPublishEvent.class);
this.registerHandler(Incoming.CatalogAdminSavePageImagesEvent, CatalogAdminSavePageImagesEvent.class);
this.registerHandler(Incoming.CatalogAdminSavePageIconEvent, CatalogAdminSavePageIconEvent.class);
}
private void registerEvent() throws Exception {
@@ -442,6 +442,8 @@ public class Incoming {
public static final int CatalogAdminMoveOfferEvent = 10056;
public static final int CatalogAdminMovePageEvent = 10057;
public static final int CatalogAdminPublishEvent = 10058;
public static final int CatalogAdminSavePageImagesEvent = 10060;
public static final int CatalogAdminSavePageIconEvent = 10061;
// Custom Prefixes
public static final int RequestUserPrefixesEvent = 7011;
@@ -21,7 +21,7 @@ public class CatalogAdminCreateOfferEvent extends MessageHandler {
}
int pageId = this.packet.readInt();
int itemId = this.packet.readInt();
String itemIds = this.packet.readString();
String catalogName = this.packet.readString();
int costCredits = this.packet.readInt();
int costPoints = this.packet.readInt();
@@ -39,12 +39,13 @@ public class CatalogAdminCreateOfferEvent extends MessageHandler {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
(pageType == CatalogPageType.BUILDER)
? "INSERT INTO catalog_items_bc (page_id, item_ids, catalog_name, order_number, extradata) VALUES (?, ?, ?, ?, ?)"
: "INSERT INTO catalog_items (page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, club_only, extradata, have_offer, offer_id, limited_stack, order_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS)) {
(pageType == CatalogPageType.BUILDER)
? "INSERT INTO catalog_items_bc (page_id, item_ids, catalog_name, order_number, extradata) VALUES (?, ?, ?, ?, ?)"
: "INSERT INTO catalog_items (page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, club_only, extradata, have_offer, offer_id, limited_stack, order_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS)) {
String cleanItemIds = (itemIds == null || itemIds.trim().isEmpty()) ? "0" : itemIds.trim();
statement.setInt(1, pageId);
statement.setString(2, String.valueOf(itemId));
statement.setString(2, cleanItemIds);
statement.setString(3, catalogName);
if (pageType == CatalogPageType.BUILDER) {
@@ -20,7 +20,7 @@ public class CatalogAdminSaveOfferEvent extends MessageHandler {
int offerId = this.packet.readInt();
int pageId = this.packet.readInt();
int itemId = this.packet.readInt();
String itemIds = this.packet.readString();
String catalogName = this.packet.readString();
int costCredits = this.packet.readInt();
int costPoints = this.packet.readInt();
@@ -34,31 +34,44 @@ public class CatalogAdminSaveOfferEvent extends MessageHandler {
int orderNumber = this.packet.readInt();
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
boolean updateItemIds = itemIds != null && !itemIds.trim().isEmpty();
String sql;
if (pageType == CatalogPageType.BUILDER) {
sql = updateItemIds
? "UPDATE catalog_items_bc SET page_id = ?, item_ids = ?, catalog_name = ?, order_number = ?, extradata = ? WHERE id = ?"
: "UPDATE catalog_items_bc SET page_id = ?, catalog_name = ?, order_number = ?, extradata = ? WHERE id = ?";
} else {
sql = updateItemIds
? "UPDATE catalog_items SET page_id = ?, item_ids = ?, catalog_name = ?, cost_credits = ?, cost_points = ?, points_type = ?, amount = ?, club_only = ?, extradata = ?, have_offer = ?, offer_id = ?, limited_stack = ?, order_number = ? WHERE id = ?"
: "UPDATE catalog_items SET page_id = ?, catalog_name = ?, cost_credits = ?, cost_points = ?, points_type = ?, amount = ?, club_only = ?, extradata = ?, have_offer = ?, offer_id = ?, limited_stack = ?, order_number = ? WHERE id = ?";
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
(pageType == CatalogPageType.BUILDER)
? "UPDATE catalog_items_bc SET page_id = ?, item_ids = ?, catalog_name = ?, order_number = ?, extradata = ? WHERE id = ?"
: "UPDATE catalog_items SET page_id = ?, item_ids = ?, catalog_name = ?, cost_credits = ?, cost_points = ?, points_type = ?, amount = ?, club_only = ?, extradata = ?, have_offer = ?, offer_id = ?, limited_stack = ?, order_number = ? WHERE id = ?")) {
statement.setInt(1, pageId);
statement.setString(2, String.valueOf(itemId));
statement.setString(3, catalogName);
PreparedStatement statement = connection.prepareStatement(sql)) {
int idx = 1;
statement.setInt(idx++, pageId);
if (updateItemIds) {
statement.setString(idx++, itemIds.trim());
}
statement.setString(idx++, catalogName);
if (pageType == CatalogPageType.BUILDER) {
statement.setInt(4, orderNumber);
statement.setString(5, extradata);
statement.setInt(6, offerId);
statement.setInt(idx++, orderNumber);
statement.setString(idx++, extradata);
statement.setInt(idx, offerId);
} else {
statement.setInt(4, costCredits);
statement.setInt(5, costPoints);
statement.setInt(6, pointsType);
statement.setInt(7, amount);
statement.setString(8, clubOnly == 1 ? "1" : "0");
statement.setString(9, extradata);
statement.setString(10, haveOffer ? "1" : "0");
statement.setInt(11, offerIdGroup);
statement.setInt(12, limitedStack);
statement.setInt(13, orderNumber);
statement.setInt(14, offerId);
statement.setInt(idx++, costCredits);
statement.setInt(idx++, costPoints);
statement.setInt(idx++, pointsType);
statement.setInt(idx++, amount);
statement.setString(idx++, clubOnly == 1 ? "1" : "0");
statement.setString(idx++, extradata);
statement.setString(idx++, haveOffer ? "1" : "0");
statement.setInt(idx++, offerIdGroup);
statement.setInt(idx++, limitedStack);
statement.setInt(idx++, orderNumber);
statement.setInt(idx, offerId);
}
statement.execute();
}
@@ -0,0 +1,41 @@
package com.eu.habbo.messages.incoming.catalog.catalogadmin;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.catalog.CatalogPage;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
public class CatalogAdminSavePageIconEvent extends MessageHandler {
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission"));
return;
}
int pageId = this.packet.readInt();
int iconId = this.packet.readInt();
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId);
if (page == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId));
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE catalog_pages SET icon_image = ? WHERE id = ?")) {
statement.setInt(1, iconId);
statement.setInt(2, pageId);
statement.execute();
}
this.client.sendResponse(new CatalogAdminResultComposer(true, "Page icon saved"));
}
}
@@ -0,0 +1,43 @@
package com.eu.habbo.messages.incoming.catalog.catalogadmin;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.catalog.CatalogPage;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
public class CatalogAdminSavePageImagesEvent extends MessageHandler {
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission"));
return;
}
int pageId = this.packet.readInt();
String headerImage = this.packet.readString();
String teaserImage = this.packet.readString();
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId);
if (page == null) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId));
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE catalog_pages SET page_headline = ?, page_teaser = ? WHERE id = ?")) {
statement.setString(1, headerImage);
statement.setString(2, teaserImage);
statement.setInt(3, pageId);
statement.execute();
}
this.client.sendResponse(new CatalogAdminResultComposer(true, "Page images saved"));
}
}
@@ -1,6 +1,7 @@
package com.eu.habbo.networking.gameserver;
import com.eu.habbo.messages.PacketManager;
import com.eu.habbo.networking.gameserver.auth.AuthHttpHandler;
import com.eu.habbo.networking.gameserver.codec.WebSocketCodec;
import com.eu.habbo.networking.gameserver.decoders.*;
import com.eu.habbo.networking.gameserver.encoders.GameServerMessageEncoder;
@@ -49,10 +50,9 @@ public class WebSocketChannelInitializer extends ChannelInitializer<SocketChanne
ch.pipeline().addLast("httpCodec", new HttpServerCodec());
ch.pipeline().addLast("httpAggregator", new HttpObjectAggregator(MAX_FRAME_SIZE));
ch.pipeline().addLast("wsHttpHandler", new WebSocketHttpHandler());
ch.pipeline().addLast("authHttpHandler", new AuthHttpHandler());
ch.pipeline().addLast("wsProtocolHandler", new WebSocketServerProtocolHandler(this.wsConfig));
ch.pipeline().addLast("wsCodec", new WebSocketCodec());
// Standard game decoders
ch.pipeline().addLast(new GamePolicyDecoder());
ch.pipeline().addLast(new GameByteFrameDecoder());
ch.pipeline().addLast(new GameByteDecoder());
@@ -64,8 +64,6 @@ public class WebSocketChannelInitializer extends ChannelInitializer<SocketChanne
ch.pipeline().addLast("idleEventHandler", new IdleTimeoutHandler(30, 60));
ch.pipeline().addLast(new GameMessageRateLimit());
ch.pipeline().addLast(new GameMessageHandler());
// Encoders
ch.pipeline().addLast("messageEncoder", new GameServerMessageEncoder());
if (PacketManager.DEBUG_SHOW_PACKETS) {
@@ -0,0 +1,460 @@
package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import com.eu.habbo.networking.gameserver.GameServerAttributes;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
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.mindrot.jbcrypt.BCrypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.sql.*;
import java.time.Instant;
import java.util.Base64;
import java.util.regex.Pattern;
public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthHttpHandler.class);
private static final String LOGIN_PATH = "/api/auth/login";
private static final String REGISTER_PATH = "/api/auth/register";
private static final String FORGOT_PATH = "/api/auth/forgot-password";
private static final String LOGOUT_PATH = "/api/auth/logout";
private static final Pattern USERNAME_RE = Pattern.compile("^[A-Za-z0-9._-]{3,32}$");
private static final Pattern EMAIL_RE = Pattern.compile("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$");
private static final SecureRandom RNG = new SecureRandom();
@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(LOGIN_PATH) && !path.equals(REGISTER_PATH)
&& !path.equals(FORGOT_PATH) && !path.equals(LOGOUT_PATH)) {
super.channelRead(ctx, msg);
return;
}
try {
handle(ctx, req, path);
} finally {
ReferenceCountUtil.release(req);
}
}
private void handle(ChannelHandlerContext ctx, FullHttpRequest req, String path) {
if (req.method() == HttpMethod.OPTIONS) {
sendCors(ctx, req);
return;
}
if (req.method() != HttpMethod.POST) {
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, errorPayload("Use POST."));
return;
}
String ip = resolveClientIp(ctx, req);
if (AuthRateLimiter.isLocked(ip)) {
long secs = AuthRateLimiter.secondsUntilUnlock(ip);
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
errorPayload("Too many attempts. Try again in " + secs + "s."));
return;
}
JsonObject body;
try {
String text = req.content().toString(StandardCharsets.UTF_8);
body = text.isEmpty() ? new JsonObject() : JsonParser.parseString(text).getAsJsonObject();
} catch (Exception e) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Invalid JSON body."));
return;
}
if (path.equals(LOGOUT_PATH)) {
handleLogout(ctx, req, body);
return;
}
String turnstileToken = readString(body, "turnstileToken");
if (!TurnstileVerifier.verify(turnstileToken, ip)) {
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.FORBIDDEN, errorPayload("Security check failed."));
return;
}
switch (path) {
case LOGIN_PATH -> handleLogin(ctx, req, body, ip);
case REGISTER_PATH -> handleRegister(ctx, req, body, ip);
case FORGOT_PATH -> handleForgot(ctx, req, body, ip);
}
}
/* ─── Logout ────────────────────────────────────────────────────────── */
private void handleLogout(ChannelHandlerContext ctx, FullHttpRequest req, com.google.gson.JsonObject body) {
String ssoTicket = readString(body, "ssoTicket");
JsonObject ok = new JsonObject();
ok.addProperty("message", "Logged out.");
if (ssoTicket == null || ssoTicket.isEmpty()) {
sendJson(ctx, req, HttpResponseStatus.OK, ok);
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement lookup = conn.prepareStatement(
"SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) {
lookup.setString(1, ssoTicket);
int userId = 0;
try (ResultSet rs = lookup.executeQuery()) {
if (rs.next()) userId = rs.getInt("id");
}
if (userId > 0) {
try (PreparedStatement clear = conn.prepareStatement(
"UPDATE users SET auth_ticket = '', online = '0' WHERE id = ? LIMIT 1")) {
clear.setInt(1, userId);
clear.executeUpdate();
}
if (Emulator.getGameServer() != null
&& Emulator.getGameServer().getGameClientManager() != null) {
com.eu.habbo.habbohotel.users.Habbo habbo =
Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (habbo != null && habbo.getClient() != null) {
Emulator.getGameServer().getGameClientManager().disposeClient(habbo.getClient());
}
}
}
} catch (Exception e) {
LOGGER.error("Logout cleanup failed for ticket", e);
}
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
/* ─── Login ─────────────────────────────────────────────────────────── */
private void handleLogin(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
String username = readString(body, "username").trim();
String password = readString(body, "password");
if (username.isEmpty() || password.isEmpty()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing credentials."));
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT id, username, password FROM users WHERE username = ? LIMIT 1")) {
stmt.setString(1, username);
try (ResultSet rs = stmt.executeQuery()) {
if (!rs.next()) {
LOGGER.info("[auth/login] user not found username='{}' ip={}", username, ip);
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED,
errorPayload("Invalid Habbo name or password."));
return;
}
int userId = rs.getInt("id");
String stored = rs.getString("password");
String storedPreview = stored == null
? "<null>"
: (stored.isEmpty() ? "<empty>" : stored.substring(0, Math.min(7, stored.length())) + "…(" + stored.length() + " chars)");
if (stored == null || stored.isEmpty() || !checkPassword(password, stored)) {
LOGGER.info("[auth/login] password mismatch for user id={} username='{}' stored='{}'",
userId, username, storedPreview);
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED,
errorPayload("Invalid Habbo name or password."));
return;
}
String ssoTicket = mintSsoTicket();
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE users SET auth_ticket = ?, ip_current = ? WHERE id = ? LIMIT 1")) {
upd.setString(1, ssoTicket);
upd.setString(2, ip == null ? "" : ip);
upd.setInt(3, userId);
upd.executeUpdate();
}
AuthRateLimiter.recordSuccess(ip);
JsonObject ok = new JsonObject();
ok.addProperty("ssoTicket", ssoTicket);
ok.addProperty("username", rs.getString("username"));
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
} catch (Exception e) {
LOGGER.error("Login query failed for username=" + username, e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
/* ─── Register ──────────────────────────────────────────────────────── */
private void handleRegister(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
if (!Emulator.getConfig().getBoolean("login.register.enabled", true)) {
sendJson(ctx, req, HttpResponseStatus.FORBIDDEN, errorPayload("Registration is closed."));
return;
}
String username = readString(body, "username").trim();
String email = readString(body, "email").trim();
String password = readString(body, "password");
if (!USERNAME_RE.matcher(username).matches()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("Username must be 3-32 chars (letters, numbers, . _ -)."));
return;
}
if (!EMAIL_RE.matcher(email).matches()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Invalid email address."));
return;
}
if (password.length() < 8) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("Password must be at least 8 characters."));
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
int maxPerIp = Emulator.getConfig().getInt("register.max_per_ip", 5);
if (maxPerIp > 0 && ip != null && !ip.isEmpty()) {
try (PreparedStatement quota = conn.prepareStatement(
"SELECT COUNT(*) FROM users WHERE ip_register = ?")) {
quota.setString(1, ip);
try (ResultSet rs = quota.executeQuery()) {
if (rs.next() && rs.getInt(1) >= maxPerIp) {
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
errorPayload("This IP has reached the maximum of "
+ maxPerIp + " registered accounts."));
return;
}
}
}
}
try (PreparedStatement check = conn.prepareStatement(
"SELECT username, mail FROM users WHERE username = ? OR mail = ? LIMIT 1")) {
check.setString(1, username);
check.setString(2, email);
try (ResultSet rs = check.executeQuery()) {
if (rs.next()) {
String existingUser = rs.getString("username");
String existingMail = rs.getString("mail");
boolean userTaken = existingUser != null && existingUser.equalsIgnoreCase(username);
boolean mailTaken = existingMail != null && existingMail.equalsIgnoreCase(email);
String message;
if (userTaken && mailTaken) message = "That Habbo name and email are already in use.";
else if (userTaken) message = "That Habbo name is already in use.";
else message = "That email address is already in use.";
sendJson(ctx, req, HttpResponseStatus.CONFLICT, errorPayload(message));
return;
}
}
}
String hashed = BCrypt.hashpw(password, BCrypt.gensalt(12));
String defaultLook = Emulator.getConfig().getValue("register.default.look",
"hr-100-7.hd-180-1.ch-210-66.lg-270-82.sh-290-80");
String defaultMotto = Emulator.getConfig().getValue("register.default.motto", "I love Habbo!");
int now = Emulator.getIntUnixTimestamp();
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO users (username, password, mail, account_created, " +
"ip_register, ip_current, last_online, last_login, motto, look, gender, " +
"credits, `rank`, home_room, machine_id, auth_ticket, online) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'M', 0, 1, 0, '', '', '0')",
Statement.RETURN_GENERATED_KEYS)) {
ins.setString(1, username);
ins.setString(2, hashed);
ins.setString(3, email);
ins.setInt(4, now);
ins.setString(5, ip == null ? "" : ip);
ins.setString(6, ip == null ? "" : ip);
ins.setInt(7, now);
ins.setInt(8, now);
ins.setString(9, defaultMotto);
ins.setString(10, defaultLook);
ins.executeUpdate();
}
JsonObject ok = new JsonObject();
ok.addProperty("message", "Welcome aboard, " + username + "! Your account is ready — log in below with the password you just chose.");
sendJson(ctx, req, HttpResponseStatus.OK, ok);
} catch (Exception e) {
LOGGER.error("Register query failed for username=" + username, e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
/* ─── Forgot password ───────────────────────────────────────────────── */
private void handleForgot(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
String email = readString(body, "email").trim();
if (!EMAIL_RE.matcher(email).matches()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Invalid email address."));
return;
}
JsonObject ok = new JsonObject();
ok.addProperty("message", "Email sent! If an account matches that address you'll find a reset link in your inbox shortly (check spam if it doesn't show up within a minute).");
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT id, username FROM users WHERE mail = ? LIMIT 1")) {
stmt.setString(1, email);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
int userId = rs.getInt("id");
String username = rs.getString("username");
String token = mintResetToken();
long expiresAt = Instant.now().getEpochSecond() + 60L * 60L; // 1h
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO password_resets (user_id, token, expires_at, created_ip) " +
"VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE " +
"token = VALUES(token), expires_at = VALUES(expires_at), created_ip = VALUES(created_ip)")) {
ins.setInt(1, userId);
ins.setString(2, token);
ins.setTimestamp(3, Timestamp.from(Instant.ofEpochSecond(expiresAt)));
ins.setString(4, ip == null ? "" : ip);
ins.executeUpdate();
}
String resetUrlBase = Emulator.getConfig().getValue("password.reset.url",
"http://localhost/reset-password");
String fullUrl = resetUrlBase + (resetUrlBase.contains("?") ? "&" : "?") + "token=" + token;
String subject = "Reset your Habbo password";
String message = "Hi " + username + ",\n\n" +
"Someone (hopefully you) requested a password reset for your Habbo account.\n" +
"Click the link below within the next hour to choose a new password:\n\n" +
fullUrl + "\n\n" +
"If you didn't request this you can safely ignore this email.";
Emulator.getThreading().getService().submit((Runnable) () -> SmtpMailService.send(email, subject, message));
}
}
} catch (Exception e) {
LOGGER.error("Forgot-password query failed for email=" + email, e);
}
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
/* ─── Helpers ───────────────────────────────────────────────────────── */
private static boolean checkPassword(String plain, String stored) {
String compatible = stored.startsWith("$2y$") ? "$2a$" + stored.substring(4) : stored;
try {
return BCrypt.checkpw(plain, compatible);
} catch (IllegalArgumentException e) {
return false;
}
}
private static String mintSsoTicket() {
byte[] buf = new byte[32];
RNG.nextBytes(buf);
return "nitro-" + Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
}
private static String mintResetToken() {
byte[] buf = new byte[32];
RNG.nextBytes(buf);
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
}
private static String readString(JsonObject obj, String key) {
if (obj == null || !obj.has(key) || obj.get(key).isJsonNull()) return "";
try {
return obj.get(key).getAsString();
} catch (Exception e) {
return "";
}
}
private static String resolveClientIp(ChannelHandlerContext ctx, FullHttpRequest req) {
String ipHeader = Emulator.getConfig() != null
? Emulator.getConfig().getValue("ws.ip.header", "")
: "";
if (!ipHeader.isEmpty() && req.headers().contains(ipHeader)) {
String hv = req.headers().get(ipHeader);
if (hv != null && !hv.isEmpty()) {
int comma = hv.indexOf(',');
return (comma > 0 ? hv.substring(0, comma) : hv).trim();
}
}
if (ctx.channel().attr(GameServerAttributes.WS_IP).get() != null) {
return ctx.channel().attr(GameServerAttributes.WS_IP).get();
}
if (ctx.channel().remoteAddress() instanceof InetSocketAddress addr) {
return addr.getAddress().getHostAddress();
}
return "";
}
private static JsonObject errorPayload(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);
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()) {
response.headers().set("Access-Control-Allow-Origin", origin);
response.headers().set("Vary", "Origin");
response.headers().set("Access-Control-Allow-Credentials", "true");
}
response.headers().set("Access-Control-Allow-Methods", "POST, OPTIONS");
response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With");
}
private static boolean isKeepAlive(FullHttpRequest req) {
String connection = req.headers().get(HttpHeaderNames.CONNECTION);
return connection == null || !"close".equalsIgnoreCase(connection);
}
}
@@ -0,0 +1,71 @@
package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
public final class AuthRateLimiter {
private static final Map<String, AtomicReference<State>> STATE = new ConcurrentHashMap<>();
private AuthRateLimiter() {}
public static boolean isLocked(String ip) {
if (!isEnabled() || ip == null || ip.isEmpty()) return false;
AtomicReference<State> ref = STATE.get(ip);
if (ref == null) return false;
State current = ref.get();
return current != null && current.lockedUntilMillis > System.currentTimeMillis();
}
public static long secondsUntilUnlock(String ip) {
AtomicReference<State> ref = STATE.get(ip);
if (ref == null) return 0;
State current = ref.get();
if (current == null) return 0;
long remainingMs = current.lockedUntilMillis - System.currentTimeMillis();
return remainingMs > 0 ? (remainingMs / 1000L) + 1L : 0L;
}
public static void recordFailure(String ip) {
if (!isEnabled() || ip == null || ip.isEmpty()) return;
long now = System.currentTimeMillis();
long windowMs = configInt("login.ratelimit.window_sec", 60) * 1000L;
int maxAttempts = configInt("login.ratelimit.max_attempts", 5);
long lockoutMs = configInt("login.ratelimit.lockout_sec", 120) * 1000L;
STATE.computeIfAbsent(ip, k -> new AtomicReference<>(new State(0, 0L, 0L)))
.updateAndGet(prev -> {
if (prev == null || (now - prev.windowStartMillis) > windowMs) {
return new State(1, now, 0L);
}
int attempts = prev.attempts + 1;
long lockedUntil = attempts >= maxAttempts ? now + lockoutMs : 0L;
return new State(attempts, prev.windowStartMillis, lockedUntil);
});
}
public static void recordSuccess(String ip) {
if (ip == null || ip.isEmpty()) return;
STATE.remove(ip);
}
private static boolean isEnabled() {
return Emulator.getConfig() != null
&& Emulator.getConfig().getBoolean("login.ratelimit.enabled", true);
}
private static int configInt(String key, int fallback) {
return Emulator.getConfig() != null ? Emulator.getConfig().getInt(key, fallback) : fallback;
}
private record State(int attempts, long windowStartMillis, long lockedUntilMillis) {}
}
@@ -0,0 +1,101 @@
package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import jakarta.mail.Authenticator;
import jakarta.mail.Message;
import jakarta.mail.PasswordAuthentication;
import jakarta.mail.Session;
import jakarta.mail.Transport;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Properties;
public final class SmtpMailService {
private static final Logger LOGGER = LoggerFactory.getLogger(SmtpMailService.class);
private SmtpMailService() {}
public static boolean send(String toAddress, String subject, String body) {
try {
String provider = Emulator.getConfig().getValue("smtp.provider", "own").toLowerCase();
String username = Emulator.getConfig().getValue("smtp.username", "");
String password = Emulator.getConfig().getValue("smtp.password", "");
String fromAddr = Emulator.getConfig().getValue("smtp.from_address", username);
String fromName = Emulator.getConfig().getValue("smtp.from_name", "Habbo Hotel");
if (toAddress == null || toAddress.isEmpty() || fromAddr == null || fromAddr.isEmpty()) {
LOGGER.warn("SMTP send aborted — missing to/from address (to={}, from={})", toAddress, fromAddr);
return false;
}
String host;
int port;
boolean useSsl;
boolean useTls;
switch (provider) {
case "gmail" -> {
host = "smtp.gmail.com";
port = 465;
useSsl = true;
useTls = false;
}
case "sendgrid" -> {
host = "smtp.sendgrid.net";
port = 587;
useSsl = false;
useTls = true;
}
case "mailgun" -> {
host = "smtp.mailgun.org";
port = 587;
useSsl = false;
useTls = true;
}
default -> {
host = Emulator.getConfig().getValue("smtp.host", "localhost");
port = Emulator.getConfig().getInt("smtp.port", 587);
useSsl = Emulator.getConfig().getBoolean("smtp.use_ssl", false);
useTls = Emulator.getConfig().getBoolean("smtp.use_tls", true);
}
}
Properties props = new Properties();
props.put("mail.smtp.host", host);
props.put("mail.smtp.port", String.valueOf(port));
props.put("mail.smtp.auth", String.valueOf(!username.isEmpty()));
if (useTls) props.put("mail.smtp.starttls.enable", "true");
if (useSsl) {
props.put("mail.smtp.ssl.enable", "true");
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
props.put("mail.smtp.socketFactory.port", String.valueOf(port));
}
props.put("mail.smtp.connectiontimeout", "10000");
props.put("mail.smtp.timeout", "10000");
Session session = username.isEmpty()
? Session.getInstance(props)
: Session.getInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(fromAddr, fromName, "UTF-8"));
message.addRecipient(Message.RecipientType.TO, new InternetAddress(toAddress));
message.setSubject(subject, "UTF-8");
message.setText(body, "UTF-8");
Transport.send(message);
return true;
} catch (Exception e) {
LOGGER.error("Failed to send SMTP mail to " + toAddress, e);
return false;
}
}
}
@@ -0,0 +1,76 @@
package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
public final class TurnstileVerifier {
private static final Logger LOGGER = LoggerFactory.getLogger(TurnstileVerifier.class);
private static final String VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
private static final HttpClient CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
private TurnstileVerifier() {}
public static boolean isEnabled() {
return Emulator.getConfig() != null
&& Emulator.getConfig().getBoolean("login.turnstile.enabled", false);
}
public static boolean verify(String token, String remoteIp) {
if (!isEnabled()) return true;
if (token == null || token.isEmpty()) return false;
String secret = Emulator.getConfig().getValue("login.turnstile.secretkey", "");
if (secret.isEmpty()) {
LOGGER.warn("login.turnstile.enabled=1 but login.turnstile.secretkey is empty — refusing the request");
return false;
}
StringBuilder form = new StringBuilder();
form.append("secret=").append(URLEncoder.encode(secret, StandardCharsets.UTF_8));
form.append("&response=").append(URLEncoder.encode(token, StandardCharsets.UTF_8));
if (remoteIp != null && !remoteIp.isEmpty()) {
form.append("&remoteip=").append(URLEncoder.encode(remoteIp, StandardCharsets.UTF_8));
}
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(VERIFY_URL))
.timeout(Duration.ofSeconds(8))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(form.toString(), StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
LOGGER.warn("Turnstile siteverify returned HTTP {} for ip={}", response.statusCode(), remoteIp);
return false;
}
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
boolean success = json.has("success") && json.get("success").getAsBoolean();
if (!success) {
LOGGER.info("Turnstile token rejected for ip={} body={}", remoteIp, response.body());
}
return success;
} catch (Exception e) {
LOGGER.error("Turnstile verification failed for ip=" + remoteIp, e);
return false;
}
}
}