Compare commits

...

23 Commits

Author SHA1 Message Date
github-actions[bot] 885bdca0c4 🆙 Bump version to 4.2.29 [skip ci] 2026-06-02 16:03:45 +00:00
DuckieTM db035294a7 Merge pull request #142 from duckietm/dev
🆙 Updated Group buy
2026-06-02 18:02:42 +02:00
duckietm 3216ba1df6 🆙 Updated Group buy 2026-06-02 18:02:25 +02:00
github-actions[bot] 8d6b969d75 🆙 Bump version to 4.2.28 [skip ci] 2026-06-02 14:06:26 +00:00
DuckieTM b9723e0298 Merge pull request #141 from duckietm/dev
🆙 Security Fix
2026-06-02 16:05:11 +02:00
duckietm c4aae676b2 🆙 Security Fix
Thanks to @Bop:

There's a group bug where you can accept anyone into a group within MS. There's no packet validation for accepting members if the group is invite only.
This is crucial because if you allow users to have rights who are group members, your rooms can be trashed. AKA YOUR EVENT ROOMS
2026-06-02 16:04:47 +02:00
github-actions[bot] 585f4dd3aa 🆙 Bump version to 4.2.27 [skip ci] 2026-06-01 06:28:06 +00:00
DuckieTM afa114d511 Merge pull request #139 from duckietm/dev
Dev
2026-06-01 08:27:01 +02:00
DuckieTM 0aadd01493 Merge pull request #138 from simoleo89/feat/wheel-admin-add-remove
feat(wheel): add & remove fortune-wheel prizes from the editor
2026-05-31 15:45:10 +02:00
simoleo89 9d98fbf9ee feat(wheel): support adding & removing fortune-wheel prizes from the editor
The prize editor could only update existing rows; savePrize was UPDATE-only,
so the admin panel had no way to add a new slice or remove an old one.

- WheelManager.savePrize now takes a sortOrder and inserts when id <= 0
  (returning the generated id) or updates + re-enables when id > 0, so a
  previously removed prize can be brought back. sort_order is persisted to
  match the editor's display order.
- New WheelManager.disablePrizesNotIn(keptIds) soft-deletes (enabled = 0)
  any prize absent from the saved authoritative list. Non-destructive: rows
  stay in the table and loadPrizes already filters enabled = 1.
- WheelAdminSavePrizesEvent collects the saved ids and disables the rest
  before reloading.

No schema change (wheel_prizes already has enabled + sort_order) and no
packet change (id = 0 / omission express insert / delete on the existing
wire). Pairs with the Nitro-V3 client editor add/remove buttons.
2026-05-31 10:49:10 +02:00
DuckieTM b38274e134 Merge pull request #137 from medievalshell/Dev
fix(bans): persist client machine fingerprint so machine/super bans work
2026-05-31 10:37:54 +02:00
medievalshell 02ab30180c fix(chat): relay unknown chat bubble ids instead of resetting to default
getBubble() fell back to NORMAL (bubble 0) for any id not in the registered
BUBBLES map, so custom client-side chat bubbles (e.g. ids 253+) rendered as
the default bubble for everyone. Now unknown positive ids (<=1000) pass
through as a transient bubble carrying that id, so the server relays it and
clients render their own .bubble-<id> style. No need to enumerate each one.
2026-05-31 03:39:23 +02:00
medievalshell da63439d53 fix(bans): persist client machine fingerprint so machine/super bans work
The Nitro client already sends a strong machine fingerprint (Thumbmark,
"IID-<hash>") via the UniqueID packet (header 2490 -> MachineIDEvent), but
the emulator only stored it on the GameClient and never copied it onto the
Habbo's HabboInfo, so it was never written to users.machine_id. As a result
machine/super bans (which read users.machine_id) matched nobody.

- MachineIDEvent: when the fingerprint arrives and the Habbo is already
  loaded, copy it onto HabboInfo and persist (run the Habbo save).
- SecureLoginEvent: if the fingerprint arrived before login, copy it onto
  HabboInfo right before the login save.

This makes machine/super bans effective without changing the client.
2026-05-31 00:04:00 +02:00
github-actions[bot] bf1a29a6e8 🆙 Bump version to 4.2.26 [skip ci] 2026-05-30 05:53:48 +00:00
DuckieTM 6391d721ff Merge pull request #136 from duckietm/dev
Dev
2026-05-30 07:52:43 +02:00
DuckieTM dfea6bcf83 🆙 Updated SQL 2026-05-30 07:52:02 +02:00
DuckieTM a7f207bb76 Merge pull request #134 from medievalshell/Dev
feat: persist `scale` for room ads / branding furni
2026-05-30 07:13:59 +02:00
duckietm b7915884b6 🆙 Update Rare-Value page 2026-05-29 08:28:01 +02:00
medievalshell 478f7bdba0 feat/fix: RCON wheel+soundboard reload, robust SSO reconnect behind Cloudflare
- RCON: add updatewheel/updatesoundboard (reload WheelManager/SoundboardManager live) so the CMS admin pages apply changes without an emulator restart.

- SSO ticket is no longer single-use: loadHabbo, session-resume and performFullDisconnect no longer clear auth_ticket. Behind Cloudflare the WS is dropped and the client retries with the same ticket; clearing it caused 'non-existing SSO token' and the 'refresh twice' / kicked-on-reconnect symptoms. The ticket now lives until its TTL (auth_ticket_expires_at), is overwritten by the CMS on the next /client load, or cleared on logout.

