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);
+ // no-op
}
-
+
/**
* Check if an event should be rate-limited.
- * If rate limit exceeded, bans the room and sends alerts.
+ * Uses a soft limiter only, without banning rooms.
* @param roomId the room ID
- * @param room the room object (for sending alerts if banned)
+ * @param room the room object
* @param eventType the event type
* @return true if the event should be blocked due to rate limiting
*/
private boolean isRateLimited(int roomId, Room room, WiredEvent.Type eventType) {
String key = roomId + ":" + eventType.name();
long now = System.currentTimeMillis();
-
+
EventRateTracker tracker = eventRateLimiters.compute(key, (k, existing) -> {
if (existing == null) {
return new EventRateTracker(now);
@@ -1045,51 +1169,46 @@ public final class WiredEngine {
existing.recordEvent(now);
return existing;
});
-
+
boolean limited = tracker.isRateLimited(now);
if (limited && tracker.shouldBan(now)) {
- // First time hitting limit in this suppression window - ban the room
- banRoom(roomId, room);
+ LOGGER.warn("Soft wired rate limit in room {} for event {}. Count in current window exceeded.",
+ roomId, eventType);
}
return limited;
}
-
+
/**
* Tracks event rate for a specific room + event type combination.
*/
private static final class EventRateTracker {
private long windowStart;
private int eventCount;
- private boolean banned;
-
+ private boolean warned;
+
EventRateTracker(long now) {
this.windowStart = now;
this.eventCount = 1;
- this.banned = false;
+ this.warned = false;
}
-
+
synchronized void recordEvent(long now) {
- // Reset window if expired
if (now - windowStart > RATE_LIMIT_WINDOW_MS) {
windowStart = now;
eventCount = 1;
- // Don't reset banned here - room ban is checked separately
+ warned = false;
} else {
eventCount++;
}
}
-
+
synchronized boolean isRateLimited(long now) {
return eventCount > MAX_EVENTS_PER_WINDOW;
}
-
- /**
- * Check if this is the first time we've hit the limit (to trigger ban).
- * Returns true only once per suppression window.
- */
+
synchronized boolean shouldBan(long now) {
- if (eventCount > MAX_EVENTS_PER_WINDOW && !banned) {
- banned = true;
+ if (eventCount > MAX_EVENTS_PER_WINDOW && !warned) {
+ warned = true;
return true;
}
return false;
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java
index a3077406..c7d0fd91 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java
@@ -46,7 +46,7 @@ import java.sql.SQLException;
* wired engine. It provides static methods for triggering events and manages
* the lifecycle of the engine.
*
- *
+ *
* Configuration Options:
*
* - {@code wired.engine.enabled} - Enable new engine (parallel mode)
@@ -54,7 +54,7 @@ import java.sql.SQLException;
* - {@code wired.engine.maxStepsPerStack} - Loop protection limit
* - {@code wired.engine.debug} - Verbose logging
*
- *
+ *
* Migration Strategy:
*
* - Set {@code wired.engine.enabled=true} to run both engines in parallel
@@ -62,7 +62,7 @@ import java.sql.SQLException;
* - Set {@code wired.engine.exclusive=true} to disable legacy engine
* - Full migration complete - WiredManager is now the only wired engine
*
- *
+ *
* @see WiredEngine
* @see WiredEvents
*/
@@ -86,10 +86,10 @@ public final class WiredManager {
/** The singleton engine instance */
private static volatile WiredEngine engine;
-
+
/** The stack index */
private static volatile RoomWiredStackIndex stackIndex;
-
+
/** Whether the engine is initialized */
private static volatile boolean initialized = false;
private WiredManager() {
@@ -119,7 +119,7 @@ public final class WiredManager {
boolean enabled = Emulator.getConfig().getBoolean(CONFIG_ENABLED, DEFAULT_ENABLED);
int maxSteps = Emulator.getConfig().getInt(CONFIG_MAX_STEPS, DEFAULT_MAX_STEPS);
boolean debug = Emulator.getConfig().getBoolean(CONFIG_DEBUG, false);
-
+
// Load additional configuration
MAXIMUM_FURNI_SELECTION = Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5);
TELEPORT_DELAY = Emulator.getConfig().getInt("wired.effect.teleport.delay", 500);
@@ -133,13 +133,13 @@ public final class WiredManager {
stackIndex = new RoomWiredStackIndex();
WiredServices services = DefaultWiredServices.getInstance();
engine = new WiredEngine(services, stackIndex, maxSteps);
-
+
// Start the centralized tick service (50ms interval)
WiredTickService.getInstance().start();
initialized = true;
-
- LOGGER.info("Wired Manager initialized - enabled: {}, maxSteps: {}, debug: {}",
+
+ LOGGER.info("Wired Manager initialized - enabled: {}, maxSteps: {}, debug: {}",
enabled, maxSteps, debug);
}
@@ -153,16 +153,16 @@ public final class WiredManager {
}
LOGGER.info("Shutting down Wired Manager...");
-
+
// Stop the tick service first
WiredTickService.getInstance().stop();
-
+
if (stackIndex != null) {
stackIndex.clearAll();
}
-
+
if (engine != null) {
- engine.clearUnseenCache();
+ engine.clearAllExecutionCaches();
}
initialized = false;
@@ -212,10 +212,22 @@ public final class WiredManager {
if (!isEnabled() || engine == null) {
return false;
}
-
+
return engine.handleEvent(event);
}
+ /**
+ * Handle a wired event using the new engine when the source trigger item is already known.
+ * Used by timed wired to avoid scanning unrelated stacks.
+ */
+ private static boolean handleEventForSourceItem(WiredEvent event, HabboItem sourceItem) {
+ if (!isEnabled() || engine == null || event == null || sourceItem == null) {
+ return false;
+ }
+
+ return engine.handleEventForSourceItem(event, sourceItem.getId());
+ }
+
/**
* Trigger when a user walks onto furniture.
*/
@@ -223,7 +235,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null || item == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.userWalksOn(room, user, item);
return handleEvent(event);
}
@@ -235,7 +247,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null || item == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.userWalksOff(room, user, item);
return handleEvent(event);
}
@@ -311,7 +323,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.userSays(room, user, message);
return handleEvent(event);
}
@@ -332,7 +344,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.userEntersRoom(room, user);
return handleEvent(event);
}
@@ -356,7 +368,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || item == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.furniStateChanged(room, user, item);
return handleEvent(event);
}
@@ -365,24 +377,24 @@ public final class WiredManager {
* Trigger a timer tick.
*/
public static boolean triggerTimerTick(Room room, HabboItem timerItem) {
- if (!isEnabled() || room == null) {
+ if (!isEnabled() || room == null || timerItem == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.timerTick(room, timerItem);
- return handleEvent(event);
+ return handleEventForSourceItem(event, timerItem);
}
/**
* Trigger a periodic timer.
*/
public static boolean triggerTimerRepeat(Room room, HabboItem timerItem) {
- if (!isEnabled() || room == null) {
+ if (!isEnabled() || room == null || timerItem == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.timerRepeat(room, timerItem);
- return handleEvent(event);
+ return handleEventForSourceItem(event, timerItem);
}
public static boolean triggerClockCounter(Room room, HabboItem counterItem) {
@@ -391,31 +403,31 @@ public final class WiredManager {
}
WiredEvent event = WiredEvents.clockCounter(room, counterItem);
- return handleEvent(event);
+ return handleEventForSourceItem(event, counterItem);
}
/**
* Trigger a long periodic timer.
*/
public static boolean triggerTimerRepeatLong(Room room, HabboItem timerItem) {
- if (!isEnabled() || room == null) {
+ if (!isEnabled() || room == null || timerItem == null) {
return false;
}
WiredEvent event = WiredEvents.timerRepeatLong(room, timerItem);
- return handleEvent(event);
+ return handleEventForSourceItem(event, timerItem);
}
/**
* Trigger a short periodic timer.
*/
public static boolean triggerTimerRepeatShort(Room room, HabboItem timerItem) {
- if (!isEnabled() || room == null) {
+ if (!isEnabled() || room == null || timerItem == null) {
return false;
}
WiredEvent event = WiredEvents.timerRepeatShort(room, timerItem);
- return handleEvent(event);
+ return handleEventForSourceItem(event, timerItem);
}
/**
@@ -425,7 +437,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.gameStarts(room);
return handleEvent(event);
}
@@ -437,7 +449,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.gameEnds(room);
return handleEvent(event);
}
@@ -449,7 +461,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || botUnit == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.botCollision(room, botUnit);
return handleEvent(event);
}
@@ -461,7 +473,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || botUnit == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.botReachedFurni(room, botUnit, item);
return handleEvent(event);
}
@@ -473,7 +485,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || botUnit == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.botReachedHabbo(room, botUnit, targetUser);
return handleEvent(event);
}
@@ -489,7 +501,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.scoreAchieved(room, user, score, scoreAdded);
return handleEvent(event);
}
@@ -501,7 +513,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.userIdles(room, user);
return handleEvent(event);
}
@@ -513,7 +525,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.userUnidles(room, user);
return handleEvent(event);
}
@@ -525,7 +537,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.userStartsDancing(room, user);
return handleEvent(event);
}
@@ -537,7 +549,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.userStopsDancing(room, user);
return handleEvent(event);
}
@@ -549,7 +561,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.teamWins(room, user);
return handleEvent(event);
}
@@ -561,7 +573,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.teamLoses(room, user);
return handleEvent(event);
}
@@ -574,7 +586,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.fromLegacy(triggerType, room, roomUnit, stuff);
return handleEvent(event);
}
@@ -586,11 +598,20 @@ public final class WiredManager {
* Call this when wired items are added/removed/moved.
*/
public static void invalidateRoom(Room room) {
- if (stackIndex != null && room != null) {
+ if (room == null) {
+ return;
+ }
+
+ if (stackIndex != null) {
stackIndex.invalidateAll(room);
- if (debugEnabled) {
- LOGGER.info("[Wired] Cache invalidated for room {}", room.getId());
- }
+ }
+
+ if (engine != null) {
+ engine.clearRoomExecutionCaches(room.getId());
+ }
+
+ if (debugEnabled) {
+ LOGGER.info("[Wired] Cache invalidated for room {}", room.getId());
}
}
@@ -601,13 +622,25 @@ public final class WiredManager {
if (stackIndex != null && room != null && tile != null) {
stackIndex.invalidate(room, tile);
}
+
+ if (engine != null && room != null) {
+ engine.clearRoomSourceStackCache(room.getId());
+ }
}
/**
* Rebuild the wired index for a room.
*/
public static void rebuildRoom(Room room) {
- if (stackIndex != null && room != null) {
+ if (room == null) {
+ return;
+ }
+
+ if (engine != null) {
+ engine.clearRoomExecutionCaches(room.getId());
+ }
+
+ if (stackIndex != null) {
stackIndex.rebuild(room);
}
}
@@ -616,19 +649,19 @@ public final class WiredManager {
/** Maximum number of furniture items that can be selected in a single wired component */
public static int MAXIMUM_FURNI_SELECTION = 5;
-
+
/** Delay in milliseconds between teleport executions */
public static int TELEPORT_DELAY = 500;
// ========== Debug Mode ==========
-
+
/** Debug mode - when enabled, logs detailed wired execution flow */
private static boolean debugEnabled = false;
/**
* Enables or disables wired debug mode.
* When enabled, detailed execution logs are written to help troubleshoot wired stacks.
- *
+ *
* @param enabled true to enable debug logging, false to disable
*/
public static void setDebugEnabled(boolean enabled) {
@@ -637,19 +670,19 @@ public final class WiredManager {
LOGGER.info("Wired debug mode ENABLED");
}
}
-
+
/**
* Checks if wired debug mode is enabled.
- *
+ *
* @return true if debug mode is active
*/
public static boolean isDebugEnabled() {
return debugEnabled;
}
-
+
/**
* Logs a debug message if debug mode is enabled.
- *
+ *
* @param message the message to log
* @param args optional format arguments
*/
@@ -660,7 +693,7 @@ public final class WiredManager {
}
// ========== JSON Utilities ==========
-
+
private static GsonBuilder gsonBuilder = null;
private static Gson cachedGson = null;
@@ -670,12 +703,12 @@ public final class WiredManager {
}
return gsonBuilder;
}
-
+
/**
* Gets a cached Gson instance. This is more efficient than calling
* getGsonBuilder().create() multiple times, as Gson instances are thread-safe
* and can be reused.
- *
+ *
* @return a cached Gson instance
*/
public static Gson getGson() {
@@ -686,50 +719,53 @@ public final class WiredManager {
}
// ========== Tick Service Integration ==========
-
+
/**
* Registers a tickable wired item with the centralized tick service.
*
* Call this when a time-based wired trigger is placed in a room or when
* a room is loaded.
*
- *
+ *
* @param room the room the item is in
* @param tickable the tickable item (e.g., WiredTriggerRepeater)
*/
public static void registerTickable(Room room, WiredTickable tickable) {
WiredTickService.getInstance().register(room, tickable);
}
-
+
/**
* Unregisters a tickable wired item from the tick service.
*
* Call this when a time-based wired trigger is picked up or when
* a room is unloaded.
*
- *
+ *
* @param room the room the item was in
* @param tickable the tickable item
*/
public static void unregisterTickable(Room room, WiredTickable tickable) {
WiredTickService.getInstance().unregister(room, tickable);
}
-
+
/**
* Unregisters all tickables for a room.
*
* Call this when a room is unloaded to clean up all tick registrations.
*
- *
+ *
* @param room the room
*/
public static void unregisterRoomTickables(Room room) {
WiredTickService.getInstance().unregisterRoom(room);
+ if (engine != null && room != null) {
+ engine.clearRoomExecutionCaches(room.getId());
+ }
}
-
+
/**
* Gets the tick service instance.
- *
+ *
* @return the WiredTickService
*/
public static WiredTickService getTickService() {
@@ -771,7 +807,7 @@ public final class WiredManager {
*
* This uses the new tick service for managing timer resets.
*
- *
+ *
* @param room the room
*/
public static void resetTimers(Room room) {
@@ -804,9 +840,9 @@ public final class WiredManager {
if (item instanceof InteractionWiredEffect && !(item instanceof WiredEffectTriggerStacks)) {
InteractionWiredEffect effect = (InteractionWiredEffect) item;
WiredEvent event = WiredEvent.builder(WiredEvent.Type.CUSTOM, room)
- .actor(roomUnit)
- .callStackDepth(callStackDepth)
- .build();
+ .actor(roomUnit)
+ .callStackDepth(callStackDepth)
+ .build();
WiredContext ctx = new WiredContext(event, effect, DefaultWiredServices.getInstance(), new WiredState(100));
effect.execute(ctx);
effect.setCooldown(millis);
@@ -823,12 +859,12 @@ public final class WiredManager {
/**
* Asynchronously drops/deletes all rewards given by a specific wired item.
* Used when a wired reward box is picked up or reset.
- *
+ *
* @param wiredId The ID of the wired item whose rewards should be deleted
*/
public static void dropRewards(int wiredId) {
Emulator.getThreading().run(() -> {
- try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
+ try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM wired_rewards_given WHERE wired_item = ?")) {
statement.setInt(1, wiredId);
statement.execute();
@@ -1066,4 +1102,3 @@ public final class WiredManager {
return false;
}
}
-
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/tick/WiredTickService.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/tick/WiredTickService.java
index 144f73d7..070718dd 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/tick/WiredTickService.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/tick/WiredTickService.java
@@ -9,133 +9,110 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
/**
* Centralized tick service for all wired timing operations.
- *
- * This service runs a single 50ms tick loop that processes all registered
- * {@link WiredTickable} items across all rooms. This replaces the old
- * per-room 500ms cycle approach and provides:
- *
- *
- *
- * - Higher resolution timing (50ms vs 500ms)
- * - Centralized management - single thread for all rooms
- * - Proper room lifecycle handling
- * - Efficient registration/unregistration
- *
- *
- * Architecture:
- *
- * WiredTickService (singleton)
- * └── ScheduledExecutorService (50ms tick)
- * └── For each room with tickables:
- * └── For each WiredTickable:
- * └── onWiredTick(room, currentTime)
- *
- *
- * Thread Safety:
- * All collections are thread-safe. The tick loop catches and logs exceptions
- * to prevent one bad item from crashing the entire service.
- *
- * @see WiredTickable
+ *
+ * This version keeps a single global tick clock, but distributes room processing
+ * across multiple single-threaded shard workers. A room is always processed on the
+ * same shard, preserving in-room order while preventing one heavy room from delaying
+ * all other rooms.
*/
public final class WiredTickService {
-
+
private static final Logger LOGGER = LoggerFactory.getLogger(WiredTickService.class);
-
- /** Default tick interval in milliseconds */
+
public static final int DEFAULT_TICK_INTERVAL_MS = 50;
-
- /** Minimum allowed tick interval (prevents CPU overload) */
public static final int MIN_TICK_INTERVAL_MS = 10;
-
- /** Maximum allowed tick interval */
public static final int MAX_TICK_INTERVAL_MS = 500;
-
- /** Singleton instance */
+
+ public static final int DEFAULT_WORKER_COUNT = Math.max(2, Math.min(8, Runtime.getRuntime().availableProcessors()));
+ public static final int MIN_WORKER_COUNT = 1;
+ public static final int MAX_WORKER_COUNT = 32;
+
+ public static final long SLOW_TICKABLE_THRESHOLD_MS = 100L;
+ public static final long SLOW_ROOM_THRESHOLD_MS = 50L;
+ public static final long SLOW_SHARD_THRESHOLD_MS = 250L;
+
private static volatile WiredTickService instance;
-
- /** The configured tick interval in milliseconds */
+
private int tickIntervalMs = DEFAULT_TICK_INTERVAL_MS;
-
- /** Whether debug logging is enabled */
private boolean debugEnabled = false;
-
- /** Thread priority for the tick service */
private int threadPriority = Thread.NORM_PRIORITY + 1;
-
- /**
- * Global tick counter - increments every tick.
- * All repeaters use this to stay synchronized.
- * Repeaters fire when (tickCount * tickIntervalMs) % repeatTime == 0
- */
- private volatile long tickCount = 0;
-
- /** The scheduled executor for the tick loop */
- private ScheduledExecutorService scheduler;
-
- /** The scheduled future for the tick task */
- private ScheduledFuture> tickTask;
-
- /** Map of room ID to set of registered tickables */
+ private int workerCount = DEFAULT_WORKER_COUNT;
+
+ /** Global logical tick counter shared by every shard. */
+ private final AtomicLong tickCount = new AtomicLong(0);
+
+ /** Schedules the global logical ticks. */
+ private ScheduledExecutorService coordinator;
+
+ /** One single-thread executor per shard, preserving order inside the shard. */
+ private ExecutorService[] shardExecutors;
+
+ /** Highest logical tick requested for each shard. */
+ private AtomicLong[] shardRequestedTicks;
+
+ /** Last logical tick fully processed by each shard. */
+ private AtomicLong[] shardProcessedTicks;
+
+ /** Whether a shard worker loop is currently scheduled/running. */
+ private AtomicBoolean[] shardScheduled;
+
private final ConcurrentHashMap> roomTickables;
-
- /** Whether the service is running */
private final AtomicBoolean running;
-
- /**
- * Private constructor for singleton.
- */
+
private WiredTickService() {
this.roomTickables = new ConcurrentHashMap<>();
this.running = new AtomicBoolean(false);
}
-
- /**
- * Loads configuration from emulator settings.
- */
+
private void loadConfiguration() {
- // Load tick interval
int configuredInterval = Emulator.getConfig().getInt("wired.tick.interval.ms", DEFAULT_TICK_INTERVAL_MS);
this.tickIntervalMs = Math.max(MIN_TICK_INTERVAL_MS, Math.min(MAX_TICK_INTERVAL_MS, configuredInterval));
-
+
if (configuredInterval != this.tickIntervalMs) {
- LOGGER.warn("wired.tick.interval.ms value {} is out of range [{}-{}], using {}",
- configuredInterval, MIN_TICK_INTERVAL_MS, MAX_TICK_INTERVAL_MS, this.tickIntervalMs);
+ LOGGER.warn(
+ "wired.tick.interval.ms value {} is out of range [{}-{}], using {}",
+ configuredInterval,
+ MIN_TICK_INTERVAL_MS,
+ MAX_TICK_INTERVAL_MS,
+ this.tickIntervalMs
+ );
}
-
- // Load debug flag
+
this.debugEnabled = Emulator.getConfig().getBoolean("wired.tick.debug", false);
-
- // Load thread priority
+
int configuredPriority = Emulator.getConfig().getInt("wired.tick.thread.priority", Thread.NORM_PRIORITY + 1);
this.threadPriority = Math.max(Thread.MIN_PRIORITY, Math.min(Thread.MAX_PRIORITY, configuredPriority));
+
+ int configuredWorkers = Emulator.getConfig().getInt("wired.tick.workers", DEFAULT_WORKER_COUNT);
+ this.workerCount = Math.max(MIN_WORKER_COUNT, Math.min(MAX_WORKER_COUNT, configuredWorkers));
+
+ if (configuredWorkers != this.workerCount) {
+ LOGGER.warn(
+ "wired.tick.workers value {} is out of range [{}-{}], using {}",
+ configuredWorkers,
+ MIN_WORKER_COUNT,
+ MAX_WORKER_COUNT,
+ this.workerCount
+ );
+ }
}
-
- /**
- * Gets the configured tick interval in milliseconds.
- *
- * @return the tick interval
- */
+
public int getTickIntervalMs() {
return tickIntervalMs;
}
-
- /**
- * Checks if debug logging is enabled.
- *
- * @return true if debug is enabled
- */
+
public boolean isDebugEnabled() {
return debugEnabled;
}
-
- /**
- * Gets the singleton instance.
- *
- * @return the WiredTickService instance
- */
+
+ public int getWorkerCount() {
+ return workerCount;
+ }
+
public static WiredTickService getInstance() {
if (instance == null) {
synchronized (WiredTickService.class) {
@@ -146,150 +123,158 @@ public final class WiredTickService {
}
return instance;
}
-
- /**
- * Starts the tick service.
- *
- * Should be called during emulator startup after WiredManager.initialize().
- *
- */
+
public synchronized void start() {
if (running.get()) {
LOGGER.warn("WiredTickService already running");
return;
}
-
- // Load configuration from emulator settings
+
loadConfiguration();
-
- LOGGER.info("Starting WiredTickService with {}ms tick interval (debug={}, priority={})...",
- tickIntervalMs, debugEnabled, threadPriority);
-
- this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
- Thread t = new Thread(r, "WiredTickService");
+
+ LOGGER.info(
+ "Starting WiredTickService with {}ms tick interval (workers={}, debug={}, priority={})...",
+ tickIntervalMs,
+ workerCount,
+ debugEnabled,
+ threadPriority
+ );
+
+ this.coordinator = Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread t = new Thread(r, "WiredTickCoordinator");
t.setDaemon(true);
t.setPriority(threadPriority);
return t;
});
-
- this.tickTask = scheduler.scheduleAtFixedRate(
- this::tick,
- tickIntervalMs,
- tickIntervalMs,
- TimeUnit.MILLISECONDS
- );
-
+
+ this.shardExecutors = new ExecutorService[workerCount];
+ this.shardRequestedTicks = new AtomicLong[workerCount];
+ this.shardProcessedTicks = new AtomicLong[workerCount];
+ this.shardScheduled = new AtomicBoolean[workerCount];
+
+ for (int i = 0; i < workerCount; i++) {
+ final int shardIndex = i;
+ this.shardExecutors[i] = Executors.newSingleThreadExecutor(r -> {
+ Thread t = new Thread(r, "WiredTickShard-" + shardIndex);
+ t.setDaemon(true);
+ t.setPriority(threadPriority);
+ return t;
+ });
+ this.shardRequestedTicks[i] = new AtomicLong(0L);
+ this.shardProcessedTicks[i] = new AtomicLong(0L);
+ this.shardScheduled[i] = new AtomicBoolean(false);
+ }
+
+ this.tickCount.set(0L);
running.set(true);
+
+ this.coordinator.scheduleAtFixedRate(
+ () -> {
+ try {
+ dispatchTick();
+ } catch (Throwable t) {
+ LOGGER.error("WiredTickService fatal coordinator error", t);
+ }
+ },
+ tickIntervalMs,
+ tickIntervalMs,
+ TimeUnit.MILLISECONDS
+ );
+
LOGGER.info("WiredTickService started successfully");
}
-
- /**
- * Stops the tick service.
- *
- * Should be called during emulator shutdown.
- *
- */
+
public synchronized void stop() {
if (!running.get()) {
return;
}
-
+
LOGGER.info("Stopping WiredTickService...");
-
running.set(false);
-
- if (tickTask != null) {
- tickTask.cancel(false);
- tickTask = null;
- }
-
- if (scheduler != null) {
- scheduler.shutdown();
+
+ if (coordinator != null) {
+ coordinator.shutdown();
try {
- if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
- scheduler.shutdownNow();
+ if (!coordinator.awaitTermination(5, TimeUnit.SECONDS)) {
+ coordinator.shutdownNow();
}
} catch (InterruptedException e) {
- scheduler.shutdownNow();
+ coordinator.shutdownNow();
Thread.currentThread().interrupt();
}
- scheduler = null;
+ coordinator = null;
}
-
+
+ if (shardExecutors != null) {
+ for (ExecutorService executor : shardExecutors) {
+ if (executor != null) {
+ executor.shutdown();
+ }
+ }
+
+ for (ExecutorService executor : shardExecutors) {
+ if (executor == null) {
+ continue;
+ }
+ try {
+ if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
+ executor.shutdownNow();
+ }
+ } catch (InterruptedException e) {
+ executor.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ shardExecutors = null;
+ shardRequestedTicks = null;
+ shardProcessedTicks = null;
+ shardScheduled = null;
+
roomTickables.clear();
LOGGER.info("WiredTickService stopped");
}
-
- /**
- * Checks if the service is running.
- *
- * @return true if running
- */
+
public boolean isRunning() {
return running.get();
}
-
- /**
- * Registers a tickable item with the service.
- *
- * The item will start receiving {@link WiredTickable#onWiredTick} calls
- * on the next tick cycle.
- *
- *
- * @param room the room the item is in
- * @param tickable the tickable item
- */
+
public void register(Room room, WiredTickable tickable) {
if (room == null || tickable == null) {
return;
}
-
+
int roomId = room.getId();
- Set tickables = roomTickables.computeIfAbsent(
- roomId,
- k -> ConcurrentHashMap.newKeySet()
- );
-
+ Set tickables = roomTickables.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet());
+
if (tickables.add(tickable)) {
tickable.onRegistered(room, System.currentTimeMillis());
}
}
-
- /**
- * Unregisters a tickable item from the service.
- *
- * @param room the room the item was in
- * @param tickable the tickable item
- */
+
public void unregister(Room room, WiredTickable tickable) {
if (room == null || tickable == null) {
return;
}
-
+
int roomId = room.getId();
Set tickables = roomTickables.get(roomId);
-
+
if (tickables != null) {
if (tickables.remove(tickable)) {
tickable.onUnregistered(room);
}
-
- // Clean up empty sets
+
if (tickables.isEmpty()) {
roomTickables.remove(roomId);
}
}
}
-
- /**
- * Unregisters a tickable by ID.
- *
- * @param roomId the room ID
- * @param tickableId the tickable item ID
- */
+
public void unregister(int roomId, int tickableId) {
Set tickables = roomTickables.get(roomId);
-
+
if (tickables != null) {
tickables.removeIf(t -> {
if (t.getId() == tickableId) {
@@ -301,162 +286,240 @@ public final class WiredTickService {
}
return false;
});
-
+
if (tickables.isEmpty()) {
roomTickables.remove(roomId);
}
}
}
-
- /**
- * Unregisters all tickables for a room.
- *
- * Should be called when a room is unloaded.
- *
- *
- * @param room the room
- */
+
public void unregisterRoom(Room room) {
if (room == null) {
return;
}
-
+
Set tickables = roomTickables.remove(room.getId());
-
+
if (tickables != null) {
- for (WiredTickable tickable : tickables) {
- tickable.onUnregistered(room);
+ WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
+ for (WiredTickable tickable : snapshot) {
+ try {
+ if (tickable != null) {
+ tickable.onUnregistered(room);
+ }
+ } catch (Throwable t) {
+ LOGGER.error(
+ "Error unregistering tickable {} from room {}",
+ tickable != null ? tickable.getId() : -1,
+ room.getId(),
+ t
+ );
+ }
}
- LOGGER.debug("Unregistered {} tickables from room {}", tickables.size(), room.getId());
+ LOGGER.debug("Unregistered {} tickables from room {}", snapshot.length, room.getId());
}
}
-
- /**
- * Resets all timers in a room.
- *
- * @param room the room
- */
+
public void resetRoomTimers(Room room) {
if (room == null) {
return;
}
-
+
Set tickables = roomTickables.get(room.getId());
-
+
if (tickables != null) {
- for (WiredTickable tickable : tickables) {
+ WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
+ for (WiredTickable tickable : snapshot) {
try {
- tickable.resetTimer();
- } catch (Exception e) {
- LOGGER.error("Error resetting timer for tickable {} in room {}",
- tickable.getId(), room.getId(), e);
+ if (tickable != null) {
+ tickable.resetTimer();
+ }
+ } catch (Throwable e) {
+ LOGGER.error(
+ "Error resetting timer for tickable {} in room {}",
+ tickable != null ? tickable.getId() : -1,
+ room.getId(),
+ e
+ );
}
}
}
}
-
- /**
- * Gets the count of registered tickables for a room.
- *
- * @param roomId the room ID
- * @return the count
- */
+
public int getTickableCount(int roomId) {
Set tickables = roomTickables.get(roomId);
return tickables != null ? tickables.size() : 0;
}
-
- /**
- * Gets the total count of registered tickables across all rooms.
- *
- * @return the total count
- */
+
public int getTotalTickableCount() {
- return roomTickables.values().stream()
- .mapToInt(Set::size)
- .sum();
+ return roomTickables.values().stream().mapToInt(Set::size).sum();
}
-
- /**
- * Gets the count of rooms with registered tickables.
- *
- * @return the room count
- */
+
public int getActiveRoomCount() {
return roomTickables.size();
}
-
- /**
- * The main tick loop.
- *
- * Called at the configured interval by the scheduler. Processes all registered tickables
- * across all rooms.
- *
- */
- private void tick() {
+
+ public long getTickCount() {
+ return tickCount.get();
+ }
+
+ private void dispatchTick() {
if (!running.get() || Emulator.isShuttingDown) {
return;
}
-
- // Increment global tick counter
- tickCount++;
-
- long startTime = System.currentTimeMillis();
- int tickablesProcessed = 0;
-
+
+ long currentTick = tickCount.incrementAndGet();
+
+ for (int shardIndex = 0; shardIndex < workerCount; shardIndex++) {
+ shardRequestedTicks[shardIndex].set(currentTick);
+ scheduleShardIfNeeded(shardIndex);
+ }
+ }
+
+ private void scheduleShardIfNeeded(int shardIndex) {
+ if (!running.get() || shardExecutors == null) {
+ return;
+ }
+
+ if (shardScheduled[shardIndex].compareAndSet(false, true)) {
+ shardExecutors[shardIndex].execute(() -> runShardLoop(shardIndex));
+ }
+ }
+
+ private void runShardLoop(int shardIndex) {
+ try {
+ while (running.get() && !Emulator.isShuttingDown) {
+ long nextTick = shardProcessedTicks[shardIndex].get() + 1L;
+ long requestedTick = shardRequestedTicks[shardIndex].get();
+
+ if (nextTick > requestedTick) {
+ break;
+ }
+
+ processShardTick(shardIndex, nextTick);
+ shardProcessedTicks[shardIndex].set(nextTick);
+ }
+ } catch (Throwable t) {
+ LOGGER.error("Fatal error in WiredTick shard {}", shardIndex, t);
+ } finally {
+ shardScheduled[shardIndex].set(false);
+ if (running.get() && shardProcessedTicks[shardIndex].get() < shardRequestedTicks[shardIndex].get()) {
+ scheduleShardIfNeeded(shardIndex);
+ }
+ }
+ }
+
+ private void processShardTick(int shardIndex, long currentTick) {
+ long shardStart = System.currentTimeMillis();
+ int processedTickables = 0;
+ int processedRooms = 0;
+
for (Map.Entry> entry : roomTickables.entrySet()) {
int roomId = entry.getKey();
- Set tickables = entry.getValue();
-
- if (tickables.isEmpty()) {
+ if (getShardIndex(roomId) != shardIndex) {
continue;
}
-
- // Get the room - skip if not loaded
+
+ Set tickables = entry.getValue();
+ if (tickables == null || tickables.isEmpty()) {
+ continue;
+ }
+
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
if (room == null || !room.isLoaded()) {
continue;
}
-
- // Skip if room is empty (optimization)
+
if (room.getCurrentHabbos().isEmpty() && room.getCurrentBots().isEmpty()) {
continue;
}
-
- // Process each tickable
- for (WiredTickable tickable : tickables) {
+
+ long roomStart = System.currentTimeMillis();
+ WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
+ if (snapshot.length == 0) {
+ continue;
+ }
+
+ processedRooms++;
+
+ for (WiredTickable tickable : snapshot) {
+ long tickableStart = System.currentTimeMillis();
+
+ if (tickable == null) {
+ continue;
+ }
+
try {
- // Verify item still belongs to this room
if (tickable.getRoomId() != roomId) {
- // Item moved to another room, unregister it
- tickables.remove(tickable);
+ unregister(roomId, tickable.getId());
continue;
}
-
- // Pass global tick count - all tickables see the same counter
- // This keeps repeaters with the same interval perfectly synchronized
- tickable.onWiredTick(room, tickCount, tickIntervalMs);
- tickablesProcessed++;
- } catch (Exception e) {
- LOGGER.error("Error in wired tick for tickable {} in room {}: {}",
- tickable.getId(), roomId, e.getMessage(), e);
+
+ tickable.onWiredTick(room, currentTick, tickIntervalMs);
+ processedTickables++;
+
+ long tickableDuration = System.currentTimeMillis() - tickableStart;
+ if (tickableDuration > SLOW_TICKABLE_THRESHOLD_MS) {
+ LOGGER.warn(
+ "Slow wired tickable: shard={}, room={}, tick={}, tickableId={}, class={}, took={}ms",
+ shardIndex,
+ roomId,
+ currentTick,
+ tickable.getId(),
+ tickable.getClass().getName(),
+ tickableDuration
+ );
+ }
+ } catch (Throwable t) {
+ long tickableDuration = System.currentTimeMillis() - tickableStart;
+ LOGGER.error(
+ "Error in wired tick for tickable {} in room {} after {}ms",
+ tickable.getId(),
+ roomId,
+ tickableDuration,
+ t
+ );
}
}
+
+ long roomDuration = System.currentTimeMillis() - roomStart;
+ if (roomDuration > SLOW_ROOM_THRESHOLD_MS) {
+ LOGGER.warn(
+ "Slow wired room tick: shard={}, room={}, tick={}, tickables={}, took={}ms",
+ shardIndex,
+ roomId,
+ currentTick,
+ snapshot.length,
+ roomDuration
+ );
+ }
}
-
- // Debug logging if enabled
- if (debugEnabled && tickablesProcessed > 0) {
- LOGGER.debug("Wired tick #{} completed: {} tickables processed in {}ms",
- tickCount, tickablesProcessed, System.currentTimeMillis() - startTime);
+
+ long shardDuration = System.currentTimeMillis() - shardStart;
+ if (shardDuration > SLOW_SHARD_THRESHOLD_MS) {
+ LOGGER.warn(
+ "Slow wired shard tick: shard={}, tick={}, rooms={}, tickables={}, took={}ms",
+ shardIndex,
+ currentTick,
+ processedRooms,
+ processedTickables,
+ shardDuration
+ );
+ }
+
+ if (debugEnabled && processedTickables > 0) {
+ LOGGER.debug(
+ "Wired shard tick completed: shard={}, tick={}, rooms={}, tickables={}, took={}ms",
+ shardIndex,
+ currentTick,
+ processedRooms,
+ processedTickables,
+ shardDuration
+ );
}
}
-
- /**
- * Gets the current global tick count.
- *
- * @return the tick count
- */
- public long getTickCount() {
- return tickCount;
+
+ private int getShardIndex(int roomId) {
+ return Math.floorMod(roomId, workerCount);
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemAsGiftEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemAsGiftEvent.java
index 98937b16..2416f7b1 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemAsGiftEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemAsGiftEvent.java
@@ -44,15 +44,20 @@ 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");
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 {
@@ -60,7 +65,6 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
try {
-
int pageId = this.packet.readInt();
int itemId = this.packet.readInt();
String extraData = this.packet.readString();
@@ -71,14 +75,22 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
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
+ );
+
int userId = 0;
- if (!Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId) && !Emulator.getGameEnvironment().getCatalogManager().giftFurnis.containsKey(spriteId)) {
+ if (!Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId)
+ && !Emulator.getGameEnvironment().getCatalogManager().giftFurnis.containsKey(spriteId)) {
+ LOGGER.error("DEBUG GIFT: 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);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
@@ -89,10 +101,12 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
Integer iItemId = Emulator.getGameEnvironment().getCatalogManager().giftWrappers.get(spriteId);
- if (iItemId == null)
+ if (iItemId == null) {
iItemId = Emulator.getGameEnvironment().getCatalogManager().giftFurnis.get(spriteId);
+ }
if (iItemId == null) {
+ LOGGER.error("DEBUG GIFT: iItemId null for spriteId={}", spriteId);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
@@ -100,9 +114,15 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
Item giftItem = Emulator.getGameEnvironment().getItemManager().getItem(iItemId);
if (giftItem == null) {
- giftItem = Emulator.getGameEnvironment().getItemManager().getItem((Integer) Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray()[Emulator.getRandom().nextInt(Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size())]);
+ LOGGER.error("DEBUG GIFT: 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())
+ ]
+ );
if (giftItem == null) {
+ LOGGER.error("DEBUG GIFT: fallback giftItem also null");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
@@ -112,6 +132,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);
try (PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE username = ?")) {
statement.setString(1, username);
@@ -128,6 +149,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
if (userId == 0) {
+ LOGGER.error("DEBUG GIFT: receiver not found -> {}", username);
this.client.sendResponse(new GiftReceiverNotFoundComposer());
return;
}
@@ -135,11 +157,17 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId);
if (page == null) {
+ LOGGER.error("DEBUG GIFT: 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());
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
@@ -147,17 +175,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
CatalogItem item = page.getCatalogItem(itemId);
if (item == null) {
+ LOGGER.error("DEBUG GIFT: 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);
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());
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
@@ -165,6 +196,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
if (item.isLimited()) {
if (item.getLimitedStack() == item.getLimitedSells()) {
+ LOGGER.error("DEBUG GIFT: LTD sold out -> itemId={}", itemId);
this.client.sendResponse(new AlertLimitedSoldOutComposer());
return;
}
@@ -173,7 +205,14 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
int totalCredits = item.getCredits();
int totalPoints = item.getPoints();
- if(totalCredits > this.client.getHabbo().getHabboInfo().getCredits() || totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType())) {
+ 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());
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
@@ -181,23 +220,34 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
CatalogLimitedConfiguration limitedConfiguration = null;
int limitedStack = 0;
int limitedNumber = 0;
+
if (item.isLimited()) {
if (Emulator.getGameEnvironment().getCatalogManager().getLimitedConfig(item).available() == 0) {
+ LOGGER.error("DEBUG GIFT: LTD available=0 -> itemId={}", itemId);
this.client.sendResponse(new AlertLimitedSoldOutComposer());
return;
}
- // Check daily LTD limits for the buyer (sender of the gift)
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) {
- this.client.getHabbo().alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total").replace("%itemname%", item.getBaseItems().iterator().next().getFullName()).replace("%limit%", ltdLimit + ""));
+ LOGGER.error("DEBUG GIFT: 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())
+ .replace("%limit%", ltdLimit + "")
+ );
return;
}
ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item");
if (this.client.getHabbo().getHabboStats().totalLtds(item.getId()) >= ltdLimit) {
- this.client.getHabbo().alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item").replace("%itemname%", item.getBaseItems().iterator().next().getFullName()).replace("%limit%", ltdLimit + ""));
+ LOGGER.error("DEBUG GIFT: 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())
+ .replace("%limit%", ltdLimit + "")
+ );
return;
}
}
@@ -210,8 +260,6 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
limitedNumber = limitedConfiguration.getNumber();
limitedStack = limitedConfiguration.getTotalSet();
-
- // Log the LTD purchase for daily limits
this.client.getHabbo().getHabboStats().addLtdLog(item.getId(), Emulator.getIntUnixTimestamp());
}
@@ -229,6 +277,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) as c FROM users_badges WHERE user_id = ? AND badge_code LIKE ?")) {
statement.setInt(1, userId);
statement.setString(2, baseItem.getName());
+
try (ResultSet rSet = statement.executeQuery()) {
if (rSet.next()) {
c = rSet.getInt("c");
@@ -244,17 +293,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
if (badgeFound) {
+ LOGGER.error("DEBUG GIFT: 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());
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());
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
@@ -278,37 +330,88 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
badgeFound = true;
}
} else if (item.getName().startsWith("rentable_bot_")) {
+ LOGGER.error("DEBUG GIFT: 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");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
} else {
- if (baseItem.getInteractionType().getType() == InteractionTrophy.class || baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class) {
- if (baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class && habbo != null && !habbo.getClient().getHabbo().getInventory().getBadgesComponent().hasBadge(extraData)) {
- ScripterManager.scripterDetected(habbo.getClient(), Emulator.getTexts().getValue("scripter.warning.catalog.badge_display").replace("%username%", habbo.getClient().getHabbo().getHabboInfo().getUsername()).replace("%badge%", extraData));
+ if (baseItem.getInteractionType().getType() == InteractionTrophy.class
+ || baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class) {
+ if (baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class
+ && habbo != null
+ && !habbo.getClient().getHabbo().getInventory().getBadgesComponent().hasBadge(extraData)) {
+ ScripterManager.scripterDetected(
+ habbo.getClient(),
+ Emulator.getTexts().getValue("scripter.warning.catalog.badge_display")
+ .replace("%username%", habbo.getClient().getHabbo().getHabboInfo().getUsername())
+ .replace("%badge%", extraData)
+ );
extraData = "UMAD";
}
- extraData = this.client.getHabbo().getHabboInfo().getUsername() + (char) 9 + Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "-" + (Calendar.getInstance().get(Calendar.MONTH) + 1) + "-" + Calendar.getInstance().get(Calendar.YEAR) + (char) 9 + extraData;
+ extraData = this.client.getHabbo().getHabboInfo().getUsername()
+ + (char) 9
+ + Calendar.getInstance().get(Calendar.DAY_OF_MONTH)
+ + "-"
+ + (Calendar.getInstance().get(Calendar.MONTH) + 1)
+ + "-"
+ + Calendar.getInstance().get(Calendar.YEAR)
+ + (char) 9
+ + extraData;
}
- if (baseItem.getInteractionType().getType() == InteractionTeleport.class || baseItem.getInteractionType().getType() == InteractionTeleportTile.class) {
- HabboItem teleportOne = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
- HabboItem teleportTwo = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
+ if (baseItem.getInteractionType().getType() == InteractionTeleport.class
+ || baseItem.getInteractionType().getType() == InteractionTeleportTile.class) {
+
+ HabboItem teleportOne = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
+ 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);
+ this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
+ return;
+ }
+
Emulator.getGameEnvironment().getItemManager().insertTeleportPair(teleportOne.getId(), teleportTwo.getId());
itemsList.add(teleportOne);
itemsList.add(teleportTwo);
+
} else if (baseItem.getInteractionType().getType() == InteractionHopper.class) {
- HabboItem hopper = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedNumber, limitedNumber, extraData);
+ HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedNumber, limitedNumber, extraData);
- Emulator.getGameEnvironment().getItemManager().insertHopper(hopper);
+ if (habboItem == null) {
+ LOGGER.error("DEBUG GIFT: hopper creation failed. baseItemId={}", baseItem.getId());
+ this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
+ return;
+ }
- itemsList.add(hopper);
- } else if (baseItem.getInteractionType().getType() == InteractionGuildFurni.class || baseItem.getInteractionType().getType() == InteractionGuildGate.class) {
- InteractionGuildFurni habboItem = (InteractionGuildFurni) Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
+ Emulator.getGameEnvironment().getItemManager().insertHopper(habboItem);
+ itemsList.add(habboItem);
+
+ } else if (baseItem.getInteractionType().getType() == InteractionGuildFurni.class
+ || baseItem.getInteractionType().getType() == InteractionGuildGate.class) {
+ 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());
+ 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());
+ this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
+ return;
+ }
+
+ InteractionGuildFurni habboItem = (InteractionGuildFurni) createdItem;
habboItem.setExtradata("");
habboItem.needsUpdate(true);
+
int guildId;
try {
guildId = Integer.parseInt(extraData);
@@ -317,15 +420,24 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
+
Emulator.getThreading().run(habboItem);
Emulator.getGameEnvironment().getGuildManager().setGuild(habboItem, guildId);
itemsList.add(habboItem);
} else {
- HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
+ 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());
+ this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
+ return;
+ }
+
itemsList.add(habboItem);
}
}
} else {
+ LOGGER.error("DEBUG GIFT: 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;
@@ -333,48 +445,85 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
}
- StringBuilder giftData = new StringBuilder(itemsList.size() + "\t");
-
- for (HabboItem i : itemsList) {
- giftData.append(i.getId()).append("\t");
- }
-
- giftData.append(color).append("\t").append(ribbonId).append("\t").append(showName ? "1" : "0").append("\t").append(message.replace("\t", "")).append("\t").append(this.client.getHabbo().getHabboInfo().getUsername()).append("\t").append(this.client.getHabbo().getHabboInfo().getLook());
-
- HabboItem gift = Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, giftData.toString(), 0, 0);
-
- if (gift == null) {
+ if (itemsList.isEmpty()) {
+ LOGGER.error("DEBUG GIFT: itemsList empty before giftData");
+ this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
+ return;
+ }
+
+ StringBuilder giftData = new StringBuilder(itemsList.size() + "\t");
+
+ for (HabboItem i : itemsList) {
+ if (i == null) {
+ LOGGER.error("DEBUG GIFT: null HabboItem detected inside itemsList");
+ this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
+ return;
+ }
+
+ giftData.append(i.getId()).append("\t");
+ }
+
+ giftData.append(color)
+ .append("\t")
+ .append(ribbonId)
+ .append("\t")
+ .append(showName ? "1" : "0")
+ .append("\t")
+ .append(message.replace("\t", ""))
+ .append("\t")
+ .append(this.client.getHabbo().getHabboInfo().getUsername())
+ .append("\t")
+ .append(this.client.getHabbo().getHabboInfo().getLook());
+
+ HabboItem gift = Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, giftData.toString(), 0, 0);
+
+ if (gift == null) {
+ LOGGER.error("DEBUG GIFT: createGift returned null");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
- // Mark limited items as sold in the database to prevent duplication after catalog reload
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;
+ }
limitedConfiguration.limitedSold(item.getId(), this.client.getHabbo(), itm);
}
}
if (this.client.getHabbo().getHabboInfo().getId() != userId) {
- AchievementManager.progressAchievement(this.client.getHabbo(), Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftGiver"));
+ AchievementManager.progressAchievement(
+ this.client.getHabbo(),
+ Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftGiver")
+ );
}
if (habbo != null) {
habbo.getClient().sendResponse(new AddHabboItemComposer(gift));
habbo.getClient().getHabbo().getInventory().getItemsComponent().addItem(gift);
habbo.getClient().sendResponse(new InventoryRefreshComposer());
+
THashMap keys = new THashMap<>();
keys.put("display", "BUBBLE");
keys.put("image", "${image.library.url}notifications/gift.gif");
keys.put("message", Emulator.getTexts().getValue("generic.gift.received.anonymous"));
+
if (showName) {
- keys.put("message", Emulator.getTexts().getValue("generic.gift.received").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
+ keys.put("message", Emulator.getTexts().getValue("generic.gift.received")
+ .replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
}
+
habbo.getClient().sendResponse(new BubbleAlertComposer(BubbleAlertKeys.RECEIVED_BADGE.key, keys));
}
if (this.client.getHabbo().getHabboInfo().getId() != userId) {
- AchievementManager.progressAchievement(userId, Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftReceiver"));
+ AchievementManager.progressAchievement(
+ userId,
+ Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftReceiver")
+ );
}
if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) {
@@ -382,6 +531,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
this.client.getHabbo().giveCredits(-totalCredits);
}
}
+
if (totalPoints > 0) {
if (item.getPointsType() == 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_PIXELS)) {
this.client.getHabbo().givePixels(-totalPoints);
@@ -390,16 +540,18 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
}
+ 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));
}
+ } catch (Exception e) {
+ LOGGER.error("Exception caught", e);
+ this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
} finally {
this.client.getHabbo().getHabboStats().isPurchasingFurniture = false;
}
} else {
+ LOGGER.error("DEBUG GIFT: cooldown blocked purchase");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
}
}
-}
+}
\ No newline at end of file
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java
index e54fc6e7..46d385b4 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java
@@ -26,9 +26,16 @@ public class RoomUserWalkEvent extends MessageHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(RoomUserWalkEvent.class);
public static final String CONTROL_KEY = "control";
+ private static final String WALK_FLOOD_COUNT_KEY = "__walkFloodCount";
+ private static final String WALK_FLOOD_WINDOW_KEY = "__walkFloodWindow";
+ private static final String WALK_LAST_X_KEY = "__walkLastX";
+ private static final String WALK_LAST_Y_KEY = "__walkLastY";
+
+ private static final int MAX_WALKS_PER_SECOND = 15;
+
@Override
public int getRatelimit() {
- return Emulator.getConfig().getInt("pathfinder.click.delay", 0);
+ return 0;
}
@Override
@@ -37,8 +44,43 @@ public class RoomUserWalkEvent extends MessageHandler {
return;
}
- int x = this.packet.readInt(); // Position X
- int y = this.packet.readInt(); // Position Y
+ int x = this.packet.readInt();
+ int y = this.packet.readInt();
+
+ RoomUnit unit = this.client.getHabbo().getRoomUnit();
+ if (unit != null) {
+ long now = System.currentTimeMillis();
+ Object windowObj = unit.getCacheable().get(WALK_FLOOD_WINDOW_KEY);
+ Object countObj = unit.getCacheable().get(WALK_FLOOD_COUNT_KEY);
+
+ long windowStart = (windowObj instanceof Long) ? (Long) windowObj : 0L;
+ int count = (countObj instanceof Integer) ? (Integer) countObj : 0;
+
+ if (now - windowStart > 1000) {
+ // New 1-second window
+ windowStart = now;
+ count = 0;
+ }
+
+ count++;
+ unit.getCacheable().put(WALK_FLOOD_WINDOW_KEY, windowStart);
+ unit.getCacheable().put(WALK_FLOOD_COUNT_KEY, count);
+
+ if (count > MAX_WALKS_PER_SECOND) {
+ unit.getCacheable().put(WALK_LAST_X_KEY, x);
+ unit.getCacheable().put(WALK_LAST_Y_KEY, y);
+ return;
+ }
+
+ Object lastX = unit.getCacheable().get(WALK_LAST_X_KEY);
+ Object lastY = unit.getCacheable().get(WALK_LAST_Y_KEY);
+ if (lastX != null && lastY != null) {
+ x = (Integer) lastX;
+ y = (Integer) lastY;
+ unit.getCacheable().remove(WALK_LAST_X_KEY);
+ unit.getCacheable().remove(WALK_LAST_Y_KEY);
+ }
+ }
Habbo habbo = getControlledHabbo();
if (habbo == null) {
diff --git a/Latest_Compiled_Version/Habbo-4.1.0-jar-with-dependencies.jar b/Latest_Compiled_Version/Habbo-4.1.0-jar-with-dependencies.jar
index 94c084b2..a075a9d4 100644
Binary files a/Latest_Compiled_Version/Habbo-4.1.0-jar-with-dependencies.jar and b/Latest_Compiled_Version/Habbo-4.1.0-jar-with-dependencies.jar differ