You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 014ca9ca48 | |||
| d189d66f9e | |||
| c272a36cc5 | |||
| 1d6e05ee57 | |||
| ea44771d69 | |||
| 1da783aff9 | |||
| e772686c4b | |||
| a00f7b01f5 | |||
| 6b4089cace | |||
| 9ea7acf05c | |||
| bab43af41e | |||
| 55b38e7b85 | |||
| 4a96c5baaf | |||
| 539c5b5b96 | |||
| 7b7154e68f | |||
| 4aabb738a3 | |||
| 691dc42627 | |||
| 226873c1fb | |||
| a06a204b39 | |||
| e213609609 | |||
| 44d38b8661 | |||
| ccadb81970 | |||
| 0a3a940946 | |||
| 4613fbe80c | |||
| 9328f4a355 | |||
| da8b947ddf | |||
| b9658d0407 | |||
| 68d3731393 | |||
| 4ef4ed1a96 | |||
| c20e273a2c | |||
| 83d418e712 | |||
| 38d05e8a06 | |||
| c83a3bad8e | |||
| 0bd23ad244 | |||
| d8f74b2477 | |||
| dac09e92d1 | |||
| fbf979419e | |||
| 6126c35779 | |||
| a1749c9eda | |||
| 525c124fa5 | |||
| 57087a31f2 | |||
| c4b3295a45 | |||
| 418c753e6c | |||
| 8419f11883 | |||
| 1a0d783ff7 | |||
| 655e039df7 | |||
| 7726691cde | |||
| 67503aeb2a | |||
| b206b32748 | |||
| ad60861a3f | |||
| b77290f5e7 | |||
| b14730d37f | |||
| 9126396973 | |||
| d321ff3b85 | |||
| 7f38a25eef |
@@ -23,6 +23,10 @@ SET NAMES utf8mb4;
|
||||
ALTER TABLE `emulator_settings`
|
||||
ADD COLUMN IF NOT EXISTS `comment` TEXT NULL DEFAULT '' AFTER `value`;
|
||||
|
||||
ALTER TABLE catalog_pages
|
||||
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL', 'BUILDER', 'BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `includes`;
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `wired_emulator_settings` (
|
||||
`key` VARCHAR(255) NOT NULL,
|
||||
`value` TEXT NOT NULL,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`, `rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`) VALUES ('acc_housekeeping', '1', 'Allow housekeeping in the client', '0', '0', '0', '0', '0', '0', '1');
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `housekeeping_log` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`timestamp` INT NOT NULL,
|
||||
`actor_id` INT NOT NULL,
|
||||
`actor_name` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`target_type` VARCHAR(16) NOT NULL DEFAULT 'user',
|
||||
`target_id` INT NOT NULL DEFAULT 0,
|
||||
`target_label` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`action` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`detail` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`success` TINYINT NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `timestamp` (`timestamp`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,70 @@
|
||||
ALTER TABLE `bots`
|
||||
MODIFY COLUMN `type` ENUM('generic','visitor_log','bartender','weapons_dealer','frank')
|
||||
NOT NULL DEFAULT 'generic';
|
||||
|
||||
INSERT INTO `permission_definitions`
|
||||
(`permission_key`, `max_value`, `comment`,
|
||||
`rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`)
|
||||
VALUES
|
||||
('acc_bot_frank', 1, 'Required to purchase the Frank mascot bot from the catalog.',
|
||||
0, 0, 0, 0, 0, 0, 1)
|
||||
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `bot_chat_responses` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`bot_type` VARCHAR(32) NOT NULL,
|
||||
`keys` VARCHAR(255) NOT NULL COMMENT 'semicolon-separated trigger words',
|
||||
`responses` TEXT NOT NULL COMMENT 'newline-separated replies; bot picks one at random',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `bot_type` (`bot_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO `bot_chat_responses` (`bot_type`, `keys`, `responses`) VALUES
|
||||
('frank', '__door_triggers', 'show me the door\nkick me\ni want to leave\nlet me out'),
|
||||
('frank', '__door_lines', 'Right this way - mind the step!\nAnd out you go. Come back soon!\nAllow me to escort you to the exit.\nThere''s the door. Farewell, true believer!'),
|
||||
('frank', '__busy_whisper', 'Sorry, I am currently busy. Please wait until I am available.'),
|
||||
('frank', 'frank', 'Hello, I''m Frank! Welcome to Habbo.'),
|
||||
('frank', 'help', 'What do you need help with?'),
|
||||
('frank', 'thanks;thank you', 'Just doing my job, true believer!'),
|
||||
('frank', 'new', 'Welcome to Habbo! I hope you have a great time here.'),
|
||||
('frank', 'rooms', 'Looking for somewhere fun? Try the Navigator - thousands of rooms to explore!'),
|
||||
('frank', 'sulake', 'Sulake is the company behind Habbo. Take a look: https://www.sulake.com'),
|
||||
('frank', 'vip;hc', 'VIP gets you more outfits, more furni, more everything. Worth it!'),
|
||||
('frank', 'music', 'Snoop Dogg, Frank Sinatra and a little Beethoven on Sundays.'),
|
||||
('frank', 'movie', 'I''m a Casablanca man. Black and white films are an underrated art.'),
|
||||
('frank', 'game', 'Battleship. Always Battleship.'),
|
||||
('frank', 'snowstorm', 'Honestly? I''m terrible at Snowstorm. Don''t tell anyone.'),
|
||||
('frank', 'furni', 'Best furniture maker in town - hands down, the folks at Sulake.'),
|
||||
('frank', 'animal;cat;pet','I have a cat called Mr. Whiskers. He runs the place, really.'),
|
||||
('frank', 'miranda', 'Miranda. The love of my life. Don''t get me started.'),
|
||||
('frank', 'frank black', 'Named after the man himself. Frank Black is a hero of mine.'),
|
||||
('frank', 'life', 'Life is like a bowl of popcorn - warm, salty and buttery.'),
|
||||
('frank', 'job;work', 'I''m sure you can find work in one of the guest rooms!'),
|
||||
('frank', 'snouthill', 'Snouthill... so many memories.'),
|
||||
('frank', 'wife', 'I had a wife once. She broke my stereo.'),
|
||||
('frank', 'baseball', 'Oh, I used to love to go down to the old ball park and watch Christy Mathewson and Honus Wagner at bat.'),
|
||||
('frank', 'mark', 'I don''t trust Mark.'),
|
||||
('frank', 'vietnam', 'Vietnam? Don''t ask. Worst trip of my life.'),
|
||||
('frank', 'pills;drugs', 'Drugs are bad, mmkay?');
|
||||
|
||||
INSERT IGNORE INTO `bot_serves` (`keys`, `item`) VALUES
|
||||
('sunflower', 1002),
|
||||
('cola;habbo cola', 19),
|
||||
('rose', 1000),
|
||||
('book', 1003),
|
||||
('tea', 27),
|
||||
('coffee', 8),
|
||||
('migraine;headache;pills', 1015),
|
||||
('radioactive liquid;radioactive', 30),
|
||||
('turkey;can of turkey', 70);
|
||||
|
||||
-- VERY IMPORTANT !!!!
|
||||
-- First check if the items_base ID and catalog_items ID is not in use !
|
||||
-- After the SQL please go to the catalog_items table and change the page_id to where your BOTS are located
|
||||
|
||||
INSERT IGNORE INTO `items_base` (`id`, `sprite_id`, `item_name`, `public_name`, `width`, `length`, `stack_height`, `allow_stack`, `allow_sit`, `allow_lay`, `allow_walk`, `allow_gift`, `allow_trade`, `allow_recycle`, `allow_marketplace_sell`, `allow_inventory_stack`, `type`, `interaction_type`, `interaction_modes_count`, `vending_ids`, `multiheight`, `customparams`)
|
||||
VALUES (19001, 0, 'bot_frank', 'Frank', 1, 1, 0.00, '0', '0', '0', '1', '0', '0', '0', '0', '0', 'r', 'default', 1, '0', '0', '0');
|
||||
|
||||
INSERT IGNORE INTO `catalog_items` (`item_ids`, `page_id`, `offer_id`, `catalog_name`, `cost_credits`, `cost_points`, `points_type`, `amount`, `extradata`)
|
||||
VALUES ('19001', 8, 19001, 'Frank', 0, 0, 0, 1, 'name:Frank;motto:Welcome to Habbo!;figure:hr-3499-33.sh-290-90.ch-3971-72-73.lg-270-73.hd-205-1-1.fa-1206-67.ha-3409-73-72;gender:m');
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.eu.habbo</groupId>
|
||||
<artifactId>Habbo</artifactId>
|
||||
<version>4.2.14</version>
|
||||
<version>4.2.24</version>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
@@ -6,6 +6,7 @@ import ch.qos.logback.core.ConsoleAppender;
|
||||
import com.eu.habbo.core.*;
|
||||
import com.eu.habbo.core.consolecommands.ConsoleCommand;
|
||||
import com.eu.habbo.database.Database;
|
||||
import com.eu.habbo.gui.EmulatorDashboard;
|
||||
import com.eu.habbo.habbohotel.GameEnvironment;
|
||||
import com.eu.habbo.habbohotel.gameclients.SessionResumeManager;
|
||||
import com.eu.habbo.networking.gameserver.GameServer;
|
||||
@@ -186,6 +187,10 @@ public final class Emulator {
|
||||
Emulator.isReady = true;
|
||||
Emulator.timeStarted = getIntUnixTimestamp();
|
||||
|
||||
if (Emulator.getConfig().getBoolean("gui.enabled", true)) {
|
||||
EmulatorDashboard.launch();
|
||||
}
|
||||
|
||||
if (Emulator.getConfig().getInt("runtime.threads") < (Runtime.getRuntime().availableProcessors() * 2)) {
|
||||
LOGGER.warn("Emulator settings runtime.threads ({}) can be increased to ({}) to possibly increase performance.",
|
||||
Emulator.getConfig().getInt("runtime.threads"),
|
||||
|
||||
@@ -0,0 +1,631 @@
|
||||
package com.eu.habbo.gui;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.monitoring.EmulatorStatsService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.CompoundBorder;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.MatteBorder;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.table.JTableHeader;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ComponentAdapter;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class EmulatorDashboard extends JFrame {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(EmulatorDashboard.class);
|
||||
|
||||
// Modern Dark Theme Colors
|
||||
private static final Color COLOR_BG = new Color(18, 18, 18);
|
||||
private static final Color COLOR_SURFACE = new Color(30, 30, 30);
|
||||
private static final Color COLOR_SURFACE_HOVER = new Color(45, 45, 45);
|
||||
private static final Color COLOR_PRIMARY = new Color(99, 102, 241);
|
||||
private static final Color COLOR_PRIMARY_SOFT = new Color(99, 102, 241, 45);
|
||||
private static final Color COLOR_SUCCESS = new Color(34, 197, 94);
|
||||
private static final Color COLOR_WARNING = new Color(245, 158, 11);
|
||||
private static final Color COLOR_TEXT = new Color(240, 240, 240);
|
||||
private static final Color COLOR_TEXT_MUTED = new Color(150, 150, 150);
|
||||
private static final Color COLOR_TEXT_SUBTLE = new Color(110, 110, 110);
|
||||
private static final Color COLOR_BORDER = new Color(50, 50, 50);
|
||||
private static final Font FONT_TITLE = new Font("Segoe UI", Font.BOLD, 26);
|
||||
private static final Font FONT_SECTION = new Font("Segoe UI", Font.BOLD, 16);
|
||||
private static final Font FONT_SMALL = new Font("Segoe UI", Font.PLAIN, 12);
|
||||
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
|
||||
|
||||
private static EmulatorDashboard instance;
|
||||
|
||||
// Overview Tab
|
||||
private final JLabel memLabel = createMetricLabel();
|
||||
private final JLabel cpuLabel = createMetricLabel();
|
||||
private final JLabel threadLabel = createMetricLabel();
|
||||
private final JLabel usersLabel = createMetricLabel();
|
||||
private final JLabel roomsLabel = createMetricLabel();
|
||||
private final JLabel wiredLabel = createMetricLabel();
|
||||
private final JLabel uptimeLabel = createStatusValueLabel();
|
||||
private final JLabel lastUpdatedLabel = createStatusValueLabel();
|
||||
private final JLabel footerStatusLabel = createStatusValueLabel();
|
||||
private final MemoryGraphPanel memoryGraph = new MemoryGraphPanel();
|
||||
|
||||
// Tables
|
||||
private final DefaultTableModel usersTableModel;
|
||||
private final DefaultTableModel roomsTableModel;
|
||||
private final DefaultTableModel wiredTableModel;
|
||||
private final JTable usersTable;
|
||||
private final JTable roomsTable;
|
||||
private final JTable wiredTable;
|
||||
private final JLabel usersCountLabel = createCountLabel();
|
||||
private final JLabel roomsCountLabel = createCountLabel();
|
||||
private final JLabel wiredCountLabel = createCountLabel();
|
||||
|
||||
// UI Components
|
||||
private final JPanel cardsPanel;
|
||||
private final CardLayout cardLayout;
|
||||
private final Map<String, JPanel> navButtons = new HashMap<>();
|
||||
private String selectedCardName = "Overview";
|
||||
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "Dashboard-Updater");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
private EmulatorDashboard() {
|
||||
setTitle("Arcturus Morningstar - System Dashboard");
|
||||
setSize(1100, 700);
|
||||
setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
|
||||
setLocationRelativeTo(null);
|
||||
getContentPane().setBackground(COLOR_BG);
|
||||
setLayout(new BorderLayout());
|
||||
|
||||
// Setup custom Look & Feel basics to remove weird Swing borders
|
||||
UIManager.put("ScrollBar.background", COLOR_BG);
|
||||
UIManager.put("ScrollBar.thumb", COLOR_SURFACE_HOVER);
|
||||
|
||||
// Sidebar
|
||||
JPanel sidebar = new JPanel();
|
||||
sidebar.setLayout(new BoxLayout(sidebar, BoxLayout.Y_AXIS));
|
||||
sidebar.setBackground(COLOR_SURFACE);
|
||||
sidebar.setPreferredSize(new Dimension(220, 0));
|
||||
sidebar.setBorder(new MatteBorder(0, 0, 0, 1, COLOR_BORDER));
|
||||
|
||||
// Sidebar Header
|
||||
JPanel brandPanel = new JPanel(new BorderLayout());
|
||||
brandPanel.setBackground(COLOR_SURFACE);
|
||||
brandPanel.setBorder(new EmptyBorder(20, 20, 30, 20));
|
||||
|
||||
JLabel brandTitle = new JLabel("Arcturus");
|
||||
brandTitle.setFont(new Font("Segoe UI", Font.BOLD, 22));
|
||||
brandTitle.setForeground(COLOR_TEXT);
|
||||
|
||||
JLabel brandSub = new JLabel("v" + Emulator.version);
|
||||
brandSub.setFont(new Font("Segoe UI", Font.PLAIN, 12));
|
||||
brandSub.setForeground(COLOR_PRIMARY);
|
||||
|
||||
brandPanel.add(brandTitle, BorderLayout.NORTH);
|
||||
brandPanel.add(brandSub, BorderLayout.SOUTH);
|
||||
sidebar.add(brandPanel);
|
||||
|
||||
// Main Cards
|
||||
cardLayout = new CardLayout();
|
||||
cardsPanel = new JPanel(cardLayout);
|
||||
cardsPanel.setBackground(COLOR_BG);
|
||||
|
||||
// Setup Tabs
|
||||
usersTableModel = createTableModel(new String[]{"ID", "Username", "Rank", "Credits", "Room ID"});
|
||||
roomsTableModel = createTableModel(new String[]{"Room ID", "Name", "Players", "Items", "Tickables", "CPU (ms)", "Est. RAM (KB)", "Thread"});
|
||||
wiredTableModel = createTableModel(new String[]{"Room ID", "Avg Tick", "Peak Tick", "Usage %", "Delayed", "Overloaded?", "Heavy?"});
|
||||
usersTable = createDashboardTable(usersTableModel);
|
||||
roomsTable = createDashboardTable(roomsTableModel);
|
||||
wiredTable = createDashboardTable(wiredTableModel);
|
||||
|
||||
cardsPanel.add(createOverviewTab(), "Overview");
|
||||
cardsPanel.add(createTableTab("Online Users", "Players currently connected to the emulator.", usersTable, usersCountLabel), "Online Users");
|
||||
cardsPanel.add(createTableTab("Active Rooms", "Loaded rooms with lightweight performance indicators.", roomsTable, roomsCountLabel), "Active Rooms");
|
||||
cardsPanel.add(createTableTab("Wired Diagnostics", "Rooms currently using wired timing, delay and execution budget.", wiredTable, wiredCountLabel), "Wired Diagnostics");
|
||||
|
||||
// Sidebar Navigation
|
||||
sidebar.add(createNavButton("Overview", "Overview"));
|
||||
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
|
||||
sidebar.add(createNavButton("Online Users", "Online Users"));
|
||||
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
|
||||
sidebar.add(createNavButton("Active Rooms", "Active Rooms"));
|
||||
sidebar.add(Box.createRigidArea(new Dimension(0, 10)));
|
||||
sidebar.add(createNavButton("Wired Diagnostics", "Wired Diagnostics"));
|
||||
sidebar.add(Box.createVerticalGlue());
|
||||
|
||||
add(sidebar, BorderLayout.WEST);
|
||||
add(cardsPanel, BorderLayout.CENTER);
|
||||
add(createStatusBar(), BorderLayout.SOUTH);
|
||||
|
||||
addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentShown(ComponentEvent e) {
|
||||
setActiveCard("Overview");
|
||||
}
|
||||
});
|
||||
|
||||
// Start updates
|
||||
scheduler.scheduleAtFixedRate(this::updateMetrics, 0, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private DefaultTableModel createTableModel(String[] cols) {
|
||||
return new DefaultTableModel(cols, 0) {
|
||||
@Override public boolean isCellEditable(int row, int column) { return false; }
|
||||
};
|
||||
}
|
||||
|
||||
private JPanel createNavButton(String text, String cardName) {
|
||||
JPanel btn = new JPanel(new BorderLayout());
|
||||
btn.setBackground(COLOR_SURFACE);
|
||||
btn.setMaximumSize(new Dimension(220, 45));
|
||||
btn.setBorder(new EmptyBorder(0, 18, 0, 0));
|
||||
btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
|
||||
navButtons.put(cardName, btn);
|
||||
|
||||
JLabel lbl = new JLabel(text);
|
||||
lbl.setFont(new Font("Segoe UI", Font.BOLD, 14));
|
||||
lbl.setForeground(COLOR_TEXT_MUTED);
|
||||
btn.add(lbl, BorderLayout.CENTER);
|
||||
|
||||
btn.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e) {
|
||||
btn.setBackground(COLOR_SURFACE_HOVER);
|
||||
lbl.setForeground(COLOR_TEXT);
|
||||
}
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
updateNavButtonStyle(cardName, btn, lbl);
|
||||
}
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
setActiveCard(cardName);
|
||||
}
|
||||
});
|
||||
|
||||
updateNavButtonStyle(cardName, btn, lbl);
|
||||
return btn;
|
||||
}
|
||||
|
||||
private JPanel createOverviewTab() {
|
||||
JPanel wrapper = new JPanel(new BorderLayout());
|
||||
wrapper.setBackground(COLOR_BG);
|
||||
wrapper.setBorder(new EmptyBorder(30, 30, 30, 30));
|
||||
|
||||
JPanel header = new JPanel(new BorderLayout(0, 14));
|
||||
header.setOpaque(false);
|
||||
|
||||
JLabel title = new JLabel("Dashboard Overview");
|
||||
title.setFont(FONT_TITLE);
|
||||
title.setForeground(COLOR_TEXT);
|
||||
|
||||
JLabel subtitle = new JLabel("Operational view of emulator health, activity and wired performance.");
|
||||
subtitle.setFont(new Font("Segoe UI", Font.PLAIN, 13));
|
||||
subtitle.setForeground(COLOR_TEXT_MUTED);
|
||||
|
||||
JPanel titleBlock = new JPanel();
|
||||
titleBlock.setOpaque(false);
|
||||
titleBlock.setLayout(new BoxLayout(titleBlock, BoxLayout.Y_AXIS));
|
||||
titleBlock.add(title);
|
||||
titleBlock.add(Box.createRigidArea(new Dimension(0, 4)));
|
||||
titleBlock.add(subtitle);
|
||||
|
||||
header.add(titleBlock, BorderLayout.NORTH);
|
||||
header.add(createOverviewMetaPanel(), BorderLayout.SOUTH);
|
||||
wrapper.add(header, BorderLayout.NORTH);
|
||||
|
||||
JPanel content = new JPanel(new GridLayout(1, 2, 20, 20));
|
||||
content.setOpaque(false);
|
||||
content.setBorder(new EmptyBorder(20, 0, 0, 0));
|
||||
|
||||
// Left Stats
|
||||
JPanel statsPanel = new JPanel(new GridLayout(3, 2, 12, 12));
|
||||
statsPanel.setOpaque(false);
|
||||
statsPanel.add(createMetricCard("Memory Allocation", memLabel));
|
||||
statsPanel.add(createMetricCard("CPU Load", cpuLabel));
|
||||
statsPanel.add(createMetricCard("Active OS Threads", threadLabel));
|
||||
statsPanel.add(createMetricCard("Connected Players", usersLabel));
|
||||
statsPanel.add(createMetricCard("Loaded Rooms", roomsLabel));
|
||||
statsPanel.add(createMetricCard("Wired Tickables", wiredLabel));
|
||||
content.add(statsPanel);
|
||||
|
||||
// Right Graph
|
||||
JPanel graphContainer = new JPanel(new BorderLayout());
|
||||
graphContainer.setBackground(COLOR_SURFACE);
|
||||
graphContainer.setBorder(BorderFactory.createCompoundBorder(
|
||||
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
|
||||
new EmptyBorder(15, 15, 15, 15)
|
||||
));
|
||||
|
||||
JLabel gTitle = new JLabel("Realtime Memory Usage");
|
||||
gTitle.setFont(FONT_SECTION);
|
||||
gTitle.setForeground(COLOR_TEXT_MUTED);
|
||||
gTitle.setBorder(new EmptyBorder(0, 0, 15, 0));
|
||||
graphContainer.add(gTitle, BorderLayout.NORTH);
|
||||
graphContainer.add(memoryGraph, BorderLayout.CENTER);
|
||||
content.add(graphContainer);
|
||||
|
||||
wrapper.add(content, BorderLayout.CENTER);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private JPanel createMetricCard(String title, JLabel valueLabel) {
|
||||
JPanel card = new JPanel(new BorderLayout());
|
||||
card.setBackground(COLOR_SURFACE);
|
||||
card.setBorder(BorderFactory.createCompoundBorder(
|
||||
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
|
||||
new EmptyBorder(15, 20, 15, 20)
|
||||
));
|
||||
|
||||
JLabel tLabel = new JLabel(title);
|
||||
tLabel.setFont(new Font("Segoe UI", Font.BOLD, 13));
|
||||
tLabel.setForeground(COLOR_TEXT_MUTED);
|
||||
|
||||
card.add(tLabel, BorderLayout.NORTH);
|
||||
card.add(valueLabel, BorderLayout.SOUTH);
|
||||
return card;
|
||||
}
|
||||
|
||||
private JLabel createMetricLabel() {
|
||||
JLabel label = new JLabel("-");
|
||||
label.setFont(new Font("Segoe UI", Font.BOLD, 28));
|
||||
label.setForeground(COLOR_TEXT);
|
||||
return label;
|
||||
}
|
||||
|
||||
private JPanel createOverviewMetaPanel() {
|
||||
JPanel panel = new JPanel(new GridLayout(1, 3, 12, 12));
|
||||
panel.setOpaque(false);
|
||||
panel.add(createStatusCard("Uptime", uptimeLabel, COLOR_PRIMARY));
|
||||
panel.add(createStatusCard("Last Refresh", lastUpdatedLabel, COLOR_SUCCESS));
|
||||
panel.add(createStatusCard("GUI Status", footerStatusLabel, COLOR_WARNING));
|
||||
return panel;
|
||||
}
|
||||
|
||||
private JPanel createStatusCard(String title, JLabel valueLabel, Color accent) {
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
panel.setBackground(COLOR_SURFACE);
|
||||
panel.setBorder(BorderFactory.createCompoundBorder(
|
||||
new MatteBorder(1, 1, 1, 1, COLOR_BORDER),
|
||||
new EmptyBorder(12, 14, 12, 14)
|
||||
));
|
||||
|
||||
JPanel accentBar = new JPanel();
|
||||
accentBar.setBackground(accent);
|
||||
accentBar.setPreferredSize(new Dimension(6, 0));
|
||||
|
||||
JPanel content = new JPanel();
|
||||
content.setOpaque(false);
|
||||
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
|
||||
|
||||
JLabel label = new JLabel(title);
|
||||
label.setFont(FONT_SMALL);
|
||||
label.setForeground(COLOR_TEXT_MUTED);
|
||||
|
||||
content.add(label);
|
||||
content.add(Box.createRigidArea(new Dimension(0, 6)));
|
||||
content.add(valueLabel);
|
||||
|
||||
panel.add(accentBar, BorderLayout.WEST);
|
||||
panel.add(content, BorderLayout.CENTER);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private JLabel createStatusValueLabel() {
|
||||
JLabel label = new JLabel("-");
|
||||
label.setFont(new Font("Segoe UI", Font.BOLD, 16));
|
||||
label.setForeground(COLOR_TEXT);
|
||||
return label;
|
||||
}
|
||||
|
||||
private JLabel createCountLabel() {
|
||||
JLabel label = new JLabel("0 rows");
|
||||
label.setFont(FONT_SMALL);
|
||||
label.setForeground(COLOR_TEXT_MUTED);
|
||||
return label;
|
||||
}
|
||||
|
||||
private JTable createDashboardTable(DefaultTableModel model) {
|
||||
JTable table = new JTable(model);
|
||||
table.setBackground(COLOR_SURFACE);
|
||||
table.setForeground(COLOR_TEXT);
|
||||
table.setGridColor(COLOR_BORDER);
|
||||
table.setRowHeight(34);
|
||||
table.setFont(new Font("Segoe UI", Font.PLAIN, 13));
|
||||
table.setFillsViewportHeight(true);
|
||||
table.setSelectionBackground(COLOR_PRIMARY);
|
||||
table.setSelectionForeground(Color.WHITE);
|
||||
table.setShowVerticalLines(false);
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.setAutoCreateRowSorter(true);
|
||||
|
||||
JTableHeader header = table.getTableHeader();
|
||||
header.setBackground(new Color(22, 22, 22));
|
||||
header.setForeground(COLOR_TEXT_MUTED);
|
||||
header.setFont(new Font("Segoe UI", Font.BOLD, 13));
|
||||
header.setPreferredSize(new Dimension(0, 38));
|
||||
header.setBorder(BorderFactory.createMatteBorder(1, 0, 1, 0, COLOR_BORDER));
|
||||
((DefaultTableCellRenderer) header.getDefaultRenderer()).setHorizontalAlignment(JLabel.LEFT);
|
||||
((DefaultTableCellRenderer) header.getDefaultRenderer()).setBorder(new EmptyBorder(0, 10, 0, 0));
|
||||
|
||||
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||
label.setBorder(new EmptyBorder(0, 10, 0, 10));
|
||||
label.setForeground(isSelected ? Color.WHITE : COLOR_TEXT);
|
||||
label.setBackground(isSelected ? COLOR_PRIMARY : ((row % 2 == 0) ? COLOR_SURFACE : new Color(35, 35, 35)));
|
||||
return label;
|
||||
}
|
||||
});
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
private JPanel createTableTab(String title, String subtitle, JTable table, JLabel countLabel) {
|
||||
JPanel wrapper = new JPanel(new BorderLayout());
|
||||
wrapper.setBackground(COLOR_BG);
|
||||
wrapper.setBorder(new EmptyBorder(30, 30, 30, 30));
|
||||
|
||||
JPanel titlePanel = new JPanel(new BorderLayout());
|
||||
titlePanel.setOpaque(false);
|
||||
|
||||
JLabel titleLbl = new JLabel(title);
|
||||
titleLbl.setFont(FONT_TITLE);
|
||||
titleLbl.setForeground(COLOR_TEXT);
|
||||
|
||||
JLabel subtitleLbl = new JLabel(subtitle);
|
||||
subtitleLbl.setFont(new Font("Segoe UI", Font.PLAIN, 13));
|
||||
subtitleLbl.setForeground(COLOR_TEXT_MUTED);
|
||||
subtitleLbl.setBorder(new EmptyBorder(6, 0, 0, 0));
|
||||
|
||||
JPanel textPanel = new JPanel();
|
||||
textPanel.setOpaque(false);
|
||||
textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.Y_AXIS));
|
||||
textPanel.add(titleLbl);
|
||||
textPanel.add(subtitleLbl);
|
||||
|
||||
titlePanel.add(textPanel, BorderLayout.WEST);
|
||||
titlePanel.add(countLabel, BorderLayout.EAST);
|
||||
wrapper.add(titlePanel, BorderLayout.NORTH);
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(table);
|
||||
scrollPane.getViewport().setBackground(COLOR_SURFACE);
|
||||
scrollPane.setBorder(new MatteBorder(1, 1, 1, 1, COLOR_BORDER));
|
||||
scrollPane.setBorder(new CompoundBorder(
|
||||
new EmptyBorder(20, 0, 0, 0),
|
||||
new MatteBorder(1, 1, 1, 1, COLOR_BORDER)
|
||||
));
|
||||
wrapper.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private JPanel createStatusBar() {
|
||||
JPanel statusBar = new JPanel(new BorderLayout());
|
||||
statusBar.setBackground(COLOR_SURFACE);
|
||||
statusBar.setBorder(new CompoundBorder(
|
||||
new MatteBorder(1, 0, 0, 0, COLOR_BORDER),
|
||||
new EmptyBorder(8, 14, 8, 14)
|
||||
));
|
||||
|
||||
JLabel left = new JLabel("Dashboard running locally");
|
||||
left.setFont(FONT_SMALL);
|
||||
left.setForeground(COLOR_TEXT_SUBTLE);
|
||||
|
||||
JLabel right = new JLabel("Tip: table columns are sortable");
|
||||
right.setFont(FONT_SMALL);
|
||||
right.setForeground(COLOR_TEXT_SUBTLE);
|
||||
|
||||
statusBar.add(left, BorderLayout.WEST);
|
||||
statusBar.add(right, BorderLayout.EAST);
|
||||
return statusBar;
|
||||
}
|
||||
|
||||
private void setActiveCard(String cardName) {
|
||||
selectedCardName = cardName;
|
||||
cardLayout.show(cardsPanel, cardName);
|
||||
navButtons.forEach((name, button) -> {
|
||||
JLabel label = (JLabel) button.getComponent(0);
|
||||
updateNavButtonStyle(name, button, label);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateNavButtonStyle(String cardName, JPanel button, JLabel label) {
|
||||
boolean active = cardName.equals(selectedCardName);
|
||||
button.setBackground(active ? COLOR_PRIMARY_SOFT : COLOR_SURFACE);
|
||||
label.setForeground(active ? COLOR_TEXT : COLOR_TEXT_MUTED);
|
||||
}
|
||||
|
||||
private void updateMetrics() {
|
||||
try {
|
||||
EmulatorStatsService.Snapshot snapshot = EmulatorStatsService.collectSnapshot();
|
||||
EmulatorStatsService.Overview overview = snapshot.overview;
|
||||
|
||||
Object[][] usersData = new Object[snapshot.users.size()][5];
|
||||
for (int i = 0; i < snapshot.users.size(); i++) {
|
||||
EmulatorStatsService.OnlineUserRow user = snapshot.users.get(i);
|
||||
usersData[i] = new Object[]{user.id, user.username, user.rank, user.credits, user.roomId};
|
||||
}
|
||||
|
||||
Object[][] roomsData = new Object[snapshot.rooms.size()][8];
|
||||
for (int i = 0; i < snapshot.rooms.size(); i++) {
|
||||
EmulatorStatsService.ActiveRoomRow room = snapshot.rooms.get(i);
|
||||
roomsData[i] = new Object[]{
|
||||
room.roomId,
|
||||
room.name,
|
||||
room.players,
|
||||
room.items,
|
||||
room.tickables,
|
||||
String.format("%.2f", room.cpuMs),
|
||||
room.estimatedRamKb,
|
||||
room.thread
|
||||
};
|
||||
}
|
||||
|
||||
Object[][] wiredData = new Object[snapshot.wired.size()][7];
|
||||
for (int i = 0; i < snapshot.wired.size(); i++) {
|
||||
EmulatorStatsService.WiredRoomRow wiredRoom = snapshot.wired.get(i);
|
||||
wiredData[i] = new Object[]{
|
||||
wiredRoom.roomId,
|
||||
wiredRoom.averageTickMs + " ms",
|
||||
wiredRoom.peakTickMs + " ms",
|
||||
wiredRoom.usagePercent + "%",
|
||||
wiredRoom.delayedEventsPending,
|
||||
wiredRoom.overloaded ? "YES" : "NO",
|
||||
wiredRoom.heavy ? "YES" : "NO"
|
||||
};
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
memLabel.setText(String.format("%d MB / %d MB", overview.memoryUsedMb, overview.memoryMaxMb));
|
||||
cpuLabel.setText(String.format("%.1f %%", overview.cpuLoadPercent));
|
||||
threadLabel.setText(String.valueOf(overview.activeOsThreads));
|
||||
usersLabel.setText(String.valueOf(overview.connectedPlayers));
|
||||
roomsLabel.setText(String.valueOf(overview.loadedRooms));
|
||||
wiredLabel.setText(String.valueOf(overview.wiredTickables));
|
||||
uptimeLabel.setText(EmulatorStatsService.formatDuration(overview.uptimeSeconds));
|
||||
lastUpdatedLabel.setText(LocalDateTime.now().format(TIME_FORMAT));
|
||||
footerStatusLabel.setText(overview.guiStatus);
|
||||
memoryGraph.addValue((long) overview.memoryUsedMb * 1024L * 1024L, (long) overview.memoryMaxMb * 1024L * 1024L);
|
||||
|
||||
usersTableModel.setDataVector(usersData, new String[]{"ID", "Username", "Rank", "Credits", "Room ID"});
|
||||
roomsTableModel.setDataVector(roomsData, new String[]{"Room ID", "Name", "Players", "Items", "Tickables", "CPU (ms)", "Est. RAM (KB)", "Thread"});
|
||||
wiredTableModel.setDataVector(wiredData, new String[]{"Room ID", "Avg Tick", "Peak Tick", "Usage %", "Delayed", "Overloaded?", "Heavy?"});
|
||||
usersCountLabel.setText(snapshot.users.size() + " rows");
|
||||
roomsCountLabel.setText(snapshot.rooms.size() + " rows");
|
||||
wiredCountLabel.setText(snapshot.wired.size() + " rows");
|
||||
});
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error updating dashboard metrics", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void launch() {
|
||||
if (instance == null) {
|
||||
instance = new EmulatorDashboard();
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
instance.setVisible(true);
|
||||
});
|
||||
}
|
||||
|
||||
private static class MemoryGraphPanel extends JPanel {
|
||||
private final LinkedList<Double> history = new LinkedList<>();
|
||||
private static final int MAX_POINTS = 100;
|
||||
|
||||
public MemoryGraphPanel() {
|
||||
setOpaque(false);
|
||||
}
|
||||
|
||||
public void addValue(long used, long max) {
|
||||
double percent = (double) used / (double) max;
|
||||
history.addLast(percent);
|
||||
if (history.size() > MAX_POINTS) {
|
||||
history.removeFirst();
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
Graphics2D g2 = (Graphics2D) g;
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
|
||||
// Background grid and labels
|
||||
g2.setFont(new Font("Segoe UI", Font.PLAIN, 10));
|
||||
long maxMemRaw = Runtime.getRuntime().maxMemory();
|
||||
|
||||
for(int i = 0; i <= 4; i++) {
|
||||
int y = i == 0 ? 15 : height * i / 4;
|
||||
if (i == 4) y = height - 5;
|
||||
|
||||
g2.setColor(COLOR_BORDER);
|
||||
g2.drawLine(0, y, width, y);
|
||||
|
||||
// Draw Y-axis numbers
|
||||
g2.setColor(COLOR_TEXT_MUTED);
|
||||
long labelVal = (long) (maxMemRaw * (1.0 - (double)i / 4.0)) / 1024 / 1024;
|
||||
g2.drawString(labelVal + " MB", 5, y - 5);
|
||||
}
|
||||
|
||||
if (history.size() < 2) return;
|
||||
|
||||
double dx = (double) width / (MAX_POINTS - 1);
|
||||
Path2D path = new Path2D.Double();
|
||||
path.moveTo(0, height);
|
||||
|
||||
int i = MAX_POINTS - history.size();
|
||||
for (Double val : history) {
|
||||
double x = i * dx;
|
||||
double y = height - (val * height);
|
||||
if (i == MAX_POINTS - history.size()) {
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// Draw line
|
||||
g2.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||
g2.setColor(COLOR_PRIMARY);
|
||||
g2.draw(path);
|
||||
|
||||
// Fill area
|
||||
path.lineTo(width, height);
|
||||
path.lineTo((MAX_POINTS - history.size()) * dx, height);
|
||||
path.closePath();
|
||||
|
||||
GradientPaint fillPaint = new GradientPaint(
|
||||
0, 0, new Color(COLOR_PRIMARY.getRed(), COLOR_PRIMARY.getGreen(), COLOR_PRIMARY.getBlue(), 120),
|
||||
0, height, new Color(COLOR_PRIMARY.getRed(), COLOR_PRIMARY.getGreen(), COLOR_PRIMARY.getBlue(), 10)
|
||||
);
|
||||
g2.setPaint(fillPaint);
|
||||
g2.fill(path);
|
||||
|
||||
Double lastValue = history.peekLast();
|
||||
if (lastValue != null) {
|
||||
String usageLabel = String.format("Usage %.1f%%", lastValue * 100.0);
|
||||
g2.setFont(new Font("Segoe UI", Font.BOLD, 12));
|
||||
FontMetrics metrics = g2.getFontMetrics();
|
||||
int labelWidth = metrics.stringWidth(usageLabel) + 16;
|
||||
int labelHeight = 24;
|
||||
int labelX = Math.max(8, width - labelWidth - 8);
|
||||
int labelY = 8;
|
||||
|
||||
g2.setColor(new Color(0, 0, 0, 130));
|
||||
g2.fillRoundRect(labelX, labelY, labelWidth, labelHeight, 12, 12);
|
||||
g2.setColor(COLOR_TEXT);
|
||||
g2.drawString(usageLabel, labelX + 8, labelY + 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatDuration(long millis) {
|
||||
long totalSeconds = Math.max(0L, millis / 1000L);
|
||||
long hours = totalSeconds / 3600L;
|
||||
long minutes = (totalSeconds % 3600L) / 60L;
|
||||
long seconds = totalSeconds % 60L;
|
||||
return String.format("%02dh %02dm %02ds", hours, minutes, seconds);
|
||||
}
|
||||
}
|
||||
@@ -138,18 +138,20 @@ public class Bot implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
if (this.needsUpdate) {
|
||||
Room localRoom = this.room;
|
||||
RoomUnit localRoomUnit = this.roomUnit;
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE bots SET name = ?, motto = ?, figure = ?, gender = ?, user_id = ?, room_id = ?, x = ?, y = ?, z = ?, rot = ?, dance = ?, freeroam = ?, chat_lines = ?, chat_auto = ?, chat_random = ?, chat_delay = ?, effect = ?, bubble_id = ? WHERE id = ?")) {
|
||||
statement.setString(1, this.name);
|
||||
statement.setString(2, this.motto);
|
||||
statement.setString(3, this.figure);
|
||||
statement.setString(4, this.gender.toString());
|
||||
statement.setInt(5, this.ownerId);
|
||||
statement.setInt(6, this.room == null ? 0 : this.room.getId());
|
||||
statement.setInt(7, this.roomUnit == null ? 0 : this.roomUnit.getX());
|
||||
statement.setInt(8, this.roomUnit == null ? 0 : this.roomUnit.getY());
|
||||
statement.setDouble(9, this.roomUnit == null ? 0 : this.roomUnit.getZ());
|
||||
statement.setInt(10, this.roomUnit == null ? 0 : this.roomUnit.getBodyRotation().getValue());
|
||||
statement.setInt(11, this.roomUnit == null ? 0 : this.roomUnit.getDanceType().getType());
|
||||
statement.setInt(6, localRoom == null ? 0 : localRoom.getId());
|
||||
statement.setInt(7, localRoomUnit == null ? 0 : localRoomUnit.getX());
|
||||
statement.setInt(8, localRoomUnit == null ? 0 : localRoomUnit.getY());
|
||||
statement.setDouble(9, localRoomUnit == null ? 0 : localRoomUnit.getZ());
|
||||
statement.setInt(10, localRoomUnit == null ? 0 : localRoomUnit.getBodyRotation().getValue());
|
||||
statement.setInt(11, localRoomUnit == null ? 0 : localRoomUnit.getDanceType().getType());
|
||||
statement.setString(12, this.canWalk ? "1" : "0");
|
||||
StringBuilder text = new StringBuilder();
|
||||
for (String s : this.chatLines) {
|
||||
@@ -282,7 +284,7 @@ public class Bot implements Runnable {
|
||||
}
|
||||
|
||||
public void onPickUp(Habbo habbo, Room room) {
|
||||
|
||||
this.stopFollowingHabbo();
|
||||
}
|
||||
|
||||
public void onUserSay(final RoomChatMessage message) {
|
||||
@@ -537,5 +539,9 @@ public class Bot implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private static final short[] DEFAULT_OWNER_ACTION_IDS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
|
||||
|
||||
public short[] getOwnerActionIds() {
|
||||
return DEFAULT_OWNER_ACTION_IDS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ public class BotManager {
|
||||
addBotDefinition("generic", Bot.class);
|
||||
addBotDefinition("bartender", ButlerBot.class);
|
||||
addBotDefinition("visitor_log", VisitorBot.class);
|
||||
addBotDefinition(FrankBot.BOT_TYPE, FrankBot.class);
|
||||
|
||||
this.reload();
|
||||
|
||||
|
||||
@@ -20,10 +20,13 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ButlerBot extends Bot {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ButlerBot.class);
|
||||
public static THashMap<THashSet<String>, Integer> serveItems = new THashMap<>();
|
||||
private static final ConcurrentHashMap<Pattern, Integer> serveItemsCompiled = new ConcurrentHashMap<>();
|
||||
|
||||
public ButlerBot(ResultSet set) throws SQLException {
|
||||
super(set);
|
||||
@@ -38,6 +41,7 @@ public class ButlerBot extends Bot {
|
||||
serveItems = new THashMap<>();
|
||||
|
||||
serveItems.clear();
|
||||
serveItemsCompiled.clear();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM bot_serves")) {
|
||||
while (set.next()) {
|
||||
@@ -45,6 +49,17 @@ public class ButlerBot extends Bot {
|
||||
THashSet<String> ks = new THashSet<>();
|
||||
Collections.addAll(ks, keys);
|
||||
serveItems.put(ks, set.getInt("item"));
|
||||
|
||||
for (String key : keys) {
|
||||
if (key != null && !key.trim().isEmpty()) {
|
||||
try {
|
||||
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(key.toLowerCase()) + "\\b");
|
||||
serveItemsCompiled.put(pattern, set.getInt("item"));
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to compile butler bot keyword pattern: {}", key, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
@@ -53,6 +68,7 @@ public class ButlerBot extends Bot {
|
||||
|
||||
public static void dispose() {
|
||||
serveItems.clear();
|
||||
serveItemsCompiled.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,74 +82,73 @@ public class ButlerBot extends Bot {
|
||||
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.commanddistance")) {
|
||||
|
||||
if (message.getUnfilteredMessage() != null) {
|
||||
for (Map.Entry<THashSet<String>, Integer> set : serveItems.entrySet()) {
|
||||
for (String keyword : set.getKey()) {
|
||||
String unfilteredLower = message.getUnfilteredMessage().toLowerCase();
|
||||
for (Map.Entry<Pattern, Integer> entry : serveItemsCompiled.entrySet()) {
|
||||
Pattern pattern = entry.getKey();
|
||||
if (pattern.matcher(unfilteredLower).matches()) {
|
||||
int itemId = entry.getValue();
|
||||
String keyword = pattern.pattern().replace("\\b", "").replace("\\Q", "").replace("\\E", "");
|
||||
|
||||
// Check if the string contains a certain keyword using a regex.
|
||||
// If keyword = tea, teapot wouldn't trigger it.
|
||||
if (message.getUnfilteredMessage().toLowerCase().matches("\\b" + keyword + "\\b")) {
|
||||
|
||||
// Enable plugins to cancel this event
|
||||
BotServerItemEvent serveEvent = new BotServerItemEvent(this, message.getHabbo(), set.getValue());
|
||||
if (Emulator.getPluginManager().fireEvent(serveEvent).isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start give handitem process
|
||||
if (this.getRoomUnit().canWalk()) {
|
||||
final String key = keyword;
|
||||
final Bot bot = this;
|
||||
|
||||
// Step 1: Look at Habbo
|
||||
bot.lookAt(serveEvent.habbo);
|
||||
|
||||
// Step 2: Prepare tasks for when the Bot (carrying the handitem) reaches the Habbo
|
||||
final List<Runnable> tasks = new ArrayList<>();
|
||||
tasks.add(new RoomUnitGiveHanditem(serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
|
||||
tasks.add(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), 0));
|
||||
|
||||
tasks.add(() -> {
|
||||
if(this.getRoom() != null) {
|
||||
String botMessage = Emulator.getTexts()
|
||||
.getValue("bots.butler.given")
|
||||
.replace("%key%", key)
|
||||
.replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
|
||||
|
||||
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), botMessage)) {
|
||||
bot.talk(botMessage);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
List<Runnable> failedReached = new ArrayList<>();
|
||||
failedReached.add(() -> {
|
||||
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.servedistance", 8)) {
|
||||
for (Runnable task : tasks) {
|
||||
task.run();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Give bot the handitem that it's going to give the Habbo
|
||||
Emulator.getThreading().run(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
|
||||
|
||||
if (distanceBetweenBotAndHabbo > Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)) {
|
||||
Emulator.getThreading().run(new RoomUnitWalkToRoomUnit(this.getRoomUnit(), serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), tasks, failedReached, Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)));
|
||||
} else {
|
||||
Emulator.getThreading().run(failedReached.get(0), 1000);
|
||||
}
|
||||
} else {
|
||||
if(this.getRoom() != null) {
|
||||
this.getRoom().giveHandItem(serveEvent.habbo, serveEvent.itemId);
|
||||
|
||||
String msg = Emulator.getTexts().getValue("bots.butler.given").replace("%key%", keyword).replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
|
||||
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), msg)) {
|
||||
this.talk(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Enable plugins to cancel this event
|
||||
BotServerItemEvent serveEvent = new BotServerItemEvent(this, message.getHabbo(), itemId);
|
||||
if (Emulator.getPluginManager().fireEvent(serveEvent).isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start give handitem process
|
||||
if (this.getRoomUnit().canWalk()) {
|
||||
final String key = keyword;
|
||||
final Bot bot = this;
|
||||
|
||||
// Step 1: Look at Habbo
|
||||
bot.lookAt(serveEvent.habbo);
|
||||
|
||||
// Step 2: Prepare tasks for when the Bot (carrying the handitem) reaches the Habbo
|
||||
final List<Runnable> tasks = new ArrayList<>();
|
||||
tasks.add(new RoomUnitGiveHanditem(serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
|
||||
tasks.add(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), 0));
|
||||
|
||||
tasks.add(() -> {
|
||||
if(this.getRoom() != null) {
|
||||
String botMessage = Emulator.getTexts()
|
||||
.getValue("bots.butler.given")
|
||||
.replace("%key%", key)
|
||||
.replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
|
||||
|
||||
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), botMessage)) {
|
||||
bot.talk(botMessage);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
List<Runnable> failedReached = new ArrayList<>();
|
||||
failedReached.add(() -> {
|
||||
if (distanceBetweenBotAndHabbo <= Emulator.getConfig().getInt("hotel.bot.butler.servedistance", 8)) {
|
||||
for (Runnable task : tasks) {
|
||||
task.run();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Give bot the handitem that it's going to give the Habbo
|
||||
Emulator.getThreading().run(new RoomUnitGiveHanditem(this.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), serveEvent.itemId));
|
||||
|
||||
if (distanceBetweenBotAndHabbo > Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)) {
|
||||
Emulator.getThreading().run(new RoomUnitWalkToRoomUnit(this.getRoomUnit(), serveEvent.habbo.getRoomUnit(), serveEvent.habbo.getHabboInfo().getCurrentRoom(), tasks, failedReached, Emulator.getConfig().getInt("hotel.bot.butler.reachdistance", 3)));
|
||||
} else {
|
||||
Emulator.getThreading().run(failedReached.get(0), 1000);
|
||||
}
|
||||
} else {
|
||||
if(this.getRoom() != null) {
|
||||
this.getRoom().giveHandItem(serveEvent.habbo, serveEvent.itemId);
|
||||
|
||||
String msg = Emulator.getTexts().getValue("bots.butler.given").replace("%key%", keyword).replace("%username%", serveEvent.habbo.getHabboInfo().getUsername());
|
||||
if (!WiredManager.triggerUserSays(this.getRoom(), this.getRoomUnit(), msg)) {
|
||||
this.talk(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
package com.eu.habbo.habbohotel.bots;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.rooms.*;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer;
|
||||
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserWhisperComposer;
|
||||
import com.eu.habbo.threading.runnables.RoomUnitWalkToLocation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class FrankBot extends ButlerBot {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FrankBot.class);
|
||||
|
||||
public static final String BOT_TYPE = "frank";
|
||||
public static final String PERMISSION_USE = "acc_bot_frank";
|
||||
private static final String KEY_DOOR_LINES = "__door_lines";
|
||||
private static final String KEY_BUSY_WHISPER = "__busy_whisper";
|
||||
private static final String KEY_DOOR_TRIGGERS = "__door_triggers";
|
||||
private static final List<String> DEFAULT_DOOR_LINES = List.of(
|
||||
"Right this way - mind the step!",
|
||||
"And out you go. Come back soon!",
|
||||
"Allow me to escort you to the exit.",
|
||||
"There's the door. Farewell, true believer!"
|
||||
);
|
||||
private static final String DEFAULT_BUSY_WHISPER =
|
||||
"Sorry, I am currently busy. Please wait until I am available.";
|
||||
private static final Pattern DEFAULT_DOOR_PATTERN = Pattern.compile(
|
||||
"\\b(show me the door|kick me|i want to leave|let me out)\\b");
|
||||
|
||||
private static final ConcurrentHashMap<Pattern, List<String>> chatResponses = new ConcurrentHashMap<>();
|
||||
private static volatile List<String> doorLines = DEFAULT_DOOR_LINES;
|
||||
private static volatile String busyWhisper = DEFAULT_BUSY_WHISPER;
|
||||
private static volatile Pattern doorTriggerPattern = DEFAULT_DOOR_PATTERN;
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private static final int MAX_CHAT_KEYWORDS = 256;
|
||||
private static final int MAX_DOOR_TRIGGERS = 32;
|
||||
private static final int MAX_MESSAGE_LEN = 256;
|
||||
private static final long BUSY_WHISPER_COOLDOWN_MS = 5000L;
|
||||
|
||||
private volatile RoomTile homeTile;
|
||||
private volatile RoomUserRotation homeRotation;
|
||||
private final AtomicBoolean busy = new AtomicBoolean(false);
|
||||
private final AtomicBoolean returnScheduled = new AtomicBoolean(false);
|
||||
private final ConcurrentHashMap<Integer, Long> lastBusyWhisperAt = new ConcurrentHashMap<>();
|
||||
|
||||
public FrankBot(ResultSet set) throws SQLException {
|
||||
super(set);
|
||||
}
|
||||
|
||||
public FrankBot(Bot bot) {
|
||||
super(bot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlace(Habbo habbo, Room room) {
|
||||
super.onPlace(habbo, room);
|
||||
if (this.getRoomUnit() != null) {
|
||||
this.homeTile = this.getRoomUnit().getCurrentLocation();
|
||||
this.homeRotation = this.getRoomUnit().getBodyRotation();
|
||||
}
|
||||
}
|
||||
|
||||
private static final short[] FRANK_OWNER_ACTIONS = new short[0];
|
||||
|
||||
@Override
|
||||
public short[] getOwnerActionIds() {
|
||||
return FRANK_OWNER_ACTIONS;
|
||||
}
|
||||
|
||||
public static void initialise() {
|
||||
chatResponses.clear();
|
||||
doorLines = DEFAULT_DOOR_LINES;
|
||||
busyWhisper = DEFAULT_BUSY_WHISPER;
|
||||
doorTriggerPattern = DEFAULT_DOOR_PATTERN;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
Statement statement = connection.createStatement();
|
||||
ResultSet set = statement.executeQuery("SELECT `keys`, `responses` FROM bot_chat_responses WHERE bot_type = '" + BOT_TYPE + "'")) {
|
||||
while (set.next()) {
|
||||
String keysRaw = set.getString("keys");
|
||||
String responsesRaw = set.getString("responses");
|
||||
|
||||
if (keysRaw == null || responsesRaw == null) continue;
|
||||
|
||||
List<String> responses = new ArrayList<>();
|
||||
for (String line : responsesRaw.split("\n")) {
|
||||
String trimmed = line.trim();
|
||||
if (!trimmed.isEmpty()) responses.add(trimmed);
|
||||
}
|
||||
|
||||
if (responses.isEmpty()) continue;
|
||||
|
||||
String firstKey = keysRaw.split(";", 2)[0].trim();
|
||||
if (firstKey.startsWith("__")) {
|
||||
switch (firstKey) {
|
||||
case KEY_DOOR_LINES:
|
||||
doorLines = new CopyOnWriteArrayList<>(responses);
|
||||
break;
|
||||
case KEY_BUSY_WHISPER:
|
||||
busyWhisper = responses.get(0);
|
||||
break;
|
||||
case KEY_DOOR_TRIGGERS:
|
||||
doorTriggerPattern = buildDoorTriggerPattern(responses);
|
||||
break;
|
||||
default:
|
||||
LOGGER.warn("FrankBot: unknown system key '{}', ignored", firstKey);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
List<String> shared = new CopyOnWriteArrayList<>(responses);
|
||||
|
||||
for (String key : keysRaw.split(";")) {
|
||||
if (chatResponses.size() >= MAX_CHAT_KEYWORDS) {
|
||||
LOGGER.warn("FrankBot: chat keyword cap ({}) reached, remaining rows ignored",
|
||||
MAX_CHAT_KEYWORDS);
|
||||
break;
|
||||
}
|
||||
String k = key == null ? "" : key.trim().toLowerCase();
|
||||
if (k.isEmpty()) continue;
|
||||
try {
|
||||
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(k) + "\\b");
|
||||
chatResponses.put(pattern, shared);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to compile Frank chat keyword pattern: {}", k, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.warn("FrankBot: could not load bot_chat_responses ({}). Frank will still serve items.", e.getMessage());
|
||||
}
|
||||
|
||||
ButlerBot.initialise();
|
||||
}
|
||||
|
||||
public static void dispose() {
|
||||
chatResponses.clear();
|
||||
doorLines = DEFAULT_DOOR_LINES;
|
||||
busyWhisper = DEFAULT_BUSY_WHISPER;
|
||||
doorTriggerPattern = DEFAULT_DOOR_PATTERN;
|
||||
ButlerBot.dispose();
|
||||
}
|
||||
|
||||
private static Pattern buildDoorTriggerPattern(List<String> triggers) {
|
||||
StringBuilder sb = new StringBuilder("\\b(");
|
||||
boolean first = true;
|
||||
int count = 0;
|
||||
for (String trigger : triggers) {
|
||||
if (count >= MAX_DOOR_TRIGGERS) {
|
||||
LOGGER.warn("FrankBot: door trigger cap ({}) reached, extra entries ignored",
|
||||
MAX_DOOR_TRIGGERS);
|
||||
break;
|
||||
}
|
||||
String t = trigger == null ? "" : trigger.trim().toLowerCase();
|
||||
if (t.isEmpty()) continue;
|
||||
if (!first) sb.append('|');
|
||||
sb.append(Pattern.quote(t));
|
||||
first = false;
|
||||
count++;
|
||||
}
|
||||
sb.append(")\\b");
|
||||
|
||||
if (first) return DEFAULT_DOOR_PATTERN;
|
||||
|
||||
try {
|
||||
return Pattern.compile(sb.toString());
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("FrankBot: failed to compile door trigger pattern from {}, falling back to default", triggers, e);
|
||||
return DEFAULT_DOOR_PATTERN;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserSay(final RoomChatMessage message) {
|
||||
Room currentRoom = this.getRoom();
|
||||
if (currentRoom == null) return;
|
||||
|
||||
Habbo asker = message.getHabbo();
|
||||
if (asker == null || asker.getClient() == null) return;
|
||||
|
||||
if (this.getRoomUnit() == null) return;
|
||||
|
||||
String raw = message.getUnfilteredMessage();
|
||||
if (raw != null && raw.length() > MAX_MESSAGE_LEN) return;
|
||||
|
||||
if (this.homeTile == null) {
|
||||
this.homeTile = this.getRoomUnit().getCurrentLocation();
|
||||
this.homeRotation = this.getRoomUnit().getBodyRotation();
|
||||
}
|
||||
|
||||
if (this.busy.get() || this.getRoomUnit().hasStatus(RoomUnitStatus.MOVE)) {
|
||||
this.whisperThrottled(asker, busyWhisper);
|
||||
return;
|
||||
}
|
||||
|
||||
if (raw != null) {
|
||||
double distance = this.getRoomUnit().getCurrentLocation().distance(asker.getRoomUnit().getCurrentLocation());
|
||||
int commandDistance = Emulator.getConfig().getInt("hotel.bot.butler.commanddistance");
|
||||
|
||||
if (distance <= commandDistance) {
|
||||
String lower = raw.toLowerCase();
|
||||
|
||||
if (doorTriggerPattern.matcher(lower).find()) {
|
||||
if (!this.busy.compareAndSet(false, true)) {
|
||||
this.whisperThrottled(asker, busyWhisper);
|
||||
return;
|
||||
}
|
||||
this.showToTheDoor(asker);
|
||||
return;
|
||||
}
|
||||
|
||||
for (java.util.Map.Entry<Pattern, List<String>> entry : chatResponses.entrySet()) {
|
||||
if (entry.getKey().matcher(lower).find()) {
|
||||
List<String> options = entry.getValue();
|
||||
if (options.isEmpty()) continue;
|
||||
|
||||
String reply = options.get(RANDOM.nextInt(options.size()));
|
||||
this.talk(reply);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.busy.compareAndSet(false, true)) {
|
||||
this.whisperThrottled(asker, busyWhisper);
|
||||
return;
|
||||
}
|
||||
super.onUserSay(message);
|
||||
this.schedulePostServeReturn(currentRoom.getId(), 0);
|
||||
}
|
||||
|
||||
private void whisperThrottled(Habbo target, String text) {
|
||||
if (target == null || text == null || text.isEmpty() || this.getRoomUnit() == null) return;
|
||||
int userId = target.getHabboInfo().getId();
|
||||
long now = System.currentTimeMillis();
|
||||
Long last = lastBusyWhisperAt.get(userId);
|
||||
if (last != null && (now - last) < BUSY_WHISPER_COOLDOWN_MS) return;
|
||||
lastBusyWhisperAt.put(userId, now);
|
||||
RoomChatMessage msg = new RoomChatMessage(text, this.getRoomUnit(), RoomChatMessageBubbles.BOT);
|
||||
target.getClient().sendResponse(new RoomUserWhisperComposer(msg));
|
||||
}
|
||||
|
||||
private void showToTheDoor(final Habbo target) {
|
||||
final Room room = this.getRoom();
|
||||
if (room == null || room.getLayout() == null || target == null) {
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
final RoomTile doorTile = room.getLayout().getDoorTile();
|
||||
if (doorTile == null) {
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lookAt(target);
|
||||
List<String> lines = doorLines;
|
||||
String line = lines.isEmpty() ? DEFAULT_DOOR_LINES.get(RANDOM.nextInt(DEFAULT_DOOR_LINES.size()))
|
||||
: lines.get(RANDOM.nextInt(lines.size()));
|
||||
this.talk(line);
|
||||
|
||||
final int targetId = target.getHabboInfo().getId();
|
||||
final int roomId = room.getId();
|
||||
final AtomicBoolean fired = new AtomicBoolean(false);
|
||||
|
||||
final Runnable kickThenReturn = () -> {
|
||||
if (!fired.compareAndSet(false, true)) return;
|
||||
Room currentRoom = this.getRoom();
|
||||
if (currentRoom == null || currentRoom.getId() != roomId) {
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
Habbo stillHere = currentRoom.getHabbo(targetId);
|
||||
if (stillHere != null) {
|
||||
currentRoom.kickHabbo(stillHere, false);
|
||||
}
|
||||
this.scheduleReturnHome(targetId, roomId, 0);
|
||||
};
|
||||
|
||||
if (this.getRoomUnit().canWalk() && !this.getRoomUnit().getCurrentLocation().equals(doorTile)) {
|
||||
List<Runnable> onArrive = new ArrayList<>();
|
||||
onArrive.add(kickThenReturn);
|
||||
|
||||
List<Runnable> onFail = new ArrayList<>();
|
||||
onFail.add(() -> Emulator.getThreading().run(kickThenReturn, 1500));
|
||||
|
||||
this.getRoomUnit().setGoalLocation(doorTile);
|
||||
Emulator.getThreading().run(
|
||||
new RoomUnitWalkToLocation(this.getRoomUnit(), doorTile, room, onArrive, onFail));
|
||||
} else {
|
||||
Emulator.getThreading().run(kickThenReturn, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int RETURN_HOME_POLL_MS = 500;
|
||||
private static final int RETURN_HOME_MAX_WAIT_MS = 8000;
|
||||
private static final int POST_SERVE_POLL_MS = 750;
|
||||
private static final int POST_SERVE_MAX_WAIT_MS = 30000;
|
||||
|
||||
private void schedulePostServeReturn(final int roomId, final int waitedMs) {
|
||||
if (waitedMs == 0 && !this.returnScheduled.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
if (waitedMs >= POST_SERVE_MAX_WAIT_MS) {
|
||||
this.returnScheduled.set(false);
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
if (this.homeTile == null) {
|
||||
this.returnScheduled.set(false);
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Emulator.getThreading().run(() -> {
|
||||
Room r = this.getRoom();
|
||||
if (r == null || r.getId() != roomId || this.getRoomUnit() == null || this.homeTile == null) {
|
||||
this.returnScheduled.set(false);
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getRoomUnit().getCurrentLocation().equals(this.homeTile)) {
|
||||
if (this.homeRotation != null && this.getRoomUnit().getBodyRotation() != this.homeRotation) {
|
||||
this.getRoomUnit().setRotation(this.homeRotation);
|
||||
r.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||
this.persistPosition();
|
||||
} else {
|
||||
this.busy.set(false);
|
||||
}
|
||||
this.returnScheduled.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean stillWalking = this.getRoomUnit().hasStatus(RoomUnitStatus.MOVE)
|
||||
|| (this.getRoomUnit().getPath() != null && !this.getRoomUnit().getPath().isEmpty());
|
||||
|
||||
if (stillWalking) {
|
||||
this.schedulePostServeReturn(roomId, waitedMs + POST_SERVE_POLL_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
this.returnScheduled.set(false);
|
||||
this.returnHome(-1, false);
|
||||
}, POST_SERVE_POLL_MS);
|
||||
}
|
||||
|
||||
private void scheduleReturnHome(final int kickedHabboId, final int roomId, final int waitedMs) {
|
||||
Room currentRoom = this.getRoom();
|
||||
if (currentRoom == null || currentRoom.getId() != roomId) return;
|
||||
|
||||
boolean stillEscorting = currentRoom.getHabbo(kickedHabboId) != null;
|
||||
|
||||
if (!stillEscorting || waitedMs >= RETURN_HOME_MAX_WAIT_MS) {
|
||||
this.returnHome(kickedHabboId, true);
|
||||
return;
|
||||
}
|
||||
|
||||
Emulator.getThreading().run(
|
||||
() -> this.scheduleReturnHome(kickedHabboId, roomId, waitedMs + RETURN_HOME_POLL_MS),
|
||||
RETURN_HOME_POLL_MS);
|
||||
}
|
||||
|
||||
private void returnHome(int kickedHabboId, boolean alwaysTeleport) {
|
||||
final Room room = this.getRoom();
|
||||
if (room == null || this.homeTile == null || this.getRoomUnit() == null) {
|
||||
this.busy.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
final Runnable teleportHome = () -> {
|
||||
Room r = this.getRoom();
|
||||
if (r == null || this.getRoomUnit() == null) return;
|
||||
|
||||
double homeZ = r.getTopHeightAt(this.homeTile.x, this.homeTile.y);
|
||||
|
||||
this.getRoomUnit().stopWalking();
|
||||
this.getRoomUnit().setZ(homeZ);
|
||||
this.getRoomUnit().setLocation(this.homeTile);
|
||||
this.getRoomUnit().setPreviousLocationZ(homeZ);
|
||||
if (this.homeRotation != null) {
|
||||
this.getRoomUnit().setRotation(this.homeRotation);
|
||||
}
|
||||
this.getRoomUnit().statusUpdate(true);
|
||||
r.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||
this.persistPosition();
|
||||
};
|
||||
|
||||
if (this.getRoomUnit().getCurrentLocation().equals(this.homeTile)) {
|
||||
if (this.homeRotation != null) {
|
||||
this.getRoomUnit().setRotation(this.homeRotation);
|
||||
room.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||
}
|
||||
this.persistPosition();
|
||||
return;
|
||||
}
|
||||
|
||||
boolean hasOtherWatchers = false;
|
||||
for (Habbo h : room.getCurrentHabbos().values()) {
|
||||
if (h.getHabboInfo().getId() != kickedHabboId) {
|
||||
hasOtherWatchers = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (alwaysTeleport || !hasOtherWatchers || !this.getRoomUnit().canWalk()) {
|
||||
teleportHome.run();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Runnable> onArrive = new ArrayList<>();
|
||||
onArrive.add(() -> {
|
||||
if (this.homeRotation != null && this.getRoom() != null) {
|
||||
this.getRoomUnit().setRotation(this.homeRotation);
|
||||
this.getRoom().sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
|
||||
}
|
||||
this.persistPosition();
|
||||
});
|
||||
|
||||
List<Runnable> onFail = new ArrayList<>();
|
||||
onFail.add(teleportHome);
|
||||
|
||||
this.getRoomUnit().setGoalLocation(this.homeTile);
|
||||
Emulator.getThreading().run(
|
||||
new RoomUnitWalkToLocation(this.getRoomUnit(), this.homeTile, room, onArrive, onFail));
|
||||
}
|
||||
|
||||
private void persistPosition() {
|
||||
this.needsUpdate(true);
|
||||
this.run();
|
||||
this.busy.set(false);
|
||||
}
|
||||
}
|
||||
@@ -1046,10 +1046,22 @@ public class CatalogManager {
|
||||
for (Item baseItem : item.getBaseItems()) {
|
||||
for (int k = 0; k < item.getItemAmount(baseItem.getId()); k++) {
|
||||
if (baseItem.getName().startsWith("rentable_bot_") || baseItem.getName().startsWith("bot_")) {
|
||||
String baseName = baseItem.getName();
|
||||
String type = item.getName().replace("rentable_bot_", "");
|
||||
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)) {
|
||||
habbo.getClient().sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
THashMap<String, String> data = new THashMap<>();
|
||||
|
||||
for (String s : item.getExtradata().split(";")) {
|
||||
|
||||
@@ -196,6 +196,7 @@ public class CommandHandler {
|
||||
addCommand(new EmptyInventoryCommand());
|
||||
addCommand(new EmptyBotsInventoryCommand());
|
||||
addCommand(new EmptyPetsInventoryCommand());
|
||||
addCommand(new EmuStatsCommand());
|
||||
addCommand(new EnableCommand());
|
||||
addCommand(new EventCommand());
|
||||
addCommand(new FacelessCommand());
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.eu.habbo.habbohotel.commands;
|
||||
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
|
||||
public class EmuStatsCommand extends Command {
|
||||
public EmuStatsCommand() {
|
||||
super(Permission.ACC_MODTOOL_ROOM_INFO, new String[]{"emustats"});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(GameClient gameClient, String[] params) {
|
||||
gameClient.getHabbo().whisper("Emulator stats are available in the Nitro stats window.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -208,10 +208,10 @@ public abstract class Game implements Runnable {
|
||||
this.state = GameState.IDLE;
|
||||
|
||||
boolean gamesActive = false;
|
||||
for (HabboItem timer : room.getFloorItems()) {
|
||||
if (timer instanceof InteractionGameTimer) {
|
||||
if (((InteractionGameTimer) timer).isRunning())
|
||||
gamesActive = true;
|
||||
for (InteractionGameTimer timer : room.getRoomSpecialTypes().getGameTimers().values()) {
|
||||
if (timer.isRunning()) {
|
||||
gamesActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,49 +6,55 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager;
|
||||
public class GamePlayer {
|
||||
|
||||
private final Habbo habbo;
|
||||
|
||||
|
||||
private GameTeamColors teamColor;
|
||||
|
||||
|
||||
private int score;
|
||||
private int wiredScore;
|
||||
|
||||
|
||||
public GamePlayer(Habbo habbo, GameTeamColors teamColor) {
|
||||
this.habbo = habbo;
|
||||
this.teamColor = teamColor;
|
||||
}
|
||||
|
||||
|
||||
public void reset() {
|
||||
this.score = 0;
|
||||
this.wiredScore = 0;
|
||||
}
|
||||
|
||||
public synchronized void addScore(int amount) {
|
||||
public void addScore(int amount) {
|
||||
addScore(amount, false);
|
||||
}
|
||||
|
||||
public synchronized void addScore(int amount, boolean isWired) {
|
||||
if (habbo.getHabboInfo().getGamePlayer() != null && this.habbo.getHabboInfo().getCurrentGame() != null && this.habbo.getHabboInfo().getCurrentRoom().getGame(this.habbo.getHabboInfo().getCurrentGame()).getTeamForHabbo(this.habbo) != null) {
|
||||
this.score += amount;
|
||||
public void addScore(int amount, boolean isWired) {
|
||||
com.eu.habbo.habbohotel.rooms.Room roomToTrigger = null;
|
||||
com.eu.habbo.habbohotel.rooms.RoomUnit roomUnitToTrigger = null;
|
||||
int currentScore = 0;
|
||||
|
||||
if (this.score < 0) this.score = 0;
|
||||
synchronized (this) {
|
||||
if (this.habbo.getHabboInfo().getGamePlayer() != null && this.habbo.getHabboInfo().getCurrentGame() != null && this.habbo.getHabboInfo().getCurrentRoom().getGame(this.habbo.getHabboInfo().getCurrentGame()).getTeamForHabbo(this.habbo) != null) {
|
||||
this.score += amount;
|
||||
|
||||
if(isWired) {
|
||||
this.wiredScore += amount;
|
||||
if (this.score < 0) this.score = 0;
|
||||
|
||||
if (this.wiredScore < 0) {
|
||||
this.wiredScore = 0;
|
||||
if (isWired) {
|
||||
this.wiredScore += amount;
|
||||
|
||||
if (this.wiredScore < 0) {
|
||||
this.wiredScore = 0;
|
||||
}
|
||||
|
||||
if (this.wiredScore > this.score) {
|
||||
this.wiredScore = this.score;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.wiredScore > this.score) {
|
||||
this.wiredScore = this.score;
|
||||
}
|
||||
roomToTrigger = this.habbo.getHabboInfo().getCurrentRoom();
|
||||
roomUnitToTrigger = this.habbo.getRoomUnit();
|
||||
currentScore = this.score;
|
||||
}
|
||||
}
|
||||
|
||||
WiredManager.triggerScoreAchieved(this.habbo.getHabboInfo().getCurrentRoom(), this.habbo.getRoomUnit(), this.score, amount);
|
||||
if (roomToTrigger != null && roomUnitToTrigger != null) {
|
||||
WiredManager.triggerScoreAchieved(roomToTrigger, roomUnitToTrigger, currentScore, amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,12 +62,10 @@ public class GamePlayer {
|
||||
return this.habbo;
|
||||
}
|
||||
|
||||
|
||||
public GameTeamColors getTeamColor() {
|
||||
return this.teamColor;
|
||||
}
|
||||
|
||||
|
||||
public int getScore() {
|
||||
return this.score;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,12 @@ public class Item implements ISerialize {
|
||||
return item.getName().toLowerCase().startsWith("a0 pet");
|
||||
}
|
||||
|
||||
public static boolean isBot(Item item) {
|
||||
if (item == null) return false;
|
||||
String name = item.getName();
|
||||
return name != null && (name.startsWith("bot_") || name.startsWith("rentable_bot_"));
|
||||
}
|
||||
|
||||
public static double getCurrentHeight(HabboItem item) {
|
||||
if (item instanceof InteractionMultiHeight && item.getBaseItem().getMultiHeights().length > 0) {
|
||||
if (item.getExtradata().isEmpty()) {
|
||||
|
||||
+3
-1
@@ -89,7 +89,9 @@ public class InteractionOneWayGate extends HabboItem {
|
||||
Emulator.getThreading().run(new RoomUnitWalkToLocation(unit, tile, room, onFail, onFail));
|
||||
|
||||
Emulator.getThreading().run(() -> {
|
||||
WiredManager.triggerUserWalksOn(room, unit, this);
|
||||
if (room.isLoaded()) {
|
||||
WiredManager.triggerUserWalksOn(room, unit, this);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
|
||||
+23
-15
@@ -93,23 +93,24 @@ public abstract class InteractionWired extends InteractionDefault {
|
||||
@Override
|
||||
public void run() {
|
||||
if (this.needsUpdate()) {
|
||||
String wiredData = this.getWiredData();
|
||||
String wiredDataRaw = this.getWiredData();
|
||||
final String wiredData = (wiredDataRaw == null) ? "" : wiredDataRaw;
|
||||
final int currentRoomId = this.getRoomId();
|
||||
final int currentId = this.getId();
|
||||
|
||||
if (wiredData == null) {
|
||||
wiredData = "";
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE items SET wired_data = ? WHERE id = ?")) {
|
||||
if (this.getRoomId() != 0) {
|
||||
statement.setString(1, wiredData);
|
||||
} else {
|
||||
statement.setString(1, "");
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE items SET wired_data = ? WHERE id = ?")) {
|
||||
if (currentRoomId != 0) {
|
||||
statement.setString(1, wiredData);
|
||||
} else {
|
||||
statement.setString(1, "");
|
||||
}
|
||||
statement.setInt(2, currentId);
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
statement.setInt(2, this.getId());
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
super.run();
|
||||
}
|
||||
@@ -216,6 +217,9 @@ public abstract class InteractionWired extends InteractionDefault {
|
||||
public static WiredSettings readSettings(ClientMessage packet, boolean isEffect)
|
||||
{
|
||||
int intParamCount = packet.readInt();
|
||||
if (intParamCount < 0 || intParamCount > 100) {
|
||||
throw new IllegalArgumentException("Invalid intParamCount: " + intParamCount);
|
||||
}
|
||||
int[] intParams = new int[intParamCount];
|
||||
|
||||
for(int i = 0; i < intParamCount; i++)
|
||||
@@ -226,6 +230,10 @@ public abstract class InteractionWired extends InteractionDefault {
|
||||
String stringParam = packet.readString();
|
||||
|
||||
int itemCount = packet.readInt();
|
||||
int selectionLimit = Emulator.getConfig() != null ? Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5) : 5;
|
||||
if (itemCount < 0 || itemCount > selectionLimit * 20) {
|
||||
throw new IllegalArgumentException("Invalid itemCount: " + itemCount + " exceeds maximum allowed limit");
|
||||
}
|
||||
int[] itemIds = new int[itemCount];
|
||||
|
||||
for(int i = 0; i < itemCount; i++)
|
||||
|
||||
+20
-11
@@ -154,6 +154,7 @@ public class InteractionGameTimer extends HabboItem {
|
||||
@Override
|
||||
public void onPickUp(Room room) {
|
||||
this.endGame(room);
|
||||
this.threadActive = false;
|
||||
|
||||
this.timeNow = this.getInitialTimeValue();
|
||||
this.setExtradata(this.timeNow + "\t" + this.baseTime);
|
||||
@@ -220,8 +221,7 @@ public class InteractionGameTimer extends HabboItem {
|
||||
room.updateItem(this);
|
||||
WiredManager.triggerGameStarts(room);
|
||||
|
||||
if (!this.threadActive) {
|
||||
this.threadActive = true;
|
||||
if (this.tryActivateTimerThread()) {
|
||||
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
|
||||
}
|
||||
} else if (client != null) {
|
||||
@@ -243,8 +243,7 @@ public class InteractionGameTimer extends HabboItem {
|
||||
} else {
|
||||
this.unpause(room);
|
||||
|
||||
if (!this.threadActive) {
|
||||
this.threadActive = true;
|
||||
if (this.tryActivateTimerThread()) {
|
||||
this.scheduleTimerRunnable(this.getTimerResumeDelayMs());
|
||||
}
|
||||
}
|
||||
@@ -257,8 +256,7 @@ public class InteractionGameTimer extends HabboItem {
|
||||
this.createNewGame(room);
|
||||
WiredManager.triggerGameStarts(room);
|
||||
|
||||
if (!this.threadActive) {
|
||||
this.threadActive = true;
|
||||
if (this.tryActivateTimerThread()) {
|
||||
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
|
||||
}
|
||||
}
|
||||
@@ -297,8 +295,7 @@ public class InteractionGameTimer extends HabboItem {
|
||||
}
|
||||
this.createNewGame(room);
|
||||
WiredManager.triggerGameStarts(room);
|
||||
if (!threadActive) {
|
||||
threadActive = true;
|
||||
if (this.tryActivateTimerThread()) {
|
||||
this.scheduleTimerRunnable(this.getTimerStartDelayMs());
|
||||
}
|
||||
}
|
||||
@@ -321,8 +318,7 @@ public class InteractionGameTimer extends HabboItem {
|
||||
this.isPaused = false;
|
||||
this.unpause(room);
|
||||
|
||||
if (!this.threadActive) {
|
||||
this.threadActive = true;
|
||||
if (this.tryActivateTimerThread()) {
|
||||
this.scheduleTimerRunnable(this.getTimerResumeDelayMs());
|
||||
}
|
||||
}
|
||||
@@ -406,7 +402,9 @@ public class InteractionGameTimer extends HabboItem {
|
||||
}
|
||||
|
||||
public void setThreadActive(boolean threadActive) {
|
||||
this.threadActive = threadActive;
|
||||
synchronized (this) {
|
||||
this.threadActive = threadActive;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
@@ -428,4 +426,15 @@ public class InteractionGameTimer extends HabboItem {
|
||||
public int getBaseTime() {
|
||||
return this.baseTime;
|
||||
}
|
||||
|
||||
public boolean tryActivateTimerThread() {
|
||||
synchronized (this) {
|
||||
if (this.threadActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.threadActive = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-14
@@ -20,12 +20,13 @@ import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.incoming.wired.WiredSaveException;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.UpdateFailedComposer;
|
||||
import gnu.trove.procedure.TObjectProcedure;
|
||||
import gnu.trove.set.hash.THashSet;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
public static final int LIMIT_ONCE = 0;
|
||||
@@ -37,10 +38,10 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
|
||||
public int limit;
|
||||
public int limitationInterval;
|
||||
public int given;
|
||||
public AtomicInteger given = new AtomicInteger(0);
|
||||
public int rewardTime;
|
||||
public boolean uniqueRewards;
|
||||
public THashSet<WiredGiveRewardItem> rewardItems = new THashSet<>();
|
||||
public List<WiredGiveRewardItem> rewardItems = new CopyOnWriteArrayList<>();
|
||||
public int userSource = WiredSourceUtil.SOURCE_TRIGGER;
|
||||
|
||||
public WiredEffectGiveReward(ResultSet set, Item baseItem) throws SQLException {
|
||||
@@ -71,9 +72,8 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
|
||||
@Override
|
||||
public String getWiredData() {
|
||||
|
||||
ArrayList<WiredGiveRewardItem> rewards = new ArrayList<>(this.rewardItems);
|
||||
return WiredManager.getGson().toJson(new JsonData(this.limit, this.given, this.rewardTime, this.uniqueRewards, this.limitationInterval, rewards, this.getDelay(), this.userSource));
|
||||
return WiredManager.getGson().toJson(new JsonData(this.limit, this.given.get(), this.rewardTime, this.uniqueRewards, this.limitationInterval, rewards, this.getDelay(), this.userSource));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -84,7 +84,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
|
||||
this.setDelay(data.delay);
|
||||
this.limit = data.limit;
|
||||
this.given = data.given;
|
||||
this.given.set(data.given);
|
||||
this.rewardTime = data.reward_time;
|
||||
this.uniqueRewards = data.unique_rewards;
|
||||
this.limitationInterval = data.limit_interval;
|
||||
@@ -96,7 +96,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
String[] data = wiredData.split(":");
|
||||
if (data.length > 0) {
|
||||
this.limit = Integer.parseInt(data[0]);
|
||||
this.given = Integer.parseInt(data[1]);
|
||||
this.given.set(Integer.parseInt(data[1]));
|
||||
this.rewardTime = Integer.parseInt(data[2]);
|
||||
this.uniqueRewards = data[3].equals("1");
|
||||
this.limitationInterval = Integer.parseInt(data[4]);
|
||||
@@ -127,7 +127,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
public void onPickUp() {
|
||||
this.limit = 0;
|
||||
this.limitationInterval = 0;
|
||||
this.given = 0;
|
||||
this.given.set(0);
|
||||
this.rewardTime = 0;
|
||||
this.uniqueRewards = false;
|
||||
this.rewardItems.clear();
|
||||
@@ -192,7 +192,7 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
this.limit = settings.getIntParams()[2];
|
||||
this.limitationInterval = settings.getIntParams()[3];
|
||||
this.userSource = settings.getIntParams()[4];
|
||||
this.given = 0;
|
||||
this.given.set(0);
|
||||
|
||||
String data = settings.getStringParam();
|
||||
|
||||
@@ -276,15 +276,15 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
}
|
||||
|
||||
public int getGiven() {
|
||||
return this.given;
|
||||
return this.given.get();
|
||||
}
|
||||
|
||||
public void setGiven(int given) {
|
||||
this.given = given;
|
||||
this.given.set(given);
|
||||
}
|
||||
|
||||
public void incrementGiven() {
|
||||
this.given++;
|
||||
this.given.incrementAndGet();
|
||||
}
|
||||
|
||||
public int getRewardTime() {
|
||||
@@ -303,11 +303,11 @@ public class WiredEffectGiveReward extends InteractionWiredEffect {
|
||||
this.uniqueRewards = uniqueRewards;
|
||||
}
|
||||
|
||||
public THashSet<WiredGiveRewardItem> getRewardItems() {
|
||||
public List<WiredGiveRewardItem> getRewardItems() {
|
||||
return this.rewardItems;
|
||||
}
|
||||
|
||||
public void setRewardItems(THashSet<WiredGiveRewardItem> rewardItems) {
|
||||
public void setRewardItems(List<WiredGiveRewardItem> rewardItems) {
|
||||
this.rewardItems = rewardItems;
|
||||
}
|
||||
}
|
||||
|
||||
+52
-44
@@ -384,61 +384,69 @@ public final class WiredVariableReferenceSupport {
|
||||
}
|
||||
|
||||
private static void upsertSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId, SharedUserAssignment assignment) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_user_wired_variables (room_id, user_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, userId);
|
||||
statement.setInt(3, sourceVariableItemId);
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_user_wired_variables (room_id, user_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, userId);
|
||||
statement.setInt(3, sourceVariableItemId);
|
||||
|
||||
if (assignment.getValue() == null) {
|
||||
statement.setNull(4, java.sql.Types.INTEGER);
|
||||
} else {
|
||||
statement.setInt(4, assignment.getValue());
|
||||
if (assignment.getValue() == null) {
|
||||
statement.setNull(4, java.sql.Types.INTEGER);
|
||||
} else {
|
||||
statement.setInt(4, assignment.getValue());
|
||||
}
|
||||
|
||||
statement.setInt(5, assignment.getCreatedAt());
|
||||
statement.setInt(6, assignment.getUpdatedAt());
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to store shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
|
||||
}
|
||||
|
||||
statement.setInt(5, assignment.getCreatedAt());
|
||||
statement.setInt(6, assignment.getUpdatedAt());
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to store shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void deleteSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ?")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, userId);
|
||||
statement.setInt(3, sourceVariableItemId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to delete shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
|
||||
}
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ?")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, userId);
|
||||
statement.setInt(3, sourceVariableItemId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to delete shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void upsertSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId, SharedRoomAssignment assignment) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_wired_variables (room_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, sourceVariableItemId);
|
||||
statement.setInt(3, assignment.getValue());
|
||||
statement.setInt(4, 0);
|
||||
statement.setInt(5, assignment.getUpdatedAt());
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to store shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
|
||||
}
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO room_wired_variables (room_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, sourceVariableItemId);
|
||||
statement.setInt(3, assignment.getValue());
|
||||
statement.setInt(4, 0);
|
||||
statement.setInt(5, assignment.getUpdatedAt());
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to store shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void deleteSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ?")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, sourceVariableItemId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to delete shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
|
||||
}
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ?")) {
|
||||
statement.setInt(1, sourceRoomId);
|
||||
statement.setInt(2, sourceVariableItemId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Failed to delete shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static String createDefinitionPrefix(int sourceRoomId, int sourceVariableItemId) {
|
||||
|
||||
+5
-1
@@ -123,7 +123,11 @@ public class WiredTriggerRepeaterLong extends InteractionWiredTrigger implements
|
||||
@Override
|
||||
public boolean saveData(WiredSettings settings) {
|
||||
if (settings.getIntParams().length < 1) return false;
|
||||
this.repeatTime = settings.getIntParams()[0] * 5000;
|
||||
int interval = settings.getIntParams()[0];
|
||||
if (interval < 1) {
|
||||
interval = 1;
|
||||
}
|
||||
this.repeatTime = interval * 5000;
|
||||
// No accumulated time reset needed - using global tick count
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ public class Permission {
|
||||
public static String ACC_SEE_WHISPERS = "acc_see_whispers";
|
||||
public static String ACC_SEE_TENTCHAT = "acc_see_tentchat";
|
||||
public static String ACC_SUPERWIRED = "acc_superwired";
|
||||
public static String ACC_HOUSEKEEPING = "acc_housekeeping";
|
||||
public static String ACC_SUPPORTTOOL = "acc_supporttool";
|
||||
public static String ACC_UNKICKABLE = "acc_unkickable";
|
||||
public static String ACC_GUILDGATE = "acc_guildgate";
|
||||
|
||||
@@ -115,19 +115,21 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
}
|
||||
|
||||
public final Object roomUnitLock = new Object();
|
||||
public final ConcurrentHashMap<RoomTile, THashSet<HabboItem>> tileCache = new ConcurrentHashMap<>();
|
||||
public final List<Integer> userVotes;
|
||||
private final TIntArrayList rights;
|
||||
private final TIntIntHashMap mutedHabbos;
|
||||
private final TIntObjectHashMap<RoomBan> bannedHabbos;
|
||||
private final Set<Game> games;
|
||||
private final TIntObjectMap<RoomMoodlightData> moodlightData;
|
||||
public volatile double lastCycleCpuMs = 0.0;
|
||||
public volatile String lastCycleThread = "N/A";
|
||||
|
||||
private final Object loadLock = new Object();
|
||||
//Use appropriately. Could potentially cause memory leaks when used incorrectly.
|
||||
public volatile boolean preventUnloading = false;
|
||||
public volatile boolean preventUncaching = false;
|
||||
public Set<ServerMessage> scheduledComposers = ConcurrentHashMap.newKeySet();
|
||||
public Set<Runnable> scheduledTasks = ConcurrentHashMap.newKeySet();
|
||||
public final java.util.concurrent.ConcurrentLinkedQueue<Runnable> scheduledTasks = new java.util.concurrent.ConcurrentLinkedQueue<>();
|
||||
public String wordQuiz = "";
|
||||
public int noVotes = 0;
|
||||
public int yesVotes = 0;
|
||||
@@ -981,8 +983,6 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
this.scheduledTasks.clear();
|
||||
this.scheduledComposers.clear();
|
||||
|
||||
this.tileCache.clear();
|
||||
|
||||
synchronized (this.mutedHabbos) {
|
||||
this.mutedHabbos.clear();
|
||||
}
|
||||
@@ -1160,10 +1160,13 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
synchronized (this.loadLock) {
|
||||
if (this.loaded) {
|
||||
try {
|
||||
long startTime = System.nanoTime();
|
||||
this.lastCycleThread = Thread.currentThread().getName();
|
||||
// Run cycle directly instead of scheduling on thread pool
|
||||
// This ensures all cycle tasks in the same tick execute synchronously
|
||||
// preventing wired desync issues
|
||||
this.cycle();
|
||||
this.lastCycleCpuMs = (System.nanoTime() - startTime) / 1000000.0;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Caught exception", e);
|
||||
}
|
||||
@@ -2320,27 +2323,37 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
sanitizedInspectMask |= sanitizedModifyMask;
|
||||
|
||||
synchronized (this.wiredSettingsLock) {
|
||||
int previousInspectMask = this.wiredInspectMask;
|
||||
int previousModifyMask = this.wiredModifyMask;
|
||||
final int finalInspectMask = sanitizedInspectMask;
|
||||
final int finalModifyMask = sanitizedModifyMask;
|
||||
final int finalId = this.id;
|
||||
final int previousInspectMask = this.wiredInspectMask;
|
||||
final int previousModifyMask = this.wiredModifyMask;
|
||||
|
||||
this.wiredInspectMask = sanitizedInspectMask;
|
||||
this.wiredModifyMask = sanitizedModifyMask;
|
||||
this.wiredSettingsLoaded = true;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO room_wired_settings (room_id, inspect_mask, modify_mask) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE inspect_mask = VALUES(inspect_mask), modify_mask = VALUES(modify_mask)")) {
|
||||
statement.setInt(1, this.id);
|
||||
statement.setInt(2, sanitizedInspectMask);
|
||||
statement.setInt(3, sanitizedModifyMask);
|
||||
statement.executeUpdate();
|
||||
this.pushWiredSettingsToCurrentHabbos();
|
||||
return true;
|
||||
} catch (SQLException e) {
|
||||
this.wiredInspectMask = previousInspectMask;
|
||||
this.wiredModifyMask = previousModifyMask;
|
||||
LOGGER.error("Caught SQL exception while saving wired room settings", e);
|
||||
return false;
|
||||
}
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO room_wired_settings (room_id, inspect_mask, modify_mask) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE inspect_mask = VALUES(inspect_mask), modify_mask = VALUES(modify_mask)")) {
|
||||
statement.setInt(1, finalId);
|
||||
statement.setInt(2, finalInspectMask);
|
||||
statement.setInt(3, finalModifyMask);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
synchronized (this.wiredSettingsLock) {
|
||||
if (this.wiredInspectMask == finalInspectMask && this.wiredModifyMask == finalModifyMask) {
|
||||
this.wiredInspectMask = previousInspectMask;
|
||||
this.wiredModifyMask = previousModifyMask;
|
||||
}
|
||||
}
|
||||
LOGGER.error("Caught SQL exception while saving wired room settings", e);
|
||||
}
|
||||
});
|
||||
|
||||
this.pushWiredSettingsToCurrentHabbos();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2878,4 +2891,20 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
public Collection<RoomUnit> getRoomUnitsAt(RoomTile tile) {
|
||||
return this.unitManager.getRoomUnitsAt(tile);
|
||||
}
|
||||
|
||||
public long getEstimatedMemoryUsage() {
|
||||
long bytes = 1024 * 10; // Base footprint
|
||||
if (this.itemManager != null) {
|
||||
bytes += this.itemManager.itemCount() * 512L;
|
||||
}
|
||||
bytes += this.getUserCount() * 2048L;
|
||||
if (this.layout != null) {
|
||||
bytes += this.layout.getMapSize() * 128L;
|
||||
}
|
||||
com.eu.habbo.habbohotel.wired.tick.WiredTickService wired = com.eu.habbo.habbohotel.wired.tick.WiredTickService.getInstance();
|
||||
if (wired != null) {
|
||||
bytes += wired.getTickableCount(this.getId()) * 256L;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,27 +313,6 @@ public class RoomChatManager {
|
||||
}
|
||||
}
|
||||
|
||||
String wiredSayMessage = roomChatMessage.getMessage();
|
||||
|
||||
// Handle commands and wired
|
||||
boolean suppressSaysOutput = false;
|
||||
if (chatType != RoomChatType.WHISPER) {
|
||||
if (CommandHandler.handleCommand(habbo.getClient(), roomChatMessage.getUnfilteredMessage())) {
|
||||
WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), wiredSayMessage);
|
||||
roomChatMessage.isCommand = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ignoreWired) {
|
||||
suppressSaysOutput = WiredManager.shouldSuppressUserSaysOutput(
|
||||
habbo.getHabboInfo().getCurrentRoom(),
|
||||
habbo.getRoomUnit(),
|
||||
wiredSayMessage,
|
||||
chatType.ordinal(),
|
||||
roomChatMessage.getBubble() != null ? roomChatMessage.getBubble().getType() : -1);
|
||||
}
|
||||
}
|
||||
|
||||
// Flood protection
|
||||
if (!habbo.hasPermission(Permission.ACC_CHAT_NO_FLOOD)) {
|
||||
final int chatCounter = habbo.getHabboStats().chatCounter.addAndGet(1);
|
||||
@@ -357,6 +336,27 @@ public class RoomChatManager {
|
||||
}
|
||||
}
|
||||
|
||||
String wiredSayMessage = roomChatMessage.getMessage();
|
||||
|
||||
// Handle commands and wired
|
||||
boolean suppressSaysOutput = false;
|
||||
if (chatType != RoomChatType.WHISPER) {
|
||||
if (CommandHandler.handleCommand(habbo.getClient(), roomChatMessage.getUnfilteredMessage())) {
|
||||
WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), wiredSayMessage);
|
||||
roomChatMessage.isCommand = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ignoreWired) {
|
||||
suppressSaysOutput = WiredManager.shouldSuppressUserSaysOutput(
|
||||
habbo.getHabboInfo().getCurrentRoom(),
|
||||
habbo.getRoomUnit(),
|
||||
wiredSayMessage,
|
||||
chatType.ordinal(),
|
||||
roomChatMessage.getBubble() != null ? roomChatMessage.getBubble().getType() : -1);
|
||||
}
|
||||
}
|
||||
|
||||
// Build prefix messages
|
||||
ServerMessage prefixMessage = null;
|
||||
|
||||
@@ -615,6 +615,9 @@ public class RoomChatManager {
|
||||
InteractionTalkingFurniture.class);
|
||||
|
||||
for (HabboItem item : items) {
|
||||
if (item.getExtradata().equals("1")) {
|
||||
continue;
|
||||
}
|
||||
if (this.room.getLayout().getTile(item.getX(), item.getY())
|
||||
.distance(habbo.getRoomUnit().getCurrentLocation()) <= Emulator.getConfig()
|
||||
.getInt("furniture.talking.range")) {
|
||||
|
||||
@@ -75,7 +75,6 @@ public class RoomCycleManager {
|
||||
final boolean[] foundRightHolder = {false};
|
||||
|
||||
boolean loaded = this.room.isLoaded();
|
||||
this.room.tileCache.clear();
|
||||
|
||||
if (loaded) {
|
||||
processScheduledTasks();
|
||||
@@ -164,13 +163,9 @@ public class RoomCycleManager {
|
||||
* Processes scheduled tasks.
|
||||
*/
|
||||
private void processScheduledTasks() {
|
||||
if (!this.room.scheduledTasks.isEmpty()) {
|
||||
Set<Runnable> tasks = this.room.scheduledTasks;
|
||||
this.room.scheduledTasks = ConcurrentHashMap.newKeySet();
|
||||
|
||||
for (Runnable runnable : tasks) {
|
||||
Emulator.getThreading().run(runnable);
|
||||
}
|
||||
Runnable task;
|
||||
while ((task = this.room.scheduledTasks.poll()) != null) {
|
||||
Emulator.getThreading().run(task);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,7 +481,7 @@ public class RoomCycleManager {
|
||||
if (!unit.hasStatus(RoomUnitStatus.LAY)) {
|
||||
BedProfile bedProfile = new BedProfile(topItem);
|
||||
double layHeight = Item.getCurrentHeight(topItem) * 1.0D + bedProfile.getLayZOffset();
|
||||
LOGGER.info("[BedProfile] item={} stackHeight={} isFlat={} isDouble={} X={} Y={} Z={}",
|
||||
LOGGER.debug("[BedProfile] item={} stackHeight={} isFlat={} isDouble={} X={} Y={} Z={}",
|
||||
topItem.getBaseItem().getName(), topItem.getBaseItem().getHeight(),
|
||||
bedProfile.isFlat(), bedProfile.isDouble(),
|
||||
bedProfile.getLayXOffset(), bedProfile.getLayYOffset(), bedProfile.getLayZOffset());
|
||||
|
||||
@@ -35,6 +35,7 @@ public class RoomFurniVariableManager {
|
||||
private final Room room;
|
||||
private final ConcurrentHashMap<Integer, ConcurrentHashMap<Integer, VariableAssignment>> activeAssignmentsByFurniId;
|
||||
private volatile boolean permanentAssignmentsLoaded;
|
||||
private final java.util.concurrent.atomic.AtomicBoolean broadcastRequested = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
|
||||
public RoomFurniVariableManager(Room room) {
|
||||
this.room = room;
|
||||
@@ -591,7 +592,22 @@ public class RoomFurniVariableManager {
|
||||
habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(this.room.getUserVariableManager().createSnapshot(), this.createSnapshot(), this.room.getRoomVariableManager().createSnapshot()));
|
||||
}
|
||||
|
||||
public void requestBroadcast() {
|
||||
if (this.broadcastRequested.compareAndSet(false, true)) {
|
||||
Emulator.getThreading().run(() -> {
|
||||
this.broadcastRequested.set(false);
|
||||
if (this.room.isLoaded()) {
|
||||
this.broadcastSnapshotRaw();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastSnapshot() {
|
||||
this.requestBroadcast();
|
||||
}
|
||||
|
||||
public void broadcastSnapshotRaw() {
|
||||
RoomUserVariableManager.Snapshot userSnapshot = this.room.getUserVariableManager().createSnapshot();
|
||||
Snapshot furniSnapshot = this.createSnapshot();
|
||||
RoomVariableManager.Snapshot roomSnapshot = this.room.getRoomVariableManager().createSnapshot();
|
||||
|
||||
@@ -148,55 +148,8 @@ public class RoomItemManager {
|
||||
item = this.roomItems.get(id);
|
||||
}
|
||||
|
||||
// Check special types if not found in main storage
|
||||
RoomSpecialTypes specialTypes = this.room.getRoomSpecialTypes();
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getBanzaiTeleporter(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getTrigger(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getEffect(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getCondition(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getGameGate(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getGameScorebord(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getGameTimer(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getFreezeExitTiles().get(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getRoller(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getNest(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getPetDrink(id);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = specialTypes.getPetFood(id);
|
||||
item = this.room.getRoomSpecialTypes().getSpecialItem(id);
|
||||
}
|
||||
|
||||
return item;
|
||||
@@ -726,7 +679,7 @@ public class RoomItemManager {
|
||||
item instanceof WiredBlob ||
|
||||
item instanceof InteractionTent ||
|
||||
item instanceof InteractionSnowboardSlope ||
|
||||
item instanceof InteractionFireworks) {
|
||||
item instanceof InteractionFireworks || item instanceof InteractionVoteCounter) {
|
||||
specialTypes.addUndefined(item);
|
||||
}
|
||||
}
|
||||
@@ -899,7 +852,7 @@ public class RoomItemManager {
|
||||
item instanceof InteractionStickyPole ||
|
||||
item instanceof WiredBlob ||
|
||||
item instanceof InteractionTent ||
|
||||
item instanceof InteractionSnowboardSlope) {
|
||||
item instanceof InteractionSnowboardSlope || item instanceof InteractionVoteCounter) {
|
||||
specialTypes.removeUndefined(item);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ public class RoomSpecialTypes {
|
||||
private final THashMap<Integer, InteractionFreezeExitTile> freezeExitTile;
|
||||
private final THashMap<Integer, HabboItem> undefined;
|
||||
private final Set<ICycleable> cycleTasks;
|
||||
private final ConcurrentHashMap<Integer, HabboItem> specialItemsById = new ConcurrentHashMap<>();
|
||||
|
||||
public RoomSpecialTypes() {
|
||||
this.banzaiTeleporters = new THashMap<>(0);
|
||||
@@ -115,11 +116,11 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addBanzaiTeleporter(InteractionBattleBanzaiTeleporter item) {
|
||||
this.banzaiTeleporters.put(item.getId(), item);
|
||||
this.banzaiTeleporters.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removeBanzaiTeleporter(InteractionBattleBanzaiTeleporter item) {
|
||||
this.banzaiTeleporters.remove(item.getId());
|
||||
this.banzaiTeleporters.remove(item.getId()); this.specialItemsById.remove(item.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionBattleBanzaiTeleporter> getBanzaiTeleporters() {
|
||||
@@ -155,11 +156,11 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addNest(InteractionNest item) {
|
||||
this.nests.put(item.getId(), item);
|
||||
this.nests.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removeNest(InteractionNest item) {
|
||||
this.nests.remove(item.getId());
|
||||
this.nests.remove(item.getId()); this.specialItemsById.remove(item.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionNest> getNests() {
|
||||
@@ -177,11 +178,11 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addPetDrink(InteractionPetDrink item) {
|
||||
this.petDrinks.put(item.getId(), item);
|
||||
this.petDrinks.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removePetDrink(InteractionPetDrink item) {
|
||||
this.petDrinks.remove(item.getId());
|
||||
this.petDrinks.remove(item.getId()); this.specialItemsById.remove(item.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionPetDrink> getPetDrinks() {
|
||||
@@ -199,11 +200,11 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addPetFood(InteractionPetFood item) {
|
||||
this.petFoods.put(item.getId(), item);
|
||||
this.petFoods.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removePetFood(InteractionPetFood petFood) {
|
||||
this.petFoods.remove(petFood.getId());
|
||||
this.petFoods.remove(petFood.getId()); this.specialItemsById.remove(petFood.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionPetFood> getPetFoods() {
|
||||
@@ -221,11 +222,11 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addPetToy(InteractionPetToy item) {
|
||||
this.petToys.put(item.getId(), item);
|
||||
this.petToys.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removePetToy(InteractionPetToy petToy) {
|
||||
this.petToys.remove(petToy.getId());
|
||||
this.petToys.remove(petToy.getId()); this.specialItemsById.remove(petToy.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionPetToy> getPetToys() {
|
||||
@@ -243,11 +244,11 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addPetTree(InteractionPetTree item) {
|
||||
this.petTrees.put(item.getId(), item);
|
||||
this.petTrees.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removePetTree(InteractionPetTree petTree) {
|
||||
this.petTrees.remove(petTree.getId());
|
||||
this.petTrees.remove(petTree.getId()); this.specialItemsById.remove(petTree.getId());
|
||||
}
|
||||
|
||||
public THashSet<InteractionPetTree> getPetTrees() {
|
||||
@@ -270,12 +271,14 @@ public class RoomSpecialTypes {
|
||||
synchronized (this.rollers) {
|
||||
this.rollers.put(item.getId(), item);
|
||||
}
|
||||
this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removeRoller(InteractionRoller roller) {
|
||||
synchronized (this.rollers) {
|
||||
this.rollers.remove(roller.getId());
|
||||
}
|
||||
this.specialItemsById.remove(roller.getId());
|
||||
}
|
||||
|
||||
public THashMap<Integer, InteractionRoller> getRollers() {
|
||||
@@ -469,11 +472,11 @@ public class RoomSpecialTypes {
|
||||
// Add to type-based index
|
||||
this.wiredTriggers.computeIfAbsent(trigger.getType(), k -> ConcurrentHashMap.newKeySet())
|
||||
.add(trigger);
|
||||
|
||||
// Add to spatial index
|
||||
long key = coordinateKey(trigger.getX(), trigger.getY());
|
||||
this.wiredTriggersByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
|
||||
.add(trigger);
|
||||
this.specialItemsById.put(trigger.getId(), trigger);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -489,7 +492,6 @@ public class RoomSpecialTypes {
|
||||
this.wiredTriggers.remove(trigger.getType());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from spatial index
|
||||
long key = coordinateKey(trigger.getX(), trigger.getY());
|
||||
Set<InteractionWiredTrigger> locationTriggers = this.wiredTriggersByLocation.get(key);
|
||||
@@ -499,6 +501,7 @@ public class RoomSpecialTypes {
|
||||
this.wiredTriggersByLocation.remove(key);
|
||||
}
|
||||
}
|
||||
this.specialItemsById.remove(trigger.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -589,11 +592,11 @@ public class RoomSpecialTypes {
|
||||
// Add to type-based index
|
||||
this.wiredEffects.computeIfAbsent(effect.getType(), k -> ConcurrentHashMap.newKeySet())
|
||||
.add(effect);
|
||||
|
||||
// Add to spatial index
|
||||
long key = coordinateKey(effect.getX(), effect.getY());
|
||||
this.wiredEffectsByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
|
||||
.add(effect);
|
||||
this.specialItemsById.put(effect.getId(), effect);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -609,7 +612,6 @@ public class RoomSpecialTypes {
|
||||
this.wiredEffects.remove(effect.getType());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from spatial index
|
||||
long key = coordinateKey(effect.getX(), effect.getY());
|
||||
Set<InteractionWiredEffect> locationEffects = this.wiredEffectsByLocation.get(key);
|
||||
@@ -619,6 +621,7 @@ public class RoomSpecialTypes {
|
||||
this.wiredEffectsByLocation.remove(key);
|
||||
}
|
||||
}
|
||||
this.specialItemsById.remove(effect.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -709,11 +712,11 @@ public class RoomSpecialTypes {
|
||||
// Add to type-based index
|
||||
this.wiredConditions.computeIfAbsent(condition.getType(), k -> ConcurrentHashMap.newKeySet())
|
||||
.add(condition);
|
||||
|
||||
// Add to spatial index
|
||||
long key = coordinateKey(condition.getX(), condition.getY());
|
||||
this.wiredConditionsByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
|
||||
.add(condition);
|
||||
this.specialItemsById.put(condition.getId(), condition);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -729,7 +732,6 @@ public class RoomSpecialTypes {
|
||||
this.wiredConditions.remove(condition.getType());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from spatial index
|
||||
long key = coordinateKey(condition.getX(), condition.getY());
|
||||
Set<InteractionWiredCondition> locationConditions = this.wiredConditionsByLocation.get(key);
|
||||
@@ -739,6 +741,7 @@ public class RoomSpecialTypes {
|
||||
this.wiredConditionsByLocation.remove(key);
|
||||
}
|
||||
}
|
||||
this.specialItemsById.remove(condition.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -805,11 +808,11 @@ public class RoomSpecialTypes {
|
||||
*/
|
||||
public void addExtra(InteractionWiredExtra extra) {
|
||||
this.wiredExtras.put(extra.getId(), extra);
|
||||
|
||||
// Add to spatial index
|
||||
long key = coordinateKey(extra.getX(), extra.getY());
|
||||
this.wiredExtrasByLocation.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
|
||||
.add(extra);
|
||||
this.specialItemsById.put(extra.getId(), extra);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -818,7 +821,6 @@ public class RoomSpecialTypes {
|
||||
*/
|
||||
public void removeExtra(InteractionWiredExtra extra) {
|
||||
this.wiredExtras.remove(extra.getId());
|
||||
|
||||
// Remove from spatial index
|
||||
long key = coordinateKey(extra.getX(), extra.getY());
|
||||
Set<InteractionWiredExtra> locationExtras = this.wiredExtrasByLocation.get(key);
|
||||
@@ -828,6 +830,7 @@ public class RoomSpecialTypes {
|
||||
this.wiredExtrasByLocation.remove(key);
|
||||
}
|
||||
}
|
||||
this.specialItemsById.remove(extra.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -880,11 +883,11 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addGameScoreboard(InteractionGameScoreboard scoreboard) {
|
||||
this.gameScoreboards.put(scoreboard.getId(), scoreboard);
|
||||
this.gameScoreboards.put(scoreboard.getId(), scoreboard); this.specialItemsById.put(scoreboard.getId(), scoreboard);
|
||||
}
|
||||
|
||||
public void removeScoreboard(InteractionGameScoreboard scoreboard) {
|
||||
this.gameScoreboards.remove(scoreboard.getId());
|
||||
this.gameScoreboards.remove(scoreboard.getId()); this.specialItemsById.remove(scoreboard.getId());
|
||||
}
|
||||
|
||||
public THashMap<Integer, InteractionFreezeScoreboard> getFreezeScoreboards() {
|
||||
@@ -980,11 +983,11 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addGameGate(InteractionGameGate gameGate) {
|
||||
this.gameGates.put(gameGate.getId(), gameGate);
|
||||
this.gameGates.put(gameGate.getId(), gameGate); this.specialItemsById.put(gameGate.getId(), gameGate);
|
||||
}
|
||||
|
||||
public void removeGameGate(InteractionGameGate gameGate) {
|
||||
this.gameGates.remove(gameGate.getId());
|
||||
this.gameGates.remove(gameGate.getId()); this.specialItemsById.remove(gameGate.getId());
|
||||
}
|
||||
|
||||
public THashMap<Integer, InteractionFreezeGate> getFreezeGates() {
|
||||
@@ -1021,11 +1024,11 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addGameTimer(InteractionGameTimer gameTimer) {
|
||||
this.gameTimers.put(gameTimer.getId(), gameTimer);
|
||||
this.gameTimers.put(gameTimer.getId(), gameTimer); this.specialItemsById.put(gameTimer.getId(), gameTimer);
|
||||
}
|
||||
|
||||
public void removeGameTimer(InteractionGameTimer gameTimer) {
|
||||
this.gameTimers.remove(gameTimer.getId());
|
||||
this.gameTimers.remove(gameTimer.getId()); this.specialItemsById.remove(gameTimer.getId());
|
||||
}
|
||||
|
||||
public THashMap<Integer, InteractionGameTimer> getGameTimers() {
|
||||
@@ -1043,7 +1046,7 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void addFreezeExitTile(InteractionFreezeExitTile freezeExitTile) {
|
||||
this.freezeExitTile.put(freezeExitTile.getId(), freezeExitTile);
|
||||
this.freezeExitTile.put(freezeExitTile.getId(), freezeExitTile); this.specialItemsById.put(freezeExitTile.getId(), freezeExitTile);
|
||||
}
|
||||
|
||||
public THashMap<Integer, InteractionFreezeExitTile> getFreezeExitTiles() {
|
||||
@@ -1051,7 +1054,7 @@ public class RoomSpecialTypes {
|
||||
}
|
||||
|
||||
public void removeFreezeExitTile(InteractionFreezeExitTile freezeExitTile) {
|
||||
this.freezeExitTile.remove(freezeExitTile.getId());
|
||||
this.freezeExitTile.remove(freezeExitTile.getId()); this.specialItemsById.remove(freezeExitTile.getId());
|
||||
}
|
||||
|
||||
public boolean hasFreezeExitTile() {
|
||||
@@ -1062,12 +1065,14 @@ public class RoomSpecialTypes {
|
||||
synchronized (this.undefined) {
|
||||
this.undefined.put(item.getId(), item);
|
||||
}
|
||||
this.specialItemsById.put(item.getId(), item);
|
||||
}
|
||||
|
||||
public void removeUndefined(HabboItem item) {
|
||||
synchronized (this.undefined) {
|
||||
this.undefined.remove(item.getId());
|
||||
}
|
||||
this.specialItemsById.remove(item.getId());
|
||||
}
|
||||
|
||||
public THashSet<HabboItem> getItemsOfType(Class<? extends HabboItem> type) {
|
||||
@@ -1130,6 +1135,10 @@ public class RoomSpecialTypes {
|
||||
this.cycleTasks.remove(task);
|
||||
}
|
||||
|
||||
public HabboItem getSpecialItem(int itemId) {
|
||||
return this.specialItemsById.get(itemId);
|
||||
}
|
||||
|
||||
public synchronized void dispose() {
|
||||
this.banzaiTeleporters.clear();
|
||||
this.nests.clear();
|
||||
@@ -1142,6 +1151,7 @@ public class RoomSpecialTypes {
|
||||
this.wiredTriggers.clear();
|
||||
this.wiredEffects.clear();
|
||||
this.wiredConditions.clear();
|
||||
this.wiredExtras.clear();
|
||||
|
||||
this.gameScoreboards.clear();
|
||||
this.gameGates.clear();
|
||||
@@ -1150,6 +1160,7 @@ public class RoomSpecialTypes {
|
||||
this.freezeExitTile.clear();
|
||||
this.undefined.clear();
|
||||
this.cycleTasks.clear();
|
||||
this.specialItemsById.clear();
|
||||
}
|
||||
|
||||
public Rectangle tentAt(RoomTile location) {
|
||||
|
||||
@@ -29,7 +29,6 @@ public class RoomTileManager {
|
||||
*/
|
||||
public void updateTile(RoomTile tile) {
|
||||
if (tile != null) {
|
||||
this.room.tileCache.remove(tile);
|
||||
this.room.getItemManager().tileCache.remove(tile);
|
||||
tile.setStackHeight(this.getStackHeight(tile.x, tile.y, false));
|
||||
tile.setState(this.calculateTileState(tile));
|
||||
@@ -41,7 +40,6 @@ public class RoomTileManager {
|
||||
*/
|
||||
public void updateTiles(THashSet<RoomTile> tiles) {
|
||||
for (RoomTile tile : tiles) {
|
||||
this.room.tileCache.remove(tile);
|
||||
this.room.getItemManager().tileCache.remove(tile);
|
||||
tile.setStackHeight(this.getStackHeight(tile.x, tile.y, false));
|
||||
tile.setState(this.calculateTileState(tile));
|
||||
|
||||
@@ -71,6 +71,24 @@ public class RoomUnitManager {
|
||||
*/
|
||||
public void clear() {
|
||||
synchronized (this.room.roomUnitLock) {
|
||||
for (Habbo habbo : this.currentHabbos.values()) {
|
||||
if (habbo.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(habbo.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(habbo.getRoomUnit());
|
||||
}
|
||||
}
|
||||
for (Bot bot : this.currentBots.valueCollection()) {
|
||||
if (bot.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(bot.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(bot.getRoomUnit());
|
||||
}
|
||||
}
|
||||
for (Pet pet : this.currentPets.valueCollection()) {
|
||||
if (pet.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(pet.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(pet.getRoomUnit());
|
||||
}
|
||||
}
|
||||
this.unitCounter = 0;
|
||||
this.currentHabbos.clear();
|
||||
this.currentPets.clear();
|
||||
@@ -222,6 +240,8 @@ public class RoomUnitManager {
|
||||
}
|
||||
|
||||
if (habbo.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(habbo.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(habbo.getRoomUnit());
|
||||
WiredManager.triggerUserLeavesRoom(this.room, habbo.getRoomUnit());
|
||||
if (WiredFreezeUtil.isFrozen(habbo.getRoomUnit())) {
|
||||
WiredFreezeUtil.unfreeze(this.room, habbo.getRoomUnit());
|
||||
@@ -646,14 +666,22 @@ public class RoomUnitManager {
|
||||
public boolean removeBot(Bot bot) {
|
||||
synchronized (this.currentBots) {
|
||||
if (this.currentBots.containsKey(bot.getId())) {
|
||||
if (bot.getRoomUnit() != null && bot.getRoomUnit().getCurrentLocation() != null) {
|
||||
bot.getRoomUnit().getCurrentLocation().removeUnit(bot.getRoomUnit());
|
||||
if (bot.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(bot.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(bot.getRoomUnit());
|
||||
if (bot.getRoomUnit().getCurrentLocation() != null) {
|
||||
bot.getRoomUnit().getCurrentLocation().removeUnit(bot.getRoomUnit());
|
||||
}
|
||||
}
|
||||
|
||||
this.currentBots.remove(bot.getId());
|
||||
bot.getRoomUnit().setInRoom(false);
|
||||
if (bot.getRoomUnit() != null) {
|
||||
bot.getRoomUnit().setInRoom(false);
|
||||
}
|
||||
bot.setRoom(null);
|
||||
this.room.sendComposer(new RoomUserRemoveComposer(bot.getRoomUnit()).compose());
|
||||
if (bot.getRoomUnit() != null) {
|
||||
this.room.sendComposer(new RoomUserRemoveComposer(bot.getRoomUnit()).compose());
|
||||
}
|
||||
bot.setRoomUnit(null);
|
||||
return true;
|
||||
}
|
||||
@@ -876,7 +904,12 @@ public class RoomUnitManager {
|
||||
* Removes a Pet from the room.
|
||||
*/
|
||||
public Pet removePet(int petId) {
|
||||
return this.currentPets.remove(petId);
|
||||
Pet pet = this.currentPets.remove(petId);
|
||||
if (pet != null && pet.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(pet.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(pet.getRoomUnit());
|
||||
}
|
||||
return pet;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1454,6 +1487,24 @@ public class RoomUnitManager {
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
for (Habbo habbo : this.currentHabbos.values()) {
|
||||
if (habbo.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(habbo.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(habbo.getRoomUnit());
|
||||
}
|
||||
}
|
||||
for (Bot bot : this.currentBots.valueCollection()) {
|
||||
if (bot.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(bot.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(bot.getRoomUnit());
|
||||
}
|
||||
}
|
||||
for (Pet pet : this.currentPets.valueCollection()) {
|
||||
if (pet.getRoomUnit() != null) {
|
||||
WiredMoveCarryHelper.cleanupRoomUnit(pet.getRoomUnit());
|
||||
WiredUserMovementHelper.cleanupRoomUnit(pet.getRoomUnit());
|
||||
}
|
||||
}
|
||||
this.currentHabbos.clear();
|
||||
this.currentBots.clear();
|
||||
this.currentPets.clear();
|
||||
|
||||
@@ -35,6 +35,7 @@ public class RoomUserVariableManager {
|
||||
|
||||
private final Room room;
|
||||
private final ConcurrentHashMap<Integer, ConcurrentHashMap<Integer, VariableAssignment>> activeAssignmentsByUserId;
|
||||
private final java.util.concurrent.atomic.AtomicBoolean broadcastRequested = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
|
||||
public RoomUserVariableManager(Room room) {
|
||||
this.room = room;
|
||||
@@ -660,7 +661,22 @@ public class RoomUserVariableManager {
|
||||
habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(this.createSnapshot(), this.room.getFurniVariableManager().createSnapshot(), this.room.getRoomVariableManager().createSnapshot()));
|
||||
}
|
||||
|
||||
public void requestBroadcast() {
|
||||
if (this.broadcastRequested.compareAndSet(false, true)) {
|
||||
Emulator.getThreading().run(() -> {
|
||||
this.broadcastRequested.set(false);
|
||||
if (this.room.isLoaded()) {
|
||||
this.broadcastSnapshotRaw();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastSnapshot() {
|
||||
this.requestBroadcast();
|
||||
}
|
||||
|
||||
public void broadcastSnapshotRaw() {
|
||||
Snapshot userSnapshot = this.createSnapshot();
|
||||
RoomFurniVariableManager.Snapshot furniSnapshot = this.room.getFurniVariableManager().createSnapshot();
|
||||
RoomVariableManager.Snapshot roomSnapshot = this.room.getRoomVariableManager().createSnapshot();
|
||||
|
||||
@@ -35,6 +35,7 @@ public class RoomVariableManager {
|
||||
private final Room room;
|
||||
private final ConcurrentHashMap<Integer, VariableAssignment> activeAssignmentsByDefinitionId;
|
||||
private volatile boolean persistentValuesLoaded;
|
||||
private final java.util.concurrent.atomic.AtomicBoolean broadcastRequested = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
|
||||
public RoomVariableManager(Room room) {
|
||||
this.room = room;
|
||||
@@ -433,7 +434,22 @@ public class RoomVariableManager {
|
||||
habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(this.room.getUserVariableManager().createSnapshot(), this.room.getFurniVariableManager().createSnapshot(), this.createSnapshot()));
|
||||
}
|
||||
|
||||
public void requestBroadcast() {
|
||||
if (this.broadcastRequested.compareAndSet(false, true)) {
|
||||
Emulator.getThreading().run(() -> {
|
||||
this.broadcastRequested.set(false);
|
||||
if (this.room.isLoaded()) {
|
||||
this.broadcastSnapshotRaw();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastSnapshot() {
|
||||
this.requestBroadcast();
|
||||
}
|
||||
|
||||
public void broadcastSnapshotRaw() {
|
||||
RoomUserVariableManager.Snapshot userSnapshot = this.room.getUserVariableManager().createSnapshot();
|
||||
RoomFurniVariableManager.Snapshot furniSnapshot = this.room.getFurniVariableManager().createSnapshot();
|
||||
Snapshot roomSnapshot = this.createSnapshot();
|
||||
|
||||
@@ -38,6 +38,7 @@ public class HabboManager {
|
||||
|
||||
private final ConcurrentHashMap<Integer, Habbo> onlineHabbos;
|
||||
private final ConcurrentHashMap<String, Habbo> onlineHabbosByName;
|
||||
private final ConcurrentHashMap<Integer, String> usernameCache = new ConcurrentHashMap<>();
|
||||
|
||||
public HabboManager() {
|
||||
long millis = System.currentTimeMillis();
|
||||
@@ -158,6 +159,26 @@ public class HabboManager {
|
||||
return this.getHabbo(id).getHabboInfo();
|
||||
}
|
||||
|
||||
public String getCachedUsername(int id) {
|
||||
String cached = this.usernameCache.get(id);
|
||||
if (cached != null) return cached;
|
||||
|
||||
Habbo online = this.getHabbo(id);
|
||||
if (online != null) {
|
||||
String name = online.getHabboInfo().getUsername();
|
||||
this.usernameCache.put(id, name);
|
||||
return name;
|
||||
}
|
||||
|
||||
HabboInfo offline = getOfflineHabboInfo(id);
|
||||
if (offline != null) {
|
||||
String name = offline.getUsername();
|
||||
this.usernameCache.put(id, name);
|
||||
return name;
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
public int getOnlineCount() {
|
||||
return this.onlineHabbos.size();
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ public class HabboStats implements Runnable {
|
||||
public long lastTradeTimestamp = Emulator.getIntUnixTimestamp();
|
||||
public long lastGiftTimestamp = Emulator.getIntUnixTimestamp();
|
||||
public long lastPurchaseTimestamp = Emulator.getIntUnixTimestamp();
|
||||
public long lastFloorplanSaveTimestamp = 0;
|
||||
public int uiFlags;
|
||||
public boolean hasGottenDefaultSavedSearches;
|
||||
private HabboInfo habboInfo;
|
||||
|
||||
@@ -43,6 +43,7 @@ import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -89,11 +90,11 @@ public class WiredHandler {
|
||||
long millis = System.currentTimeMillis();
|
||||
List<LegacyExecutionPlan> executionPlans = new ArrayList<>();
|
||||
|
||||
List<RoomTile> triggeredTiles = new ArrayList<>();
|
||||
LinkedHashSet<Long> triggeredTiles = new LinkedHashSet<>();
|
||||
for (InteractionWiredTrigger trigger : triggers) {
|
||||
RoomTile tile = room.getLayout().getTile(trigger.getX(), trigger.getY());
|
||||
long coordinateKey = toTileCoordinateKey(trigger.getX(), trigger.getY());
|
||||
|
||||
if (triggeredTiles.contains(tile))
|
||||
if (!triggeredTiles.add(coordinateKey))
|
||||
continue;
|
||||
|
||||
LegacyExecutionPlan executionPlan = new LegacyExecutionPlan();
|
||||
@@ -103,8 +104,6 @@ public class WiredHandler {
|
||||
|
||||
if (triggerType.equals(WiredTriggerType.SAY_SOMETHING))
|
||||
talked = true;
|
||||
|
||||
triggeredTiles.add(tile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,20 +138,19 @@ public class WiredHandler {
|
||||
long millis = System.currentTimeMillis();
|
||||
List<LegacyExecutionPlan> executionPlans = new ArrayList<>();
|
||||
|
||||
List<RoomTile> triggeredTiles = new ArrayList<>();
|
||||
LinkedHashSet<Long> triggeredTiles = new LinkedHashSet<>();
|
||||
for (InteractionWiredTrigger trigger : triggers) {
|
||||
if (trigger.getClass() != triggerType) continue;
|
||||
|
||||
RoomTile tile = room.getLayout().getTile(trigger.getX(), trigger.getY());
|
||||
long coordinateKey = toTileCoordinateKey(trigger.getX(), trigger.getY());
|
||||
|
||||
if (triggeredTiles.contains(tile))
|
||||
if (!triggeredTiles.add(coordinateKey))
|
||||
continue;
|
||||
|
||||
LegacyExecutionPlan executionPlan = new LegacyExecutionPlan();
|
||||
|
||||
if (handle(trigger, roomUnit, room, stuff, executionPlan)) {
|
||||
executionPlans.add(executionPlan);
|
||||
triggeredTiles.add(tile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +185,11 @@ public class WiredHandler {
|
||||
WiredExtraExecutionLimit executionLimitExtra = null;
|
||||
WiredExtraRandom randomExtra = null;
|
||||
|
||||
int conditionEvaluationMode = WiredExtraOrEval.MODE_ALL;
|
||||
int conditionEvaluationValue = 1;
|
||||
boolean hasExtraUnseen = false;
|
||||
boolean hasExtraExecuteInOrder = false;
|
||||
|
||||
for (InteractionWiredExtra extra : extras) {
|
||||
if (executionLimitExtra == null && extra instanceof WiredExtraExecutionLimit) {
|
||||
executionLimitExtra = (WiredExtraExecutionLimit) extra;
|
||||
@@ -195,18 +198,22 @@ public class WiredHandler {
|
||||
if (randomExtra == null && extra instanceof WiredExtraRandom) {
|
||||
randomExtra = (WiredExtraRandom) extra;
|
||||
}
|
||||
|
||||
if (!hasExtraUnseen && extra instanceof WiredExtraUnseen) {
|
||||
hasExtraUnseen = true;
|
||||
}
|
||||
|
||||
if (!hasExtraExecuteInOrder && extra instanceof WiredExtraExecuteInOrder) {
|
||||
hasExtraExecuteInOrder = true;
|
||||
}
|
||||
|
||||
if (extra instanceof WiredExtraOrEval) {
|
||||
conditionEvaluationMode = ((WiredExtraOrEval) extra).getEvaluationMode();
|
||||
conditionEvaluationValue = ((WiredExtraOrEval) extra).getCompareValue();
|
||||
}
|
||||
}
|
||||
|
||||
if (!conditions.isEmpty()) {
|
||||
int conditionEvaluationMode = WiredExtraOrEval.MODE_ALL;
|
||||
int conditionEvaluationValue = 1;
|
||||
for (InteractionWiredExtra extra : extras) {
|
||||
if (extra instanceof WiredExtraOrEval) {
|
||||
conditionEvaluationMode = ((WiredExtraOrEval) extra).getEvaluationMode();
|
||||
conditionEvaluationValue = ((WiredExtraOrEval) extra).getCompareValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!evaluateConditions(conditions, roomUnit, room, stuff, conditionEvaluationMode, conditionEvaluationValue)) {
|
||||
for (InteractionWiredCondition condition : conditions) {
|
||||
@@ -230,9 +237,6 @@ public class WiredHandler {
|
||||
|
||||
trigger.setCooldown(millis);
|
||||
|
||||
boolean hasExtraUnseen = room.getRoomSpecialTypes().hasExtraType(trigger.getX(), trigger.getY(), WiredExtraUnseen.class);
|
||||
boolean hasExtraExecuteInOrder = room.getRoomSpecialTypes().hasExtraType(trigger.getX(), trigger.getY(), WiredExtraExecuteInOrder.class);
|
||||
|
||||
for (InteractionWiredExtra extra : extras) {
|
||||
extra.activateBox(room, roomUnit, millis);
|
||||
}
|
||||
@@ -244,7 +248,7 @@ public class WiredHandler {
|
||||
executionPlan.executeInOrder = hasExtraExecuteInOrder;
|
||||
|
||||
if (hasExtraUnseen) {
|
||||
for (InteractionWiredExtra extra : room.getRoomSpecialTypes().getExtras(trigger.getX(), trigger.getY())) {
|
||||
for (InteractionWiredExtra extra : extras) {
|
||||
if (extra instanceof WiredExtraUnseen) {
|
||||
extra.setExtradata(extra.getExtradata().equals("1") ? "0" : "1");
|
||||
InteractionWiredEffect effect = ((WiredExtraUnseen) extra).getUnseenEffect(effectList);
|
||||
@@ -357,20 +361,14 @@ public class WiredHandler {
|
||||
}
|
||||
}
|
||||
|
||||
LinkedHashSet<Integer> delays = new LinkedHashSet<>();
|
||||
Map<Integer, List<InteractionWiredEffect>> delayBatches = new LinkedHashMap<>();
|
||||
for (InteractionWiredEffect effect : queueableEffects) {
|
||||
delays.add(effect.getDelay());
|
||||
delayBatches.computeIfAbsent(effect.getDelay(), ignored -> new ArrayList<>()).add(effect);
|
||||
}
|
||||
|
||||
for (Integer delay : delays) {
|
||||
List<InteractionWiredEffect> delayBatch = new ArrayList<>();
|
||||
|
||||
for (InteractionWiredEffect effect : queueableEffects) {
|
||||
if (effect.getDelay() == delay) {
|
||||
delayBatch.add(effect);
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<Integer, List<InteractionWiredEffect>> entry : delayBatches.entrySet()) {
|
||||
Integer delay = entry.getKey();
|
||||
List<InteractionWiredEffect> delayBatch = entry.getValue();
|
||||
if (delayBatch.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
@@ -424,11 +422,19 @@ public class WiredHandler {
|
||||
|
||||
public static GsonBuilder getGsonBuilder() {
|
||||
if(gsonBuilder == null) {
|
||||
gsonBuilder = new GsonBuilder();
|
||||
synchronized (WiredHandler.class) {
|
||||
if (gsonBuilder == null) {
|
||||
gsonBuilder = new GsonBuilder();
|
||||
}
|
||||
}
|
||||
}
|
||||
return gsonBuilder;
|
||||
}
|
||||
|
||||
private static long toTileCoordinateKey(int x, int y) {
|
||||
return (((long) x) << 32) | (y & 0xffffffffL);
|
||||
}
|
||||
|
||||
public static boolean executeEffectsAtTiles(THashSet<RoomTile> tiles, final RoomUnit roomUnit, final Room room, final Object[] stuff) {
|
||||
for (RoomTile tile : tiles) {
|
||||
if (room != null) {
|
||||
@@ -470,7 +476,7 @@ public class WiredHandler {
|
||||
|
||||
private static void completeReward(Habbo habbo, WiredEffectGiveReward wiredBox, WiredGiveRewardItem reward, int successCode) {
|
||||
if (wiredBox.limit > 0)
|
||||
wiredBox.given++;
|
||||
wiredBox.incrementGiven();
|
||||
|
||||
persistReward(wiredBox.getId(), habbo.getHabboInfo().getId(), reward.id, Emulator.getIntUnixTimestamp());
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(successCode));
|
||||
@@ -569,93 +575,124 @@ public class WiredHandler {
|
||||
|
||||
public static boolean getReward(Habbo habbo, WiredEffectGiveReward wiredBox) {
|
||||
if (wiredBox.limit > 0) {
|
||||
if (wiredBox.limit - wiredBox.given == 0) {
|
||||
if (wiredBox.limit - wiredBox.getGiven() == 0) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.LIMITED_NO_MORE_AVAILABLE));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) as row_count, wired_rewards_given.* FROM wired_rewards_given WHERE user_id = ? AND wired_item = ? ORDER BY timestamp DESC LIMIT ?", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)) {
|
||||
WiredGiveRewardItem rewardToGive = null;
|
||||
int failureCode = -1;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM wired_rewards_given WHERE user_id = ? AND wired_item = ? ORDER BY timestamp DESC LIMIT ?", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)) {
|
||||
statement.setInt(1, habbo.getHabboInfo().getId());
|
||||
statement.setInt(2, wiredBox.getId());
|
||||
statement.setInt(3, wiredBox.rewardItems.size());
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.first()) {
|
||||
if (set.getInt("row_count") >= 1) {
|
||||
set.last();
|
||||
int rowCount = set.getRow();
|
||||
set.first();
|
||||
|
||||
if (rowCount >= 1) {
|
||||
if (wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_ONCE) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED));
|
||||
return false;
|
||||
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED;
|
||||
}
|
||||
}
|
||||
|
||||
set.beforeFirst();
|
||||
if (set.next()) {
|
||||
if (failureCode == -1) {
|
||||
if (wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_N_MINUTES) {
|
||||
if (Emulator.getIntUnixTimestamp() - set.getInt("timestamp") <= 60) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_MINUTE));
|
||||
return false;
|
||||
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_MINUTE;
|
||||
}
|
||||
}
|
||||
|
||||
if (wiredBox.uniqueRewards) {
|
||||
if (set.getInt("row_count") == wiredBox.rewardItems.size()) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALL_COLLECTED));
|
||||
return false;
|
||||
if (failureCode == -1 && wiredBox.uniqueRewards) {
|
||||
if (rowCount == wiredBox.rewardItems.size()) {
|
||||
failureCode = WiredRewardAlertComposer.REWARD_ALL_COLLECTED;
|
||||
}
|
||||
}
|
||||
|
||||
if (wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_N_HOURS) {
|
||||
if (failureCode == -1 && wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_N_HOURS) {
|
||||
if (!(Emulator.getIntUnixTimestamp() - set.getInt("timestamp") >= (3600 * wiredBox.limitationInterval))) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_HOUR));
|
||||
return false;
|
||||
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_HOUR;
|
||||
}
|
||||
}
|
||||
|
||||
if (wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_N_DAY) {
|
||||
if (failureCode == -1 && wiredBox.rewardTime == WiredEffectGiveReward.LIMIT_N_DAY) {
|
||||
if (!(Emulator.getIntUnixTimestamp() - set.getInt("timestamp") >= (86400 * wiredBox.limitationInterval))) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_TODAY));
|
||||
return false;
|
||||
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_TODAY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wiredBox.uniqueRewards) {
|
||||
for (WiredGiveRewardItem item : wiredBox.rewardItems) {
|
||||
set.beforeFirst();
|
||||
boolean found = false;
|
||||
if (failureCode == -1) {
|
||||
if (wiredBox.uniqueRewards) {
|
||||
for (WiredGiveRewardItem item : wiredBox.rewardItems) {
|
||||
set.beforeFirst();
|
||||
boolean found = false;
|
||||
|
||||
while (set.next()) {
|
||||
if (set.getInt("reward_id") == item.id)
|
||||
found = true;
|
||||
while (set.next()) {
|
||||
if (set.getInt("reward_id") == item.id)
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
rewardToGive = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
return giveReward(habbo, wiredBox, item);
|
||||
if (rewardToGive == null) {
|
||||
failureCode = WiredRewardAlertComposer.REWARD_ALL_COLLECTED;
|
||||
}
|
||||
}
|
||||
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALL_COLLECTED));
|
||||
return false;
|
||||
} else {
|
||||
int randomNumber = Emulator.getRandom().nextInt(101);
|
||||
|
||||
int count = 0;
|
||||
for (WiredGiveRewardItem item : wiredBox.rewardItems) {
|
||||
if (randomNumber >= count && randomNumber <= (count + item.probability)) {
|
||||
return giveReward(habbo, wiredBox, item);
|
||||
}
|
||||
|
||||
count += item.probability;
|
||||
}
|
||||
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.UNLUCKY_NO_REWARD));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (failureCode != -1) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(failureCode));
|
||||
return false;
|
||||
}
|
||||
|
||||
// If no unique reward was determined and there are no failures, pick a random reward or the first unique one
|
||||
if (rewardToGive == null) {
|
||||
if (wiredBox.uniqueRewards) {
|
||||
if (!wiredBox.rewardItems.isEmpty()) {
|
||||
rewardToGive = wiredBox.rewardItems.get(0);
|
||||
} else {
|
||||
failureCode = WiredRewardAlertComposer.REWARD_ALL_COLLECTED;
|
||||
}
|
||||
} else {
|
||||
int randomNumber = Emulator.getRandom().nextInt(101);
|
||||
int count = 0;
|
||||
for (WiredGiveRewardItem item : wiredBox.rewardItems) {
|
||||
if (randomNumber >= count && randomNumber <= (count + item.probability)) {
|
||||
rewardToGive = item;
|
||||
break;
|
||||
}
|
||||
count += item.probability;
|
||||
}
|
||||
|
||||
if (rewardToGive == null) {
|
||||
failureCode = WiredRewardAlertComposer.UNLUCKY_NO_REWARD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failureCode != -1) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(failureCode));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rewardToGive != null) {
|
||||
return giveReward(habbo, wiredBox, rewardToGive);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
+19
-23
@@ -71,27 +71,12 @@ public final class RoomWiredStackIndex implements WiredStackIndex {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
if (useCache) {
|
||||
Map<WiredEvent.Type, List<WiredStack>> roomCache = cache.get(room.getId());
|
||||
if (roomCache != null) {
|
||||
List<WiredStack> cached = roomCache.get(type);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
return cache.computeIfAbsent(room.getId(), k -> new ConcurrentHashMap<>())
|
||||
.computeIfAbsent(type, t -> buildStacks(room, t));
|
||||
} else {
|
||||
return buildStacks(room, type);
|
||||
}
|
||||
|
||||
// Build stacks for this event type
|
||||
List<WiredStack> stacks = buildStacks(room, type);
|
||||
|
||||
// Cache the result
|
||||
if (useCache) {
|
||||
cache.computeIfAbsent(room.getId(), k -> new ConcurrentHashMap<>())
|
||||
.put(type, stacks);
|
||||
}
|
||||
|
||||
return stacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -206,16 +191,27 @@ public final class RoomWiredStackIndex implements WiredStackIndex {
|
||||
THashSet<InteractionWiredExtra> extras = specialTypes.getExtras(x, y);
|
||||
int conditionEvaluationMode = WiredExtraOrEval.MODE_ALL;
|
||||
int conditionEvaluationValue = 1;
|
||||
boolean useRandom = specialTypes.hasExtraType(x, y, WiredExtraRandom.class);
|
||||
boolean useUnseen = specialTypes.hasExtraType(x, y, WiredExtraUnseen.class);
|
||||
boolean executeInOrder = specialTypes.hasExtraType(x, y, WiredExtraExecuteInOrder.class);
|
||||
boolean useRandom = false;
|
||||
boolean useUnseen = false;
|
||||
boolean executeInOrder = false;
|
||||
|
||||
if (extras != null) {
|
||||
for (InteractionWiredExtra extra : extras) {
|
||||
if (!useRandom && extra instanceof WiredExtraRandom) {
|
||||
useRandom = true;
|
||||
}
|
||||
|
||||
if (!useUnseen && extra instanceof WiredExtraUnseen) {
|
||||
useUnseen = true;
|
||||
}
|
||||
|
||||
if (!executeInOrder && extra instanceof WiredExtraExecuteInOrder) {
|
||||
executeInOrder = true;
|
||||
}
|
||||
|
||||
if (extra instanceof WiredExtraOrEval) {
|
||||
conditionEvaluationMode = ((WiredExtraOrEval) extra).getEvaluationMode();
|
||||
conditionEvaluationValue = ((WiredExtraOrEval) extra).getCompareValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,10 +86,10 @@ public final class WiredEngine {
|
||||
public static int MONITOR_USAGE_WINDOW_MS = 1000;
|
||||
|
||||
/** Monitor execution cap per room window */
|
||||
public static int MONITOR_USAGE_LIMIT = 1000;
|
||||
public static int MONITOR_USAGE_LIMIT = 50000;
|
||||
|
||||
/** Maximum delayed events allowed per room at the same time */
|
||||
public static int MONITOR_DELAYED_EVENTS_LIMIT = 100;
|
||||
public static int MONITOR_DELAYED_EVENTS_LIMIT = 50000;
|
||||
|
||||
/** Average execution threshold that marks overload */
|
||||
public static int MONITOR_OVERLOAD_AVERAGE_MS = 50;
|
||||
@@ -180,14 +180,19 @@ public final class WiredEngine {
|
||||
|
||||
int roomId = room.getId();
|
||||
|
||||
if (this.isRoomBanned(roomId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Soft rate limiting to prevent rapid-fire event spam without banning whole rooms
|
||||
if (isRateLimited(roomId, room, event.getType())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check and increment recursion depth to prevent infinite loops
|
||||
int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0);
|
||||
if (currentDepth >= MAX_RECURSION_DEPTH) {
|
||||
int currentDepth = roomRecursionDepth.merge(roomId, 1, Integer::sum);
|
||||
if (currentDepth > MAX_RECURSION_DEPTH) {
|
||||
roomRecursionDepth.merge(roomId, -1, Integer::sum);
|
||||
getDiagnostics(roomId).recordRecursionTimeout(
|
||||
System.currentTimeMillis(),
|
||||
String.format("Recursion depth %d/%d while handling %s", currentDepth, MAX_RECURSION_DEPTH, event.getType().name()),
|
||||
@@ -199,18 +204,12 @@ public final class WiredEngine {
|
||||
debug(room, "RECURSION LIMIT REACHED - aborting to prevent crash");
|
||||
return false;
|
||||
}
|
||||
roomRecursionDepth.put(roomId, currentDepth + 1);
|
||||
|
||||
try {
|
||||
return handleEventInternal(event, room, negateConditions);
|
||||
} finally {
|
||||
// Decrement recursion depth
|
||||
int newDepth = roomRecursionDepth.getOrDefault(roomId, 1) - 1;
|
||||
if (newDepth <= 0) {
|
||||
roomRecursionDepth.remove(roomId);
|
||||
} else {
|
||||
roomRecursionDepth.put(roomId, newDepth);
|
||||
}
|
||||
roomRecursionDepth.compute(roomId, (k, v) -> (v == null || v <= 1) ? null : v - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,28 +233,27 @@ public final class WiredEngine {
|
||||
|
||||
int roomId = room.getId();
|
||||
|
||||
if (this.isRoomBanned(roomId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isRateLimited(roomId, room, event.getType())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0);
|
||||
if (currentDepth >= MAX_RECURSION_DEPTH) {
|
||||
int currentDepth = roomRecursionDepth.merge(roomId, 1, Integer::sum);
|
||||
if (currentDepth > MAX_RECURSION_DEPTH) {
|
||||
roomRecursionDepth.merge(roomId, -1, Integer::sum);
|
||||
LOGGER.warn("Wired recursion limit reached in room {} (depth: {}). " +
|
||||
"Possible infinite loop detected (source item execution). Aborting.", roomId, currentDepth);
|
||||
debug(room, "RECURSION LIMIT REACHED - aborting source-item execution");
|
||||
return false;
|
||||
}
|
||||
roomRecursionDepth.put(roomId, currentDepth + 1);
|
||||
|
||||
try {
|
||||
return handleEventForSourceItemInternal(event, room, sourceItemId);
|
||||
} finally {
|
||||
int newDepth = roomRecursionDepth.getOrDefault(roomId, 1) - 1;
|
||||
if (newDepth <= 0) {
|
||||
roomRecursionDepth.remove(roomId);
|
||||
} else {
|
||||
roomRecursionDepth.put(roomId, newDepth);
|
||||
}
|
||||
roomRecursionDepth.compute(roomId, (k, v) -> (v == null || v <= 1) ? null : v - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1094,7 +1092,13 @@ public final class WiredEngine {
|
||||
}
|
||||
|
||||
private void animateFilteredSelectorBox(Room room, InteractionWiredEffect wiredEffect) {
|
||||
if (room == null || wiredEffect == null || room.isHideWired()) {
|
||||
if (room == null || wiredEffect == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If wired is hidden, skip animation but ensure any stale token is cleaned up
|
||||
if (room.isHideWired()) {
|
||||
this.filteredSelectorAnimationTokens.remove(wiredEffect.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1364,11 +1368,10 @@ public final class WiredEngine {
|
||||
? String.valueOf(stack.triggerItem().getId())
|
||||
: "default";
|
||||
|
||||
int current = unseenIndices.getOrDefault(key, -1);
|
||||
int next = (current + 1) % effectCount;
|
||||
unseenIndices.put(key, next);
|
||||
|
||||
return next;
|
||||
return unseenIndices.compute(key, (k, current) -> {
|
||||
if (current == null) current = -1;
|
||||
return (current + 1) % effectCount;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1622,6 +1625,8 @@ public final class WiredEngine {
|
||||
clearRoomRecursionDepth(roomId);
|
||||
clearRoomRateLimiters(roomId);
|
||||
clearRoomSourceStackCache(roomId);
|
||||
clearRoomDiagnostics(roomId);
|
||||
clearRoomBan(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1684,38 +1689,46 @@ public final class WiredEngine {
|
||||
* @param room the room object
|
||||
*/
|
||||
private void banRoom(int roomId, Room room, WiredEvent.Type eventType, int eventCount) {
|
||||
long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS;
|
||||
bannedRooms.put(roomId, banExpiry);
|
||||
getDiagnostics(roomId).recordKilled(
|
||||
System.currentTimeMillis(),
|
||||
String.format("Rate limit exceeded for %s with %d event(s) in %dms", eventType.name(), eventCount, RATE_LIMIT_WINDOW_MS),
|
||||
eventType.name(),
|
||||
0
|
||||
);
|
||||
|
||||
long banMinutes = WIRED_BAN_DURATION_MS / 60000;
|
||||
|
||||
// Send alert to all users in the room
|
||||
String roomAlertMessage = Emulator.getTexts().getValue("wired.abuse.room.alert")
|
||||
.replace("%minutes%", String.valueOf(banMinutes));
|
||||
room.sendComposer(new GenericAlertComposer(roomAlertMessage).compose());
|
||||
|
||||
// Send scripter bubble alert to staff with room link
|
||||
THashMap<String, String> keys = new THashMap<>();
|
||||
keys.put("title", Emulator.getTexts().getValue("wired.abuse.staff.title"));
|
||||
keys.put("message", Emulator.getTexts().getValue("wired.abuse.staff.message")
|
||||
.replace("%roomname%", room.getName())
|
||||
.replace("%owner%", room.getOwnerName())
|
||||
.replace("%minutes%", String.valueOf(banMinutes)));
|
||||
keys.put("linkUrl", "event:navigator/goto/" + roomId);
|
||||
keys.put("linkTitle", Emulator.getTexts().getValue("wired.abuse.staff.link"));
|
||||
Emulator.getGameEnvironment().getHabboManager().sendPacketToHabbosWithPermission(
|
||||
new BubbleAlertComposer("admin.staffalert", keys).compose(),
|
||||
"acc_modtool_room_info"
|
||||
);
|
||||
|
||||
LOGGER.warn("Wired abuse detected in room {} ({}). Owner: {}. Wired banned for {} minutes.",
|
||||
roomId, room.getName(), room.getOwnerName(), banMinutes);
|
||||
|
||||
// Only actually ban the room if ban duration is configured (> 0)
|
||||
if (WIRED_BAN_DURATION_MS > 0) {
|
||||
long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS;
|
||||
bannedRooms.put(roomId, banExpiry);
|
||||
|
||||
long banMinutes = WIRED_BAN_DURATION_MS / 60000;
|
||||
|
||||
// Send alert to all users in the room
|
||||
String roomAlertMessage = Emulator.getTexts().getValue("wired.abuse.room.alert")
|
||||
.replace("%minutes%", String.valueOf(banMinutes));
|
||||
room.sendComposer(new GenericAlertComposer(roomAlertMessage).compose());
|
||||
|
||||
// Send scripter bubble alert to staff with room link
|
||||
THashMap<String, String> keys = new THashMap<>();
|
||||
keys.put("title", Emulator.getTexts().getValue("wired.abuse.staff.title"));
|
||||
keys.put("message", Emulator.getTexts().getValue("wired.abuse.staff.message")
|
||||
.replace("%roomname%", room.getName())
|
||||
.replace("%owner%", room.getOwnerName())
|
||||
.replace("%minutes%", String.valueOf(banMinutes)));
|
||||
keys.put("linkUrl", "event:navigator/goto/" + roomId);
|
||||
keys.put("linkTitle", Emulator.getTexts().getValue("wired.abuse.staff.link"));
|
||||
Emulator.getGameEnvironment().getHabboManager().sendPacketToHabbosWithPermission(
|
||||
new BubbleAlertComposer("admin.staffalert", keys).compose(),
|
||||
"acc_modtool_room_info"
|
||||
);
|
||||
|
||||
LOGGER.warn("Wired abuse detected in room {} ({}). Owner: {}. Wired banned for {} minutes.",
|
||||
roomId, room.getName(), room.getOwnerName(), banMinutes);
|
||||
} else {
|
||||
// Ban duration is 0 - only log, do not spam alerts or put a ban entry
|
||||
LOGGER.warn("Wired rate limit exceeded in room {} ({}) for event {} ({} events). Ban disabled (wired.abuse.ban.duration.ms=0).",
|
||||
roomId, room.getName(), eventType.name(), eventCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -913,10 +913,7 @@ public final class WiredManager {
|
||||
if (room != null) {
|
||||
room.getFurniVariableManager().clearTransientAssignments();
|
||||
room.getRoomVariableManager().clearTransientAssignments();
|
||||
}
|
||||
|
||||
if (engine != null && room != null) {
|
||||
engine.clearRoomExecutionCaches(room.getId());
|
||||
invalidateRoom(room);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,18 +1109,16 @@ public final class WiredManager {
|
||||
}
|
||||
|
||||
private static void persistReward(int wiredId, int habboId, int rewardId, int timestamp) {
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO wired_rewards_given (wired_item, user_id, reward_id, timestamp) VALUES ( ?, ?, ?, ?)")) {
|
||||
statement.setInt(1, wiredId);
|
||||
statement.setInt(2, habboId);
|
||||
statement.setInt(3, rewardId);
|
||||
statement.setInt(4, timestamp);
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
});
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO wired_rewards_given (wired_item, user_id, reward_id, timestamp) VALUES (?, ?, ?, ?)")) {
|
||||
statement.setInt(1, wiredId);
|
||||
statement.setInt(2, habboId);
|
||||
statement.setInt(3, rewardId);
|
||||
statement.setInt(4, timestamp);
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void completeReward(Habbo habbo, WiredEffectGiveReward wiredBox, WiredGiveRewardItem reward, int successCode) {
|
||||
@@ -1246,96 +1241,128 @@ public final class WiredManager {
|
||||
}
|
||||
|
||||
public static boolean getReward(Habbo habbo, WiredEffectGiveReward wiredBox) {
|
||||
if (wiredBox.getLimit() > 0) {
|
||||
if (wiredBox.getLimit() - wiredBox.getGiven() == 0) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.LIMITED_NO_MORE_AVAILABLE));
|
||||
synchronized (wiredBox) {
|
||||
if (wiredBox.getLimit() > 0) {
|
||||
if (wiredBox.getLimit() - wiredBox.getGiven() == 0) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.LIMITED_NO_MORE_AVAILABLE));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
WiredGiveRewardItem rewardToGive = null;
|
||||
int failureCode = -1;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM wired_rewards_given WHERE user_id = ? AND wired_item = ? ORDER BY timestamp DESC LIMIT ?", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)) {
|
||||
statement.setInt(1, habbo.getHabboInfo().getId());
|
||||
statement.setInt(2, wiredBox.getId());
|
||||
statement.setInt(3, wiredBox.getRewardItems().size());
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.first()) {
|
||||
set.last();
|
||||
int rowCount = set.getRow();
|
||||
set.first();
|
||||
|
||||
if (rowCount >= 1) {
|
||||
if (wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_ONCE) {
|
||||
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED;
|
||||
}
|
||||
}
|
||||
|
||||
if (failureCode == -1) {
|
||||
if (wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_N_MINUTES) {
|
||||
if (Emulator.getIntUnixTimestamp() - set.getInt("timestamp") <= 60) {
|
||||
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_MINUTE;
|
||||
}
|
||||
}
|
||||
|
||||
if (failureCode == -1 && wiredBox.isUniqueRewards()) {
|
||||
if (rowCount == wiredBox.getRewardItems().size()) {
|
||||
failureCode = WiredRewardAlertComposer.REWARD_ALL_COLLECTED;
|
||||
}
|
||||
}
|
||||
|
||||
if (failureCode == -1 && wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_N_HOURS) {
|
||||
if (!(Emulator.getIntUnixTimestamp() - set.getInt("timestamp") >= (3600 * wiredBox.getLimitationInterval()))) {
|
||||
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_HOUR;
|
||||
}
|
||||
}
|
||||
|
||||
if (failureCode == -1 && wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_N_DAY) {
|
||||
if (!(Emulator.getIntUnixTimestamp() - set.getInt("timestamp") >= (86400 * wiredBox.getLimitationInterval()))) {
|
||||
failureCode = WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_TODAY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failureCode == -1) {
|
||||
if (wiredBox.isUniqueRewards()) {
|
||||
for (WiredGiveRewardItem item : wiredBox.getRewardItems()) {
|
||||
set.beforeFirst();
|
||||
boolean found = false;
|
||||
|
||||
while (set.next()) {
|
||||
if (set.getInt("reward_id") == item.id)
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
rewardToGive = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (rewardToGive == null) {
|
||||
failureCode = WiredRewardAlertComposer.REWARD_ALL_COLLECTED;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) as row_count, wired_rewards_given.* FROM wired_rewards_given WHERE user_id = ? AND wired_item = ? ORDER BY timestamp DESC LIMIT ?", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)) {
|
||||
statement.setInt(1, habbo.getHabboInfo().getId());
|
||||
statement.setInt(2, wiredBox.getId());
|
||||
statement.setInt(3, wiredBox.getRewardItems().size());
|
||||
if (failureCode != -1) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(failureCode));
|
||||
return false;
|
||||
}
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.first()) {
|
||||
if (set.getInt("row_count") >= 1) {
|
||||
if (wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_ONCE) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
set.beforeFirst();
|
||||
if (set.next()) {
|
||||
if (wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_N_MINUTES) {
|
||||
if (Emulator.getIntUnixTimestamp() - set.getInt("timestamp") <= 60) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_MINUTE));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (wiredBox.isUniqueRewards()) {
|
||||
if (set.getInt("row_count") == wiredBox.getRewardItems().size()) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALL_COLLECTED));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_N_HOURS) {
|
||||
if (!(Emulator.getIntUnixTimestamp() - set.getInt("timestamp") >= (3600 * wiredBox.getLimitationInterval()))) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_HOUR));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (wiredBox.getRewardTime() == WiredEffectGiveReward.LIMIT_N_DAY) {
|
||||
if (!(Emulator.getIntUnixTimestamp() - set.getInt("timestamp") >= (86400 * wiredBox.getLimitationInterval()))) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED_THIS_TODAY));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wiredBox.isUniqueRewards()) {
|
||||
for (WiredGiveRewardItem item : wiredBox.getRewardItems()) {
|
||||
set.beforeFirst();
|
||||
boolean found = false;
|
||||
|
||||
while (set.next()) {
|
||||
if (set.getInt("reward_id") == item.id)
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
return giveReward(habbo, wiredBox, item);
|
||||
}
|
||||
}
|
||||
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALL_COLLECTED));
|
||||
return false;
|
||||
if (rewardToGive == null) {
|
||||
if (wiredBox.isUniqueRewards()) {
|
||||
if (!wiredBox.getRewardItems().isEmpty()) {
|
||||
rewardToGive = wiredBox.getRewardItems().get(0);
|
||||
} else {
|
||||
int randomNumber = Emulator.getRandom().nextInt(101);
|
||||
|
||||
int count = 0;
|
||||
for (WiredGiveRewardItem item : wiredBox.getRewardItems()) {
|
||||
if (randomNumber >= count && randomNumber <= (count + item.probability)) {
|
||||
return giveReward(habbo, wiredBox, item);
|
||||
}
|
||||
|
||||
count += item.probability;
|
||||
failureCode = WiredRewardAlertComposer.REWARD_ALL_COLLECTED;
|
||||
}
|
||||
} else {
|
||||
int randomNumber = Emulator.getRandom().nextInt(101);
|
||||
int count = 0;
|
||||
for (WiredGiveRewardItem item : wiredBox.getRewardItems()) {
|
||||
if (randomNumber >= count && randomNumber <= (count + item.probability)) {
|
||||
rewardToGive = item;
|
||||
break;
|
||||
}
|
||||
count += item.probability;
|
||||
}
|
||||
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.UNLUCKY_NO_REWARD));
|
||||
return false;
|
||||
if (rewardToGive == null) {
|
||||
failureCode = WiredRewardAlertComposer.UNLUCKY_NO_REWARD;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
if (failureCode != -1) {
|
||||
habbo.getClient().sendResponse(new WiredRewardAlertComposer(failureCode));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rewardToGive != null) {
|
||||
return giveReward(habbo, wiredBox, rewardToGive);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1105,4 +1105,11 @@ public final class WiredMoveCarryHelper {
|
||||
this.expiresAt = System.currentTimeMillis() + USER_FOLLOWER_TTL_MS;
|
||||
}
|
||||
}
|
||||
|
||||
public static void cleanupRoomUnit(RoomUnit roomUnit) {
|
||||
if (roomUnit != null) {
|
||||
SUPPRESSED_STATUS_COMPOSER_UNTIL.remove(roomUnit.getId());
|
||||
ACTIVE_USER_FOLLOWERS.remove(roomUnit.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+77
-65
@@ -266,19 +266,19 @@ public final class WiredRoomDiagnostics {
|
||||
private final ArrayDeque<HistoryEntry> history;
|
||||
private final int maxHistoryEntries;
|
||||
|
||||
private long windowStartedAt;
|
||||
private int usageCurrentWindow;
|
||||
private int delayedEventsPending;
|
||||
private long totalExecutionMsCurrentWindow;
|
||||
private int executionSamplesCurrentWindow;
|
||||
private int averageExecutionMs;
|
||||
private int peakExecutionMs;
|
||||
private int consecutiveHeavyWindows;
|
||||
private int consecutiveOverloadWindows;
|
||||
private boolean heavy;
|
||||
private String peakExecutionSourceLabel;
|
||||
private int peakExecutionSourceId;
|
||||
private String peakExecutionReason;
|
||||
private final java.util.concurrent.atomic.AtomicLong windowStartedAt = new java.util.concurrent.atomic.AtomicLong();
|
||||
private final java.util.concurrent.atomic.AtomicInteger usageCurrentWindow = new java.util.concurrent.atomic.AtomicInteger();
|
||||
private final java.util.concurrent.atomic.AtomicInteger delayedEventsPending = new java.util.concurrent.atomic.AtomicInteger();
|
||||
private final java.util.concurrent.atomic.AtomicLong totalExecutionMsCurrentWindow = new java.util.concurrent.atomic.AtomicLong();
|
||||
private final java.util.concurrent.atomic.AtomicInteger executionSamplesCurrentWindow = new java.util.concurrent.atomic.AtomicInteger();
|
||||
private volatile int averageExecutionMs;
|
||||
private volatile int peakExecutionMs;
|
||||
private volatile int consecutiveHeavyWindows;
|
||||
private volatile int consecutiveOverloadWindows;
|
||||
private volatile boolean heavy;
|
||||
private volatile String peakExecutionSourceLabel;
|
||||
private volatile int peakExecutionSourceId;
|
||||
private volatile String peakExecutionReason;
|
||||
|
||||
public WiredRoomDiagnostics(int usageWindowMs, int usageLimitPerWindow, int delayedEventsLimit,
|
||||
int overloadAverageThresholdMs, int overloadPeakThresholdMs,
|
||||
@@ -310,67 +310,69 @@ public final class WiredRoomDiagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean tryConsumeExecutionBudget(int estimatedCost, long now, String sourceLabel, int sourceId, String reason) {
|
||||
rollWindow(now);
|
||||
public boolean tryConsumeExecutionBudget(int estimatedCost, long now, String sourceLabel, int sourceId, String reason) {
|
||||
rollWindowIfNeeded(now);
|
||||
|
||||
int normalizedCost = Math.max(0, estimatedCost);
|
||||
if ((this.usageCurrentWindow + normalizedCost) > this.usageLimitPerWindow) {
|
||||
int currentUsage = this.usageCurrentWindow.addAndGet(normalizedCost);
|
||||
if (currentUsage > this.usageLimitPerWindow) {
|
||||
record(Type.EXECUTION_CAP, now,
|
||||
buildExecutionCapReason(normalizedCost, reason),
|
||||
buildExecutionCapReason(normalizedCost, reason, currentUsage),
|
||||
sourceLabel,
|
||||
sourceId);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.usageCurrentWindow += normalizedCost;
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean tryScheduleDelayedEvent(long now, String sourceLabel, int sourceId, String reason) {
|
||||
rollWindow(now);
|
||||
public boolean tryScheduleDelayedEvent(long now, String sourceLabel, int sourceId, String reason) {
|
||||
rollWindowIfNeeded(now);
|
||||
|
||||
if ((this.delayedEventsPending + 1) > this.delayedEventsLimit) {
|
||||
int currentPending = this.delayedEventsPending.incrementAndGet();
|
||||
if (currentPending > this.delayedEventsLimit) {
|
||||
record(Type.DELAYED_EVENTS_CAP, now,
|
||||
buildDelayedCapReason(reason),
|
||||
buildDelayedCapReason(reason, currentPending),
|
||||
sourceLabel,
|
||||
sourceId);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.delayedEventsPending++;
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized void completeDelayedEvent() {
|
||||
if (this.delayedEventsPending > 0) {
|
||||
this.delayedEventsPending--;
|
||||
}
|
||||
public void completeDelayedEvent() {
|
||||
this.delayedEventsPending.updateAndGet(v -> v > 0 ? v - 1 : 0);
|
||||
}
|
||||
|
||||
public synchronized void recordExecution(long elapsedMs, long now, String sourceLabel, int sourceId, String reason) {
|
||||
rollWindow(now);
|
||||
public void recordExecution(long elapsedMs, long now, String sourceLabel, int sourceId, String reason) {
|
||||
rollWindowIfNeeded(now);
|
||||
|
||||
int normalizedElapsed = (int) Math.max(0L, elapsedMs);
|
||||
|
||||
this.totalExecutionMsCurrentWindow += normalizedElapsed;
|
||||
this.executionSamplesCurrentWindow++;
|
||||
this.averageExecutionMs = (int) Math.round(this.totalExecutionMsCurrentWindow / (double) this.executionSamplesCurrentWindow);
|
||||
long total = this.totalExecutionMsCurrentWindow.addAndGet(normalizedElapsed);
|
||||
int samples = this.executionSamplesCurrentWindow.incrementAndGet();
|
||||
this.averageExecutionMs = (int) Math.round(total / (double) samples);
|
||||
|
||||
if (normalizedElapsed >= this.peakExecutionMs) {
|
||||
this.peakExecutionMs = normalizedElapsed;
|
||||
this.peakExecutionSourceLabel = sanitizeSourceLabel(sourceLabel);
|
||||
this.peakExecutionSourceId = Math.max(0, sourceId);
|
||||
this.peakExecutionReason = sanitizeReason(reason);
|
||||
synchronized (this) {
|
||||
if (normalizedElapsed >= this.peakExecutionMs) {
|
||||
this.peakExecutionMs = normalizedElapsed;
|
||||
this.peakExecutionSourceLabel = sanitizeSourceLabel(sourceLabel);
|
||||
this.peakExecutionSourceId = Math.max(0, sourceId);
|
||||
this.peakExecutionReason = sanitizeReason(reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void recordKilled(long now, String reason, String sourceLabel, int sourceId) {
|
||||
rollWindow(now);
|
||||
public void recordKilled(long now, String reason, String sourceLabel, int sourceId) {
|
||||
rollWindowIfNeeded(now);
|
||||
record(Type.KILLED, now, reason, sourceLabel, sourceId);
|
||||
}
|
||||
|
||||
public synchronized void recordRecursionTimeout(long now, String reason, String sourceLabel, int sourceId) {
|
||||
rollWindow(now);
|
||||
public void recordRecursionTimeout(long now, String reason, String sourceLabel, int sourceId) {
|
||||
rollWindowIfNeeded(now);
|
||||
record(Type.RECURSION_TIMEOUT, now, reason, sourceLabel, sourceId);
|
||||
}
|
||||
|
||||
@@ -394,7 +396,7 @@ public final class WiredRoomDiagnostics {
|
||||
}
|
||||
|
||||
public synchronized Snapshot snapshot(int recursionDepthCurrent, int recursionDepthLimit, long killedUntilMs, long now) {
|
||||
rollWindow(now);
|
||||
rollWindowIfNeeded(now);
|
||||
|
||||
List<LogEntry> logEntries = new ArrayList<>(Type.values().length);
|
||||
List<HistoryEntry> historyEntries = new ArrayList<>(this.history.size());
|
||||
@@ -422,10 +424,10 @@ public final class WiredRoomDiagnostics {
|
||||
}
|
||||
|
||||
return new Snapshot(
|
||||
this.usageCurrentWindow,
|
||||
this.usageCurrentWindow.get(),
|
||||
this.usageLimitPerWindow,
|
||||
this.heavy,
|
||||
this.delayedEventsPending,
|
||||
this.delayedEventsPending.get(),
|
||||
this.delayedEventsLimit,
|
||||
this.averageExecutionMs,
|
||||
this.peakExecutionMs,
|
||||
@@ -444,30 +446,40 @@ public final class WiredRoomDiagnostics {
|
||||
);
|
||||
}
|
||||
|
||||
private void rollWindow(long now) {
|
||||
if (this.windowStartedAt <= 0L) {
|
||||
this.windowStartedAt = now;
|
||||
private void rollWindowIfNeeded(long now) {
|
||||
long startedAt = this.windowStartedAt.get();
|
||||
if (startedAt <= 0L) {
|
||||
this.windowStartedAt.compareAndSet(startedAt, now);
|
||||
return;
|
||||
}
|
||||
|
||||
while ((now - this.windowStartedAt) >= this.usageWindowMs) {
|
||||
evaluateWindow(this.windowStartedAt + this.usageWindowMs);
|
||||
this.windowStartedAt += this.usageWindowMs;
|
||||
this.usageCurrentWindow = 0;
|
||||
this.totalExecutionMsCurrentWindow = 0L;
|
||||
this.executionSamplesCurrentWindow = 0;
|
||||
this.averageExecutionMs = 0;
|
||||
this.peakExecutionMs = 0;
|
||||
this.peakExecutionSourceLabel = null;
|
||||
this.peakExecutionSourceId = 0;
|
||||
this.peakExecutionReason = null;
|
||||
if ((now - startedAt) >= this.usageWindowMs) {
|
||||
synchronized (this) {
|
||||
startedAt = this.windowStartedAt.get();
|
||||
if ((now - startedAt) >= this.usageWindowMs) {
|
||||
while ((now - startedAt) >= this.usageWindowMs) {
|
||||
evaluateWindow(startedAt + this.usageWindowMs);
|
||||
startedAt += this.usageWindowMs;
|
||||
|
||||
this.usageCurrentWindow.set(0);
|
||||
this.totalExecutionMsCurrentWindow.set(0L);
|
||||
this.executionSamplesCurrentWindow.set(0);
|
||||
this.averageExecutionMs = 0;
|
||||
this.peakExecutionMs = 0;
|
||||
this.peakExecutionSourceLabel = null;
|
||||
this.peakExecutionSourceId = 0;
|
||||
this.peakExecutionReason = null;
|
||||
}
|
||||
this.windowStartedAt.set(startedAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void evaluateWindow(long now) {
|
||||
int usagePercent = (int) Math.round((this.usageCurrentWindow * 100D) / this.usageLimitPerWindow);
|
||||
int delayedPercent = (int) Math.round((this.delayedEventsPending * 100D) / this.delayedEventsLimit);
|
||||
boolean overloadWindow = (this.executionSamplesCurrentWindow > 0)
|
||||
int usagePercent = (int) Math.round((this.usageCurrentWindow.get() * 100D) / this.usageLimitPerWindow);
|
||||
int delayedPercent = (int) Math.round((this.delayedEventsPending.get() * 100D) / this.delayedEventsLimit);
|
||||
boolean overloadWindow = (this.executionSamplesCurrentWindow.get() > 0)
|
||||
&& ((this.averageExecutionMs >= this.overloadAverageThresholdMs) || (this.peakExecutionMs >= this.overloadPeakThresholdMs));
|
||||
boolean heavyWindow = (usagePercent >= this.heavyUsageThresholdPercent)
|
||||
|| (delayedPercent >= this.heavyDelayedThresholdPercent)
|
||||
@@ -516,22 +528,22 @@ public final class WiredRoomDiagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
private String buildExecutionCapReason(int normalizedCost, String reason) {
|
||||
private String buildExecutionCapReason(int normalizedCost, String reason, int currentUsage) {
|
||||
return joinReason(
|
||||
reason,
|
||||
String.format("Estimated stack cost %d would exceed usage budget %d/%d in %dms window",
|
||||
normalizedCost,
|
||||
this.usageCurrentWindow,
|
||||
currentUsage,
|
||||
this.usageLimitPerWindow,
|
||||
this.usageWindowMs)
|
||||
);
|
||||
}
|
||||
|
||||
private String buildDelayedCapReason(String reason) {
|
||||
private String buildDelayedCapReason(String reason, int currentPending) {
|
||||
return joinReason(
|
||||
reason,
|
||||
String.format("Pending delayed events would exceed queue %d/%d",
|
||||
this.delayedEventsPending,
|
||||
currentPending,
|
||||
this.delayedEventsLimit)
|
||||
);
|
||||
}
|
||||
@@ -544,7 +556,7 @@ public final class WiredRoomDiagnostics {
|
||||
this.overloadAverageThresholdMs,
|
||||
this.peakExecutionMs,
|
||||
this.overloadPeakThresholdMs,
|
||||
this.executionSamplesCurrentWindow,
|
||||
this.executionSamplesCurrentWindow.get(),
|
||||
this.usageWindowMs)
|
||||
);
|
||||
}
|
||||
|
||||
+77
-3
@@ -33,6 +33,8 @@ import java.util.Locale;
|
||||
|
||||
public final class WiredTextPlaceholderUtil {
|
||||
private static final char PRESERVED_SPACE = '\u00A0';
|
||||
private static final int MAX_PLACEHOLDER_EXPANSION_LENGTH = 16384;
|
||||
private static final int MAX_PLACEHOLDER_REPLACEMENTS = 512;
|
||||
|
||||
private WiredTextPlaceholderUtil() {
|
||||
}
|
||||
@@ -56,13 +58,20 @@ public final class WiredTextPlaceholderUtil {
|
||||
|
||||
String resolvedText = text;
|
||||
|
||||
int replacementCount = 0;
|
||||
|
||||
for (InteractionWiredExtra extra : WiredExecutionOrderUtil.sort(extras)) {
|
||||
if (extra instanceof WiredExtraTextOutputUsername) {
|
||||
WiredExtraTextOutputUsername usernameExtra = (WiredExtraTextOutputUsername) extra;
|
||||
String placeholderToken = usernameExtra.getPlaceholderToken();
|
||||
|
||||
if (!placeholderToken.isEmpty() && resolvedText.contains(placeholderToken)) {
|
||||
resolvedText = resolvedText.replace(placeholderToken, buildUsernameReplacement(ctx, usernameExtra));
|
||||
resolvedText = replaceWithBudget(resolvedText, placeholderToken, buildUsernameReplacement(ctx, usernameExtra));
|
||||
replacementCount++;
|
||||
}
|
||||
|
||||
if (shouldStopPlaceholderExpansion(resolvedText, replacementCount)) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -73,7 +82,12 @@ public final class WiredTextPlaceholderUtil {
|
||||
String placeholderToken = furniExtra.getPlaceholderToken();
|
||||
|
||||
if (!placeholderToken.isEmpty() && resolvedText.contains(placeholderToken)) {
|
||||
resolvedText = resolvedText.replace(placeholderToken, buildFurniNameReplacement(ctx, furniExtra));
|
||||
resolvedText = replaceWithBudget(resolvedText, placeholderToken, buildFurniNameReplacement(ctx, furniExtra));
|
||||
replacementCount++;
|
||||
}
|
||||
|
||||
if (shouldStopPlaceholderExpansion(resolvedText, replacementCount)) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -84,7 +98,12 @@ public final class WiredTextPlaceholderUtil {
|
||||
String placeholderToken = variableExtra.getPlaceholderToken();
|
||||
|
||||
if (!placeholderToken.isEmpty() && resolvedText.contains(placeholderToken)) {
|
||||
resolvedText = resolvedText.replace(placeholderToken, buildVariableReplacement(ctx, variableExtra));
|
||||
resolvedText = replaceWithBudget(resolvedText, placeholderToken, buildVariableReplacement(ctx, variableExtra));
|
||||
replacementCount++;
|
||||
}
|
||||
|
||||
if (shouldStopPlaceholderExpansion(resolvedText, replacementCount)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +111,61 @@ public final class WiredTextPlaceholderUtil {
|
||||
return preserveRepeatedSpaces(resolvedText);
|
||||
}
|
||||
|
||||
private static boolean shouldStopPlaceholderExpansion(String resolvedText, int replacementCount) {
|
||||
return replacementCount >= MAX_PLACEHOLDER_REPLACEMENTS
|
||||
|| (resolvedText != null && resolvedText.length() >= MAX_PLACEHOLDER_EXPANSION_LENGTH);
|
||||
}
|
||||
|
||||
private static String replaceWithBudget(String input, String placeholderToken, String replacement) {
|
||||
if (input == null || input.isEmpty() || placeholderToken == null || placeholderToken.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (replacement == null) {
|
||||
replacement = "";
|
||||
}
|
||||
|
||||
int matchIndex = input.indexOf(placeholderToken);
|
||||
if (matchIndex < 0) {
|
||||
return input;
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder(Math.min(MAX_PLACEHOLDER_EXPANSION_LENGTH, input.length()));
|
||||
int searchIndex = 0;
|
||||
|
||||
while (matchIndex >= 0) {
|
||||
builder.append(input, searchIndex, matchIndex);
|
||||
|
||||
int remainingCapacity = MAX_PLACEHOLDER_EXPANSION_LENGTH - builder.length();
|
||||
if (remainingCapacity <= 0) {
|
||||
return builder.substring(0, MAX_PLACEHOLDER_EXPANSION_LENGTH);
|
||||
}
|
||||
|
||||
if (replacement.length() <= remainingCapacity) {
|
||||
builder.append(replacement);
|
||||
} else {
|
||||
builder.append(replacement, 0, remainingCapacity);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
searchIndex = matchIndex + placeholderToken.length();
|
||||
if (builder.length() >= MAX_PLACEHOLDER_EXPANSION_LENGTH) {
|
||||
return builder.substring(0, MAX_PLACEHOLDER_EXPANSION_LENGTH);
|
||||
}
|
||||
|
||||
matchIndex = input.indexOf(placeholderToken, searchIndex);
|
||||
}
|
||||
|
||||
int remainingCapacity = MAX_PLACEHOLDER_EXPANSION_LENGTH - builder.length();
|
||||
if (remainingCapacity <= 0) {
|
||||
return builder.substring(0, MAX_PLACEHOLDER_EXPANSION_LENGTH);
|
||||
}
|
||||
|
||||
int tailLength = Math.min(input.length() - searchIndex, remainingCapacity);
|
||||
builder.append(input, searchIndex, searchIndex + tailLength);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String preserveRepeatedSpaces(String text) {
|
||||
if (text == null || text.length() < 2) {
|
||||
return text;
|
||||
|
||||
@@ -587,4 +587,10 @@ public final class WiredUserMovementHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void cleanupRoomUnit(RoomUnit roomUnit) {
|
||||
if (roomUnit != null) {
|
||||
SUPPRESSED_STATUS_COMPOSER_UNTIL.remove(roomUnit.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-25
@@ -16,6 +16,7 @@ import java.time.LocalTime;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.time.temporal.WeekFields;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
@@ -23,7 +24,7 @@ import java.util.stream.Stream;
|
||||
public class WiredHighscoreManager {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WiredHighscoreManager.class);
|
||||
|
||||
private final HashMap<Integer, List<WiredHighscoreDataEntry>> data = new HashMap<>();
|
||||
private final ConcurrentHashMap<Integer, List<WiredHighscoreDataEntry>> data = new ConcurrentHashMap<>();
|
||||
|
||||
private final static String locale = (System.getProperty("user.language") != null ? System.getProperty("user.language") : "en");
|
||||
private final static String country = (System.getProperty("user.country") != null ? System.getProperty("user.country") : "US");
|
||||
@@ -60,15 +61,12 @@ public class WiredHighscoreManager {
|
||||
|
||||
private void loadHighscoreData() {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM items_highscore_data")) {
|
||||
statement.setFetchSize(1000);
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
WiredHighscoreDataEntry entry = new WiredHighscoreDataEntry(set);
|
||||
|
||||
if (!this.data.containsKey(entry.getItemId())) {
|
||||
this.data.put(entry.getItemId(), new ArrayList<>());
|
||||
}
|
||||
|
||||
this.data.get(entry.getItemId()).add(entry);
|
||||
this.data.computeIfAbsent(entry.getItemId(), k -> Collections.synchronizedList(new ArrayList<>())).add(entry);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
@@ -77,33 +75,39 @@ public class WiredHighscoreManager {
|
||||
}
|
||||
|
||||
public void addHighscoreData(WiredHighscoreDataEntry entry) {
|
||||
if (!this.data.containsKey(entry.getItemId())) {
|
||||
this.data.put(entry.getItemId(), new ArrayList<>());
|
||||
}
|
||||
this.data.computeIfAbsent(entry.getItemId(), k -> Collections.synchronizedList(new ArrayList<>())).add(entry);
|
||||
|
||||
this.data.get(entry.getItemId()).add(entry);
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO `items_highscore_data` (`item_id`, `user_ids`, `score`, `is_win`, `timestamp`) VALUES (?, ?, ?, ?, ?)")) {
|
||||
statement.setInt(1, entry.getItemId());
|
||||
statement.setString(2, String.join(",", entry.getUserIds().stream().map(Object::toString).collect(Collectors.toList())));
|
||||
statement.setInt(3, entry.getScore());
|
||||
statement.setInt(4, entry.isWin() ? 1 : 0);
|
||||
statement.setInt(5, entry.getTimestamp());
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO `items_highscore_data` (`item_id`, `user_ids`, `score`, `is_win`, `timestamp`) VALUES (?, ?, ?, ?, ?)")) {
|
||||
statement.setInt(1, entry.getItemId());
|
||||
statement.setString(2, String.join(",", entry.getUserIds().stream().map(Object::toString).collect(Collectors.toList())));
|
||||
statement.setInt(3, entry.getScore());
|
||||
statement.setInt(4, entry.isWin() ? 1 : 0);
|
||||
statement.setInt(5, entry.getTimestamp());
|
||||
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public List<WiredHighscoreRow> getHighscoreRowsForItem(int itemId, WiredHighscoreClearType clearType, WiredHighscoreScoreType scoreType) {
|
||||
if (!this.data.containsKey(itemId)) return null;
|
||||
|
||||
Stream<WiredHighscoreRow> highscores = new ArrayList<>(this.data.get(itemId)).stream()
|
||||
List<WiredHighscoreDataEntry> list = this.data.get(itemId);
|
||||
if (list == null) return null;
|
||||
|
||||
List<WiredHighscoreDataEntry> copy;
|
||||
synchronized (list) {
|
||||
copy = new ArrayList<>(list);
|
||||
}
|
||||
|
||||
Stream<WiredHighscoreRow> highscores = copy.stream()
|
||||
.filter(entry -> this.timeMatchesEntry(entry, clearType) && (scoreType != WiredHighscoreScoreType.MOSTWIN || entry.isWin()))
|
||||
.map(entry -> new WiredHighscoreRow(
|
||||
entry.getUserIds().stream()
|
||||
.map(id -> Emulator.getGameEnvironment().getHabboManager().getHabboInfo(id).getUsername())
|
||||
.map(id -> Emulator.getGameEnvironment().getHabboManager().getCachedUsername(id))
|
||||
.collect(Collectors.toList()),
|
||||
entry.getScore()
|
||||
));
|
||||
@@ -167,7 +171,7 @@ public class WiredHighscoreManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
public HashMap<Integer, List<WiredHighscoreDataEntry>> getData() {
|
||||
public Map<Integer, List<WiredHighscoreDataEntry>> getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
@@ -176,7 +180,7 @@ public class WiredHighscoreManager {
|
||||
}
|
||||
|
||||
public void setEntriesForItemId(int itemId, List<WiredHighscoreDataEntry> entries) {
|
||||
this.data.put(itemId, entries);
|
||||
this.data.put(itemId, Collections.synchronizedList(entries));
|
||||
}
|
||||
|
||||
private long getTodayStartTimestamp() {
|
||||
|
||||
@@ -60,11 +60,15 @@ public final class WiredTickService {
|
||||
/** Whether a shard worker loop is currently scheduled/running. */
|
||||
private AtomicBoolean[] shardScheduled;
|
||||
|
||||
private final ConcurrentHashMap<Integer, Set<WiredTickable>> roomTickables;
|
||||
private final ConcurrentHashMap<Integer, Set<WiredTickable>>[] shardRoomTickables;
|
||||
private final AtomicBoolean running;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private WiredTickService() {
|
||||
this.roomTickables = new ConcurrentHashMap<>();
|
||||
this.shardRoomTickables = new ConcurrentHashMap[MAX_WORKER_COUNT];
|
||||
for (int i = 0; i < MAX_WORKER_COUNT; i++) {
|
||||
this.shardRoomTickables[i] = new ConcurrentHashMap<>();
|
||||
}
|
||||
this.running = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
@@ -232,7 +236,9 @@ public final class WiredTickService {
|
||||
shardProcessedTicks = null;
|
||||
shardScheduled = null;
|
||||
|
||||
roomTickables.clear();
|
||||
for (int i = 0; i < MAX_WORKER_COUNT; i++) {
|
||||
shardRoomTickables[i].clear();
|
||||
}
|
||||
LOGGER.info("WiredTickService stopped");
|
||||
}
|
||||
|
||||
@@ -246,7 +252,8 @@ public final class WiredTickService {
|
||||
}
|
||||
|
||||
int roomId = room.getId();
|
||||
Set<WiredTickable> tickables = roomTickables.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet());
|
||||
int shardIndex = getShardIndex(roomId);
|
||||
Set<WiredTickable> tickables = shardRoomTickables[shardIndex].computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet());
|
||||
|
||||
if (tickables.add(tickable)) {
|
||||
tickable.onRegistered(room, System.currentTimeMillis());
|
||||
@@ -259,7 +266,8 @@ public final class WiredTickService {
|
||||
}
|
||||
|
||||
int roomId = room.getId();
|
||||
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
||||
int shardIndex = getShardIndex(roomId);
|
||||
Set<WiredTickable> tickables = shardRoomTickables[shardIndex].get(roomId);
|
||||
|
||||
if (tickables != null) {
|
||||
if (tickables.remove(tickable)) {
|
||||
@@ -267,13 +275,14 @@ public final class WiredTickService {
|
||||
}
|
||||
|
||||
if (tickables.isEmpty()) {
|
||||
roomTickables.remove(roomId);
|
||||
shardRoomTickables[shardIndex].remove(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void unregister(int roomId, int tickableId) {
|
||||
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
||||
int shardIndex = getShardIndex(roomId);
|
||||
Set<WiredTickable> tickables = shardRoomTickables[shardIndex].get(roomId);
|
||||
|
||||
if (tickables != null) {
|
||||
tickables.removeIf(t -> {
|
||||
@@ -288,7 +297,7 @@ public final class WiredTickService {
|
||||
});
|
||||
|
||||
if (tickables.isEmpty()) {
|
||||
roomTickables.remove(roomId);
|
||||
shardRoomTickables[shardIndex].remove(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,11 +307,12 @@ public final class WiredTickService {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<WiredTickable> tickables = roomTickables.remove(room.getId());
|
||||
int roomId = room.getId();
|
||||
int shardIndex = getShardIndex(roomId);
|
||||
Set<WiredTickable> tickables = shardRoomTickables[shardIndex].remove(roomId);
|
||||
|
||||
if (tickables != null) {
|
||||
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
|
||||
for (WiredTickable tickable : snapshot) {
|
||||
for (WiredTickable tickable : tickables) {
|
||||
try {
|
||||
if (tickable != null) {
|
||||
tickable.onUnregistered(room);
|
||||
@@ -316,7 +326,7 @@ public final class WiredTickService {
|
||||
);
|
||||
}
|
||||
}
|
||||
LOGGER.debug("Unregistered {} tickables from room {}", snapshot.length, room.getId());
|
||||
LOGGER.debug("Unregistered {} tickables from room {}", tickables.size(), room.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,11 +335,12 @@ public final class WiredTickService {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<WiredTickable> tickables = roomTickables.get(room.getId());
|
||||
int roomId = room.getId();
|
||||
int shardIndex = getShardIndex(roomId);
|
||||
Set<WiredTickable> tickables = shardRoomTickables[shardIndex].get(roomId);
|
||||
|
||||
if (tickables != null) {
|
||||
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
|
||||
for (WiredTickable tickable : snapshot) {
|
||||
for (WiredTickable tickable : tickables) {
|
||||
try {
|
||||
if (tickable != null) {
|
||||
tickable.resetTimer();
|
||||
@@ -347,16 +358,25 @@ public final class WiredTickService {
|
||||
}
|
||||
|
||||
public int getTickableCount(int roomId) {
|
||||
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
||||
int shardIndex = getShardIndex(roomId);
|
||||
Set<WiredTickable> tickables = shardRoomTickables[shardIndex].get(roomId);
|
||||
return tickables != null ? tickables.size() : 0;
|
||||
}
|
||||
|
||||
public int getTotalTickableCount() {
|
||||
return roomTickables.values().stream().mapToInt(Set::size).sum();
|
||||
int count = 0;
|
||||
for (int i = 0; i < MAX_WORKER_COUNT; i++) {
|
||||
count += shardRoomTickables[i].values().stream().mapToInt(Set::size).sum();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public int getActiveRoomCount() {
|
||||
return roomTickables.size();
|
||||
int count = 0;
|
||||
for (int i = 0; i < MAX_WORKER_COUNT; i++) {
|
||||
count += shardRoomTickables[i].size();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public long getTickCount() {
|
||||
@@ -396,6 +416,12 @@ public final class WiredTickService {
|
||||
break;
|
||||
}
|
||||
|
||||
// If lagging by more than 5 ticks (250ms), skip intermediate ticks to avoid CPU starvation
|
||||
if (requestedTick - nextTick > 5) {
|
||||
nextTick = requestedTick - 5;
|
||||
shardProcessedTicks[shardIndex].set(nextTick);
|
||||
}
|
||||
|
||||
processShardTick(shardIndex, nextTick);
|
||||
shardProcessedTicks[shardIndex].set(nextTick);
|
||||
}
|
||||
@@ -414,12 +440,8 @@ public final class WiredTickService {
|
||||
int processedTickables = 0;
|
||||
int processedRooms = 0;
|
||||
|
||||
for (Map.Entry<Integer, Set<WiredTickable>> entry : roomTickables.entrySet()) {
|
||||
for (Map.Entry<Integer, Set<WiredTickable>> entry : shardRoomTickables[shardIndex].entrySet()) {
|
||||
int roomId = entry.getKey();
|
||||
if (getShardIndex(roomId) != shardIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Set<WiredTickable> tickables = entry.getValue();
|
||||
if (tickables == null || tickables.isEmpty()) {
|
||||
continue;
|
||||
@@ -435,14 +457,9 @@ public final class WiredTickService {
|
||||
}
|
||||
|
||||
long roomStart = System.currentTimeMillis();
|
||||
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
|
||||
if (snapshot.length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
processedRooms++;
|
||||
|
||||
for (WiredTickable tickable : snapshot) {
|
||||
for (WiredTickable tickable : tickables) {
|
||||
long tickableStart = System.currentTimeMillis();
|
||||
|
||||
if (tickable == null) {
|
||||
@@ -489,7 +506,7 @@ public final class WiredTickService {
|
||||
shardIndex,
|
||||
roomId,
|
||||
currentTick,
|
||||
snapshot.length,
|
||||
tickables.size(),
|
||||
roomDuration
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.eu.habbo.messages;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.monitoring.EmulatorNetworkStats;
|
||||
import com.eu.habbo.messages.incoming.Incoming;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.incoming.achievements.RequestAchievementConfigurationEvent;
|
||||
@@ -180,6 +181,8 @@ public class PacketManager {
|
||||
return;
|
||||
|
||||
try {
|
||||
EmulatorNetworkStats.recordIncoming(packet.bytesAvailable() + 6);
|
||||
|
||||
if (this.isRegistered(packet.getMessageId())) {
|
||||
Class<? extends MessageHandler> handlerClass = this.incoming.get(packet.getMessageId());
|
||||
|
||||
@@ -716,5 +719,31 @@ public class PacketManager {
|
||||
this.registerHandler(Incoming.YouTubeRoomPlayEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomPlayEvent.class);
|
||||
this.registerHandler(Incoming.YouTubeRoomWatchingEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomWatchingEvent.class);
|
||||
this.registerHandler(Incoming.YouTubeRoomSettingsEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomSettingsEvent.class);
|
||||
|
||||
// Housekeeping (in-client admin panel)
|
||||
this.registerHandler(Incoming.HousekeepingFindUserByNameEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindUserByNameEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingFindUserByIdEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindUserByIdEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingBanUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingBanUserEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingUnbanUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingUnbanUserEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingMuteUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingMuteUserEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingKickUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingKickUserEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingForceDisconnectUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingForceDisconnectUserEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingSetUserRankEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSetUserRankEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingTradeLockUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingTradeLockUserEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingResetUserPasswordEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingResetUserPasswordEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingFindRoomByIdEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindRoomByIdEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingSearchRoomsEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSearchRoomsEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingRoomStateEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingRoomStateEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingMuteRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingMuteRoomEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingKickAllFromRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingKickAllFromRoomEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingTransferRoomOwnershipEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingTransferRoomOwnershipEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingDeleteRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingDeleteRoomEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingGiveCreditsEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGiveCreditsEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingGiveCurrencyEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGiveCurrencyEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingGrantItemEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGrantItemEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingSetHcSubscriptionEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSetHcSubscriptionEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingSendHotelAlertEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSendHotelAlertEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingGetDashboardEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGetDashboardEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingListActionLogEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingListActionLogEvent.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,4 +460,30 @@ public class Incoming {
|
||||
public static final int YouTubeRoomPlayEvent = 8001;
|
||||
public static final int YouTubeRoomWatchingEvent = 8002;
|
||||
public static final int YouTubeRoomSettingsEvent = 8003;
|
||||
|
||||
// Housekeeping (in-client admin panel) — IDs 9100..9199 reserved
|
||||
public static final int HousekeepingFindUserByNameEvent = 9100;
|
||||
public static final int HousekeepingFindUserByIdEvent = 9101;
|
||||
public static final int HousekeepingBanUserEvent = 9102;
|
||||
public static final int HousekeepingUnbanUserEvent = 9103;
|
||||
public static final int HousekeepingMuteUserEvent = 9104;
|
||||
public static final int HousekeepingKickUserEvent = 9105;
|
||||
public static final int HousekeepingForceDisconnectUserEvent = 9106;
|
||||
public static final int HousekeepingSetUserRankEvent = 9107;
|
||||
public static final int HousekeepingTradeLockUserEvent = 9108;
|
||||
public static final int HousekeepingResetUserPasswordEvent = 9109;
|
||||
public static final int HousekeepingFindRoomByIdEvent = 9110;
|
||||
public static final int HousekeepingSearchRoomsEvent = 9111;
|
||||
public static final int HousekeepingRoomStateEvent = 9112;
|
||||
public static final int HousekeepingMuteRoomEvent = 9113;
|
||||
public static final int HousekeepingKickAllFromRoomEvent = 9114;
|
||||
public static final int HousekeepingTransferRoomOwnershipEvent = 9115;
|
||||
public static final int HousekeepingDeleteRoomEvent = 9116;
|
||||
public static final int HousekeepingGiveCreditsEvent = 9117;
|
||||
public static final int HousekeepingGiveCurrencyEvent = 9118;
|
||||
public static final int HousekeepingGrantItemEvent = 9119;
|
||||
public static final int HousekeepingSetHcSubscriptionEvent = 9120;
|
||||
public static final int HousekeepingSendHotelAlertEvent = 9121;
|
||||
public static final int HousekeepingGetDashboardEvent = 9122;
|
||||
public static final int HousekeepingListActionLogEvent = 9123;
|
||||
}
|
||||
|
||||
+109
-78
@@ -34,6 +34,9 @@ import java.util.Calendar;
|
||||
public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CatalogBuyItemAsGiftEvent.class);
|
||||
|
||||
private static final int USERNAME_MAX = 32;
|
||||
private static final int EXTRADATA_MAX = 256;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
@@ -41,41 +44,44 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
LOGGER.error("DEBUG GIFT: entered CatalogBuyItemAsGiftEvent.handle()");
|
||||
|
||||
if (Emulator.getIntUnixTimestamp() - this.client.getHabbo().getHabboStats().lastGiftTimestamp >= CatalogManager.PURCHASE_COOLDOWN) {
|
||||
this.client.getHabbo().getHabboStats().lastGiftTimestamp = Emulator.getIntUnixTimestamp();
|
||||
|
||||
if (ShutdownEmulator.timestamp > 0) {
|
||||
LOGGER.error("DEBUG GIFT: emulator closing");
|
||||
LOGGER.debug("emulator closing");
|
||||
this.client.sendResponse(new HotelWillCloseInMinutesComposer((ShutdownEmulator.timestamp - Emulator.getIntUnixTimestamp()) / 60));
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.client.getHabbo().getHabboStats().isPurchasingFurniture) {
|
||||
LOGGER.error("DEBUG GIFT: isPurchasingFurniture already true");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
} else {
|
||||
synchronized (this.client.getHabbo().getHabboStats()) {
|
||||
if (this.client.getHabbo().getHabboStats().isPurchasingFurniture) {
|
||||
LOGGER.debug("isPurchasingFurniture already true");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
this.client.getHabbo().getHabboStats().isPurchasingFurniture = true;
|
||||
}
|
||||
|
||||
int paidCredits = 0;
|
||||
int paidPoints = 0;
|
||||
int paidPointsType = 0;
|
||||
|
||||
try {
|
||||
int pageId = this.packet.readInt();
|
||||
int itemId = this.packet.readInt();
|
||||
String extraData = this.packet.readString();
|
||||
if (extraData.length() > EXTRADATA_MAX) extraData = extraData.substring(0, EXTRADATA_MAX);
|
||||
String username = this.packet.readString();
|
||||
String message = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo());
|
||||
if (username.length() > USERNAME_MAX) username = username.substring(0, USERNAME_MAX);
|
||||
int messageMax = Emulator.getConfig().getInt("hotel.gifts.length.max", 300);
|
||||
String rawMessage = this.packet.readString();
|
||||
if (rawMessage.length() > messageMax) rawMessage = rawMessage.substring(0, messageMax);
|
||||
String message = Emulator.getGameEnvironment().getWordFilter().filter(rawMessage, this.client.getHabbo());
|
||||
int spriteId = this.packet.readInt();
|
||||
int color = this.packet.readInt();
|
||||
int ribbonId = this.packet.readInt();
|
||||
boolean showName = this.packet.readBoolean();
|
||||
|
||||
LOGGER.error(
|
||||
"DEBUG GIFT: pageId={}, itemId={}, extraData={}, username={}, spriteId={}, color={}, ribbonId={}, showName={}, message={}",
|
||||
pageId, itemId, extraData, username, spriteId, color, ribbonId, showName, message
|
||||
);
|
||||
LOGGER.debug("Gift request: pageId={}, itemId={}, spriteId={}, color={}, ribbonId={}", pageId, itemId, spriteId, color, ribbonId);
|
||||
|
||||
int userId = 0;
|
||||
|
||||
@@ -87,21 +93,17 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
if (!Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId)
|
||||
&& !Emulator.getGameEnvironment().getCatalogManager().giftFurnis.containsKey(spriteId)) {
|
||||
LOGGER.error("DEBUG GIFT: invalid spriteId for gift wrapper/furni -> {}", spriteId);
|
||||
LOGGER.debug("invalid spriteId for gift wrapper/furni -> {}", spriteId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!GiftConfigurationComposer.BOX_TYPES.contains(color) || !GiftConfigurationComposer.RIBBON_TYPES.contains(ribbonId)) {
|
||||
LOGGER.error("DEBUG GIFT: invalid color/ribbon -> color={}, ribbonId={}", color, ribbonId);
|
||||
LOGGER.debug("invalid color/ribbon -> color={}, ribbonId={}", color, ribbonId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.length() > Emulator.getConfig().getInt("hotel.gifts.length.max", 300)) {
|
||||
message = message.substring(0, Emulator.getConfig().getInt("hotel.gifts.length.max", 300));
|
||||
}
|
||||
|
||||
Integer iItemId = Emulator.getGameEnvironment().getCatalogManager().giftWrappers.get(spriteId);
|
||||
|
||||
if (iItemId == null) {
|
||||
@@ -109,7 +111,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
|
||||
if (iItemId == null) {
|
||||
LOGGER.error("DEBUG GIFT: iItemId null for spriteId={}", spriteId);
|
||||
LOGGER.debug("iItemId null for spriteId={}", spriteId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
@@ -117,7 +119,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
Item giftItem = Emulator.getGameEnvironment().getItemManager().getItem(iItemId);
|
||||
|
||||
if (giftItem == null) {
|
||||
LOGGER.error("DEBUG GIFT: direct giftItem null, trying random fallback. iItemId={}", iItemId);
|
||||
LOGGER.debug("direct giftItem null, trying random fallback. iItemId={}", iItemId);
|
||||
giftItem = Emulator.getGameEnvironment().getItemManager().getItem(
|
||||
(Integer) Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray()[
|
||||
Emulator.getRandom().nextInt(Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size())
|
||||
@@ -125,7 +127,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
);
|
||||
|
||||
if (giftItem == null) {
|
||||
LOGGER.error("DEBUG GIFT: fallback giftItem also null");
|
||||
LOGGER.debug("fallback giftItem also null");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
@@ -135,7 +137,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
|
||||
|
||||
if (habbo == null) {
|
||||
LOGGER.error("DEBUG GIFT: target user not online, checking DB -> {}", username);
|
||||
LOGGER.debug("target user not online, checking DB -> {}", username);
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE username = ?")) {
|
||||
statement.setString(1, username);
|
||||
|
||||
@@ -152,7 +154,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
|
||||
if (userId == 0) {
|
||||
LOGGER.error("DEBUG GIFT: receiver not found -> {}", username);
|
||||
LOGGER.debug("receiver not found -> {}", username);
|
||||
this.client.sendResponse(new GiftReceiverNotFoundComposer());
|
||||
return;
|
||||
}
|
||||
@@ -160,38 +162,47 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId);
|
||||
|
||||
if (page == null) {
|
||||
LOGGER.error("DEBUG GIFT: page null -> {}", pageId);
|
||||
LOGGER.debug("page null -> {}", pageId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (page.getRank() > this.client.getHabbo().getHabboInfo().getRank().getId() || !page.isEnabled() || !page.isVisible()) {
|
||||
LOGGER.error("DEBUG GIFT: page access denied. pageRank={}, userRank={}, enabled={}, visible={}",
|
||||
page.getRank(),
|
||||
this.client.getHabbo().getHabboInfo().getRank().getId(),
|
||||
page.isEnabled(),
|
||||
page.isVisible());
|
||||
LOGGER.debug("page access denied. pageRank={}, userRank={}, enabled={}, visible={}", page.getRank(), this.client.getHabbo().getHabboInfo().getRank().getId(), page.isEnabled(), page.isVisible());
|
||||
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
|
||||
return;
|
||||
}
|
||||
|
||||
CatalogItem item = page.getCatalogItem(itemId);
|
||||
|
||||
// Search-results gift sends the catalog offer_id as
|
||||
// itemId, not catalog_items.id - see the same fix in
|
||||
// CatalogBuyItemEvent. Fall back to scanning the
|
||||
// page for the matching offer_id.
|
||||
if (item == null) {
|
||||
LOGGER.error("DEBUG GIFT: catalog item null -> {}", itemId);
|
||||
for (CatalogItem candidate : page.getCatalogItems().valueCollection()) {
|
||||
if (candidate != null && candidate.getOfferId() == itemId) {
|
||||
item = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
LOGGER.debug("catalog item null -> {}", itemId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.isClubOnly() && !this.client.getHabbo().getHabboStats().hasActiveClub()) {
|
||||
LOGGER.error("DEBUG GIFT: item requires club -> itemId={}", itemId);
|
||||
LOGGER.debug("item requires club -> itemId={}", itemId);
|
||||
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.REQUIRES_CLUB));
|
||||
return;
|
||||
}
|
||||
|
||||
for (Item baseItem : item.getBaseItems()) {
|
||||
if (!baseItem.allowGift()) {
|
||||
LOGGER.error("DEBUG GIFT: base item not giftable -> baseItemId={}, name={}", baseItem.getId(), baseItem.getName());
|
||||
LOGGER.debug("base item not giftable -> baseItemId={}, name={}", baseItem.getId(), baseItem.getName());
|
||||
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
|
||||
return;
|
||||
}
|
||||
@@ -199,7 +210,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
if (item.isLimited()) {
|
||||
if (item.getLimitedStack() == item.getLimitedSells()) {
|
||||
LOGGER.error("DEBUG GIFT: LTD sold out -> itemId={}", itemId);
|
||||
LOGGER.debug("LTD sold out -> itemId={}", itemId);
|
||||
this.client.sendResponse(new AlertLimitedSoldOutComposer());
|
||||
return;
|
||||
}
|
||||
@@ -208,14 +219,14 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
int totalCredits = item.getCredits();
|
||||
int totalPoints = item.getPoints();
|
||||
|
||||
// Paid wrapping (giftWrappers) costs hotel.gifts.special.price; default furni wrap is free.
|
||||
boolean isPaidWrap = Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId);
|
||||
int wrapFee = isPaidWrap ? Emulator.getConfig().getInt("hotel.gifts.special.price", 0) : 0;
|
||||
totalCredits += wrapFee;
|
||||
|
||||
if (totalCredits > this.client.getHabbo().getHabboInfo().getCredits()
|
||||
|| totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType())) {
|
||||
LOGGER.error("DEBUG GIFT: not enough currency. creditsNeeded={}, creditsHave={}, pointsNeeded={}, pointsHave={}, pointsType={}",
|
||||
totalCredits,
|
||||
this.client.getHabbo().getHabboInfo().getCredits(),
|
||||
totalPoints,
|
||||
this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType()),
|
||||
item.getPointsType());
|
||||
LOGGER.debug("not enough currency. creditsNeeded={}, pointsNeeded={}, pointsType={}", totalCredits, totalPoints, item.getPointsType());
|
||||
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
|
||||
return;
|
||||
}
|
||||
@@ -226,7 +237,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
if (item.isLimited()) {
|
||||
if (Emulator.getGameEnvironment().getCatalogManager().getLimitedConfig(item).available() == 0) {
|
||||
LOGGER.error("DEBUG GIFT: LTD available=0 -> itemId={}", itemId);
|
||||
LOGGER.debug("LTD available=0 -> itemId={}", itemId);
|
||||
this.client.sendResponse(new AlertLimitedSoldOutComposer());
|
||||
return;
|
||||
}
|
||||
@@ -234,7 +245,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) {
|
||||
int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total");
|
||||
if (this.client.getHabbo().getHabboStats().totalLtds() >= ltdLimit) {
|
||||
LOGGER.error("DEBUG GIFT: sender reached daily total LTD limit");
|
||||
LOGGER.debug("sender reached daily total LTD limit");
|
||||
this.client.getHabbo().alert(
|
||||
Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total")
|
||||
.replace("%itemname%", item.getBaseItems().iterator().next().getFullName())
|
||||
@@ -245,7 +256,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item");
|
||||
if (this.client.getHabbo().getHabboStats().totalLtds(item.getId()) >= ltdLimit) {
|
||||
LOGGER.error("DEBUG GIFT: sender reached daily LTD item limit");
|
||||
LOGGER.debug("sender reached daily LTD item limit");
|
||||
this.client.getHabbo().alert(
|
||||
Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item")
|
||||
.replace("%itemname%", item.getBaseItems().iterator().next().getFullName())
|
||||
@@ -296,20 +307,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
|
||||
if (badgeFound) {
|
||||
LOGGER.error("DEBUG GIFT: receiver already has badge");
|
||||
LOGGER.debug("receiver already has badge");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.ALREADY_HAVE_BADGE));
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.getAmount() > 1 || item.getBaseItems().size() > 1) {
|
||||
LOGGER.error("DEBUG GIFT: unsupported multi amount/baseItems. amount={}, baseItems={}", item.getAmount(), item.getBaseItems().size());
|
||||
LOGGER.debug("unsupported multi amount/baseItems. amount={}, baseItems={}", item.getAmount(), item.getBaseItems().size());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
for (Item baseItem : item.getBaseItems()) {
|
||||
if (item.getItemAmount(baseItem.getId()) > 1) {
|
||||
LOGGER.error("DEBUG GIFT: unsupported item amount > 1 for baseItemId={}", baseItem.getId());
|
||||
LOGGER.debug("unsupported item amount > 1 for baseItemId={}", baseItem.getId());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
@@ -333,11 +344,11 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
badgeFound = true;
|
||||
}
|
||||
} else if (item.getName().startsWith("rentable_bot_")) {
|
||||
LOGGER.error("DEBUG GIFT: rentable bot gifts not supported");
|
||||
LOGGER.debug("rentable bot gifts not supported");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
} else if (Item.isPet(baseItem)) {
|
||||
LOGGER.error("DEBUG GIFT: pet gifts not supported");
|
||||
LOGGER.debug("pet gifts not supported");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
} else {
|
||||
@@ -373,8 +384,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
HabboItem teleportTwo = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
|
||||
|
||||
if (teleportOne == null || teleportTwo == null) {
|
||||
LOGGER.error("DEBUG GIFT: teleport creation failed. baseItemId={}, teleportOneNull={}, teleportTwoNull={}",
|
||||
baseItem.getId(), teleportOne == null, teleportTwo == null);
|
||||
LOGGER.debug("teleport creation failed. baseItemId={}, teleportOneNull={}, teleportTwoNull={}", baseItem.getId(), teleportOne == null, teleportTwo == null);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
@@ -387,7 +397,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedNumber, limitedNumber, extraData);
|
||||
|
||||
if (habboItem == null) {
|
||||
LOGGER.error("DEBUG GIFT: hopper creation failed. baseItemId={}", baseItem.getId());
|
||||
LOGGER.debug("hopper creation failed. baseItemId={}", baseItem.getId());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
@@ -400,13 +410,13 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
HabboItem createdItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
|
||||
|
||||
if (createdItem == null) {
|
||||
LOGGER.error("DEBUG GIFT: guild item creation failed. baseItemId={}", baseItem.getId());
|
||||
LOGGER.debug("guild item creation failed. baseItemId={}", baseItem.getId());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(createdItem instanceof InteractionGuildFurni)) {
|
||||
LOGGER.error("DEBUG GIFT: created guild item has wrong class -> {}", createdItem.getClass().getName());
|
||||
LOGGER.debug("created guild item has wrong class -> {}", createdItem.getClass().getName());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
@@ -431,7 +441,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
|
||||
|
||||
if (habboItem == null) {
|
||||
LOGGER.error("DEBUG GIFT: normal item creation failed. baseItemId={}, baseItemName={}", baseItem.getId(), baseItem.getName());
|
||||
LOGGER.debug("normal item creation failed. baseItemId={}, baseItemName={}", baseItem.getId(), baseItem.getName());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
@@ -440,7 +450,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOGGER.error("DEBUG GIFT: avatar_effect not supported");
|
||||
LOGGER.debug("avatar_effect not supported");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
this.client.sendResponse(new GenericAlertComposer(Emulator.getTexts().getValue("error.catalog.buy.not_yet")));
|
||||
return;
|
||||
@@ -449,7 +459,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
|
||||
if (itemsList.isEmpty()) {
|
||||
LOGGER.error("DEBUG GIFT: itemsList empty before giftData");
|
||||
LOGGER.debug("itemsList empty before giftData");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
@@ -458,7 +468,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
for (HabboItem i : itemsList) {
|
||||
if (i == null) {
|
||||
LOGGER.error("DEBUG GIFT: null HabboItem detected inside itemsList");
|
||||
LOGGER.debug("null HabboItem detected inside itemsList");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
@@ -478,10 +488,37 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
.append("\t")
|
||||
.append(this.client.getHabbo().getHabboInfo().getLook());
|
||||
|
||||
// Deduct currency before createGift so a failure here leaves the sender unpaid rather than gifted.
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS) && totalCredits > 0) {
|
||||
this.client.getHabbo().giveCredits(-totalCredits);
|
||||
paidCredits = totalCredits;
|
||||
}
|
||||
|
||||
if (totalPoints > 0) {
|
||||
if (item.getPointsType() == 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_PIXELS)) {
|
||||
this.client.getHabbo().givePixels(-totalPoints);
|
||||
paidPoints = totalPoints;
|
||||
paidPointsType = 0;
|
||||
} else if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_POINTS)) {
|
||||
this.client.getHabbo().givePoints(item.getPointsType(), -totalPoints);
|
||||
paidPoints = totalPoints;
|
||||
paidPointsType = item.getPointsType();
|
||||
}
|
||||
}
|
||||
|
||||
HabboItem gift = Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, giftData.toString(), 0, 0);
|
||||
|
||||
if (gift == null) {
|
||||
LOGGER.error("DEBUG GIFT: createGift returned null");
|
||||
LOGGER.debug("createGift returned null");
|
||||
if (paidCredits > 0) {
|
||||
this.client.getHabbo().giveCredits(paidCredits);
|
||||
paidCredits = 0;
|
||||
}
|
||||
if (paidPoints > 0) {
|
||||
if (paidPointsType == 0) this.client.getHabbo().givePixels(paidPoints);
|
||||
else this.client.getHabbo().givePoints(paidPointsType, paidPoints);
|
||||
paidPoints = 0;
|
||||
}
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
@@ -489,9 +526,8 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
if (limitedConfiguration != null) {
|
||||
for (HabboItem itm : itemsList) {
|
||||
if (itm == null) {
|
||||
LOGGER.error("DEBUG GIFT: null item before limitedSold()");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
// Trip the catch path so the deduction is refunded.
|
||||
throw new IllegalStateException("null item before limitedSold()");
|
||||
}
|
||||
limitedConfiguration.limitedSold(item.getId(), this.client.getHabbo(), itm);
|
||||
}
|
||||
@@ -529,31 +565,26 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) {
|
||||
if (totalCredits > 0) {
|
||||
this.client.getHabbo().giveCredits(-totalCredits);
|
||||
}
|
||||
}
|
||||
// Gift fully delivered; commit cooldown and clear refund tracking so the catch block can't double-refund.
|
||||
this.client.getHabbo().getHabboStats().lastGiftTimestamp = Emulator.getIntUnixTimestamp();
|
||||
paidCredits = 0;
|
||||
paidPoints = 0;
|
||||
|
||||
if (totalPoints > 0) {
|
||||
if (item.getPointsType() == 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_PIXELS)) {
|
||||
this.client.getHabbo().givePixels(-totalPoints);
|
||||
} else if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_POINTS)) {
|
||||
this.client.getHabbo().givePoints(item.getPointsType(), -totalPoints);
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.error("DEBUG GIFT: success sending PurchaseOKComposer");
|
||||
this.client.sendResponse(new PurchaseOKComposer(item));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Exception caught", e);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
if (paidCredits > 0) this.client.getHabbo().giveCredits(paidCredits);
|
||||
if (paidPoints > 0) {
|
||||
if (paidPointsType == 0) this.client.getHabbo().givePixels(paidPoints);
|
||||
else this.client.getHabbo().givePoints(paidPointsType, paidPoints);
|
||||
}
|
||||
} finally {
|
||||
this.client.getHabbo().getHabboStats().isPurchasingFurniture = false;
|
||||
}
|
||||
} else {
|
||||
LOGGER.error("DEBUG GIFT: cooldown blocked purchase");
|
||||
LOGGER.debug("cooldown blocked purchase");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
}
|
||||
}
|
||||
|
||||
+41
-7
@@ -5,6 +5,7 @@ import com.eu.habbo.habbohotel.bots.BotManager;
|
||||
import com.eu.habbo.habbohotel.catalog.*;
|
||||
import com.eu.habbo.habbohotel.catalog.layouts.*;
|
||||
import com.eu.habbo.habbohotel.items.FurnitureType;
|
||||
import com.eu.habbo.habbohotel.items.Item;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.pets.PetManager;
|
||||
import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport;
|
||||
@@ -201,15 +202,48 @@ public class CatalogBuyItemEvent extends MessageHandler {
|
||||
|
||||
else
|
||||
item = page.getCatalogItem(itemId);
|
||||
// temp patch, can a dev with better knowledge than me look into this asap pls.
|
||||
if (page instanceof BotsLayout) {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_BOTS) && this.client.getHabbo().getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
|
||||
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
|
||||
return;
|
||||
|
||||
// Search-results buy sends the catalog offer_id as itemId
|
||||
// (FurnitureOffer.offerId is derived from furnidata's
|
||||
// purchaseOfferId, which matches `catalog_items.offer_id`),
|
||||
// not the `catalog_items.id` primary key that getCatalogItem
|
||||
// expects. Fall back to scanning the page for the matching
|
||||
// offer_id so the search → buy flow works.
|
||||
if (item == null && !(page instanceof RecentPurchasesLayout)) {
|
||||
for (CatalogItem candidate : page.getCatalogItems().valueCollection()) {
|
||||
if (candidate != null && candidate.getOfferId() == itemId) {
|
||||
item = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (page instanceof PetsLayout) {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_PETS) && this.client.getHabbo().getInventory().getPetsComponent().getPets().size() >= PetManager.MAXIMUM_PET_INVENTORY_SIZE) {
|
||||
// Inventory cap check based on the actual base items the
|
||||
// purchase will create, not the page layout - bots/pets
|
||||
// can legitimately live on bundle pages, search results,
|
||||
// recent-purchases, etc., and the layout-instanceof check
|
||||
// missed all those paths. Mirrors the bot/pet branches
|
||||
// inside CatalogManager.purchaseItem (Item.isBot / isPet
|
||||
// and the same prefix check) so detection stays in sync.
|
||||
boolean itemHasBot = false;
|
||||
boolean itemHasPet = false;
|
||||
|
||||
if (item != null) {
|
||||
for (Item baseItem : item.getBaseItems()) {
|
||||
if (baseItem == null) continue;
|
||||
if (Item.isBot(baseItem)) itemHasBot = true;
|
||||
if (Item.isPet(baseItem)) itemHasPet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (itemHasBot && !this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_BOTS)
|
||||
&& this.client.getHabbo().getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
|
||||
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemHasPet) {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_PETS)
|
||||
&& this.client.getHabbo().getInventory().getPetsComponent().getPets().size() >= PetManager.MAXIMUM_PET_INVENTORY_SIZE) {
|
||||
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.pets.max.inventory").replace("%amount%", PetManager.MAXIMUM_PET_INVENTORY_SIZE + ""));
|
||||
return;
|
||||
}
|
||||
|
||||
+14
-1
@@ -36,8 +36,21 @@ public class CatalogAdminCreatePageEvent extends MessageHandler {
|
||||
pageLayout = CatalogPageLayouts.default_3x3;
|
||||
}
|
||||
|
||||
if (parentId != -1 && Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(parentId) == null) {
|
||||
this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + parentId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (iconType < 0) iconType = 0;
|
||||
if (minRank < 1) minRank = 1;
|
||||
if (orderNum < 0) orderNum = 0;
|
||||
if (caption == null) caption = "";
|
||||
if (caption2 == null) caption2 = "";
|
||||
if (caption.length() > 128) caption = caption.substring(0, 128);
|
||||
if (caption2.length() > 25) caption2 = caption2.substring(0, 25);
|
||||
|
||||
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().createCatalogPage(
|
||||
caption, caption2, 0, iconType, pageLayout, minRank, parentId, pageType, catalogMode
|
||||
caption, caption2, 0, iconType, pageLayout, minRank, parentId, pageType, catalogMode
|
||||
);
|
||||
|
||||
if (page == null) {
|
||||
|
||||
+43
-7
@@ -1,6 +1,7 @@
|
||||
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.catalog.CatalogPageType;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
@@ -11,6 +12,9 @@ import java.sql.PreparedStatement;
|
||||
|
||||
public class CatalogAdminMovePageEvent extends MessageHandler {
|
||||
|
||||
private static final int MAX_PARENT_WALK = 64;
|
||||
private static final int ROOT_PARENT_ID = -1;
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
|
||||
@@ -24,12 +28,10 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
|
||||
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
|
||||
String tableName = (pageType == CatalogPageType.BUILDER) ? "catalog_pages_bc" : "catalog_pages";
|
||||
|
||||
// Special values: -1 = toggle enabled, -2 = toggle visible
|
||||
if (newParentId == -1) {
|
||||
// Toggle enabled
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE " + tableName + " SET enabled = IF(enabled = '1', '0', '1') WHERE id = ?")) {
|
||||
"UPDATE " + tableName + " SET enabled = IF(enabled = '1', '0', '1') WHERE id = ?")) {
|
||||
statement.setInt(1, pageId);
|
||||
statement.execute();
|
||||
}
|
||||
@@ -38,21 +40,43 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
|
||||
}
|
||||
|
||||
if (newParentId == -2) {
|
||||
// Toggle visible
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE " + tableName + " SET visible = IF(visible = '1', '0', '1') WHERE id = ?")) {
|
||||
"UPDATE " + tableName + " SET visible = IF(visible = '1', '0', '1') WHERE id = ?")) {
|
||||
statement.setInt(1, pageId);
|
||||
statement.execute();
|
||||
}
|
||||
this.client.sendResponse(new CatalogAdminResultComposer(true, "Visibility toggled"));
|
||||
return;
|
||||
}
|
||||
|
||||
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType);
|
||||
if (page == null) {
|
||||
this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (newParentId == pageId) {
|
||||
this.client.sendResponse(new CatalogAdminResultComposer(false, "A page cannot be its own parent"));
|
||||
return;
|
||||
}
|
||||
|
||||
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(newParentId);
|
||||
if (parent == null) {
|
||||
this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + newParentId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.wouldCreateCycle(pageId, newParentId)) {
|
||||
this.client.sendResponse(new CatalogAdminResultComposer(false, "Refusing to move: that would create a cycle"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (newIndex < 0) newIndex = 0;
|
||||
|
||||
// Normal move: update parent and order
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE " + tableName + " SET parent_id = ?, order_num = ? WHERE id = ?")) {
|
||||
"UPDATE " + tableName + " SET parent_id = ?, order_num = ? WHERE id = ?")) {
|
||||
statement.setInt(1, newParentId);
|
||||
statement.setInt(2, newIndex);
|
||||
statement.setInt(3, pageId);
|
||||
@@ -61,4 +85,16 @@ public class CatalogAdminMovePageEvent extends MessageHandler {
|
||||
|
||||
this.client.sendResponse(new CatalogAdminResultComposer(true, "Page moved"));
|
||||
}
|
||||
|
||||
private boolean wouldCreateCycle(int pageId, int parentId) {
|
||||
int current = parentId;
|
||||
for (int hops = 0; hops < MAX_PARENT_WALK; hops++) {
|
||||
if (current == ROOT_PARENT_ID) return false;
|
||||
if (current == pageId) return true;
|
||||
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(current);
|
||||
if (parent == null) return false;
|
||||
current = parent.getParentId();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+97
-6
@@ -2,16 +2,35 @@ 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.catalog.CatalogPageLayouts;
|
||||
import com.eu.habbo.habbohotel.catalog.CatalogPageType;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.safety.Safelist;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
|
||||
public class CatalogAdminSavePageEvent extends MessageHandler {
|
||||
|
||||
private static final int MAX_CAPTION_LENGTH = 128;
|
||||
private static final int MAX_CAPTION_SAVE_LENGTH = 25;
|
||||
private static final int MAX_HEADLINE_LENGTH = 1024;
|
||||
private static final int MAX_TEASER_LENGTH = 64;
|
||||
private static final int MAX_TEXT_LENGTH = 8192;
|
||||
private static final int MAX_PARENT_WALK = 64;
|
||||
private static final int ROOT_PARENT_ID = -1;
|
||||
|
||||
private static final Safelist PAGE_HTML_SAFELIST = new Safelist()
|
||||
.addTags("b", "i", "u", "br", "span", "div", "p", "a", "strong", "em", "img")
|
||||
.addAttributes("a", "href", "target", "class", "style")
|
||||
.addAttributes("img", "src", "alt", "class", "style")
|
||||
.addAttributes(":all", "class", "style")
|
||||
.addProtocols("a", "href", "http", "https", "mailto", "#")
|
||||
.addProtocols("img", "src", "http", "https", "data");
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
|
||||
@@ -34,7 +53,7 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
|
||||
String textDetails = this.packet.readString();
|
||||
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
|
||||
CatalogPageType catalogMode = CatalogPageType.fromString(this.packet.readString());
|
||||
|
||||
String text1 = this.packet.bytesAvailable() > 0 ? this.packet.readString() : "";
|
||||
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType);
|
||||
|
||||
if (page == null) {
|
||||
@@ -42,9 +61,55 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
CatalogPageLayouts.valueOf(layout);
|
||||
} catch (IllegalArgumentException | NullPointerException e) {
|
||||
this.client.sendResponse(new CatalogAdminResultComposer(false, "Invalid layout: " + layout));
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentId != ROOT_PARENT_ID) {
|
||||
if (parentId == pageId) {
|
||||
this.client.sendResponse(new CatalogAdminResultComposer(false, "A page cannot be its own parent"));
|
||||
return;
|
||||
}
|
||||
|
||||
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(parentId);
|
||||
if (parent == null) {
|
||||
this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + parentId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.wouldCreateCycle(pageId, parentId)) {
|
||||
this.client.sendResponse(new CatalogAdminResultComposer(false, "Refusing to re-parent: that would create a cycle"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (iconType < 0) iconType = 0;
|
||||
if (minRank < 1) minRank = 1;
|
||||
if (orderNum < 0) orderNum = 0;
|
||||
|
||||
headline = this.sanitizeHtml(headline);
|
||||
teaser = this.sanitizeHtml(teaser);
|
||||
textDetails = this.sanitizeHtml(textDetails);
|
||||
text1 = this.sanitizeHtml(text1);
|
||||
|
||||
caption = this.clampLength(caption, MAX_CAPTION_LENGTH);
|
||||
caption2 = this.clampLength(caption2, MAX_CAPTION_SAVE_LENGTH);
|
||||
headline = this.clampLength(headline, MAX_HEADLINE_LENGTH);
|
||||
teaser = this.clampLength(teaser, MAX_TEASER_LENGTH);
|
||||
textDetails = this.clampLength(textDetails, MAX_TEXT_LENGTH);
|
||||
text1 = this.clampLength(text1, MAX_TEXT_LENGTH);
|
||||
|
||||
if (headline.isEmpty() && page.getHeaderImage() != null) headline = page.getHeaderImage();
|
||||
if (teaser.isEmpty() && page.getTeaserImage() != null) teaser = page.getTeaserImage();
|
||||
if (textDetails.isEmpty() && page.getTextDetails() != null) textDetails = page.getTextDetails();
|
||||
if (text1.isEmpty() && page.getTextOne() != null) text1 = page.getTextOne();
|
||||
|
||||
String query = (pageType == CatalogPageType.BUILDER)
|
||||
? "UPDATE catalog_pages_bc SET caption = ?, page_layout = ?, icon_image = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ? WHERE id = ?"
|
||||
: "UPDATE catalog_pages SET caption = ?, caption_save = ?, page_layout = ?, icon_image = ?, min_rank = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ?, catalog_mode = ? WHERE id = ?";
|
||||
? "UPDATE catalog_pages_bc SET caption = ?, page_layout = ?, icon_image = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ?, page_text1 = ? WHERE id = ?"
|
||||
: "UPDATE catalog_pages SET caption = ?, caption_save = ?, page_layout = ?, icon_image = ?, min_rank = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ?, page_text1 = ?, catalog_mode = ? WHERE id = ?";
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(query)) {
|
||||
@@ -60,7 +125,8 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
|
||||
statement.setString(8, headline);
|
||||
statement.setString(9, teaser);
|
||||
statement.setString(10, textDetails);
|
||||
statement.setInt(11, pageId);
|
||||
statement.setString(11, text1);
|
||||
statement.setInt(12, pageId);
|
||||
} else {
|
||||
statement.setString(2, caption2);
|
||||
statement.setString(3, layout);
|
||||
@@ -73,8 +139,9 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
|
||||
statement.setString(10, headline);
|
||||
statement.setString(11, teaser);
|
||||
statement.setString(12, textDetails);
|
||||
statement.setString(13, catalogMode.name());
|
||||
statement.setInt(14, pageId);
|
||||
statement.setString(13, text1);
|
||||
statement.setString(14, catalogMode.name());
|
||||
statement.setInt(15, pageId);
|
||||
}
|
||||
|
||||
statement.execute();
|
||||
@@ -82,4 +149,28 @@ public class CatalogAdminSavePageEvent extends MessageHandler {
|
||||
|
||||
this.client.sendResponse(new CatalogAdminResultComposer(true, "Page saved"));
|
||||
}
|
||||
|
||||
private boolean wouldCreateCycle(int pageId, int parentId) {
|
||||
int current = parentId;
|
||||
for (int hops = 0; hops < MAX_PARENT_WALK; hops++) {
|
||||
if (current == ROOT_PARENT_ID) return false;
|
||||
if (current == pageId) return true;
|
||||
CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(current);
|
||||
if (parent == null) return false;
|
||||
current = parent.getParentId();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String clampLength(String value, int max) {
|
||||
if (value == null) return "";
|
||||
if (value.length() <= max) return value;
|
||||
return value.substring(0, max);
|
||||
}
|
||||
|
||||
|
||||
private String sanitizeHtml(String value) {
|
||||
if (value == null || value.isEmpty()) return "";
|
||||
return Jsoup.clean(value, PAGE_HTML_SAFELIST);
|
||||
}
|
||||
}
|
||||
|
||||
+227
-123
@@ -4,23 +4,36 @@ import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.*;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboItem;
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer;
|
||||
import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer;
|
||||
import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer;
|
||||
import com.eu.habbo.messages.outgoing.rooms.ForwardToRoomComposer;
|
||||
import gnu.trove.set.hash.THashSet;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class FloorPlanEditorSaveEvent extends MessageHandler {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FloorPlanEditorSaveEvent.class);
|
||||
|
||||
public static int MAXIMUM_FLOORPLAN_WIDTH_LENGTH = 64;
|
||||
public static int MAXIMUM_FLOORPLAN_SIZE = 64 * 64;
|
||||
|
||||
private static final int SAVE_COOLDOWN_SECONDS = 3;
|
||||
private static final int MAX_AUTO_PICKUP_ITEMS = 500;
|
||||
private static final Pattern ALLOWED_MAP_CHARS = Pattern.compile("[a-zA-Z0-9\r]+");
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
@@ -38,153 +51,244 @@ public class FloorPlanEditorSaveEvent extends MessageHandler {
|
||||
if (room == null)
|
||||
return;
|
||||
|
||||
if (room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) {
|
||||
StringJoiner errors = new StringJoiner("<br />");
|
||||
String map = this.packet.readString();
|
||||
map = map.replace("X", "x");
|
||||
if (!(room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER))) {
|
||||
return;
|
||||
}
|
||||
|
||||
String[] mapRows = map.split("\r");
|
||||
long now = Emulator.getIntUnixTimestamp();
|
||||
if (now - this.client.getHabbo().getHabboStats().lastFloorplanSaveTimestamp < SAVE_COOLDOWN_SECONDS) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, "Please wait a few seconds before saving again."));
|
||||
return;
|
||||
}
|
||||
|
||||
int firstRowSize = mapRows[0].length();
|
||||
StringJoiner errors = new StringJoiner("<br />");
|
||||
String map = this.packet.readString();
|
||||
|
||||
if (Emulator.getConfig().getBoolean("hotel.room.floorplan.check.enabled")) {
|
||||
if (!map.matches("[a-zA-Z0-9\r]+")) errors.add("${notification.floorplan_editor.error.title}");
|
||||
if (map == null || map.length() > MAXIMUM_FLOORPLAN_SIZE) {
|
||||
LOGGER.warn("Floorplan save rejected (oversize): user={} room={} mapLen={}",
|
||||
this.client.getHabbo().getHabboInfo().getId(), room.getId(), map == null ? 0 : map.length());
|
||||
return;
|
||||
}
|
||||
|
||||
Arrays.stream(mapRows)
|
||||
.filter(line -> line.length() != firstRowSize)
|
||||
.findAny()
|
||||
.ifPresent(s -> errors.add("(General): Line " + (Arrays.asList(mapRows).indexOf(s) + 1) + " is of different length than line 1"));
|
||||
if (!ALLOWED_MAP_CHARS.matcher(map).matches()) {
|
||||
LOGGER.warn("Floorplan save rejected (illegal chars): user={} room={}",
|
||||
this.client.getHabbo().getHabboInfo().getId(), room.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
if (map.isEmpty() || map.replace("x", "").replace("\r", "").isEmpty()) {
|
||||
errors.add("${notification.floorplan_editor.error.message.effective_height_is_0}");
|
||||
map = map.replace("X", "x");
|
||||
|
||||
String[] mapRows = map.split("\r");
|
||||
|
||||
if (mapRows.length == 0 || mapRows.length > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) {
|
||||
return;
|
||||
}
|
||||
|
||||
int firstRowSize = mapRows[0].length();
|
||||
|
||||
if (firstRowSize == 0 || firstRowSize > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String row : mapRows) {
|
||||
if (row.length() != firstRowSize) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (Emulator.getConfig().getBoolean("hotel.room.floorplan.check.enabled")) {
|
||||
if (map.replace("x", "").replace("\r", "").isEmpty()) {
|
||||
errors.add("${notification.floorplan_editor.error.message.effective_height_is_0}");
|
||||
}
|
||||
}
|
||||
|
||||
int doorX = this.packet.readInt();
|
||||
int doorY = this.packet.readInt();
|
||||
|
||||
if (doorX < 0 || doorX >= firstRowSize || doorY < 0 || doorY >= mapRows.length) {
|
||||
errors.add("${notification.floorplan_editor.error.message.entry_tile_outside_map}");
|
||||
} else if (mapRows[doorY].charAt(doorX) == 'x') {
|
||||
errors.add("${notification.floorplan_editor.error.message.entry_not_on_tile}");
|
||||
}
|
||||
|
||||
int doorRotation = this.packet.readInt();
|
||||
if (doorRotation < 0 || doorRotation > 7) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_entry_tile_direction}");
|
||||
}
|
||||
|
||||
int wallSize = this.packet.readInt();
|
||||
if (wallSize < -2 || wallSize > 1) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_wall_thickness}");
|
||||
}
|
||||
int floorSize = this.packet.readInt();
|
||||
if (floorSize < -2 || floorSize > 1) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_floor_thickness}");
|
||||
}
|
||||
|
||||
int wallHeight = -1;
|
||||
if (this.packet.bytesAvailable() >= 4)
|
||||
wallHeight = this.packet.readInt();
|
||||
|
||||
if (wallHeight < -1 || wallHeight > 15) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_walls_fixed_height}");
|
||||
}
|
||||
|
||||
boolean autoPickup = false;
|
||||
if (this.packet.bytesAvailable() >= 1) {
|
||||
autoPickup = this.packet.readBoolean();
|
||||
}
|
||||
|
||||
if (errors.length() > 0) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
THashSet<RoomTile> locked_tileList = room.getLockedTiles();
|
||||
THashSet<RoomTile> new_tileList = new THashSet<>();
|
||||
THashSet<HabboItem> itemsToPickup = new THashSet<>();
|
||||
int blockedX = -1;
|
||||
int blockedY = -1;
|
||||
blockingRoomItemScan:
|
||||
for (int y = 0; y < mapRows.length; y++) {
|
||||
for (int x = 0; x < firstRowSize; x++) {
|
||||
|
||||
RoomTile tile = room.getLayout().getTile((short) x, (short) y);
|
||||
new_tileList.add(tile);
|
||||
String square = String.valueOf(mapRows[y].charAt(x));
|
||||
short height;
|
||||
|
||||
if (square.equalsIgnoreCase("x") && room.getTopItemAt(x, y) != null) {
|
||||
if (autoPickup) {
|
||||
THashSet<HabboItem> here = room.getItemsAt(x, y);
|
||||
if (here != null) itemsToPickup.addAll(here);
|
||||
continue;
|
||||
}
|
||||
blockedX = x;
|
||||
blockedY = y;
|
||||
break blockingRoomItemScan;
|
||||
}
|
||||
|
||||
if (map.length() > MAXIMUM_FLOORPLAN_SIZE) {
|
||||
errors.add("${notification.floorplan_editor.error.message.too_large_area}");
|
||||
}
|
||||
|
||||
if (mapRows.length > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) errors.add("${notification.floorplan_editor.error.message.too_large_height}");
|
||||
else if (Arrays.stream(mapRows).anyMatch(l -> l.length() > MAXIMUM_FLOORPLAN_WIDTH_LENGTH || l.isEmpty())) errors.add("${notification.floorplan_editor.error.message.too_large_width}");
|
||||
|
||||
if (errors.length() > 0) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString()));
|
||||
try {
|
||||
if (square.isEmpty()) {
|
||||
height = 0;
|
||||
} else if (Emulator.isNumeric(square)) {
|
||||
height = Short.parseShort(square);
|
||||
} else {
|
||||
int idx = "abcdefghijklmnopqrstuvwxyz".indexOf(square.toLowerCase());
|
||||
if (idx < 0) {
|
||||
return;
|
||||
}
|
||||
height = (short) Math.min(26, 10 + idx);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int doorX = this.packet.readInt();
|
||||
int doorY = this.packet.readInt();
|
||||
|
||||
if (doorX < 0 || doorX > firstRowSize || doorY < 0 || doorY >= mapRows.length) {
|
||||
errors.add("${notification.floorplan_editor.error.message.entry_tile_outside_map}");
|
||||
}
|
||||
|
||||
if (doorY < mapRows.length && doorX < mapRows[doorY].length() && mapRows[doorY].charAt(doorX) == 'x') {
|
||||
errors.add("${notification.floorplan_editor.error.message.entry_not_on_tile}");
|
||||
}
|
||||
|
||||
int doorRotation = this.packet.readInt();
|
||||
if (doorRotation < 0 || doorRotation > 7) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_entry_tile_direction}");
|
||||
}
|
||||
|
||||
int wallSize = this.packet.readInt();
|
||||
if (wallSize < -2 || wallSize > 1) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_wall_thickness}");
|
||||
}
|
||||
int floorSize = this.packet.readInt();
|
||||
if (floorSize < -2 || floorSize > 1) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_floor_thickness}");
|
||||
}
|
||||
|
||||
int wallHeight = -1;
|
||||
if (this.packet.bytesAvailable() >= 4)
|
||||
wallHeight = this.packet.readInt();
|
||||
|
||||
if (wallHeight < -1 || wallHeight > 15) {
|
||||
errors.add("${notification.floorplan_editor.error.message.invalid_walls_fixed_height}");
|
||||
}
|
||||
|
||||
THashSet<RoomTile> locked_tileList = room.getLockedTiles();
|
||||
THashSet<RoomTile> new_tileList = new THashSet<>();
|
||||
blockingRoomItemScan:
|
||||
for (int y = 0; y < mapRows.length; y++) {
|
||||
for (int x = 0; x < firstRowSize; x++) {
|
||||
|
||||
RoomTile tile = room.getLayout().getTile((short) x, (short) y);
|
||||
new_tileList.add(tile);
|
||||
String square = String.valueOf(mapRows[y].charAt(x));
|
||||
short height;
|
||||
|
||||
if (square.equalsIgnoreCase("x") && room.getTopItemAt(x, y) != null) {
|
||||
errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
|
||||
break blockingRoomItemScan;
|
||||
} else {
|
||||
if (square.isEmpty()) {
|
||||
height = 0;
|
||||
} else if (Emulator.isNumeric(square)) {
|
||||
height = Short.parseShort(square);
|
||||
} else {
|
||||
height = (short) (10 + "ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(square.toUpperCase()));
|
||||
}
|
||||
}
|
||||
|
||||
if (tile != null && tile.state != RoomTileState.INVALID && height != tile.z && room.getTopItemAt(x, y) != null) {
|
||||
errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
|
||||
break blockingRoomItemScan;
|
||||
if (tile != null && tile.state != RoomTileState.INVALID && height != tile.z && room.getTopItemAt(x, y) != null) {
|
||||
if (autoPickup) {
|
||||
THashSet<HabboItem> here = room.getItemsAt(x, y);
|
||||
if (here != null) itemsToPickup.addAll(here);
|
||||
continue;
|
||||
}
|
||||
blockedX = x;
|
||||
blockedY = y;
|
||||
break blockingRoomItemScan;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (blockedX < 0) {
|
||||
locked_tileList.removeAll(new_tileList);
|
||||
if (!locked_tileList.isEmpty()) {
|
||||
errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
|
||||
if (autoPickup) {
|
||||
for (RoomTile lt : locked_tileList) {
|
||||
THashSet<HabboItem> here = room.getItemsAt(lt.x, lt.y);
|
||||
if (here != null) itemsToPickup.addAll(here);
|
||||
}
|
||||
} else {
|
||||
RoomTile first = locked_tileList.iterator().next();
|
||||
blockedX = first.x;
|
||||
blockedY = first.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (blockedX >= 0) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key,
|
||||
"${notification.floorplan_editor.error.message.change_blocked_by_room_item} (" + blockedX + ", " + blockedY + ")"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (errors.length() > 0) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString()));
|
||||
if (autoPickup && !itemsToPickup.isEmpty()) {
|
||||
if (itemsToPickup.size() > MAX_AUTO_PICKUP_ITEMS) {
|
||||
LOGGER.warn("Floorplan auto-pickup rejected (over cap): user={} room={} itemCount={} cap={}",
|
||||
this.client.getHabbo().getHabboInfo().getId(), room.getId(), itemsToPickup.size(), MAX_AUTO_PICKUP_ITEMS);
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key,
|
||||
"Too many items would be picked up (" + itemsToPickup.size() + " > " + MAX_AUTO_PICKUP_ITEMS + "). Remove some furniture manually and save again."));
|
||||
return;
|
||||
}
|
||||
|
||||
RoomLayout layout = room.getLayout();
|
||||
|
||||
if (layout instanceof CustomRoomLayout) {
|
||||
layout.setDoorX((short) doorX);
|
||||
layout.setDoorY((short) doorY);
|
||||
layout.setDoorDirection(doorRotation);
|
||||
layout.setHeightmap(map);
|
||||
layout.parse();
|
||||
|
||||
if (layout.getDoorTile() == null) {
|
||||
this.client.getHabbo().alert("Error");
|
||||
((CustomRoomLayout) layout).needsUpdate(false);
|
||||
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
|
||||
return;
|
||||
}
|
||||
((CustomRoomLayout) layout).needsUpdate(true);
|
||||
Emulator.getThreading().run((CustomRoomLayout) layout);
|
||||
} else {
|
||||
layout = Emulator.getGameEnvironment().getRoomManager().insertCustomLayout(room, map, doorX, doorY, doorRotation);
|
||||
Map<Integer, ArrayList<HabboItem>> byOwner = new HashMap<>();
|
||||
for (HabboItem itm : itemsToPickup) {
|
||||
if (itm == null) continue;
|
||||
byOwner.computeIfAbsent(itm.getUserId(), k -> new ArrayList<>()).add(itm);
|
||||
room.pickUpItem(itm, null);
|
||||
}
|
||||
|
||||
if (layout != null) {
|
||||
room.setHasCustomLayout(true);
|
||||
room.setNeedsUpdate(true);
|
||||
room.setLayout(layout);
|
||||
room.setWallSize(wallSize);
|
||||
room.setFloorSize(floorSize);
|
||||
room.setWallHeight(wallHeight);
|
||||
room.save();
|
||||
Collection<Habbo> habbos = new ArrayList<>(room.getUserCount());
|
||||
habbos.addAll(room.getHabbos());
|
||||
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
|
||||
room = Emulator.getGameEnvironment().getRoomManager().loadRoom(room.getId());
|
||||
ServerMessage message = new ForwardToRoomComposer(room.getId()).compose();
|
||||
for (Habbo habbo : habbos) {
|
||||
habbo.getClient().sendResponse(message);
|
||||
for (Map.Entry<Integer, ArrayList<HabboItem>> entry : byOwner.entrySet()) {
|
||||
Habbo owner = Emulator.getGameEnvironment().getHabboManager().getHabbo(entry.getKey());
|
||||
if (owner == null) continue;
|
||||
for (HabboItem itm : entry.getValue()) {
|
||||
owner.getClient().sendResponse(new AddHabboItemComposer(itm));
|
||||
}
|
||||
owner.getClient().sendResponse(new InventoryRefreshComposer());
|
||||
}
|
||||
|
||||
LOGGER.info("Floorplan auto-pickup: user={} room={} itemCount={} owners={}",
|
||||
this.client.getHabbo().getHabboInfo().getId(), room.getId(), itemsToPickup.size(), byOwner.size());
|
||||
}
|
||||
|
||||
RoomLayout layout = room.getLayout();
|
||||
|
||||
if (layout instanceof CustomRoomLayout) {
|
||||
layout.setDoorX((short) doorX);
|
||||
layout.setDoorY((short) doorY);
|
||||
layout.setDoorDirection(doorRotation);
|
||||
layout.setHeightmap(map);
|
||||
layout.parse();
|
||||
|
||||
if (layout.getDoorTile() == null) {
|
||||
this.client.getHabbo().alert("Error");
|
||||
((CustomRoomLayout) layout).needsUpdate(false);
|
||||
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
|
||||
return;
|
||||
}
|
||||
((CustomRoomLayout) layout).needsUpdate(true);
|
||||
Emulator.getThreading().run((CustomRoomLayout) layout);
|
||||
} else {
|
||||
layout = Emulator.getGameEnvironment().getRoomManager().insertCustomLayout(room, map, doorX, doorY, doorRotation);
|
||||
}
|
||||
|
||||
if (layout != null) {
|
||||
room.setHasCustomLayout(true);
|
||||
room.setNeedsUpdate(true);
|
||||
room.setLayout(layout);
|
||||
room.setWallSize(wallSize);
|
||||
room.setFloorSize(floorSize);
|
||||
room.setWallHeight(wallHeight);
|
||||
room.save();
|
||||
|
||||
this.client.getHabbo().getHabboStats().lastFloorplanSaveTimestamp = now;
|
||||
LOGGER.info("Floorplan saved: user={} room={} mapLen={} rows={} cols={}",
|
||||
this.client.getHabbo().getHabboInfo().getId(), room.getId(), map.length(), mapRows.length, firstRowSize);
|
||||
|
||||
Collection<Habbo> habbos = new ArrayList<>(room.getUserCount());
|
||||
habbos.addAll(room.getHabbos());
|
||||
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
|
||||
room = Emulator.getGameEnvironment().getRoomManager().loadRoom(room.getId());
|
||||
ServerMessage message = new ForwardToRoomComposer(room.getId()).compose();
|
||||
for (Habbo habbo : habbos) {
|
||||
habbo.getClient().sendResponse(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolBan;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolBanType;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Apply an arbitrary-duration account ban. Duration is taken in hours
|
||||
* from the wire and converted to seconds for ModToolManager.ban —
|
||||
* unlike ModToolSanctionBanEvent which only accepts the four fixed
|
||||
* Habbo-protocol banType buckets.
|
||||
*/
|
||||
public class HousekeepingBanUserEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.ban";
|
||||
private static final int SECONDS_IN_HOUR = 3600;
|
||||
// 100-year ceiling, matches ModToolSanctionBanEvent's permanent ban.
|
||||
private static final int MAX_DURATION_SECONDS = 100 * 365 * 24 * 3600;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
String reason = this.packet.readString();
|
||||
int hours = this.packet.readInt();
|
||||
|
||||
if (userId <= 0 || hours <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
long durationLong = (long) hours * SECONDS_IN_HOUR;
|
||||
int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong;
|
||||
|
||||
List<ModToolBan> bans = Emulator.getGameEnvironment().getModToolManager()
|
||||
.ban(userId, this.client.getHabbo(), reason != null ? reason : "", duration, ModToolBanType.ACCOUNT, 0);
|
||||
|
||||
if (bans == null || bans.isEmpty()) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.ban_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
// ModToolBan doesn't expose the `bans` table autoinc id on the
|
||||
// object, so we return the target user id as the actionId — it's
|
||||
// the only stable handle the client can use until a dedicated
|
||||
// housekeeping_log row id supersedes it.
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Permanently delete a room. Mirrors the minimum-viable subset of
|
||||
* RequestDeleteRoomEvent: eject all users from the live room, dispose
|
||||
* + uncache, then DELETE FROM rooms. Pets/guild/custom-layout cleanup
|
||||
* is intentionally skipped on this slice — leftover rows in those
|
||||
* tables become orphans but don't crash the emulator; a follow-up
|
||||
* pass can cascade once we have a HK audit-log row to attach the
|
||||
* orphan-cleanup to.
|
||||
*/
|
||||
public class HousekeepingDeleteRoomEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "room.delete";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = this.packet.readInt();
|
||||
|
||||
if (roomId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
|
||||
|
||||
if (room != null) {
|
||||
room.ejectAll();
|
||||
room.preventUnloading = false;
|
||||
room.dispose();
|
||||
Emulator.getGameEnvironment().getRoomManager().uncacheRoom(room);
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM rooms WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, roomId);
|
||||
int rows = statement.executeUpdate();
|
||||
|
||||
if (rows == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingRoomDetailComposer;
|
||||
|
||||
public class HousekeepingFindRoomByIdEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = this.packet.readInt();
|
||||
|
||||
if (roomId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingRoomDetailComposer(null));
|
||||
return;
|
||||
}
|
||||
|
||||
// loadRoom covers both the in-memory cache (already-loaded rooms)
|
||||
// and the offline path (SELECT * FROM rooms WHERE id=?). Pass
|
||||
// false for loadData so we don't pull furni/bots/pets just to
|
||||
// render an HK panel summary — getOwnerName / getUserCount work
|
||||
// on the pre-loaded skeleton.
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
|
||||
|
||||
this.client.sendResponse(new HousekeepingRoomDetailComposer(room));
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingUserDetailComposer;
|
||||
|
||||
public class HousekeepingFindUserByIdEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
|
||||
if (userId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingUserDetailComposer(null));
|
||||
return;
|
||||
}
|
||||
|
||||
// HabboManager.getHabboInfo(int) returns the in-memory record for
|
||||
// online users and falls through to the offline SQL lookup
|
||||
// otherwise, so a single call covers both branches.
|
||||
HabboInfo info = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId);
|
||||
|
||||
this.client.sendResponse(new HousekeepingUserDetailComposer(info));
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingUserDetailComposer;
|
||||
|
||||
public class HousekeepingFindUserByNameEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String username = this.packet.readString();
|
||||
|
||||
if (username == null || username.isEmpty()) {
|
||||
this.client.sendResponse(new HousekeepingUserDetailComposer(null));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
|
||||
HabboInfo info = online != null ? online.getHabboInfo() : HabboManager.getOfflineHabboInfo(username);
|
||||
|
||||
this.client.sendResponse(new HousekeepingUserDetailComposer(info));
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
/**
|
||||
* Force-close the session of an online user. Unlike kick (which only
|
||||
* removes them from the current room), this drops their socket. Equivalent
|
||||
* to /disconnect in command form but issued through the HK panel.
|
||||
*/
|
||||
public class HousekeepingForceDisconnectUserEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.disconnect";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
String reason = this.packet.readString();
|
||||
|
||||
if (userId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (target == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_offline"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (reason != null && !reason.isEmpty()) {
|
||||
target.alert(reason);
|
||||
}
|
||||
|
||||
// ACK first so the action result lands before the target's socket
|
||||
// closes (otherwise an alerted user on the same emulator thread may
|
||||
// already be torn down when we try to write).
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
|
||||
target.disconnect();
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingDashboardComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class HousekeepingGetDashboardEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int onlineUsers = Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().size();
|
||||
int activeRooms = 0;
|
||||
int totalUsers = 0;
|
||||
int totalRooms = 0;
|
||||
int pendingTickets = 0;
|
||||
int sanctionsLast24h = 0;
|
||||
int now = Emulator.getIntUnixTimestamp();
|
||||
|
||||
// activeRooms = loaded rooms with at least one user
|
||||
try {
|
||||
for (var room : Emulator.getGameEnvironment().getRoomManager().getActiveRooms()) {
|
||||
if (room != null && room.getUserCount() > 0) activeRooms++;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// fall through with 0
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM users");
|
||||
ResultSet rs = statement.executeQuery()) {
|
||||
if (rs.next()) totalUsers = rs.getInt(1);
|
||||
}
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM rooms");
|
||||
ResultSet rs = statement.executeQuery()) {
|
||||
if (rs.next()) totalRooms = rs.getInt(1);
|
||||
}
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM support_tickets WHERE state = 0");
|
||||
ResultSet rs = statement.executeQuery()) {
|
||||
if (rs.next()) pendingTickets = rs.getInt(1);
|
||||
}
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM bans WHERE timestamp > ?")) {
|
||||
statement.setInt(1, now - (24 * 3600));
|
||||
try (ResultSet rs = statement.executeQuery()) {
|
||||
if (rs.next()) sanctionsLast24h = rs.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (SQLException ignored) {
|
||||
// Surface 0s rather than failing the whole dashboard on a missing
|
||||
// optional table — the HK panel can render partial data.
|
||||
}
|
||||
|
||||
int uptime = (int) ((System.currentTimeMillis() - HOUSEKEEPING_BOOT_MILLIS) / 1000);
|
||||
String version = "Arcturus-Morningstar-Extended";
|
||||
|
||||
this.client.sendResponse(new HousekeepingDashboardComposer(
|
||||
onlineUsers,
|
||||
totalUsers,
|
||||
activeRooms,
|
||||
totalRooms,
|
||||
onlineUsers, // peakOnlineToday — not tracked, use current as best-effort
|
||||
onlineUsers, // peakOnlineAllTime — same
|
||||
pendingTickets,
|
||||
sanctionsLast24h,
|
||||
Math.max(uptime, 0),
|
||||
version
|
||||
));
|
||||
}
|
||||
|
||||
// Approximate uptime — captured at class-load time rather than emu startup
|
||||
// (Emulator.java doesn't expose a public startup timestamp). For HK panel
|
||||
// headline metrics this is close enough; if tighter accuracy is needed
|
||||
// later, plumb Emulator.startup through and read it here.
|
||||
private static final long HOUSEKEEPING_BOOT_MILLIS = System.currentTimeMillis();
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class HousekeepingGiveCreditsEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.give_credits";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
int amount = this.packet.readInt();
|
||||
|
||||
if (userId <= 0 || amount == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (online != null) {
|
||||
// giveCredits already pushes UserCreditsComposer and persists via the
|
||||
// standard HabboInfo write path; nothing extra needed for the online branch.
|
||||
online.giveCredits(amount);
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE users SET credits = credits + ? WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, amount);
|
||||
statement.setInt(2, userId);
|
||||
int rows = statement.executeUpdate();
|
||||
|
||||
if (rows == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Generic non-credits currency grant. Wire field `currencyType`:
|
||||
* 0 => duckets / pixels, 5 => diamonds, 101 => seasonal-primary.
|
||||
* Online users go through Habbo.givePoints / givePixels which dispatches
|
||||
* a UserCurrencyComposer; offline goes straight to `users_currency`.
|
||||
*/
|
||||
public class HousekeepingGiveCurrencyEvent extends MessageHandler {
|
||||
private static final int CURRENCY_DUCKETS = 0;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
int currencyType = this.packet.readInt();
|
||||
int amount = this.packet.readInt();
|
||||
|
||||
String actionKey = "user.give_currency_" + currencyType;
|
||||
|
||||
if (userId <= 0 || amount == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (online != null) {
|
||||
// givePixels writes users_currency type=0 and ships UserCurrency;
|
||||
// givePoints(type, amount) is the generalised path for everything else.
|
||||
if (currencyType == CURRENCY_DUCKETS) {
|
||||
online.givePixels(amount);
|
||||
} else {
|
||||
online.givePoints(currencyType, amount);
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO users_currency (user_id, type, amount) VALUES (?, ?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE amount = amount + VALUES(amount)")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setInt(2, currencyType);
|
||||
statement.setInt(3, amount);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Grant a furni item (by items_base id) `quantity` times. Each row in
|
||||
* the `items` table is one furni unit; quantity > 1 just batches the
|
||||
* insert. The online user's HabboInventory isn't proactively refreshed
|
||||
* — they'll see the new items next time they open the hand inventory
|
||||
* (or after a relog).
|
||||
*/
|
||||
public class HousekeepingGrantItemEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.grant_item";
|
||||
private static final int MAX_QUANTITY_PER_CALL = 100;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
int itemId = this.packet.readInt();
|
||||
int quantity = this.packet.readInt();
|
||||
|
||||
if (userId <= 0 || itemId <= 0 || quantity <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (quantity > MAX_QUANTITY_PER_CALL) {
|
||||
quantity = MAX_QUANTITY_PER_CALL;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("INSERT INTO items (user_id, item_id, extra_data) VALUES (?, ?, '')")) {
|
||||
for (int i = 0; i < quantity; i++) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setInt(2, itemId);
|
||||
statement.addBatch();
|
||||
}
|
||||
statement.executeBatch();
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.economy_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
public class HousekeepingKickAllFromRoomEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "room.kick_all";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = this.packet.readInt();
|
||||
|
||||
if (roomId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
|
||||
|
||||
if (room == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
room.ejectAll();
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
/**
|
||||
* Kick a user out of their current room. Mirrors ModToolManager.kick
|
||||
* (leave room + alert), but the legacy method gates on ACC_SUPPORTTOOL,
|
||||
* which would force HK operators to also hold the support-tool permission.
|
||||
* Replicating the few lines locally keeps the HK module self-gated on
|
||||
* ACC_HOUSEKEEPING.
|
||||
*/
|
||||
public class HousekeepingKickUserEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.kick";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
String reason = this.packet.readString();
|
||||
|
||||
if (userId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (target == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_offline"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.hasPermission(Permission.ACC_UNKICKABLE)) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.target_unkickable"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.getHabboInfo().getCurrentRoom() != null) {
|
||||
Emulator.getGameEnvironment().getRoomManager().leaveRoom(target, target.getHabboInfo().getCurrentRoom());
|
||||
}
|
||||
|
||||
if (reason != null && !reason.isEmpty()) {
|
||||
target.alert(reason);
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionLogComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Read the housekeeping_log audit table. The table isn't part of the
|
||||
* base FullDatabase.sql yet — operators who want audit have to create
|
||||
* it once:
|
||||
*
|
||||
* CREATE TABLE IF NOT EXISTS `housekeeping_log` (
|
||||
* `id` INT NOT NULL AUTO_INCREMENT,
|
||||
* `timestamp` INT NOT NULL,
|
||||
* `actor_id` INT NOT NULL,
|
||||
* `actor_name` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
* `target_type` VARCHAR(16) NOT NULL DEFAULT 'user',
|
||||
* `target_id` INT NOT NULL DEFAULT 0,
|
||||
* `target_label` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
* `action` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
* `detail` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
* `success` TINYINT NOT NULL DEFAULT 1,
|
||||
* PRIMARY KEY (`id`), KEY `timestamp` (`timestamp`)
|
||||
* ) ENGINE=InnoDB;
|
||||
*
|
||||
* If the table is missing we swallow the SQL error and return an empty
|
||||
* list — the panel just shows "no audit entries" instead of breaking.
|
||||
* Writing into the table is a follow-up: each HK handler will append
|
||||
* a row once the table exists; for now the listing is read-only.
|
||||
*/
|
||||
public class HousekeepingListActionLogEvent extends MessageHandler {
|
||||
private static final int HARD_LIMIT = 500;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int limit = Math.min(Math.max(this.packet.readInt(), 1), HARD_LIMIT);
|
||||
|
||||
List<HousekeepingActionLogComposer.Row> rows = new ArrayList<>();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT id, timestamp, actor_id, actor_name, target_type, target_id, target_label, action, detail, success " +
|
||||
"FROM housekeeping_log ORDER BY id DESC LIMIT ?")) {
|
||||
statement.setInt(1, limit);
|
||||
|
||||
try (ResultSet rs = statement.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
rows.add(new HousekeepingActionLogComposer.Row(
|
||||
rs.getInt("id"),
|
||||
rs.getInt("timestamp"),
|
||||
rs.getInt("actor_id"),
|
||||
rs.getString("actor_name"),
|
||||
rs.getString("target_type"),
|
||||
rs.getInt("target_id"),
|
||||
rs.getString("target_label"),
|
||||
rs.getString("action"),
|
||||
rs.getString("detail"),
|
||||
rs.getInt("success") == 1
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (SQLException ignored) {
|
||||
// table absent — return empty list, log via emu logger left to the panel UI
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionLogComposer(rows));
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
/**
|
||||
* Toggle room-wide mute. Habbo's Room.setMuted is boolean, not duration-
|
||||
* scoped, so the wire `minutes` arg picks the semantic: minutes==0 =>
|
||||
* unmute, minutes>0 => mute. An emulator-side scheduled unmute could
|
||||
* use the value as a timer, but for now the mute stays until the
|
||||
* operator unmutes manually — the minutes is reserved as a forward-
|
||||
* compat field on the wire.
|
||||
*/
|
||||
public class HousekeepingMuteRoomEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "room.mute";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = this.packet.readInt();
|
||||
int minutes = this.packet.readInt();
|
||||
|
||||
if (roomId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
|
||||
|
||||
if (room == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
room.setMuted(minutes > 0);
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
/**
|
||||
* Apply an arbitrary-duration in-room mute. Habbo.mute is a session-only
|
||||
* mute (it stores remaining seconds on the live Habbo object), so the
|
||||
* target must be online for the action to take effect — when the target
|
||||
* isn't online the handler returns ok=false with `user_offline` so the
|
||||
* UI can fall back to ModToolSanctionMute or surface a clear error.
|
||||
*/
|
||||
public class HousekeepingMuteUserEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.mute";
|
||||
private static final int SECONDS_IN_MINUTE = 60;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
String reason = this.packet.readString();
|
||||
int minutes = this.packet.readInt();
|
||||
|
||||
if (userId <= 0 || minutes <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (target == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_offline"));
|
||||
return;
|
||||
}
|
||||
|
||||
target.mute(minutes * SECONDS_IN_MINUTE, false);
|
||||
|
||||
if (reason != null && !reason.isEmpty()) {
|
||||
target.alert(reason);
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Reset a user's password to a fresh random 12-character alphanumeric
|
||||
* string. Persists a BCrypt `$2a$` hash of the new password into
|
||||
* `users.password` (matches what `AuthHttpUtil.checkPassword` /
|
||||
* `SessionEndpoints` / `AccountChangeEndpoints` already write and read),
|
||||
* clears `auth_ticket` so any active session can't be re-used to bypass
|
||||
* the reset, and ships the PLAINTEXT new password back to the operator
|
||||
* in the action-result `message` so they can communicate it out-of-band.
|
||||
*/
|
||||
public class HousekeepingResetUserPasswordEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.reset_password";
|
||||
private static final String PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
|
||||
private static final int PASSWORD_LENGTH = 12;
|
||||
private static final int BCRYPT_COST = 10;
|
||||
|
||||
private static final SecureRandom RNG = new SecureRandom();
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
|
||||
if (userId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
String plain = randomPassword();
|
||||
String hash;
|
||||
|
||||
try {
|
||||
hash = BCrypt.hashpw(plain, BCrypt.gensalt(BCRYPT_COST));
|
||||
} catch (IllegalArgumentException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.hash_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE users SET password = ?, auth_ticket = '' WHERE id = ? LIMIT 1")) {
|
||||
statement.setString(1, hash);
|
||||
statement.setInt(2, userId);
|
||||
int rows = statement.executeUpdate();
|
||||
|
||||
if (rows == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Plaintext flows through `message` — the client surfaces it via the
|
||||
// status banner so the operator can read it once. SSL is on the
|
||||
// operator: the only secure transport for the WS is wss://.
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, plain));
|
||||
}
|
||||
|
||||
private static String randomPassword() {
|
||||
StringBuilder sb = new StringBuilder(PASSWORD_LENGTH);
|
||||
|
||||
for (int i = 0; i < PASSWORD_LENGTH; i++) {
|
||||
sb.append(PASSWORD_ALPHABET.charAt(RNG.nextInt(PASSWORD_ALPHABET.length())));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomState;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
/**
|
||||
* Toggle the room state between OPEN (open) and LOCKED (closed). The
|
||||
* client picks which transition it wants via the boolean — true => OPEN,
|
||||
* false => LOCKED. Persists state through `Room.save()` so the change
|
||||
* outlives an unload.
|
||||
*/
|
||||
public class HousekeepingRoomStateEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = this.packet.readInt();
|
||||
boolean open = this.packet.readBoolean();
|
||||
String actionKey = open ? "room.open" : "room.close";
|
||||
|
||||
if (roomId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
|
||||
|
||||
if (room == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.room_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
room.setState(open ? RoomState.OPEN : RoomState.LOCKED);
|
||||
room.save();
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, roomId, ""));
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingRoomListComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Search rooms by name. `exactMatch=true` => `name = ?` (used by the
|
||||
* findByName autocomplete that wants a unique hit). `exactMatch=false`
|
||||
* => `name LIKE concat(?, '%')` (used by the prefix dropdown).
|
||||
*
|
||||
* Both branches go through the same packet because the wire shape is
|
||||
* identical — the client picks which mode it wants by toggling the
|
||||
* boolean.
|
||||
*/
|
||||
public class HousekeepingSearchRoomsEvent extends MessageHandler {
|
||||
private static final int HARD_LIMIT = 50;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String query = this.packet.readString();
|
||||
boolean exactMatch = this.packet.readBoolean();
|
||||
int limit = Math.min(Math.max(this.packet.readInt(), 1), HARD_LIMIT);
|
||||
|
||||
if (query == null) query = "";
|
||||
query = query.trim();
|
||||
|
||||
if (query.isEmpty()) {
|
||||
this.client.sendResponse(new HousekeepingRoomListComposer(new ArrayList<>()));
|
||||
return;
|
||||
}
|
||||
|
||||
String sql = exactMatch
|
||||
? "SELECT id FROM rooms WHERE name = ? LIMIT ?"
|
||||
: "SELECT id FROM rooms WHERE name LIKE ? LIMIT ?";
|
||||
|
||||
List<Room> rooms = new ArrayList<>();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, exactMatch ? query : query + "%");
|
||||
statement.setInt(2, limit);
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(set.getInt("id"), false);
|
||||
|
||||
if (room != null) rooms.add(room);
|
||||
}
|
||||
}
|
||||
} catch (SQLException ignored) {
|
||||
// fall through with whatever we collected before the failure
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingRoomListComposer(rooms));
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.StaffAlertWithLinkComposer;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Mirrors :ha — staff alert with sender attribution, broadcast to
|
||||
* every online user whose `blockStaffAlerts` flag isn't set. Composed
|
||||
* once and forwarded by reference (sendResponse compiles to the same
|
||||
* underlying buffer) so the broadcast is O(N habbos) wire writes,
|
||||
* not O(N) compose calls.
|
||||
*/
|
||||
public class HousekeepingSendHotelAlertEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "hotel.alert";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String message = this.packet.readString();
|
||||
|
||||
if (message == null || message.trim().isEmpty()) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.alert_empty"));
|
||||
return;
|
||||
}
|
||||
|
||||
String body = message + "\r\n-" + this.client.getHabbo().getHabboInfo().getUsername();
|
||||
ServerMessage broadcast = new StaffAlertWithLinkComposer(body, "").compose();
|
||||
|
||||
int reached = 0;
|
||||
|
||||
for (Map.Entry<Integer, Habbo> entry : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().entrySet()) {
|
||||
Habbo habbo = entry.getValue();
|
||||
|
||||
if (habbo == null || habbo.getClient() == null) continue;
|
||||
if (habbo.getHabboStats() != null && habbo.getHabboStats().blockStaffAlerts) continue;
|
||||
|
||||
habbo.getClient().sendResponse(broadcast);
|
||||
reached++;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, reached, ""));
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Extend a user's HC by `days`. Adds to the existing club_expire_timestamp
|
||||
* if it's still in the future, otherwise stretches from `now`. Days==0
|
||||
* means cancel the active subscription (timestamp clamped to `now`).
|
||||
*/
|
||||
public class HousekeepingSetHcSubscriptionEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.set_hc";
|
||||
private static final int SECONDS_IN_DAY = 24 * 3600;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
int days = this.packet.readInt();
|
||||
|
||||
if (userId <= 0 || days < 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
int now = Emulator.getIntUnixTimestamp();
|
||||
int newExpire;
|
||||
|
||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (days == 0) {
|
||||
newExpire = now;
|
||||
} else if (online != null) {
|
||||
int current = online.getHabboStats().getClubExpireTimestamp();
|
||||
newExpire = (current > now ? current : now) + (days * SECONDS_IN_DAY);
|
||||
} else {
|
||||
newExpire = now + (days * SECONDS_IN_DAY); // best-effort offline; can't read previous expiry cheaply
|
||||
}
|
||||
|
||||
if (online != null) {
|
||||
online.getHabboStats().setClubExpireTimestamp(newExpire);
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET club_expire_timestamp = ? WHERE user_id = ? LIMIT 1")) {
|
||||
statement.setInt(1, newExpire);
|
||||
statement.setInt(2, userId);
|
||||
int rows = statement.executeUpdate();
|
||||
|
||||
if (rows == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.permissions.PermissionsManager;
|
||||
import com.eu.habbo.habbohotel.permissions.Rank;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
import com.eu.habbo.messages.outgoing.users.UserPermissionsComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class HousekeepingSetUserRankEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.set_rank";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
int rankId = this.packet.readInt();
|
||||
|
||||
if (userId <= 0 || rankId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
PermissionsManager permissions = Emulator.getGameEnvironment().getPermissionsManager();
|
||||
|
||||
if (!permissions.rankExists(rankId)) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
Rank rank = permissions.getRank(rankId);
|
||||
|
||||
// Persist for the offline path. Online users get their in-memory
|
||||
// HabboInfo.rank rebound below so server-side hasPermission()
|
||||
// checks land on the new permission set without a relogin.
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE users SET rank = ? WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, rankId);
|
||||
statement.setInt(2, userId);
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (online != null) {
|
||||
online.getHabboInfo().setRank(rank);
|
||||
// Ship the refreshed permissions snapshot — same payload the
|
||||
// :update_permissions command emits when a rank is rebound.
|
||||
online.getClient().sendResponse(new UserPermissionsComposer(online));
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Apply an arbitrary-duration trade lock. Writes
|
||||
* `users_settings.trade_locked_until = now + hours*3600` so the lock
|
||||
* survives logout/login — that column is the canonical timestamp the
|
||||
* mod-tool user-info composer queries on. Online users also get their
|
||||
* in-memory HabboStats.allowTrade flag cleared so the lock takes
|
||||
* effect on the active session without waiting for a relog.
|
||||
*/
|
||||
public class HousekeepingTradeLockUserEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.trade_lock";
|
||||
private static final int SECONDS_IN_HOUR = 3600;
|
||||
private static final int MAX_DURATION_SECONDS = 100 * 365 * 24 * 3600;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
int hours = this.packet.readInt();
|
||||
String reason = this.packet.readString();
|
||||
|
||||
if (userId <= 0 || hours <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
long durationLong = (long) hours * SECONDS_IN_HOUR;
|
||||
int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong;
|
||||
int lockedUntil = Emulator.getIntUnixTimestamp() + duration;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET trade_locked_until = ? WHERE user_id = ? LIMIT 1")) {
|
||||
statement.setInt(1, lockedUntil);
|
||||
statement.setInt(2, userId);
|
||||
int rows = statement.executeUpdate();
|
||||
|
||||
if (rows == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
|
||||
|
||||
if (online != null) {
|
||||
online.getHabboStats().setAllowTrade(false);
|
||||
|
||||
if (reason != null && !reason.isEmpty()) {
|
||||
online.alert(reason);
|
||||
}
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Transfer ownership of a room to a different user. Updates both
|
||||
* `rooms.owner_id` and `rooms.owner_name` so the cached owner name on
|
||||
* the navigator stays in sync without forcing a relog. The room is
|
||||
* touched via direct SQL rather than via Room.setOwnerId() because
|
||||
* the room may not be loaded.
|
||||
*/
|
||||
public class HousekeepingTransferRoomOwnershipEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "room.transfer";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = this.packet.readInt();
|
||||
int newOwnerId = this.packet.readInt();
|
||||
|
||||
if (roomId <= 0 || newOwnerId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
HabboInfo newOwner = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(newOwnerId);
|
||||
|
||||
if (newOwner == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.new_owner_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("UPDATE rooms SET owner_id = ?, owner_name = ? WHERE id = ? LIMIT 1")) {
|
||||
statement.setInt(1, newOwnerId);
|
||||
statement.setString(2, newOwner.getUsername());
|
||||
statement.setInt(3, roomId);
|
||||
int rows = statement.executeUpdate();
|
||||
|
||||
if (rows == 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
|
||||
return;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
public class HousekeepingUnbanUserEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "user.unban";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = this.packet.readInt();
|
||||
|
||||
if (userId <= 0) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
|
||||
return;
|
||||
}
|
||||
|
||||
HabboInfo info = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId);
|
||||
|
||||
if (info == null) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
// ModToolManager.unban only takes a username; the SQL UPDATE
|
||||
// happens against active bans (ban_expire > now), so calling it
|
||||
// on a never-banned user is a benign no-op that returns false.
|
||||
boolean cleared = Emulator.getGameEnvironment().getModToolManager().unban(info.getUsername());
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, cleared, cleared ? userId : 0, cleared ? "" : "housekeeping.error.no_active_ban"));
|
||||
}
|
||||
}
|
||||
+3
@@ -34,6 +34,9 @@ public class BotSaveSettingsEvent extends MessageHandler {
|
||||
if (bot == null)
|
||||
return;
|
||||
|
||||
if (bot.getOwnerActionIds().length == 0)
|
||||
return;
|
||||
|
||||
int settingId = this.packet.readInt();
|
||||
|
||||
switch (settingId) {
|
||||
|
||||
+1
-2
@@ -27,9 +27,8 @@ public class RoomUserSignEvent extends MessageHandler {
|
||||
WiredManager.triggerUserPerformsAction(room, this.client.getHabbo().getRoomUnit(), WiredUserActionType.SIGN, event.sign);
|
||||
|
||||
if(signId <= 10) {
|
||||
|
||||
int userId = this.client.getHabbo().getHabboInfo().getId();
|
||||
for (HabboItem item : room.getFloorItems()) {
|
||||
for (HabboItem item : room.getRoomSpecialTypes().getItemsOfType(InteractionVoteCounter.class)) {
|
||||
if (item instanceof InteractionVoteCounter) {
|
||||
((InteractionVoteCounter)item).vote(room, userId, signId);
|
||||
}
|
||||
|
||||
@@ -586,4 +586,12 @@ public class Outgoing {
|
||||
public static final int YouTubeRoomWatchersComposer = 8002;
|
||||
public static final int YouTubeRoomSettingsComposer = 8003;
|
||||
|
||||
// Housekeeping (in-client admin panel) — IDs 9200..9299 reserved
|
||||
public static final int HousekeepingUserDetailComposer = 9200;
|
||||
public static final int HousekeepingActionResultComposer = 9201;
|
||||
public static final int HousekeepingRoomDetailComposer = 9202;
|
||||
public static final int HousekeepingRoomListComposer = 9203;
|
||||
public static final int HousekeepingDashboardComposer = 9204;
|
||||
public static final int HousekeepingActionLogComposer = 9205;
|
||||
|
||||
}
|
||||
|
||||
+3
-1
@@ -41,6 +41,7 @@ public class CatalogPagesListComposer extends MessageComposer {
|
||||
this.response.appendBoolean(true);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(-1);
|
||||
this.response.appendInt(-1);
|
||||
this.response.appendString("root");
|
||||
this.response.appendString("");
|
||||
this.response.appendInt(0);
|
||||
@@ -68,7 +69,8 @@ public class CatalogPagesListComposer extends MessageComposer {
|
||||
|
||||
this.response.appendBoolean(category.isVisible());
|
||||
this.response.appendInt(category.getIconImage());
|
||||
this.response.appendInt(category.isEnabled() ? category.getId() : -1);
|
||||
this.response.appendInt(category.isEnabled() || this.hasPermission ? category.getId() : -1);
|
||||
this.response.appendInt(category.getParentId());
|
||||
this.response.appendString(category.getPageName());
|
||||
this.response.appendString(category.getCaption() + (this.hasPermission ? " (" + category.getId() + ")" : ""));
|
||||
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.eu.habbo.messages.outgoing.housekeeping;
|
||||
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class HousekeepingActionLogComposer extends MessageComposer {
|
||||
public static class Row {
|
||||
public final int id;
|
||||
public final int timestamp;
|
||||
public final int actorId;
|
||||
public final String actorName;
|
||||
public final String targetType;
|
||||
public final int targetId;
|
||||
public final String targetLabel;
|
||||
public final String action;
|
||||
public final String detail;
|
||||
public final boolean success;
|
||||
|
||||
public Row(int id, int timestamp, int actorId, String actorName, String targetType, int targetId,
|
||||
String targetLabel, String action, String detail, boolean success) {
|
||||
this.id = id;
|
||||
this.timestamp = timestamp;
|
||||
this.actorId = actorId;
|
||||
this.actorName = actorName != null ? actorName : "";
|
||||
this.targetType = targetType != null ? targetType : "user";
|
||||
this.targetId = targetId;
|
||||
this.targetLabel = targetLabel != null ? targetLabel : "";
|
||||
this.action = action != null ? action : "";
|
||||
this.detail = detail != null ? detail : "";
|
||||
this.success = success;
|
||||
}
|
||||
}
|
||||
|
||||
private final List<Row> rows;
|
||||
|
||||
public HousekeepingActionLogComposer(List<Row> rows) {
|
||||
this.rows = rows;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.HousekeepingActionLogComposer);
|
||||
this.response.appendInt(this.rows != null ? this.rows.size() : 0);
|
||||
|
||||
if (this.rows != null) {
|
||||
for (Row r : this.rows) {
|
||||
this.response.appendInt(r.id);
|
||||
this.response.appendInt(r.timestamp);
|
||||
this.response.appendInt(r.actorId);
|
||||
this.response.appendString(r.actorName);
|
||||
this.response.appendString(r.targetType);
|
||||
this.response.appendInt(r.targetId);
|
||||
this.response.appendString(r.targetLabel);
|
||||
this.response.appendString(r.action);
|
||||
this.response.appendString(r.detail);
|
||||
this.response.appendBoolean(r.success);
|
||||
}
|
||||
}
|
||||
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.eu.habbo.messages.outgoing.housekeeping;
|
||||
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
/**
|
||||
* Generic ack for any housekeeping action (ban, mute, kick, give-credits,
|
||||
* room-close, …). The client matches it back to the originating call via
|
||||
* the `actionKey` field, which lets multiple in-flight actions share the
|
||||
* same event stream without ordering bugs.
|
||||
*/
|
||||
public class HousekeepingActionResultComposer extends MessageComposer {
|
||||
private final String actionKey;
|
||||
private final boolean ok;
|
||||
private final int actionId;
|
||||
private final String message;
|
||||
|
||||
public HousekeepingActionResultComposer(String actionKey, boolean ok, int actionId, String message) {
|
||||
this.actionKey = actionKey != null ? actionKey : "";
|
||||
this.ok = ok;
|
||||
this.actionId = actionId;
|
||||
this.message = message != null ? message : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.HousekeepingActionResultComposer);
|
||||
this.response.appendString(this.actionKey);
|
||||
this.response.appendBoolean(this.ok);
|
||||
this.response.appendInt(this.actionId);
|
||||
this.response.appendString(this.message);
|
||||
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.eu.habbo.messages.outgoing.housekeeping;
|
||||
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
public class HousekeepingDashboardComposer extends MessageComposer {
|
||||
private final int onlineUsers;
|
||||
private final int totalUsers;
|
||||
private final int activeRooms;
|
||||
private final int totalRooms;
|
||||
private final int peakOnlineToday;
|
||||
private final int peakOnlineAllTime;
|
||||
private final int pendingTickets;
|
||||
private final int sanctionsLast24h;
|
||||
private final int serverUptimeSeconds;
|
||||
private final String serverVersion;
|
||||
|
||||
public HousekeepingDashboardComposer(int onlineUsers, int totalUsers, int activeRooms, int totalRooms,
|
||||
int peakOnlineToday, int peakOnlineAllTime, int pendingTickets,
|
||||
int sanctionsLast24h, int serverUptimeSeconds, String serverVersion) {
|
||||
this.onlineUsers = onlineUsers;
|
||||
this.totalUsers = totalUsers;
|
||||
this.activeRooms = activeRooms;
|
||||
this.totalRooms = totalRooms;
|
||||
this.peakOnlineToday = peakOnlineToday;
|
||||
this.peakOnlineAllTime = peakOnlineAllTime;
|
||||
this.pendingTickets = pendingTickets;
|
||||
this.sanctionsLast24h = sanctionsLast24h;
|
||||
this.serverUptimeSeconds = serverUptimeSeconds;
|
||||
this.serverVersion = serverVersion != null ? serverVersion : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.HousekeepingDashboardComposer);
|
||||
this.response.appendInt(this.onlineUsers);
|
||||
this.response.appendInt(this.totalUsers);
|
||||
this.response.appendInt(this.activeRooms);
|
||||
this.response.appendInt(this.totalRooms);
|
||||
this.response.appendInt(this.peakOnlineToday);
|
||||
this.response.appendInt(this.peakOnlineAllTime);
|
||||
this.response.appendInt(this.pendingTickets);
|
||||
this.response.appendInt(this.sanctionsLast24h);
|
||||
this.response.appendInt(this.serverUptimeSeconds);
|
||||
this.response.appendString(this.serverVersion);
|
||||
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.eu.habbo.messages.outgoing.housekeeping;
|
||||
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomState;
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
public class HousekeepingRoomDetailComposer extends MessageComposer {
|
||||
private final Room room;
|
||||
|
||||
public HousekeepingRoomDetailComposer(Room room) {
|
||||
this.room = room;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.HousekeepingRoomDetailComposer);
|
||||
|
||||
if (this.room == null) {
|
||||
this.response.appendBoolean(false);
|
||||
return this.response;
|
||||
}
|
||||
|
||||
this.response.appendBoolean(true);
|
||||
appendRoomFields(this.response, this.room);
|
||||
|
||||
return this.response;
|
||||
}
|
||||
|
||||
/** Shared by HousekeepingRoomListComposer too. */
|
||||
public static void appendRoomFields(ServerMessage response, Room room) {
|
||||
response.appendInt(room.getId());
|
||||
response.appendString(safe(room.getName()));
|
||||
response.appendString(safe(room.getDescription()));
|
||||
response.appendInt(room.getOwnerId());
|
||||
response.appendString(safe(room.getOwnerName()));
|
||||
response.appendInt(room.getUserCount());
|
||||
response.appendInt(room.getUsersMax());
|
||||
response.appendBoolean(room.getState() != null && room.getState() != RoomState.OPEN);
|
||||
response.appendBoolean(room.isMuted());
|
||||
response.appendBoolean(room.isPublicRoom());
|
||||
response.appendInt(0); // createdAt — Room doesn't expose; left as 0 until a schema-side timestamp surfaces.
|
||||
}
|
||||
|
||||
private static String safe(String value) {
|
||||
return value != null ? value : "";
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.eu.habbo.messages.outgoing.housekeeping;
|
||||
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class HousekeepingRoomListComposer extends MessageComposer {
|
||||
private final List<Room> rooms;
|
||||
|
||||
public HousekeepingRoomListComposer(List<Room> rooms) {
|
||||
this.rooms = rooms;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.HousekeepingRoomListComposer);
|
||||
this.response.appendInt(this.rooms != null ? this.rooms.size() : 0);
|
||||
|
||||
if (this.rooms != null) {
|
||||
for (Room room : this.rooms) {
|
||||
HousekeepingRoomDetailComposer.appendRoomFields(this.response, room);
|
||||
}
|
||||
}
|
||||
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.eu.habbo.messages.outgoing.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolBan;
|
||||
import com.eu.habbo.habbohotel.permissions.Rank;
|
||||
import com.eu.habbo.habbohotel.users.HabboInfo;
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
public class HousekeepingUserDetailComposer extends MessageComposer {
|
||||
private static final int CURRENCY_DUCKETS = 0;
|
||||
private static final int CURRENCY_DIAMONDS = 5;
|
||||
|
||||
private final HabboInfo info;
|
||||
|
||||
public HousekeepingUserDetailComposer(HabboInfo info) {
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.HousekeepingUserDetailComposer);
|
||||
|
||||
if (this.info == null) {
|
||||
this.response.appendBoolean(false);
|
||||
return this.response;
|
||||
}
|
||||
|
||||
Rank rank = this.info.getRank();
|
||||
ModToolBan ban = Emulator.getGameEnvironment().getModToolManager().checkForBan(this.info.getId());
|
||||
|
||||
this.response.appendBoolean(true);
|
||||
this.response.appendInt(this.info.getId());
|
||||
this.response.appendString(safe(this.info.getUsername()));
|
||||
this.response.appendString(safe(this.info.getMotto()));
|
||||
this.response.appendString(safe(this.info.getLook()));
|
||||
this.response.appendInt(rank != null ? rank.getId() : 0);
|
||||
this.response.appendString(rank != null ? safe(rank.getName()) : "");
|
||||
this.response.appendBoolean(this.info.isOnline());
|
||||
this.response.appendInt(this.info.getLastOnline());
|
||||
this.response.appendInt(this.info.getCredits());
|
||||
this.response.appendInt(this.info.getCurrencyAmount(CURRENCY_DUCKETS));
|
||||
this.response.appendInt(this.info.getCurrencyAmount(CURRENCY_DIAMONDS));
|
||||
this.response.appendString(safe(this.info.getMail()));
|
||||
this.response.appendString(safe(this.info.getIpLogin()));
|
||||
this.response.appendBoolean(ban != null);
|
||||
// Mute / trade-lock surface as future packet extensions; see the
|
||||
// optional-trailing-field parser pattern on the renderer side.
|
||||
this.response.appendBoolean(false);
|
||||
this.response.appendBoolean(false);
|
||||
|
||||
return this.response;
|
||||
}
|
||||
|
||||
private static String safe(String value) {
|
||||
return value != null ? value : "";
|
||||
}
|
||||
}
|
||||
+16
-28
@@ -86,7 +86,7 @@ public class RoomUsersComposer extends MessageComposer {
|
||||
this.response.appendInt(habbo.getHabboInfo().getId());
|
||||
this.response.appendString(habbo.getHabboInfo().getUsername());
|
||||
this.response.appendString(habbo.getHabboInfo().getMotto());
|
||||
this.response.appendInt(habbo.getHabboInfo().getInfostandBg());
|
||||
this.response.appendInt(habbo.getHabboInfo().getInfostandBg());
|
||||
this.response.appendInt(habbo.getHabboInfo().getInfostandStand());
|
||||
this.response.appendInt(habbo.getHabboInfo().getInfostandOverlay());
|
||||
this.response.appendInt(habbo.getHabboInfo().getInfostandCardBg());
|
||||
@@ -129,7 +129,7 @@ public class RoomUsersComposer extends MessageComposer {
|
||||
this.response.appendInt(0 - this.bot.getId());
|
||||
this.response.appendString(this.bot.getName());
|
||||
this.response.appendString(this.bot.getMotto());
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
@@ -143,17 +143,11 @@ public class RoomUsersComposer extends MessageComposer {
|
||||
this.response.appendString(this.bot.getGender().name().toUpperCase());
|
||||
this.response.appendInt(this.bot.getOwnerId());
|
||||
this.response.appendString(this.bot.getOwnerName());
|
||||
this.response.appendInt(10);
|
||||
this.response.appendShort(0);
|
||||
this.response.appendShort(1);
|
||||
this.response.appendShort(2);
|
||||
this.response.appendShort(3);
|
||||
this.response.appendShort(4);
|
||||
this.response.appendShort(5);
|
||||
this.response.appendShort(6);
|
||||
this.response.appendShort(7);
|
||||
this.response.appendShort(8);
|
||||
this.response.appendShort(9);
|
||||
short[] singleActions = this.bot.getOwnerActionIds();
|
||||
this.response.appendInt(singleActions.length);
|
||||
for (short action : singleActions) {
|
||||
this.response.appendShort(action);
|
||||
}
|
||||
this.response.appendString("unknown");
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
@@ -163,10 +157,10 @@ public class RoomUsersComposer extends MessageComposer {
|
||||
this.response.appendInt(0 - bot.getId());
|
||||
this.response.appendString(bot.getName());
|
||||
this.response.appendString(bot.getMotto());
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendString(bot.getFigure());
|
||||
this.response.appendInt(bot.getRoomUnit().getId());
|
||||
this.response.appendInt(bot.getRoomUnit().getX());
|
||||
@@ -177,17 +171,11 @@ public class RoomUsersComposer extends MessageComposer {
|
||||
this.response.appendString(bot.getGender().name().toUpperCase());
|
||||
this.response.appendInt(bot.getOwnerId());
|
||||
this.response.appendString(bot.getOwnerName());
|
||||
this.response.appendInt(10);
|
||||
this.response.appendShort(0);
|
||||
this.response.appendShort(1);
|
||||
this.response.appendShort(2);
|
||||
this.response.appendShort(3);
|
||||
this.response.appendShort(4);
|
||||
this.response.appendShort(5);
|
||||
this.response.appendShort(6);
|
||||
this.response.appendShort(7);
|
||||
this.response.appendShort(8);
|
||||
this.response.appendShort(9);
|
||||
short[] listActions = bot.getOwnerActionIds();
|
||||
this.response.appendInt(listActions.length);
|
||||
for (short action : listActions) {
|
||||
this.response.appendShort(action);
|
||||
}
|
||||
this.response.appendString("unknown");
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
|
||||
@@ -125,10 +125,28 @@ public class UserProfileComposer extends MessageComposer {
|
||||
this.response.appendString(customizationData.prefixEffect);
|
||||
this.response.appendString(customizationData.prefixFont);
|
||||
this.response.appendString(customizationData.displayOrder);
|
||||
this.response.appendInt(this.getTotalBadges());
|
||||
|
||||
return this.response;
|
||||
}
|
||||
|
||||
private int getTotalBadges() {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT COUNT(DISTINCT badge_code) AS total_badges FROM users_badges WHERE user_id = ?")) {
|
||||
statement.setInt(1, this.habboInfo.getId());
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
if (set.next()) {
|
||||
return set.getInt("total_badges");
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception while loading total badges for extended profile", e);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public HabboInfo getHabboInfo() {
|
||||
return habboInfo;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.eu.habbo.monitoring;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public final class EmulatorNetworkStats {
|
||||
private static final AtomicLong INCOMING_PACKETS = new AtomicLong();
|
||||
private static final AtomicLong OUTGOING_PACKETS = new AtomicLong();
|
||||
private static final AtomicLong INCOMING_BYTES = new AtomicLong();
|
||||
private static final AtomicLong OUTGOING_BYTES = new AtomicLong();
|
||||
|
||||
private EmulatorNetworkStats() {
|
||||
}
|
||||
|
||||
public static void recordIncoming(int byteCount) {
|
||||
INCOMING_PACKETS.incrementAndGet();
|
||||
if (byteCount > 0) {
|
||||
INCOMING_BYTES.addAndGet(byteCount);
|
||||
}
|
||||
}
|
||||
|
||||
public static void recordOutgoing(int byteCount) {
|
||||
OUTGOING_PACKETS.incrementAndGet();
|
||||
if (byteCount > 0) {
|
||||
OUTGOING_BYTES.addAndGet(byteCount);
|
||||
}
|
||||
}
|
||||
|
||||
public static long getIncomingPackets() {
|
||||
return INCOMING_PACKETS.get();
|
||||
}
|
||||
|
||||
public static long getOutgoingPackets() {
|
||||
return OUTGOING_PACKETS.get();
|
||||
}
|
||||
|
||||
public static long getIncomingBytes() {
|
||||
return INCOMING_BYTES.get();
|
||||
}
|
||||
|
||||
public static long getOutgoingBytes() {
|
||||
return OUTGOING_BYTES.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
package com.eu.habbo.monitoring;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.wired.core.WiredManager;
|
||||
import com.eu.habbo.habbohotel.wired.core.WiredRoomDiagnostics;
|
||||
import com.eu.habbo.habbohotel.wired.tick.WiredTickService;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import com.zaxxer.hikari.HikariPoolMXBean;
|
||||
|
||||
import java.lang.management.GarbageCollectorMXBean;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.OperatingSystemMXBean;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
|
||||
public final class EmulatorStatsService {
|
||||
private static final long CACHE_TTL_MS = 1_000L;
|
||||
private static final int MAX_HISTORY_POINTS = 90;
|
||||
|
||||
private static final ArrayDeque<MemoryPoint> MEMORY_HISTORY = new ArrayDeque<>();
|
||||
|
||||
private static volatile Snapshot cachedSnapshot = null;
|
||||
private static volatile long cachedAt = 0L;
|
||||
private static volatile int peakPlayers = 0;
|
||||
private static volatile int peakWebSocketSessions = 0;
|
||||
private static volatile long previousIncomingPackets = 0L;
|
||||
private static volatile long previousOutgoingPackets = 0L;
|
||||
private static volatile long previousIncomingBytes = 0L;
|
||||
private static volatile long previousOutgoingBytes = 0L;
|
||||
private static volatile long previousGcCount = 0L;
|
||||
private static volatile long previousGcTimeMs = 0L;
|
||||
private static volatile long previousTelemetryAt = 0L;
|
||||
|
||||
private EmulatorStatsService() {
|
||||
}
|
||||
|
||||
public static Snapshot collectSnapshot() {
|
||||
long now = System.currentTimeMillis();
|
||||
Snapshot current = cachedSnapshot;
|
||||
|
||||
if (current != null && (now - cachedAt) < CACHE_TTL_MS) {
|
||||
return current;
|
||||
}
|
||||
|
||||
synchronized (EmulatorStatsService.class) {
|
||||
current = cachedSnapshot;
|
||||
|
||||
if (current != null && (now - cachedAt) < CACHE_TTL_MS) {
|
||||
return current;
|
||||
}
|
||||
|
||||
Snapshot built = buildSnapshot(now);
|
||||
cachedSnapshot = built;
|
||||
cachedAt = now;
|
||||
return built;
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatDuration(long totalSeconds) {
|
||||
long hours = totalSeconds / 3600L;
|
||||
long minutes = (totalSeconds % 3600L) / 60L;
|
||||
long seconds = totalSeconds % 60L;
|
||||
|
||||
return String.format("%02dh %02dm %02ds", hours, minutes, seconds);
|
||||
}
|
||||
|
||||
private static Snapshot buildSnapshot(long now) {
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
|
||||
long totalMemBytes = runtime.totalMemory();
|
||||
long freeMemBytes = runtime.freeMemory();
|
||||
long usedMemBytes = totalMemBytes - freeMemBytes;
|
||||
long maxMemBytes = runtime.maxMemory();
|
||||
|
||||
int usedMemMb = (int) (usedMemBytes / 1024L / 1024L);
|
||||
int maxMemMb = (int) (maxMemBytes / 1024L / 1024L);
|
||||
int estimatedAllocMb = (int) (totalMemBytes / 1024L / 1024L);
|
||||
double memoryUsagePercent = maxMemBytes > 0
|
||||
? (usedMemBytes * 100D) / maxMemBytes
|
||||
: 0D;
|
||||
|
||||
OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
|
||||
double cpuLoadPercent = 0D;
|
||||
|
||||
if (osBean instanceof com.sun.management.OperatingSystemMXBean managedOsBean) {
|
||||
cpuLoadPercent = Math.max(0D, managedOsBean.getProcessCpuLoad() * 100D);
|
||||
}
|
||||
|
||||
int threadCount = ManagementFactory.getThreadMXBean().getThreadCount();
|
||||
|
||||
List<Habbo> habbos = List.of();
|
||||
List<Room> rooms = List.of();
|
||||
int webSocketSessions = 0;
|
||||
|
||||
if (Emulator.getGameEnvironment() != null) {
|
||||
if (Emulator.getGameEnvironment().getHabboManager() != null) {
|
||||
habbos = Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().values().stream().toList();
|
||||
}
|
||||
|
||||
if (Emulator.getGameEnvironment().getRoomManager() != null) {
|
||||
rooms = Emulator.getGameEnvironment().getRoomManager().getActiveRooms();
|
||||
}
|
||||
}
|
||||
|
||||
if (Emulator.getGameServer() != null && Emulator.getGameServer().getGameClientManager() != null) {
|
||||
webSocketSessions = Emulator.getGameServer().getGameClientManager().getSessions().size();
|
||||
}
|
||||
|
||||
peakPlayers = Math.max(peakPlayers, habbos.size());
|
||||
peakWebSocketSessions = Math.max(peakWebSocketSessions, webSocketSessions);
|
||||
|
||||
WiredTickService wiredTickService = WiredTickService.getInstance();
|
||||
int totalTickables = (wiredTickService != null) ? wiredTickService.getTotalTickableCount() : 0;
|
||||
|
||||
appendMemoryHistory(now, usedMemMb, maxMemMb, memoryUsagePercent);
|
||||
|
||||
double averageRoomCycleMs = 0D;
|
||||
double worstRoomCycleMs = 0D;
|
||||
int worstRoomCycleRoomId = 0;
|
||||
String worstRoomCycleRoomName = "-";
|
||||
|
||||
long totalDelayedEventsPending = 0L;
|
||||
int overloadedWiredRooms = 0;
|
||||
int heavyWiredRooms = 0;
|
||||
double wiredActivityPerSecond = 0D;
|
||||
|
||||
List<OnlineUserRow> users = new ArrayList<>(habbos.size());
|
||||
for (Habbo habbo : habbos) {
|
||||
int roomId = (habbo.getHabboInfo().getCurrentRoom() != null) ? habbo.getHabboInfo().getCurrentRoom().getId() : 0;
|
||||
|
||||
users.add(new OnlineUserRow(
|
||||
habbo.getHabboInfo().getId(),
|
||||
habbo.getHabboInfo().getUsername(),
|
||||
habbo.getHabboInfo().getRank().getName(),
|
||||
habbo.getHabboInfo().getCurrencyAmount(0),
|
||||
roomId
|
||||
));
|
||||
}
|
||||
|
||||
List<ActiveRoomRow> activeRooms = new ArrayList<>(rooms.size());
|
||||
List<WiredRoomRow> wiredRooms = new ArrayList<>();
|
||||
List<WiredTopRoomRow> wiredTopRooms = new ArrayList<>();
|
||||
|
||||
double roomCycleAccumulator = 0D;
|
||||
int roomCycleSamples = 0;
|
||||
|
||||
for (Room room : rooms) {
|
||||
int tickables = (wiredTickService != null) ? wiredTickService.getTickableCount(room.getId()) : 0;
|
||||
double roomCycleMs = Math.max(0D, room.lastCycleCpuMs);
|
||||
|
||||
roomCycleAccumulator += roomCycleMs;
|
||||
roomCycleSamples++;
|
||||
|
||||
if (roomCycleMs >= worstRoomCycleMs) {
|
||||
worstRoomCycleMs = roomCycleMs;
|
||||
worstRoomCycleRoomId = room.getId();
|
||||
worstRoomCycleRoomName = room.getName();
|
||||
}
|
||||
|
||||
activeRooms.add(new ActiveRoomRow(
|
||||
room.getId(),
|
||||
room.getName(),
|
||||
room.getUserCount(),
|
||||
room.itemCount(),
|
||||
tickables,
|
||||
room.lastCycleCpuMs,
|
||||
room.getEstimatedMemoryUsage() / 1024L,
|
||||
room.lastCycleThread
|
||||
));
|
||||
|
||||
WiredRoomDiagnostics.Snapshot diagnostics = WiredManager.getDiagnosticsSnapshot(room.getId());
|
||||
|
||||
if (diagnostics == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean shouldShow = diagnostics.getAverageExecutionMs() > 0
|
||||
|| diagnostics.getPeakExecutionMs() > 0
|
||||
|| tickables > 0
|
||||
|| diagnostics.getDelayedEventsPending() > 0;
|
||||
|
||||
if (!shouldShow) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int usagePercent = (int) Math.round((diagnostics.getUsageCurrentWindow() * 100D) / Math.max(1, diagnostics.getUsageLimitPerWindow()));
|
||||
double roomActivityPerSecond = (diagnostics.getUsageCurrentWindow() * 1000D) / Math.max(1, diagnostics.getUsageWindowMs());
|
||||
|
||||
totalDelayedEventsPending += diagnostics.getDelayedEventsPending();
|
||||
wiredActivityPerSecond += roomActivityPerSecond;
|
||||
|
||||
if (diagnostics.getAverageExecutionMs() >= diagnostics.getOverloadAverageThresholdMs()) {
|
||||
overloadedWiredRooms++;
|
||||
}
|
||||
|
||||
if (diagnostics.isHeavy()) {
|
||||
heavyWiredRooms++;
|
||||
}
|
||||
|
||||
wiredRooms.add(new WiredRoomRow(
|
||||
room.getId(),
|
||||
diagnostics.getAverageExecutionMs(),
|
||||
diagnostics.getPeakExecutionMs(),
|
||||
usagePercent,
|
||||
diagnostics.getDelayedEventsPending(),
|
||||
diagnostics.getAverageExecutionMs() >= diagnostics.getOverloadAverageThresholdMs(),
|
||||
diagnostics.isHeavy()
|
||||
));
|
||||
|
||||
wiredTopRooms.add(new WiredTopRoomRow(
|
||||
room.getId(),
|
||||
room.getName(),
|
||||
usagePercent,
|
||||
diagnostics.getAverageExecutionMs(),
|
||||
diagnostics.getPeakExecutionMs(),
|
||||
diagnostics.getDelayedEventsPending(),
|
||||
roomActivityPerSecond,
|
||||
diagnostics.isHeavy()
|
||||
));
|
||||
}
|
||||
|
||||
if (roomCycleSamples > 0) {
|
||||
averageRoomCycleMs = roomCycleAccumulator / roomCycleSamples;
|
||||
}
|
||||
|
||||
wiredTopRooms.sort(Comparator
|
||||
.comparingInt((WiredTopRoomRow row) -> row.usagePercent).reversed()
|
||||
.thenComparingInt(row -> row.averageTickMs).reversed()
|
||||
.thenComparingInt(row -> row.peakTickMs).reversed());
|
||||
|
||||
if (wiredTopRooms.size() > 5) {
|
||||
wiredTopRooms = new ArrayList<>(wiredTopRooms.subList(0, 5));
|
||||
}
|
||||
|
||||
HikariPoolMetrics hikariPoolMetrics = collectHikariPoolMetrics();
|
||||
SchedulerMetrics schedulerMetrics = collectSchedulerMetrics();
|
||||
NetworkMetrics networkMetrics = collectNetworkMetrics(now);
|
||||
GarbageCollectorMetrics garbageCollectorMetrics = collectGarbageCollectorMetrics(now);
|
||||
|
||||
Overview overview = new Overview(
|
||||
Emulator.getOnlineTime(),
|
||||
now,
|
||||
cpuLoadPercent >= 80D ? "Attention needed" : "Healthy",
|
||||
usedMemMb,
|
||||
maxMemMb,
|
||||
estimatedAllocMb,
|
||||
memoryUsagePercent,
|
||||
cpuLoadPercent,
|
||||
threadCount,
|
||||
habbos.size(),
|
||||
rooms.size(),
|
||||
totalTickables,
|
||||
peakPlayers,
|
||||
webSocketSessions,
|
||||
peakWebSocketSessions,
|
||||
averageRoomCycleMs,
|
||||
worstRoomCycleMs,
|
||||
worstRoomCycleRoomId,
|
||||
worstRoomCycleRoomName,
|
||||
totalDelayedEventsPending,
|
||||
overloadedWiredRooms,
|
||||
heavyWiredRooms,
|
||||
wiredActivityPerSecond
|
||||
);
|
||||
|
||||
return new Snapshot(
|
||||
overview,
|
||||
new ArrayList<>(MEMORY_HISTORY),
|
||||
users,
|
||||
activeRooms,
|
||||
wiredRooms,
|
||||
wiredTopRooms,
|
||||
hikariPoolMetrics,
|
||||
schedulerMetrics,
|
||||
networkMetrics,
|
||||
garbageCollectorMetrics
|
||||
);
|
||||
}
|
||||
|
||||
private static HikariPoolMetrics collectHikariPoolMetrics() {
|
||||
HikariDataSource dataSource = (Emulator.getDatabase() != null) ? Emulator.getDatabase().getDataSource() : null;
|
||||
HikariPoolMXBean poolMxBean = (dataSource != null) ? dataSource.getHikariPoolMXBean() : null;
|
||||
|
||||
if (poolMxBean == null) {
|
||||
return new HikariPoolMetrics(0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
return new HikariPoolMetrics(
|
||||
poolMxBean.getActiveConnections(),
|
||||
poolMxBean.getIdleConnections(),
|
||||
poolMxBean.getTotalConnections(),
|
||||
poolMxBean.getThreadsAwaitingConnection(),
|
||||
dataSource.getMaximumPoolSize()
|
||||
);
|
||||
}
|
||||
|
||||
private static SchedulerMetrics collectSchedulerMetrics() {
|
||||
if (Emulator.getThreading() == null) {
|
||||
return new SchedulerMetrics(0, 0, 0, 0, false);
|
||||
}
|
||||
|
||||
if (!(Emulator.getThreading().getService() instanceof ScheduledThreadPoolExecutor executor)) {
|
||||
return new SchedulerMetrics(0, 0, 0, 0, false);
|
||||
}
|
||||
|
||||
return new SchedulerMetrics(
|
||||
executor.getQueue().size(),
|
||||
executor.getActiveCount(),
|
||||
executor.getPoolSize(),
|
||||
executor.getCompletedTaskCount(),
|
||||
!executor.isShutdown()
|
||||
);
|
||||
}
|
||||
|
||||
private static NetworkMetrics collectNetworkMetrics(long now) {
|
||||
long incomingPackets = EmulatorNetworkStats.getIncomingPackets();
|
||||
long outgoingPackets = EmulatorNetworkStats.getOutgoingPackets();
|
||||
long incomingBytes = EmulatorNetworkStats.getIncomingBytes();
|
||||
long outgoingBytes = EmulatorNetworkStats.getOutgoingBytes();
|
||||
|
||||
long previousAt = previousTelemetryAt;
|
||||
long elapsedMs = (previousAt > 0L) ? Math.max(1L, now - previousAt) : CACHE_TTL_MS;
|
||||
|
||||
double incomingPacketsPerSecond = ((incomingPackets - previousIncomingPackets) * 1000D) / elapsedMs;
|
||||
double outgoingPacketsPerSecond = ((outgoingPackets - previousOutgoingPackets) * 1000D) / elapsedMs;
|
||||
double incomingKilobytesPerSecond = ((incomingBytes - previousIncomingBytes) / 1024D) * 1000D / elapsedMs;
|
||||
double outgoingKilobytesPerSecond = ((outgoingBytes - previousOutgoingBytes) / 1024D) * 1000D / elapsedMs;
|
||||
|
||||
previousIncomingPackets = incomingPackets;
|
||||
previousOutgoingPackets = outgoingPackets;
|
||||
previousIncomingBytes = incomingBytes;
|
||||
previousOutgoingBytes = outgoingBytes;
|
||||
previousTelemetryAt = now;
|
||||
|
||||
return new NetworkMetrics(
|
||||
Math.max(0D, incomingPacketsPerSecond),
|
||||
Math.max(0D, outgoingPacketsPerSecond),
|
||||
Math.max(0D, incomingKilobytesPerSecond),
|
||||
Math.max(0D, outgoingKilobytesPerSecond),
|
||||
incomingPackets,
|
||||
outgoingPackets
|
||||
);
|
||||
}
|
||||
|
||||
private static GarbageCollectorMetrics collectGarbageCollectorMetrics(long now) {
|
||||
long totalCollections = 0L;
|
||||
long totalCollectionTimeMs = 0L;
|
||||
|
||||
for (GarbageCollectorMXBean garbageCollectorMXBean : ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||
long collectionCount = garbageCollectorMXBean.getCollectionCount();
|
||||
long collectionTime = garbageCollectorMXBean.getCollectionTime();
|
||||
|
||||
if (collectionCount > 0) {
|
||||
totalCollections += collectionCount;
|
||||
}
|
||||
|
||||
if (collectionTime > 0) {
|
||||
totalCollectionTimeMs += collectionTime;
|
||||
}
|
||||
}
|
||||
|
||||
long lastObservedPauseMs = Math.max(0L, totalCollectionTimeMs - previousGcTimeMs);
|
||||
long collectionsSinceLastSample = Math.max(0L, totalCollections - previousGcCount);
|
||||
|
||||
previousGcCount = totalCollections;
|
||||
previousGcTimeMs = totalCollectionTimeMs;
|
||||
|
||||
return new GarbageCollectorMetrics(
|
||||
totalCollections,
|
||||
totalCollectionTimeMs,
|
||||
collectionsSinceLastSample,
|
||||
lastObservedPauseMs,
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
private static void appendMemoryHistory(long timestamp, int usedMemMb, int maxMemMb, double usagePercent) {
|
||||
MEMORY_HISTORY.addLast(new MemoryPoint(timestamp, usedMemMb, maxMemMb, usagePercent));
|
||||
|
||||
while (MEMORY_HISTORY.size() > MAX_HISTORY_POINTS) {
|
||||
MEMORY_HISTORY.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Snapshot {
|
||||
public final Overview overview;
|
||||
public final List<MemoryPoint> memoryHistory;
|
||||
public final List<OnlineUserRow> users;
|
||||
public final List<ActiveRoomRow> rooms;
|
||||
public final List<WiredRoomRow> wired;
|
||||
public final List<WiredTopRoomRow> wiredTopRooms;
|
||||
public final HikariPoolMetrics databasePool;
|
||||
public final SchedulerMetrics scheduler;
|
||||
public final NetworkMetrics network;
|
||||
public final GarbageCollectorMetrics garbageCollector;
|
||||
|
||||
public Snapshot(Overview overview, List<MemoryPoint> memoryHistory, List<OnlineUserRow> users, List<ActiveRoomRow> rooms, List<WiredRoomRow> wired, List<WiredTopRoomRow> wiredTopRooms, HikariPoolMetrics databasePool, SchedulerMetrics scheduler, NetworkMetrics network, GarbageCollectorMetrics garbageCollector) {
|
||||
this.overview = overview;
|
||||
this.memoryHistory = memoryHistory;
|
||||
this.users = users;
|
||||
this.rooms = rooms;
|
||||
this.wired = wired;
|
||||
this.wiredTopRooms = wiredTopRooms;
|
||||
this.databasePool = databasePool;
|
||||
this.scheduler = scheduler;
|
||||
this.network = network;
|
||||
this.garbageCollector = garbageCollector;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Overview {
|
||||
public final long uptimeSeconds;
|
||||
public final long lastRefreshEpochMs;
|
||||
public final String guiStatus;
|
||||
public final int memoryUsedMb;
|
||||
public final int memoryMaxMb;
|
||||
public final int memoryAllocatedMb;
|
||||
public final double memoryUsagePercent;
|
||||
public final double cpuLoadPercent;
|
||||
public final int activeOsThreads;
|
||||
public final int connectedPlayers;
|
||||
public final int loadedRooms;
|
||||
public final int wiredTickables;
|
||||
public final int peakPlayers;
|
||||
public final int activeWebSocketSessions;
|
||||
public final int peakWebSocketSessions;
|
||||
public final double averageRoomCycleMs;
|
||||
public final double worstRoomCycleMs;
|
||||
public final int worstRoomCycleRoomId;
|
||||
public final String worstRoomCycleRoomName;
|
||||
public final long delayedEventsPending;
|
||||
public final int overloadedWiredRooms;
|
||||
public final int heavyWiredRooms;
|
||||
public final double wiredActivityPerSecond;
|
||||
|
||||
public Overview(long uptimeSeconds, long lastRefreshEpochMs, String guiStatus, int memoryUsedMb, int memoryMaxMb, int memoryAllocatedMb, double memoryUsagePercent, double cpuLoadPercent, int activeOsThreads, int connectedPlayers, int loadedRooms, int wiredTickables, int peakPlayers, int activeWebSocketSessions, int peakWebSocketSessions, double averageRoomCycleMs, double worstRoomCycleMs, int worstRoomCycleRoomId, String worstRoomCycleRoomName, long delayedEventsPending, int overloadedWiredRooms, int heavyWiredRooms, double wiredActivityPerSecond) {
|
||||
this.uptimeSeconds = uptimeSeconds;
|
||||
this.lastRefreshEpochMs = lastRefreshEpochMs;
|
||||
this.guiStatus = guiStatus;
|
||||
this.memoryUsedMb = memoryUsedMb;
|
||||
this.memoryMaxMb = memoryMaxMb;
|
||||
this.memoryAllocatedMb = memoryAllocatedMb;
|
||||
this.memoryUsagePercent = memoryUsagePercent;
|
||||
this.cpuLoadPercent = cpuLoadPercent;
|
||||
this.activeOsThreads = activeOsThreads;
|
||||
this.connectedPlayers = connectedPlayers;
|
||||
this.loadedRooms = loadedRooms;
|
||||
this.wiredTickables = wiredTickables;
|
||||
this.peakPlayers = peakPlayers;
|
||||
this.activeWebSocketSessions = activeWebSocketSessions;
|
||||
this.peakWebSocketSessions = peakWebSocketSessions;
|
||||
this.averageRoomCycleMs = averageRoomCycleMs;
|
||||
this.worstRoomCycleMs = worstRoomCycleMs;
|
||||
this.worstRoomCycleRoomId = worstRoomCycleRoomId;
|
||||
this.worstRoomCycleRoomName = worstRoomCycleRoomName;
|
||||
this.delayedEventsPending = delayedEventsPending;
|
||||
this.overloadedWiredRooms = overloadedWiredRooms;
|
||||
this.heavyWiredRooms = heavyWiredRooms;
|
||||
this.wiredActivityPerSecond = wiredActivityPerSecond;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MemoryPoint {
|
||||
public final long timestamp;
|
||||
public final int usedMb;
|
||||
public final int maxMb;
|
||||
public final double usagePercent;
|
||||
|
||||
public MemoryPoint(long timestamp, int usedMb, int maxMb, double usagePercent) {
|
||||
this.timestamp = timestamp;
|
||||
this.usedMb = usedMb;
|
||||
this.maxMb = maxMb;
|
||||
this.usagePercent = usagePercent;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class OnlineUserRow {
|
||||
public final int id;
|
||||
public final String username;
|
||||
public final String rank;
|
||||
public final int credits;
|
||||
public final int roomId;
|
||||
|
||||
public OnlineUserRow(int id, String username, String rank, int credits, int roomId) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.rank = rank;
|
||||
this.credits = credits;
|
||||
this.roomId = roomId;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ActiveRoomRow {
|
||||
public final int roomId;
|
||||
public final String name;
|
||||
public final int players;
|
||||
public final int items;
|
||||
public final int tickables;
|
||||
public final double cpuMs;
|
||||
public final long estimatedRamKb;
|
||||
public final String thread;
|
||||
|
||||
public ActiveRoomRow(int roomId, String name, int players, int items, int tickables, double cpuMs, long estimatedRamKb, String thread) {
|
||||
this.roomId = roomId;
|
||||
this.name = name;
|
||||
this.players = players;
|
||||
this.items = items;
|
||||
this.tickables = tickables;
|
||||
this.cpuMs = cpuMs;
|
||||
this.estimatedRamKb = estimatedRamKb;
|
||||
this.thread = thread;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class WiredRoomRow {
|
||||
public final int roomId;
|
||||
public final long averageTickMs;
|
||||
public final long peakTickMs;
|
||||
public final int usagePercent;
|
||||
public final int delayedEventsPending;
|
||||
public final boolean overloaded;
|
||||
public final boolean heavy;
|
||||
|
||||
public WiredRoomRow(int roomId, long averageTickMs, long peakTickMs, int usagePercent, int delayedEventsPending, boolean overloaded, boolean heavy) {
|
||||
this.roomId = roomId;
|
||||
this.averageTickMs = averageTickMs;
|
||||
this.peakTickMs = peakTickMs;
|
||||
this.usagePercent = usagePercent;
|
||||
this.delayedEventsPending = delayedEventsPending;
|
||||
this.overloaded = overloaded;
|
||||
this.heavy = heavy;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class WiredTopRoomRow {
|
||||
public final int roomId;
|
||||
public final String name;
|
||||
public final int usagePercent;
|
||||
public final int averageTickMs;
|
||||
public final int peakTickMs;
|
||||
public final int delayedEventsPending;
|
||||
public final double activityPerSecond;
|
||||
public final boolean heavy;
|
||||
|
||||
public WiredTopRoomRow(int roomId, String name, int usagePercent, int averageTickMs, int peakTickMs, int delayedEventsPending, double activityPerSecond, boolean heavy) {
|
||||
this.roomId = roomId;
|
||||
this.name = name;
|
||||
this.usagePercent = usagePercent;
|
||||
this.averageTickMs = averageTickMs;
|
||||
this.peakTickMs = peakTickMs;
|
||||
this.delayedEventsPending = delayedEventsPending;
|
||||
this.activityPerSecond = activityPerSecond;
|
||||
this.heavy = heavy;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class HikariPoolMetrics {
|
||||
public final int activeConnections;
|
||||
public final int idleConnections;
|
||||
public final int totalConnections;
|
||||
public final int waitingThreads;
|
||||
public final int maxConnections;
|
||||
|
||||
public HikariPoolMetrics(int activeConnections, int idleConnections, int totalConnections, int waitingThreads, int maxConnections) {
|
||||
this.activeConnections = activeConnections;
|
||||
this.idleConnections = idleConnections;
|
||||
this.totalConnections = totalConnections;
|
||||
this.waitingThreads = waitingThreads;
|
||||
this.maxConnections = maxConnections;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class SchedulerMetrics {
|
||||
public final int queuedTasks;
|
||||
public final int activeThreads;
|
||||
public final int poolSize;
|
||||
public final long completedTasks;
|
||||
public final boolean running;
|
||||
|
||||
public SchedulerMetrics(int queuedTasks, int activeThreads, int poolSize, long completedTasks, boolean running) {
|
||||
this.queuedTasks = queuedTasks;
|
||||
this.activeThreads = activeThreads;
|
||||
this.poolSize = poolSize;
|
||||
this.completedTasks = completedTasks;
|
||||
this.running = running;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class NetworkMetrics {
|
||||
public final double incomingPacketsPerSecond;
|
||||
public final double outgoingPacketsPerSecond;
|
||||
public final double incomingKilobytesPerSecond;
|
||||
public final double outgoingKilobytesPerSecond;
|
||||
public final long totalIncomingPackets;
|
||||
public final long totalOutgoingPackets;
|
||||
|
||||
public NetworkMetrics(double incomingPacketsPerSecond, double outgoingPacketsPerSecond, double incomingKilobytesPerSecond, double outgoingKilobytesPerSecond, long totalIncomingPackets, long totalOutgoingPackets) {
|
||||
this.incomingPacketsPerSecond = incomingPacketsPerSecond;
|
||||
this.outgoingPacketsPerSecond = outgoingPacketsPerSecond;
|
||||
this.incomingKilobytesPerSecond = incomingKilobytesPerSecond;
|
||||
this.outgoingKilobytesPerSecond = outgoingKilobytesPerSecond;
|
||||
this.totalIncomingPackets = totalIncomingPackets;
|
||||
this.totalOutgoingPackets = totalOutgoingPackets;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GarbageCollectorMetrics {
|
||||
public final long totalCollections;
|
||||
public final long totalCollectionTimeMs;
|
||||
public final long collectionsSinceLastSample;
|
||||
public final long lastObservedPauseMs;
|
||||
public final long sampledAtEpochMs;
|
||||
|
||||
public GarbageCollectorMetrics(long totalCollections, long totalCollectionTimeMs, long collectionsSinceLastSample, long lastObservedPauseMs, long sampledAtEpochMs) {
|
||||
this.totalCollections = totalCollections;
|
||||
this.totalCollectionTimeMs = totalCollectionTimeMs;
|
||||
this.collectionsSinceLastSample = collectionsSinceLastSample;
|
||||
this.lastObservedPauseMs = lastObservedPauseMs;
|
||||
this.sampledAtEpochMs = sampledAtEpochMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -14,6 +14,7 @@ import com.eu.habbo.networking.gameserver.encoders.GameServerMessageEncoder;
|
||||
import com.eu.habbo.networking.gameserver.encoders.GameServerMessageLogger;
|
||||
import com.eu.habbo.networking.gameserver.handlers.IdleTimeoutHandler;
|
||||
import com.eu.habbo.networking.gameserver.handlers.WebSocketHttpHandler;
|
||||
import com.eu.habbo.networking.gameserver.stats.EmuStatsHttpHandler;
|
||||
import com.eu.habbo.networking.gameserver.ssl.SSLCertificateLoader;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
@@ -62,6 +63,7 @@ public class WebSocketChannelInitializer extends ChannelInitializer<SocketChanne
|
||||
ch.pipeline().addLast("authHttpHandler", new AuthHttpHandler());
|
||||
ch.pipeline().addLast("badgeHttpHandler", new BadgeHttpHandler());
|
||||
ch.pipeline().addLast("badgeLeaderboardHttpHandler", new BadgeLeaderboardHttpHandler());
|
||||
ch.pipeline().addLast("emuStatsHttpHandler", new EmuStatsHttpHandler());
|
||||
ch.pipeline().addLast("wsProtocolHandler", new WebSocketServerProtocolHandler(this.wsConfig));
|
||||
ch.pipeline().addLast("wsFrameAggregator", new WebSocketFrameAggregator(MAX_FRAME_SIZE));
|
||||
ch.pipeline().addLast("wsCodec", new WebSocketCodec());
|
||||
|
||||
+2
@@ -1,5 +1,6 @@
|
||||
package com.eu.habbo.networking.gameserver.encoders;
|
||||
|
||||
import com.eu.habbo.monitoring.EmulatorNetworkStats;
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
@@ -16,6 +17,7 @@ public class GameServerMessageEncoder extends MessageToByteEncoder<ServerMessage
|
||||
ByteBuf buf = message.get();
|
||||
|
||||
try {
|
||||
EmulatorNetworkStats.recordOutgoing(buf.readableBytes());
|
||||
out.writeBytes(buf);
|
||||
} finally {
|
||||
// Release copied buffer.
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package com.eu.habbo.networking.gameserver.stats;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.monitoring.EmulatorStatsService;
|
||||
import com.eu.habbo.networking.gameserver.auth.AccessTokenService;
|
||||
import com.eu.habbo.networking.gameserver.auth.CorsOriginGate;
|
||||
import com.google.gson.Gson;
|
||||
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 java.nio.charset.StandardCharsets;
|
||||
|
||||
public class EmuStatsHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final String BASE_PATH = "/api/emustats";
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
@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 (!BASE_PATH.equals(path)) {
|
||||
super.channelRead(ctx, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
handle(ctx, req);
|
||||
} finally {
|
||||
ReferenceCountUtil.release(req);
|
||||
}
|
||||
}
|
||||
|
||||
private void handle(ChannelHandlerContext ctx, FullHttpRequest req) {
|
||||
if (req.method() == HttpMethod.OPTIONS) {
|
||||
sendCors(ctx, req);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) {
|
||||
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, "{\"error\":\"Use GET.\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = authenticate(req);
|
||||
if (userId <= 0) {
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, "{\"error\":\"Unauthorized.\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
Habbo habbo = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
|
||||
if (habbo == null || !habbo.hasPermission(Permission.ACC_MODTOOL_ROOM_INFO)) {
|
||||
sendJson(ctx, req, HttpResponseStatus.FORBIDDEN, "{\"error\":\"Forbidden.\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
EmulatorStatsService.Snapshot snapshot = EmulatorStatsService.collectSnapshot();
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, GSON.toJson(snapshot));
|
||||
}
|
||||
|
||||
private static int authenticate(FullHttpRequest req) {
|
||||
String header = req.headers().get(HttpHeaderNames.AUTHORIZATION);
|
||||
if (header == null || header.isBlank()) return 0;
|
||||
|
||||
String token = header.startsWith("Bearer ") ? header.substring(7).trim() : header.trim();
|
||||
return AccessTokenService.verify(token);
|
||||
}
|
||||
|
||||
private static void sendCors(ChannelHandlerContext ctx, FullHttpRequest req) {
|
||||
if (!CorsOriginGate.isAllowed(req)) {
|
||||
FullHttpResponse forbidden = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN);
|
||||
forbidden.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
|
||||
ctx.writeAndFlush(forbidden).addListener(ChannelFutureListener.CLOSE);
|
||||
return;
|
||||
}
|
||||
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT);
|
||||
applyCors(req, response);
|
||||
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
|
||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
|
||||
private static void sendJson(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, String json) {
|
||||
boolean headRequest = req.method() == HttpMethod.HEAD;
|
||||
byte[] bytes = (json == null ? "{}" : json).getBytes(StandardCharsets.UTF_8);
|
||||
FullHttpResponse response = headRequest
|
||||
? new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status)
|
||||
: new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes));
|
||||
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8");
|
||||
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-store, no-cache, must-revalidate");
|
||||
applyCors(req, response);
|
||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
|
||||
private static void applyCors(FullHttpRequest req, FullHttpResponse response) {
|
||||
String origin = req.headers().get(HttpHeaderNames.ORIGIN);
|
||||
if (origin != null && !origin.isEmpty() && CorsOriginGate.isAllowed(req)) {
|
||||
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
|
||||
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
||||
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, OPTIONS");
|
||||
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Authorization, Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api");
|
||||
response.headers().set(HttpHeaderNames.VARY, "Origin");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,8 +123,8 @@ public class PluginManager {
|
||||
WiredEngine.RATE_LIMIT_WINDOW_MS = Emulator.getConfig().getInt("wired.abuse.rate.limit.window.ms", 10000);
|
||||
WiredEngine.WIRED_BAN_DURATION_MS = Emulator.getConfig().getInt("wired.abuse.ban.duration.ms", 600000);
|
||||
WiredEngine.MONITOR_USAGE_WINDOW_MS = Emulator.getConfig().getInt("wired.monitor.usage.window.ms", 1000);
|
||||
WiredEngine.MONITOR_USAGE_LIMIT = Emulator.getConfig().getInt("wired.monitor.usage.limit", 1000);
|
||||
WiredEngine.MONITOR_DELAYED_EVENTS_LIMIT = Emulator.getConfig().getInt("wired.monitor.delayed.events.limit", 100);
|
||||
WiredEngine.MONITOR_USAGE_LIMIT = Emulator.getConfig().getInt("wired.monitor.usage.limit", 50000);
|
||||
WiredEngine.MONITOR_DELAYED_EVENTS_LIMIT = Emulator.getConfig().getInt("wired.monitor.delayed.events.limit", 50000);
|
||||
WiredEngine.MONITOR_OVERLOAD_AVERAGE_MS = Emulator.getConfig().getInt("wired.monitor.overload.average.ms", 50);
|
||||
WiredEngine.MONITOR_OVERLOAD_PEAK_MS = Emulator.getConfig().getInt("wired.monitor.overload.peak.ms", 150);
|
||||
WiredEngine.MONITOR_OVERLOAD_CONSECUTIVE_WINDOWS = Emulator.getConfig().getInt("wired.monitor.overload.consecutive.windows", 2);
|
||||
|
||||
@@ -22,6 +22,9 @@ public class BotFollowHabbo implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (this.bot == null || this.bot.getRoom() == null || this.bot.getRoom() != this.room) {
|
||||
return;
|
||||
}
|
||||
if (this.bot != null) {
|
||||
if (this.habbo != null && this.bot.getFollowingHabboId() == this.habbo.getHabboInfo().getId()) {
|
||||
if (this.habbo.getHabboInfo().getCurrentRoom() != null && this.habbo.getHabboInfo().getCurrentRoom() == this.room) {
|
||||
|
||||
@@ -38,6 +38,10 @@ public class RoomUnitWalkToRoomUnit implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (this.room == null || !this.room.isLoaded() || this.walker == null || this.target == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.goalTile == null) {
|
||||
this.findNewLocation();
|
||||
Emulator.getThreading().run(this, 500);
|
||||
@@ -48,9 +52,8 @@ public class RoomUnitWalkToRoomUnit implements Runnable {
|
||||
if (this.walker.getCurrentLocation().distance(this.goalTile) <= this.minDistance) {
|
||||
for (Runnable r : this.targetReached) {
|
||||
Emulator.getThreading().run(r);
|
||||
|
||||
WiredManager.triggerBotReachedHabbo(this.room, this.walker, this.target);
|
||||
}
|
||||
WiredManager.triggerBotReachedHabbo(this.room, this.walker, this.target);
|
||||
} else {
|
||||
Emulator.getThreading().run(this, 500);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ public class GameTimer implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
timer.setThreadActive(false);
|
||||
|
||||
if (timer.getRoomId() == 0) {
|
||||
timer.setRunning(false);
|
||||
return;
|
||||
@@ -23,7 +25,6 @@ public class GameTimer implements Runnable {
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(timer.getRoomId());
|
||||
|
||||
if (room == null || !timer.isRunning() || timer.isPaused()) {
|
||||
timer.setThreadActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,10 +32,10 @@ public class GameTimer implements Runnable {
|
||||
if (timer.getTimeNow() < 0) timer.setTimeNow(0);
|
||||
|
||||
if (timer.getTimeNow() > 0) {
|
||||
timer.setThreadActive(true);
|
||||
Emulator.getThreading().run(this, 1000);
|
||||
if (timer.tryActivateTimerThread()) {
|
||||
Emulator.getThreading().run(this, 1000);
|
||||
}
|
||||
} else {
|
||||
timer.setThreadActive(false);
|
||||
timer.setTimeNow(0);
|
||||
timer.endGame(room);
|
||||
WiredManager.triggerGameEnds(room);
|
||||
|
||||
@@ -14,28 +14,30 @@ public class GameUpCounter implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
timer.setThreadActive(false);
|
||||
|
||||
if (timer.getRoomId() == 0) {
|
||||
timer.setRunning(false);
|
||||
timer.setThreadActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(timer.getRoomId());
|
||||
|
||||
if (room == null || !timer.isRunning() || timer.isPaused()) {
|
||||
timer.setThreadActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
int tickDelayMs = (int) timer.getNextTickDelayMs();
|
||||
timer.advanceCounterInMs(tickDelayMs);
|
||||
WiredManager.triggerClockCounter(room, timer);
|
||||
if (timer.getCurrentTimeInMs() % 1000 == 0) {
|
||||
WiredManager.triggerClockCounter(room, timer);
|
||||
}
|
||||
|
||||
if (timer.getCurrentTimeInMs() < timer.getMaximumTimeInMs()) {
|
||||
timer.setThreadActive(true);
|
||||
Emulator.getThreading().run(this, timer.getNextTickDelayMs());
|
||||
if (timer.tryActivateTimerThread()) {
|
||||
Emulator.getThreading().run(this, timer.getNextTickDelayMs());
|
||||
}
|
||||
} else {
|
||||
timer.setThreadActive(false);
|
||||
timer.setCurrentTimeInMs(timer.getMaximumTimeInMs());
|
||||
timer.endGame(room);
|
||||
WiredManager.triggerGameEnds(room);
|
||||
|
||||
Reference in New Issue
Block a user