- SessionResume: restoreSsoTicket only restores when auth_ticket is empty (don't clobber a fresh CMS ticket); GameClient.dispose only parks/disconnects when the habbo is still attached to this client (a fast reconnect may have re-attached it to the new connection).
2026-05-29 04:45:34 +02:00
medievalshell c255f1e1b4 fix: guard RoomBundleLayout against null RoomManager during catalog init
CatalogManager.loadFurnitureValues() (rare-values feature) iterates every catalog page during GameEnvironment.load(); for a RoomBundleLayout this calls getRoomManager().loadRoom(), but RoomManager is constructed after CatalogManager so getRoomManager() returns null -> NullPointerException -> boot aborts. Null-guard the room load so the bundle resolves lazily at runtime instead.
2026-05-29 00:45:02 +02:00
medievalshell 9c831a9da4 feat: grant acc_wheeladmin to staff ranks for the wheel prize editor
The wheel prize editor is gated on acc_wheeladmin (client Settings button +
server WheelAdmin{Get,Save}PrizesEvent). Upstream 008_soundboard_fortune_wheel
registers the key but only grants rank_7 (its 7-rank hotel). This portable,
idempotent migration grants it to the same ranks as acc_ads_background via
dynamic SQL over the per-rank columns — no hardcoded rank ids. Apply then
:update_permissions or restart.
2026-05-28 22:47:15 +02:00
Medievalshell 08d1ae97a7 Merge branch 'duckietm:main' into Dev 2026-05-28 22:16:17 +02:00
medievalshell 8dd5155562 feat: persist scale for room ads / branding furni
InteractionRoomAds now carries a `scale` default value (100) alongside
imageUrl/clickUrl/offsetX/Y/Z, so the image zoom set in the client's
position editor is stored and broadcast like the other branding fields.
2026-05-28 15:30:33 +02:00
21 changed files with 458 additions and 218 deletions
@@ -1,78 +1,89 @@
-- Soundboard
-- The room flag column + sounds table are also created at boot by
ALTER TABLE `rooms` ADD COLUMN IF NOT EXISTS `soundboard_enabled` TINYINT(1) NOT NULL DEFAULT 0;
ALTER TABLE `rooms`
ADD COLUMN IF NOT EXISTS `soundboard_enabled` TINYINT(1) NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS `soundboard_sounds` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(64) NOT NULL DEFAULT '', -- pad label shown in the client
`url` VARCHAR(255) NOT NULL DEFAULT '', -- audio url (uploaded via CMS, like custom badges)
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(64) NOT NULL DEFAULT '', -- pad label shown in the client
`url` VARCHAR(255) NOT NULL DEFAULT '', -- audio url (uploaded via CMS, like custom badges)
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Fortune Wheel
-- Tables are also created at boot by WheelManager (CREATE TABLE IF NOT EXISTS),
-- so applying this file is only needed to seed prizes + settings.
-- ----------------------------------------------------------------------------
-- Fortune Wheel — tables
-- ----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `wheel_prizes` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`type` VARCHAR(16) NOT NULL DEFAULT 'nothing', -- item | badge | credits | points | spin | nothing
`value` VARCHAR(64) NOT NULL DEFAULT '', -- item: base item id ; badge: badge code ; others: unused
`amount` INT(11) NOT NULL DEFAULT 1, -- item qty / credits / points / extra spins
`points_type` INT(11) NOT NULL DEFAULT 5, -- for type=points (diamond default 5)
`weight` INT(11) NOT NULL DEFAULT 1, -- relative probability
`label` VARCHAR(64) NOT NULL DEFAULT '', -- slice label override (optional)
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
`id` INT(11) NOT NULL AUTO_INCREMENT,
`type` VARCHAR(16) NOT NULL DEFAULT 'nothing', -- item | badge | credits | points | spin | nothing
`value` VARCHAR(64) NOT NULL DEFAULT '', -- item: base item id ; badge: badge code ; others: unused
`amount` INT(11) NOT NULL DEFAULT 1, -- item qty / credits / points / extra spins
`points_type` INT(11) NOT NULL DEFAULT 5, -- for type=points (diamond default 5)
`weight` INT(11) NOT NULL DEFAULT 1, -- relative probability
`label` VARCHAR(64) NOT NULL DEFAULT '', -- slice label override (optional)
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `wheel_user_state` (
`user_id` INT(11) NOT NULL,
`free_spins` INT(11) NOT NULL DEFAULT 0, -- remaining free spins for the current day
`extra_spins` INT(11) NOT NULL DEFAULT 0, -- bought / won spins
`last_reset` INT(11) NOT NULL DEFAULT 0, -- day index of last daily reset (unix / 86400)
`user_id` INT(11) NOT NULL,
`free_spins` INT(11) NOT NULL DEFAULT 0, -- remaining free spins for the current day
`extra_spins` INT(11) NOT NULL DEFAULT 0, -- bought / won spins
`last_reset` INT(11) NOT NULL DEFAULT 0, -- day index of last daily reset (unix / 86400)
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `wheel_recent_wins` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`username` VARCHAR(64) NOT NULL DEFAULT '',
`look` VARCHAR(255) NOT NULL DEFAULT '',
`prize_label` VARCHAR(64) NOT NULL DEFAULT '',
`won_at` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_wheel_recent_wins_id` (`id`)
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`username` VARCHAR(64) NOT NULL DEFAULT '',
`look` VARCHAR(255) NOT NULL DEFAULT '',
`prize_label` VARCHAR(64) NOT NULL DEFAULT '',
`won_at` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
('wheel.free_spins_per_day', '1', 'Fortune wheel: free spins granted each day.')
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
('wheel.spin_cost', '50', 'Fortune wheel: cost of one extra spin.')
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
('wheel.spin_cost_type', '5', 'Fortune wheel: currency type for the spin cost (5 = diamonds; -1 = credits).')
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
INSERT INTO `wheel_prizes` (`type`, `amount`, `points_type`, `weight`, `label`, `sort_order`) VALUES
('points',25, 5, 20, '25 diamonds',1),
('points',50, 5, 12, '50 diamonds',2),
('points',200, 5, 3, '200 diamonds',3),
('credits',100, 0, 15, '100 credits',4),
('spin',1, 0, 15, '1 Extra spin', 5),
('spin',2, 0, 6, '2 Extra spins',6),
('nothing',0, 0, 29, 'Oh to bad!',7);
INSERT INTO `permission_definitions`
(`permission_key`, `max_value`, `comment`,
`rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`)
VALUES
('acc_wheeladmin', 1, 'Required to open the Fortune Wheel settings popup and edit prize rows.',
0, 0, 0, 0, 0, 0, 1)
('wheel.free_spins_per_day', '1', 'Fortune wheel: free spins granted each day.'),
('wheel.spin_cost', '50', 'Fortune wheel: cost of one extra spin.'),
('wheel.spin_cost_type', '5', 'Fortune wheel: currency type for the spin cost (5 = diamonds; -1 = credits).')
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
INSERT INTO `wheel_prizes` (`type`, `amount`, `points_type`, `weight`, `label`, `sort_order`)
SELECT `type`, `amount`, `points_type`, `weight`, `label`, `sort_order`
FROM (
SELECT 'points' AS `type`, 25 AS `amount`, 5 AS `points_type`, 20 AS `weight`, '25 diamonds' AS `label`, 1 AS `sort_order`
UNION ALL SELECT 'points', 50, 5, 12, '50 diamonds', 2
UNION ALL SELECT 'points', 200, 5, 3, '200 diamonds', 3
UNION ALL SELECT 'credits', 100, 0, 15, '100 credits', 4
UNION ALL SELECT 'spin', 1, 0, 15, '1 Extra spin', 5
UNION ALL SELECT 'spin', 2, 0, 6, '2 Extra spins', 6
UNION ALL SELECT 'nothing', 0, 0, 29, 'Oh to bad!', 7
) AS seed
WHERE NOT EXISTS (SELECT 1 FROM `wheel_prizes`);
INSERT IGNORE INTO `permission_definitions` (`permission_key`, `max_value`, `comment`)
VALUES (
'acc_wheeladmin',
1,
'Allows opening the Fortune Wheel prize editor (FortuneWheelSettingsView) to add/edit prize slices. Gated server-side by the same key.'
);
SET @cols := NULL;
SELECT GROUP_CONCAT(CONCAT('dst.`', `column_name`, '` = src.`', `column_name`, '`') SEPARATOR ', ')
INTO @cols
FROM `information_schema`.`columns`
WHERE `table_schema` = DATABASE()
AND `table_name` = 'permission_definitions'
AND `column_name` REGEXP '^rank_[0-9]+$';
SET @sql := CONCAT(
'UPDATE `permission_definitions` dst ',
'JOIN `permission_definitions` src ON src.`permission_key` = ''acc_ads_background'' ',
'SET ', @cols, ' ',
'WHERE dst.`permission_key` = ''acc_wheeladmin'''
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.2.25</version>
<version>4.2.29</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -202,8 +202,8 @@ public class CatalogManager {
public final Item ecotronItem;
public final THashMap<Integer, CatalogLimitedConfiguration> limitedNumbers;
private final List<Voucher> vouchers;
// spriteId -> [credits, points, pointsType], derived from catalog_items (see loadFurnitureValues)
public final TIntObjectMap<int[]> furnitureValues;
private volatile byte[] rareValuesPayloadCache;
public CatalogManager() {
long millis = System.currentTimeMillis();
@@ -249,10 +249,6 @@ public class CatalogManager {
this.loadFurnitureValues();
}
// Builds spriteId -> [credits, points, pointsType] from catalog_items so the
// client can show a furni's "value" (toolbar price guide + infostand line).
// Only single-item, single-amount FLOOR/WALL sales are considered, so bundles
// and multi-packs don't pollute the per-rare price. First clean entry wins.
private synchronized void loadFurnitureValues() {
this.furnitureValues.clear();
final int diamondType = Emulator.getConfig().getInt("seasonal.currency.diamond", 5);
@@ -266,8 +262,6 @@ public class CatalogManager {
int points = catalogItem.getPoints();
int pointsType = catalogItem.getPointsType();
// Only diamond-priced items — both the "Valore Rari" panel and the
// infostand value line show diamonds only.
if (points <= 0 || pointsType != diamondType)
continue;
@@ -291,13 +285,39 @@ public class CatalogManager {
}
}
this.rebuildRareValuesPayloadCache();
LOGGER.info("Furniture Values -> Loaded! ({} entries)", this.furnitureValues.size());
}
private void rebuildRareValuesPayloadCache() {
try (java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(this.furnitureValues.size() * 16 + 8);
java.io.DataOutputStream out = new java.io.DataOutputStream(baos)) {
out.writeInt(this.furnitureValues.size());
TIntObjectIterator<int[]> iterator = this.furnitureValues.iterator();
while (iterator.hasNext()) {
iterator.advance();
int[] value = iterator.value();
out.writeInt(iterator.key()); // spriteId
out.writeInt(value[0]); // credits
out.writeInt(value[1]); // points
out.writeInt(value[2]); // pointsType
}
this.rareValuesPayloadCache = baos.toByteArray();
} catch (java.io.IOException e) {
LOGGER.error("Failed to build rare values payload cache", e);
this.rareValuesPayloadCache = null;
}
}
public TIntObjectMap<int[]> getFurnitureValues() {
return this.furnitureValues;
}
public byte[] getRareValuesPayloadSnapshot() {
return this.rareValuesPayloadCache;
}
private synchronized void loadLimitedNumbers() {
this.limitedNumbers.clear();
@@ -1104,9 +1124,6 @@ public class CatalogManager {
type = type.replace("bot_", "");
type = type.replace("visitor_logger", "visitor_log");
// Permission gate keyed on the canonical base-item name
// (admin-controlled but stable), not the catalog page name
// which can be renamed and bypass the check.
if (("bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)
|| ("rentable_bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)) {
if (!habbo.getClient().getHabbo().hasPermission(com.eu.habbo.habbohotel.bots.FrankBot.PERMISSION_USE)) {
@@ -42,14 +42,18 @@ public class RoomBundleLayout extends SingleBundle {
}
if (this.room == null) {
if (this.roomId > 0) {
this.room = Emulator.getGameEnvironment().getRoomManager().loadRoom(this.roomId);
RoomManager roomManager = Emulator.getGameEnvironment().getRoomManager();
if (this.roomId > 0 && roomManager != null) {
this.room = roomManager.loadRoom(this.roomId);
if (this.room != null)
this.room.preventUnloading = true;
} else {
} else if (this.roomId <= 0) {
LOGGER.error("No room id specified for room bundle {}({})", this.getPageName(), this.getId());
}
// roomManager can be null when CatalogManager.loadFurnitureValues() runs
// during GameEnvironment.load() before RoomManager is constructed; in that
// case skip eager room loading — the bundle resolves lazily at runtime.
}
if (this.room == null) {
@@ -153,7 +153,13 @@ public class GameClient {
this.channel.close();
if (this.habbo != null) {
if (this.habbo.isOnline()) {
// Agisci sull'Habbo SOLO se è ancora attaccato a QUESTO client. Su un
// reconnect veloce (drop Cloudflare → il client riconnette) l'Habbo può
// essere già stato riassegnato alla NUOVA connessione (session resume):
// in quel caso questo dispose della vecchia connessione NON deve
// parcheggiarlo né disconnetterlo, altrimenti ucciderebbe la sessione
// appena ripristinata (era la causa del "Bye"/kick al 2° reconnect).
if (this.habbo.getClient() == this && this.habbo.isOnline()) {
// Try to park the habbo in the grace period instead of immediate disconnect
boolean parked = SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket);
@@ -118,16 +118,32 @@ public class SessionResumeManager {
LOGGER.error("[SessionResume] Error during deferred disconnect", e);
}
clearSsoTicket(habbo.getHabboInfo().getId());
// NON svuotare il ticket SSO qui. Dietro Cloudflare la pagina si ricarica
// lentamente (~15s) e la grace (5s) scade prima che la nuova connessione
// arrivi: svuotando il ticket si cancellava quello NUOVO appena scritto dal
// CMS per il refresh → "non-existing SSO token" → bisognava refreshare 2 volte.
// Il ticket vive col suo TTL (auth_ticket_expires_at) e viene sovrascritto dal
// CMS al prossimo /client o azzerato al logout.
}
private void restoreSsoTicket(int userId, String ssoTicket) {
// Restore the old ticket ONLY if no fresh ticket has been written in the
// meantime. On a hard-refresh the CMS writes a NEW auth_ticket for the same
// user before this parking restore runs; without the guard we'd clobber it
// with the old ticket, so the new connection's SSO wouldn't be found and the
// client would get "session expired" on the first attempt. The guard means:
// normal reconnect (ticket cleared to '' after login) -> restore; hard-refresh
// (CMS already wrote a new ticket) -> leave the new ticket untouched.
try (var connection = Emulator.getDatabase().getDataSource().getConnection();
var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) {
var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? AND (auth_ticket = '' OR auth_ticket IS NULL) LIMIT 1")) {
statement.setString(1, ssoTicket);
statement.setInt(2, userId);
statement.execute();
LOGGER.info("[SessionResume] Restored SSO ticket for user {} during grace period", userId);
int updated = statement.executeUpdate();
if (updated > 0) {
LOGGER.info("[SessionResume] Restored SSO ticket for user {} during grace period", userId);
} else {
LOGGER.info("[SessionResume] Skipped SSO restore for user {} — a newer ticket is already present (likely a fresh login/hard-refresh)", userId);
}
} catch (Exception e) {
LOGGER.error("[SessionResume] Failed to restore SSO ticket for user " + userId, e);
}
@@ -29,6 +29,10 @@ public class InteractionRoomAds extends InteractionCustomValues {
{
this.put("offsetZ", "0");
}
{
this.put("scale", "100");
}
};
public InteractionRoomAds(ResultSet set, Item baseItem) throws SQLException {
@@ -134,7 +134,18 @@ public class RoomChatMessageBubbles {
}
public static RoomChatMessageBubbles getBubble(int id) {
return BUBBLES.getOrDefault(id, NORMAL);
RoomChatMessageBubbles bubble = BUBBLES.get(id);
if (bubble != null) return bubble;
// Custom chat bubbles (client-side only, e.g. ids 253+) are not registered
// above. Instead of falling back to NORMAL (which made them render as the
// default bubble), pass the id through so the server relays it as-is and
// the client renders its own .bubble-<id> style. Capped to avoid abuse.
if (id > 0 && id <= 1000) {
return new RoomChatMessageBubbles(id, "CUSTOM_" + id, "", true, false);
}
return NORMAL;
}
private static void registerBubble(RoomChatMessageBubbles bubble) {
@@ -132,15 +132,12 @@ public class HabboManager {
Emulator.getPluginManager().fireEvent(new UserRegisteredEvent(habbo));
}
if (!Emulator.debugging) {
try (PreparedStatement stmt = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) {
stmt.setString(1, "");
stmt.setInt(2, habbo.getHabboInfo().getId());
stmt.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
// NB: il ticket SSO NON viene svuotato qui di proposito. Dietro
// Cloudflare il WebSocket viene droppato e il client ritenta più
// volte con lo STESSO ticket: se lo consumassimo al primo uso, i
// retry (e l'hard-refresh) fallirebbero con "non-existing SSO token".
// Il ticket resta valido fino alla scadenza (auth_ticket_expires_at,
// TTL gestito dal CMS) o finché il CMS non ne scrive uno nuovo / logout.
}
}
} catch (SQLException e) {
@@ -12,9 +12,11 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
@@ -330,26 +332,88 @@ public class WheelManager {
return true;
}
public void savePrize(int id, String type, String value, int amount, int pointsType, int weight, String label) {
/**
* Persists a single prize. An {@code id <= 0} inserts a brand-new prize and
* returns its generated id; a positive id updates the existing row (and
* re-enables it, so a previously soft-deleted prize can be brought back).
* {@code sortOrder} reflects the prize's position in the editor so the
* wheel layout matches what the admin sees. Returns the effective row id,
* or {@code 0} if the write failed.
*/
public int savePrize(int id, String type, String value, int amount, int pointsType, int weight, String label, int sortOrder) {
String safeType = (type != null && VALID_PRIZE_TYPES.contains(type)) ? type : "nothing";
String safeValue = truncate(value, MAX_STRING_LEN);
String safeLabel = truncate(label, MAX_STRING_LEN);
int safeAmount = clamp(amount, 0, MAX_PRIZE_AMOUNT);
int safeWeight = clamp(weight, 0, MAX_WEIGHT);
int safeSort = clamp(sortOrder, 0, MAX_PRIZES_PER_SAVE);
if (id > 0) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE wheel_prizes SET type = ?, value = ?, amount = ?, points_type = ?, weight = ?, label = ?, sort_order = ?, enabled = 1 WHERE id = ?")) {
statement.setString(1, safeType);
statement.setString(2, safeValue);
statement.setInt(3, safeAmount);
statement.setInt(4, pointsType);
statement.setInt(5, safeWeight);
statement.setString(6, safeLabel);
statement.setInt(7, safeSort);
statement.setInt(8, id);
statement.executeUpdate();
return id;
} catch (SQLException e) {
LOGGER.error("Failed to save wheel prize {}", id, e);
return 0;
}
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE wheel_prizes SET type = ?, value = ?, amount = ?, points_type = ?, weight = ?, label = ? WHERE id = ?")) {
"INSERT INTO wheel_prizes (type, value, amount, points_type, weight, label, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?, 1, ?)",
Statement.RETURN_GENERATED_KEYS)) {
statement.setString(1, safeType);
statement.setString(2, safeValue);
statement.setInt(3, safeAmount);
statement.setInt(4, pointsType);
statement.setInt(5, safeWeight);
statement.setString(6, safeLabel);
statement.setInt(7, id);
statement.setInt(7, safeSort);
statement.executeUpdate();
try (ResultSet keys = statement.getGeneratedKeys()) {
if (keys.next()) return keys.getInt(1);
}
} catch (SQLException e) {
LOGGER.error("Failed to insert wheel prize", e);
}
return 0;
}
/**
* Soft-deletes every enabled prize whose id is not in {@code keptIds} by
* setting {@code enabled = 0}. This is intentionally non-destructive: rows
* stay in the table (so historical references and re-enabling remain
* possible) but {@link #loadPrizes()} only ever loads {@code enabled = 1}.
* An empty set disables all prizes.
*/
public void disablePrizesNotIn(Set<Integer> keptIds) {
if (keptIds == null) return;
StringBuilder sql = new StringBuilder("UPDATE wheel_prizes SET enabled = 0 WHERE enabled = 1");
if (!keptIds.isEmpty()) {
StringJoiner ids = new StringJoiner(",", " AND id NOT IN (", ")");
for (Integer keptId : keptIds) {
ids.add(Integer.toString(keptId));
}
sql.append(ids);
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(sql.toString())) {
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to save wheel prize {}", id, e);
LOGGER.error("Failed to disable removed wheel prizes", e);
}
}
@@ -25,45 +25,55 @@ public class GuildAcceptMembershipEvent extends MessageHandler {
int userId = this.packet.readInt();
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
if (guild == null) {
return;
}
GuildMember actorMember = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo());
boolean canAccept = guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId()
|| this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)
|| (actorMember != null && (actorMember.getRank().equals(GuildRank.ADMIN) || actorMember.getRank().equals(GuildRank.OWNER)));
if (!canAccept) {
return;
}
GuildMember targetMember = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId);
if (targetMember == null) {
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.NO_LONGER_MEMBER));
return;
}
if (targetMember.getRank().type != GuildRank.REQUESTED.type) {
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.ALREADY_ACCEPTED));
return;
}
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
if (guild != null) {
GuildMember groupMember = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo());
if (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId()
|| this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)
|| (groupMember != null && (groupMember.getRank().equals(GuildRank.ADMIN) || groupMember.getRank().equals(GuildRank.OWNER)))) {
if (habbo != null) {
if (habbo.getHabboStats().hasGuild(guild.getId())) {
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.ALREADY_ACCEPTED));
return;
} else {
//Check the user has requested
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, habbo);
if (member == null || member.getRank().type != GuildRank.REQUESTED.type) {
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.NO_LONGER_MEMBER));
return;
} else {
GuildAcceptedMembershipEvent event = new GuildAcceptedMembershipEvent(guild, userId, habbo);
Emulator.getPluginManager().fireEvent(event);
if (!event.isCancelled()) {
habbo.getHabboStats().addGuild(guild.getId());
Emulator.getGameEnvironment().getGuildManager().joinGuild(guild, this.client, habbo.getHabboInfo().getId(), true);
guild.decreaseRequestCount();
guild.increaseMemberCount();
this.client.sendResponse(new GuildRefreshMembersListComposer(guild));
Room room = habbo.getHabboInfo().getCurrentRoom();
if (room != null) {
if (room.getGuildId() == guildId) {
habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId)));
room.refreshRightsForHabbo(habbo);
}
}
}
}
}
} else {
Emulator.getGameEnvironment().getGuildManager().joinGuild(guild, this.client, userId, true);
}
GuildAcceptedMembershipEvent event = new GuildAcceptedMembershipEvent(guild, userId, habbo);
Emulator.getPluginManager().fireEvent(event);
if (event.isCancelled()) {
return;
}
if (habbo != null) {
habbo.getHabboStats().addGuild(guild.getId());
}
Emulator.getGameEnvironment().getGuildManager().joinGuild(guild, this.client, userId, true);
guild.decreaseRequestCount();
guild.increaseMemberCount();
this.client.sendResponse(new GuildRefreshMembersListComposer(guild));
if (habbo != null) {
Room room = habbo.getHabboInfo().getCurrentRoom();
if (room != null && room.getGuildId() == guildId) {
habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId)));
room.refreshRightsForHabbo(habbo);
}
}
}
@@ -30,20 +30,69 @@ public class RequestGuildBuyEvent extends MessageHandler {
final String name = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo());
final String description = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo());
if(name.length() > 29){
if (name.length() == 0 || name.length() > 29) {
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.INVALID_GUILD_NAME));
return;
}
if(description.length() > 254){
if (description.length() > 254) {
return;
}
if (Emulator.getConfig().getBoolean("catalog.guild.hc_required", true) && !this.client.getHabbo().getHabboStats().hasActiveClub()) {
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.HC_REQUIRED));
return;
}
int roomId = this.packet.readInt();
Room r = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
if (r == null) {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
if (r.hasGuild() || r.getGuildId() != 0) {
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.ROOM_ALREADY_IN_USE));
return;
}
if (r.getOwnerId() != this.client.getHabbo().getHabboInfo().getId()) {
String message = Emulator.getTexts().getValue("scripter.warning.guild.buy.owner").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%roomname%", r.getName().replace("%owner%", r.getOwnerName()));
ScripterManager.scripterDetected(this.client, message);
LOGGER.info(message);
return;
}
int colorOne = this.packet.readInt();
int colorTwo = this.packet.readInt();
int count = this.packet.readInt();
StringBuilder badge = new StringBuilder();
byte base = 1;
while (base < count) {
int id = this.packet.readInt();
int color = this.packet.readInt();
int pos = this.packet.readInt();
if (base == 1) {
badge.append("b");
} else {
badge.append("s");
}
badge.append(id < 100 ? "0" : "").append(id < 10 ? "0" : "").append(id).append(color < 10 ? "0" : "").append(color).append(pos);
base += 3;
}
// Only charge the player once every step has been validated. Previously the
// credits were deducted before the room was checked, so a purchase that
// failed afterwards (missing room, room already used by a guild, not the
// owner) still took the credits without ever creating the group.
if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) {
int guildPrice = Emulator.getConfig().getInt("catalog.guild.price");
if (this.client.getHabbo().getHabboInfo().getCredits() >= guildPrice) {
@@ -54,78 +103,34 @@ public class RequestGuildBuyEvent extends MessageHandler {
}
}
int roomId = this.packet.readInt();
Guild guild = Emulator.getGameEnvironment().getGuildManager().createGuild(this.client.getHabbo(), roomId, r.getName(), name, description, badge.toString(), colorOne, colorTwo);
Room r = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
r.setGuild(guild.getId());
r.removeAllRights();
r.setNeedsUpdate(true);
if (r != null) {
if (r.hasGuild()) {
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.ROOM_ALREADY_IN_USE));
return;
Emulator.getGameEnvironment().getGuildManager().addGuild(guild);
if (Emulator.getConfig().getBoolean("imager.internal.enabled")) {
Emulator.getBadgeImager().generate(guild);
}
this.client.sendResponse(new PurchaseOKComposer());
this.client.sendResponse(new GuildBoughtComposer(guild));
r.refreshGuild(guild);
for (Habbo habbo : r.getHabbos()) {
if (habbo.getClient() == null) {
continue;
}
if (r.getOwnerId() == this.client.getHabbo().getHabboInfo().getId()) {
if (r.getGuildId() == 0) {
int colorOne = this.packet.readInt();
int colorTwo = this.packet.readInt();
habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, null));
int count = this.packet.readInt();
StringBuilder badge = new StringBuilder();
byte base = 1;
while (base < count) {
int id = this.packet.readInt();
int color = this.packet.readInt();
int pos = this.packet.readInt();
if (base == 1) {
badge.append("b");
} else {
badge.append("s");
}
badge.append(id < 100 ? "0" : "").append(id < 10 ? "0" : "").append(id).append(color < 10 ? "0" : "").append(color).append(pos);
base += 3;
}
Guild guild = Emulator.getGameEnvironment().getGuildManager().createGuild(this.client.getHabbo(), roomId, r.getName(), name, description, badge.toString(), colorOne, colorTwo);
r.setGuild(guild.getId());
r.removeAllRights();
r.setNeedsUpdate(true);
Emulator.getGameEnvironment().getGuildManager().addGuild(guild);
if (Emulator.getConfig().getBoolean("imager.internal.enabled")) {
Emulator.getBadgeImager().generate(guild);
}
this.client.sendResponse(new PurchaseOKComposer());
this.client.sendResponse(new GuildBoughtComposer(guild));
r.refreshGuild(guild);
for (Habbo habbo : r.getHabbos()) {
if (habbo.getClient() == null) {
continue;
}
habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, null));
if (habbo.getHabboInfo().getId() != this.client.getHabbo().getHabboInfo().getId()) {
habbo.getClient().sendResponse(new RoomDataComposer(r, habbo, true, false));
}
}
Emulator.getPluginManager().fireEvent(new GuildPurchasedEvent(guild, this.client.getHabbo()));
}
} else {
String message = Emulator.getTexts().getValue("scripter.warning.guild.buy.owner").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%roomname%", r.getName().replace("%owner%", r.getOwnerName()));
ScripterManager.scripterDetected(this.client, message);
LOGGER.info(message);
if (habbo.getHabboInfo().getId() != this.client.getHabbo().getHabboInfo().getId()) {
habbo.getClient().sendResponse(new RoomDataComposer(r, habbo, true, false));
}
}
Emulator.getPluginManager().fireEvent(new GuildPurchasedEvent(guild, this.client.getHabbo()));
}
}
@@ -1,5 +1,6 @@
package com.eu.habbo.messages.incoming.handshake;
import com.eu.habbo.Emulator;
import com.eu.habbo.messages.NoAuthMessage;
import com.eu.habbo.messages.incoming.MessageHandler;
import org.slf4j.Logger;
@@ -24,6 +25,15 @@ public class MachineIDEvent extends MessageHandler {
this.client.setMachineId(storedMachineId);
// Persist the machine fingerprint onto the user so machine/super bans can
// target it (createOfflineUserBan copies users.machine_id). The Nitro client
// sends this UniqueID packet right after the SSO ticket, so the Habbo is
// normally already loaded by the time we get here.
if (!storedMachineId.isEmpty() && this.client.getHabbo() != null && this.client.getHabbo().getHabboInfo() != null) {
this.client.getHabbo().getHabboInfo().setMachineID(storedMachineId);
Emulator.getThreading().run(this.client.getHabbo());
}
LOGGER.debug("Setting client MachineId to {}", storedMachineId);
}
}
@@ -133,17 +133,10 @@ public class SecureLoginEvent extends MessageHandler {
this.client.setHabbo(habbo);
this.client.setMachineId(habbo.getHabboInfo().getMachineID());
// Clear the SSO ticket now that session is resumed (prevent reuse)
if (!Emulator.debugging) {
try (java.sql.Connection conn = Emulator.getDatabase().getDataSource().getConnection();
java.sql.PreparedStatement stmt = conn.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) {
stmt.setString(1, "");
stmt.setInt(2, habbo.getHabboInfo().getId());
stmt.execute();
} catch (Exception e) {
LOGGER.error("Failed to clear SSO ticket after session resume", e);
}
}
// NB: NON svuotiamo il ticket SSO qui (vedi HabboManager.loadHabbo):
// dietro Cloudflare il client ritenta la connessione con lo stesso
// ticket, quindi deve restare valido fino alla scadenza TTL. Consumarlo
// farebbe fallire i retry / l'hard-refresh con "non-existing SSO token".
} else {
// Normal login — load from database
habbo = Emulator.getGameEnvironment().getHabboManager().loadHabbo(sso);
@@ -168,6 +161,12 @@ public class SecureLoginEvent extends MessageHandler {
throw new NullPointerException(habbo.getHabboInfo().getUsername() + " has a NON EXISTING RANK!");
}
// If the machine fingerprint already arrived (UniqueID before login),
// persist it so machine/super bans can target this user.
if (this.client.getMachineId() != null && !this.client.getMachineId().isEmpty()) {
this.client.getHabbo().getHabboInfo().setMachineID(this.client.getMachineId());
}
Emulator.getThreading().run(habbo);
Emulator.getGameEnvironment().getHabboManager().addHabbo(habbo);
} catch (Exception e) {
@@ -1,11 +1,10 @@
package com.eu.habbo.messages.incoming.rarevalues;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.catalog.CatalogManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.rarevalues.RareValuesComposer;
// Client requests the furni value map once on load. Public info (catalog prices),
// no permission gate. Rate limited since the payload is large.
public class RequestRareValuesEvent extends MessageHandler {
@Override
public int getRatelimit() {
@@ -14,8 +13,15 @@ public class RequestRareValuesEvent extends MessageHandler {
@Override
public void handle() throws Exception {
this.client.sendResponse(new RareValuesComposer(
Emulator.getGameEnvironment().getCatalogManager().getFurnitureValues()
));
if (this.client.getHabbo() == null) return;
CatalogManager catalog = Emulator.getGameEnvironment().getCatalogManager();
byte[] snapshot = catalog.getRareValuesPayloadSnapshot();
if (snapshot != null) {
this.client.sendResponse(new RareValuesComposer(snapshot));
return;
}
this.client.sendResponse(new RareValuesComposer(catalog.getFurnitureValues()));
}
}
@@ -6,6 +6,9 @@ import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.wheel.WheelAdminPrizesComposer;
import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer;
import java.util.HashSet;
import java.util.Set;
public class WheelAdminSavePrizesEvent extends MessageHandler {
public static final String PERMISSION_KEY = "acc_wheeladmin";
@@ -25,6 +28,12 @@ public class WheelAdminSavePrizesEvent extends MessageHandler {
int count = this.packet.readInt();
if (count <= 0 || count > WheelManager.MAX_PRIZES_PER_SAVE) return;
// The client sends the full authoritative list of prizes in display
// order. id <= 0 means "insert a new prize"; any existing prize whose
// id is absent from this list was removed in the editor and gets
// soft-disabled below.
Set<Integer> keptIds = new HashSet<>();
for (int i = 0; i < count; i++) {
int id = this.packet.readInt();
String type = this.packet.readString();
@@ -33,9 +42,11 @@ public class WheelAdminSavePrizesEvent extends MessageHandler {
int pointsType = this.packet.readInt();
int weight = this.packet.readInt();
String label = this.packet.readString();
wheel.savePrize(id, type, value, amount, pointsType, weight, label);
int savedId = wheel.savePrize(id, type, value, amount, pointsType, weight, label, i);
if (savedId > 0) keptIds.add(savedId);
}
wheel.disablePrizesNotIn(keptIds);
wheel.reload();
this.client.sendResponse(new WheelAdminPrizesComposer(wheel.getPrizes()));
@@ -6,18 +6,29 @@ import com.eu.habbo.messages.outgoing.Outgoing;
import gnu.trove.iterator.TIntObjectIterator;
import gnu.trove.map.TIntObjectMap;
// Sends the full spriteId -> value map to the client. Consumed by the toolbar
// price guide and the furni infostand "value" line. See CatalogManager#loadFurnitureValues.
public class RareValuesComposer extends MessageComposer {
private final TIntObjectMap<int[]> values;
private final byte[] snapshot;
public RareValuesComposer(byte[] snapshot) {
this.values = null;
this.snapshot = snapshot;
}
public RareValuesComposer(TIntObjectMap<int[]> values) {
this.values = values;
this.snapshot = null;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.RareValuesComposer);
if (this.snapshot != null) {
this.response.appendRawBytes(this.snapshot);
return this.response;
}
this.response.appendInt(this.values.size());
TIntObjectIterator<int[]> iterator = this.values.iterator();
@@ -0,0 +1,21 @@
package com.eu.habbo.messages.rcon;
import com.eu.habbo.Emulator;
import com.google.gson.Gson;
// Ricarica i suoni della Soundboard dal DB (live), così i suoni aggiunti/caricati
// dal CMS (/admin/soundboard) si applicano senza riavviare l'emulatore.
public class UpdateSoundboard extends RCONMessage<UpdateSoundboard.SoundboardJSON> {
public UpdateSoundboard() {
super(SoundboardJSON.class);
}
@Override
public void handle(Gson gson, SoundboardJSON object) {
Emulator.getGameEnvironment().getSoundboardManager().reload();
}
static class SoundboardJSON {
}
}
@@ -0,0 +1,21 @@
package com.eu.habbo.messages.rcon;
import com.eu.habbo.Emulator;
import com.google.gson.Gson;
// Ricarica i premi/settings della Ruota della Fortuna dal DB (live), così le
// modifiche fatte dal CMS (/admin/wheel) si applicano senza riavviare l'emulatore.
public class UpdateWheel extends RCONMessage<UpdateWheel.WheelJSON> {
public UpdateWheel() {
super(WheelJSON.class);
}
@Override
public void handle(Gson gson, WheelJSON object) {
Emulator.getGameEnvironment().getWheelManager().reload();
}
static class WheelJSON {
}
}
@@ -11,6 +11,7 @@ import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.handler.codec.UnsupportedMessageTypeException;
import io.netty.handler.ssl.NotSslRecordException;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,6 +40,19 @@ public class GameMessageHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (!(msg instanceof ClientMessage)) {
try {
if (Emulator.getConfig().getBoolean("debug.mode")) {
LOGGER.debug("Discarding non-game message {} from {}",
msg.getClass().getSimpleName(), ctx.channel().remoteAddress());
}
} finally {
ReferenceCountUtil.release(msg);
ctx.channel().close();
}
return;
}
ClientMessage message = (ClientMessage) msg;
try {
@@ -45,6 +45,8 @@ public class RCONServer extends Server {
this.addRCONMessage("sendroombundle", SendRoomBundle.class);
this.addRCONMessage("setrank", SetRank.class);
this.addRCONMessage("updatewordfilter", UpdateWordfilter.class);
this.addRCONMessage("updatewheel", UpdateWheel.class);
this.addRCONMessage("updatesoundboard", UpdateSoundboard.class);
this.addRCONMessage("updatecatalog", UpdateCatalog.class);
this.addRCONMessage("executecommand", ExecuteCommand.class);
this.addRCONMessage("progressachievement", ProgressAchievement.class);