diff --git a/Database Updates/005_WiredTickService.sql b/Database Updates/005_WiredTickService.sql new file mode 100644 index 00000000..865a40de --- /dev/null +++ b/Database Updates/005_WiredTickService.sql @@ -0,0 +1 @@ +INSERT INTO emulator_settings (`key`, `value`) VALUES ('wired.tick.workers', '6'); \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java index db624a5a..c2f4d1f1 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java @@ -39,7 +39,7 @@ import java.util.concurrent.ConcurrentHashMap; * It receives {@link WiredEvent} objects, finds matching stacks via {@link WiredStackIndex}, * evaluates conditions, and executes effects. *

- * + * *

Execution Flow:

*
    *
  1. Receive event via {@link #handleEvent(WiredEvent)}
  2. @@ -49,14 +49,14 @@ import java.util.concurrent.ConcurrentHashMap; *
  3. Execute effects (respecting random/unseen modifiers)
  4. *
  5. Handle delays for timed effects
  6. *
- * + * *

Safety Features:

* - * + * * @see WiredEvent * @see WiredContext * @see WiredStackIndex @@ -64,38 +64,41 @@ import java.util.concurrent.ConcurrentHashMap; public final class WiredEngine { private static final Logger LOGGER = LoggerFactory.getLogger(WiredEngine.class); - + /** Maximum recursion depth to prevent infinite loops (e.g., collision + chase) */ public static int MAX_RECURSION_DEPTH = 10; - + /** Maximum events of same type per room within rate limit window before banning */ public static int MAX_EVENTS_PER_WINDOW = 100; - + /** Time window for counting rapid events (milliseconds) */ public static long RATE_LIMIT_WINDOW_MS = 10000; - + /** Duration to ban wired execution in a room after abuse detected (milliseconds) */ public static long WIRED_BAN_DURATION_MS = 600000; private final WiredServices services; private final WiredStackIndex index; private final int maxStepsPerStack; - + /** Track unseen effect indices per room+tile for round-robin selection */ private final ConcurrentHashMap unseenIndices; - + /** Track recursion depth per room to prevent infinite loops */ private final ConcurrentHashMap roomRecursionDepth; - + /** Track event timestamps per room+eventType for rate limiting: key = "roomId:eventType" */ private final ConcurrentHashMap eventRateLimiters; - + /** Track rooms that are banned from wired execution: roomId -> ban expiry timestamp */ private final ConcurrentHashMap bannedRooms; + /** Cache room+eventType+sourceItemId -> matching stacks for source-triggered timer events */ + private final ConcurrentHashMap> sourceStacksByTriggerKey; + /** * Create a new wired engine. - * + * * @param services the services for performing side effects * @param index the stack index for finding matching stacks * @param maxStepsPerStack maximum steps per stack execution (loop protection) @@ -104,7 +107,7 @@ public final class WiredEngine { if (services == null) throw new IllegalArgumentException("Services cannot be null"); if (index == null) throw new IllegalArgumentException("Index cannot be null"); if (maxStepsPerStack <= 0) throw new IllegalArgumentException("Max steps must be positive"); - + this.services = services; this.index = index; this.maxStepsPerStack = maxStepsPerStack; @@ -112,11 +115,12 @@ public final class WiredEngine { this.roomRecursionDepth = new ConcurrentHashMap<>(); this.eventRateLimiters = new ConcurrentHashMap<>(); this.bannedRooms = new ConcurrentHashMap<>(); + this.sourceStacksByTriggerKey = new ConcurrentHashMap<>(); } /** * Handle a wired event by finding and executing matching stacks. - * + * * @param event the event to handle * @return true if any stack was triggered (useful for SAY_SOMETHING to suppress message) */ @@ -129,20 +133,14 @@ public final class WiredEngine { if (room == null || !room.isLoaded()) { return false; } - + int roomId = room.getId(); - - // Check if room is banned from wired execution - if (isRoomBanned(roomId)) { - return false; - } - - // Check rate limiting to prevent rapid-fire event spam (e.g., collision + chase loop) + + // Soft rate limiting to prevent rapid-fire event spam without banning whole rooms if (isRateLimited(roomId, room, event.getType())) { - // Room has been banned, all events will be dropped return false; } - + // Check and increment recursion depth to prevent infinite loops int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0); if (currentDepth >= MAX_RECURSION_DEPTH) { @@ -152,7 +150,7 @@ public final class WiredEngine { return false; } roomRecursionDepth.put(roomId, currentDepth + 1); - + try { return handleEventInternal(event, room); } finally { @@ -165,7 +163,129 @@ public final class WiredEngine { } } } - + + /** + * Handle a wired event when the source trigger item is already known. + * This is mainly used by timed wired triggers to avoid scanning unrelated stacks. + * + * @param event the event to handle + * @param sourceItemId the trigger item id that originated the event + * @return true if any matching stack was triggered + */ + public boolean handleEventForSourceItem(WiredEvent event, int sourceItemId) { + if (event == null || sourceItemId <= 0) { + return false; + } + + Room room = event.getRoom(); + if (room == null || !room.isLoaded()) { + return false; + } + + int roomId = room.getId(); + + if (isRateLimited(roomId, room, event.getType())) { + return false; + } + + int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0); + if (currentDepth >= MAX_RECURSION_DEPTH) { + 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); + } + } + } + + /** + * Internal event handling optimized for a known source trigger item. + */ + private boolean handleEventForSourceItemInternal(WiredEvent event, Room room, int sourceItemId) { + List stacks = getStacksForSourceItem(room, event.getType(), sourceItemId); + if (stacks.isEmpty()) { + return false; + } + + debug(room, "Processing {} stacks for event type {} from source item {}", stacks.size(), event.getType(), sourceItemId); + + boolean anyTriggered = false; + boolean suppressSaysOutput = false; + long triggerTime = event.getCreatedAtMs(); + + for (WiredStack stack : stacks) { + try { + boolean triggered = processStack(stack, event, triggerTime); + if (triggered) { + anyTriggered = true; + + if ((event.getType() == WiredEvent.Type.USER_SAYS) + && (stack.triggerItem() instanceof WiredTriggerHabboSaysKeyword) + && ((WiredTriggerHabboSaysKeyword) stack.triggerItem()).isHideMessage()) { + suppressSaysOutput = true; + } + } + } catch (WiredLimitException limitEx) { + debug(room, "Stack execution stopped (limit): {}", limitEx.getMessage()); + } catch (Exception ex) { + LOGGER.error("Error processing source wired stack in room {} for item {}: {}", + room.getId(), sourceItemId, ex.getMessage(), ex); + debug(room, "Source stack error: {}", ex.getMessage()); + } + } + + if (event.getType() == WiredEvent.Type.USER_SAYS) { + return suppressSaysOutput; + } + + return anyTriggered; + } + + /** + * Find all stacks for a specific room/event/source item combination. + * Multiple stacks can legally share the same trigger item. + */ + private List getStacksForSourceItem(Room room, WiredEvent.Type eventType, int sourceItemId) { + String cacheKey = room.getId() + ":" + eventType.name() + ":" + sourceItemId; + + List cached = sourceStacksByTriggerKey.get(cacheKey); + if (cached != null) { + return cached; + } + + List allStacks = index.getStacks(room, eventType); + if (allStacks.isEmpty()) { + sourceStacksByTriggerKey.put(cacheKey, Collections.emptyList()); + return Collections.emptyList(); + } + + List matching = new ArrayList<>(); + for (WiredStack stack : allStacks) { + if (stack == null || stack.triggerItem() == null) { + continue; + } + + if (stack.triggerItem().getId() == sourceItemId) { + matching.add(stack); + } + } + + List result = matching.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(matching); + sourceStacksByTriggerKey.put(cacheKey, result); + return result; + } + /** * Internal event handling after recursion check. */ @@ -232,12 +352,12 @@ public final class WiredEngine { // Initial step for trigger state.step(); - - debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})", - event.getType(), - stack.triggerItem() != null ? stack.triggerItem().getId() : "null", - stack.conditions().size(), - stack.effects().size()); + + debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})", + event.getType(), + stack.triggerItem() != null ? stack.triggerItem().getId() : "null", + stack.conditions().size(), + stack.effects().size()); // Run selectors before conditions so targets are available List executedSelectors = Collections.emptyList(); @@ -409,11 +529,11 @@ public final class WiredEngine { */ private void executeEffects(WiredStack stack, WiredContext ctx, long currentTime) { List effects = stack.effects(); - + if (effects.isEmpty()) { return; } - + // Selectors already executed before conditions; only run regular effects here List regulars = new ArrayList<>(); for (IWiredEffect e : effects) { @@ -484,7 +604,7 @@ public final class WiredEngine { ctx.state().step(); try { effect.execute(ctx); - + // Activate box animation after execution if (effect instanceof InteractionWiredEffect) { InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; @@ -599,7 +719,7 @@ public final class WiredEngine { Collections.shuffle(result, Emulator.getRandom()); return new ArrayList<>(result.subList(0, limit)); } - + /** * Schedule a delayed effect execution. */ @@ -610,15 +730,15 @@ public final class WiredEngine { long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger); Room room = ctx.room(); RoomUnit actor = ctx.actor().orElse(null); - + Emulator.getThreading().run(() -> { if (!room.isLoaded() || room.getHabbos().isEmpty()) { return; } - + try { effect.execute(ctx); - + // Activate box animation after execution if (effect instanceof InteractionWiredEffect) { InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; @@ -753,14 +873,14 @@ public final class WiredEngine { * Get the next unseen index for round-robin selection. */ private int getNextUnseenIndex(WiredStack stack, int effectCount) { - String key = stack.triggerItem() != null + String key = stack.triggerItem() != null ? String.valueOf(stack.triggerItem().getId()) : "default"; - + int current = unseenIndices.getOrDefault(key, -1); int next = (current + 1) % effectCount; unseenIndices.put(key, next); - + return next; } @@ -773,7 +893,7 @@ public final class WiredEngine { // This event is checked for cancellation THashSet legacyEffects = new THashSet<>(); THashSet legacyConditions = new THashSet<>(); - + // Extract effects (all effects should now implement both interfaces) for (IWiredEffect eff : stack.effects()) { if (eff instanceof InteractionWiredEffect) { @@ -785,7 +905,7 @@ public final class WiredEngine { legacyConditions.add((InteractionWiredCondition) cond); } } - + WiredStackTriggeredEvent triggeredEvent = new WiredStackTriggeredEvent( event.getRoom(), event.getActor().orElse(null), @@ -793,7 +913,7 @@ public final class WiredEngine { legacyEffects, legacyConditions ); - + return !Emulator.getPluginManager().fireEvent(triggeredEvent).isCancelled(); } return true; @@ -806,7 +926,7 @@ public final class WiredEngine { if (stack.triggerItem() instanceof InteractionWiredTrigger) { THashSet legacyEffects = new THashSet<>(); THashSet legacyConditions = new THashSet<>(); - + for (IWiredEffect eff : stack.effects()) { if (eff instanceof InteractionWiredEffect) { legacyEffects.add((InteractionWiredEffect) eff); @@ -817,7 +937,7 @@ public final class WiredEngine { legacyConditions.add((InteractionWiredCondition) cond); } } - + Emulator.getPluginManager().fireEvent(new WiredStackExecutedEvent( event.getRoom(), event.getActor().orElse(null), @@ -832,10 +952,16 @@ public final class WiredEngine { * Log a debug message if debug mode is enabled. */ private void debug(Room room, String format, Object... args) { - if (WiredManager.isDebugEnabled()) { - String message = String.format(format.replace("{}", "%s"), args); - LOGGER.info("[WiredEngine][Room {}] {}", room.getId(), message); + if (!WiredManager.isDebugEnabled()) { + return; } + + if (!LOGGER.isDebugEnabled()) { + return; + } + + String message = String.format(format.replace("{}", "%s"), args); + LOGGER.debug("[WiredEngine][Room {}] {}", room.getId(), message); } /** @@ -845,10 +971,10 @@ public final class WiredEngine { if (triggerItem == null || room.getRoomSpecialTypes() == null) { return; } - + THashSet extras = room.getRoomSpecialTypes().getExtras( triggerItem.getX(), triggerItem.getY()); - + if (extras != null) { for (InteractionWiredExtra extra : extras) { extra.activateBox(room, roomUnit, millis); @@ -926,7 +1052,7 @@ public final class WiredEngine { public void clearUnseenCache() { unseenIndices.clear(); } - + /** * Clear recursion tracking for a specific room. * Should be called when a room is unloaded. @@ -935,14 +1061,14 @@ public final class WiredEngine { public void clearRoomRecursionDepth(int roomId) { roomRecursionDepth.remove(roomId); } - + /** * Clear all recursion tracking. */ public void clearAllRecursionDepth() { roomRecursionDepth.clear(); } - + /** * Get the current recursion depth for a room (for debugging). * @param roomId the room ID @@ -951,7 +1077,7 @@ public final class WiredEngine { public int getRecursionDepth(int roomId) { return roomRecursionDepth.getOrDefault(roomId, 0); } - + /** * Clear rate limiters for a specific room. * Should be called when a room is unloaded. @@ -961,83 +1087,81 @@ public final class WiredEngine { String prefix = roomId + ":"; eventRateLimiters.keySet().removeIf(key -> key.startsWith(prefix)); } - + + /** + * Clear cached source-stack lookups for a specific room. + * @param roomId the room ID + */ + public void clearRoomSourceStackCache(int roomId) { + String prefix = roomId + ":"; + sourceStacksByTriggerKey.keySet().removeIf(key -> key.startsWith(prefix)); + } + + /** + * Clear all cached source-stack lookups. + */ + public void clearAllSourceStackCache() { + sourceStacksByTriggerKey.clear(); + } + + /** + * Clear all execution-related caches for a specific room. + * @param roomId the room ID + */ + public void clearRoomExecutionCaches(int roomId) { + clearRoomRecursionDepth(roomId); + clearRoomRateLimiters(roomId); + clearRoomSourceStackCache(roomId); + } + + /** + * Clear all execution-related caches. + */ + public void clearAllExecutionCaches() { + clearAllRecursionDepth(); + eventRateLimiters.clear(); + clearAllSourceStackCache(); + clearUnseenCache(); + } + /** * Clear room ban for a specific room. - * Should be called when a room is unloaded. * @param roomId the room ID */ public void clearRoomBan(int roomId) { - bannedRooms.remove(roomId); + // no-op } - + /** * Check if a room is currently banned from wired execution. * @param roomId the room ID * @return true if wired is banned in this room */ private boolean isRoomBanned(int roomId) { - Long banExpiry = bannedRooms.get(roomId); - if (banExpiry == null) { - return false; - } - - if (System.currentTimeMillis() >= banExpiry) { - // Ban expired, remove it - bannedRooms.remove(roomId); - return false; - } - - return true; + return false; } - + /** - * Ban wired execution in a room for WIRED_BAN_DURATION_MS. - * Sends alerts to all users in the room and a scripter alert to staff. + * Ban wired execution in a room. * @param roomId the room ID - * @param room the room object (for sending alerts) + * @param room the room object */ private void banRoom(int roomId, Room room) { - 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 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:

*
    *
  1. Set {@code wired.engine.enabled=true} to run both engines in parallel
  2. @@ -62,7 +62,7 @@ import java.sql.SQLException; *
  3. Set {@code wired.engine.exclusive=true} to disable legacy engine
  4. *
  5. Full migration complete - WiredManager is now the only wired engine
  6. *
- * + * * @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/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..ed4de646 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