🆙 Wired Core updates

Thanks to Migueg
This commit is contained in:
DuckieTM
2026-04-04 16:19:46 +02:00
parent 0916ff1e0b
commit e6f824aedd
5 changed files with 695 additions and 477 deletions
@@ -0,0 +1 @@
INSERT INTO emulator_settings (`key`, `value`) VALUES ('wired.tick.workers', '6');
@@ -39,7 +39,7 @@ import java.util.concurrent.ConcurrentHashMap;
* It receives {@link WiredEvent} objects, finds matching stacks via {@link WiredStackIndex}, * It receives {@link WiredEvent} objects, finds matching stacks via {@link WiredStackIndex},
* evaluates conditions, and executes effects. * evaluates conditions, and executes effects.
* </p> * </p>
* *
* <h3>Execution Flow:</h3> * <h3>Execution Flow:</h3>
* <ol> * <ol>
* <li>Receive event via {@link #handleEvent(WiredEvent)}</li> * <li>Receive event via {@link #handleEvent(WiredEvent)}</li>
@@ -49,14 +49,14 @@ import java.util.concurrent.ConcurrentHashMap;
* <li>Execute effects (respecting random/unseen modifiers)</li> * <li>Execute effects (respecting random/unseen modifiers)</li>
* <li>Handle delays for timed effects</li> * <li>Handle delays for timed effects</li>
* </ol> * </ol>
* *
* <h3>Safety Features:</h3> * <h3>Safety Features:</h3>
* <ul> * <ul>
* <li>Step limits via {@link WiredState} prevent infinite loops</li> * <li>Step limits via {@link WiredState} prevent infinite loops</li>
* <li>Effect cooldowns prevent rapid re-triggering</li> * <li>Effect cooldowns prevent rapid re-triggering</li>
* <li>Exceptions are caught and logged, not propagated</li> * <li>Exceptions are caught and logged, not propagated</li>
* </ul> * </ul>
* *
* @see WiredEvent * @see WiredEvent
* @see WiredContext * @see WiredContext
* @see WiredStackIndex * @see WiredStackIndex
@@ -64,38 +64,41 @@ import java.util.concurrent.ConcurrentHashMap;
public final class WiredEngine { public final class WiredEngine {
private static final Logger LOGGER = LoggerFactory.getLogger(WiredEngine.class); private static final Logger LOGGER = LoggerFactory.getLogger(WiredEngine.class);
/** Maximum recursion depth to prevent infinite loops (e.g., collision + chase) */ /** Maximum recursion depth to prevent infinite loops (e.g., collision + chase) */
public static int MAX_RECURSION_DEPTH = 10; public static int MAX_RECURSION_DEPTH = 10;
/** Maximum events of same type per room within rate limit window before banning */ /** Maximum events of same type per room within rate limit window before banning */
public static int MAX_EVENTS_PER_WINDOW = 100; public static int MAX_EVENTS_PER_WINDOW = 100;
/** Time window for counting rapid events (milliseconds) */ /** Time window for counting rapid events (milliseconds) */
public static long RATE_LIMIT_WINDOW_MS = 10000; public static long RATE_LIMIT_WINDOW_MS = 10000;
/** Duration to ban wired execution in a room after abuse detected (milliseconds) */ /** Duration to ban wired execution in a room after abuse detected (milliseconds) */
public static long WIRED_BAN_DURATION_MS = 600000; public static long WIRED_BAN_DURATION_MS = 600000;
private final WiredServices services; private final WiredServices services;
private final WiredStackIndex index; private final WiredStackIndex index;
private final int maxStepsPerStack; private final int maxStepsPerStack;
/** Track unseen effect indices per room+tile for round-robin selection */ /** Track unseen effect indices per room+tile for round-robin selection */
private final ConcurrentHashMap<String, Integer> unseenIndices; private final ConcurrentHashMap<String, Integer> unseenIndices;
/** Track recursion depth per room to prevent infinite loops */ /** Track recursion depth per room to prevent infinite loops */
private final ConcurrentHashMap<Integer, Integer> roomRecursionDepth; private final ConcurrentHashMap<Integer, Integer> roomRecursionDepth;
/** Track event timestamps per room+eventType for rate limiting: key = "roomId:eventType" */ /** Track event timestamps per room+eventType for rate limiting: key = "roomId:eventType" */
private final ConcurrentHashMap<String, EventRateTracker> eventRateLimiters; private final ConcurrentHashMap<String, EventRateTracker> eventRateLimiters;
/** Track rooms that are banned from wired execution: roomId -> ban expiry timestamp */ /** Track rooms that are banned from wired execution: roomId -> ban expiry timestamp */
private final ConcurrentHashMap<Integer, Long> bannedRooms; private final ConcurrentHashMap<Integer, Long> bannedRooms;
/** Cache room+eventType+sourceItemId -> matching stacks for source-triggered timer events */
private final ConcurrentHashMap<String, List<WiredStack>> sourceStacksByTriggerKey;
/** /**
* Create a new wired engine. * Create a new wired engine.
* *
* @param services the services for performing side effects * @param services the services for performing side effects
* @param index the stack index for finding matching stacks * @param index the stack index for finding matching stacks
* @param maxStepsPerStack maximum steps per stack execution (loop protection) * @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 (services == null) throw new IllegalArgumentException("Services cannot be null");
if (index == null) throw new IllegalArgumentException("Index cannot be null"); if (index == null) throw new IllegalArgumentException("Index cannot be null");
if (maxStepsPerStack <= 0) throw new IllegalArgumentException("Max steps must be positive"); if (maxStepsPerStack <= 0) throw new IllegalArgumentException("Max steps must be positive");
this.services = services; this.services = services;
this.index = index; this.index = index;
this.maxStepsPerStack = maxStepsPerStack; this.maxStepsPerStack = maxStepsPerStack;
@@ -112,11 +115,12 @@ public final class WiredEngine {
this.roomRecursionDepth = new ConcurrentHashMap<>(); this.roomRecursionDepth = new ConcurrentHashMap<>();
this.eventRateLimiters = new ConcurrentHashMap<>(); this.eventRateLimiters = new ConcurrentHashMap<>();
this.bannedRooms = new ConcurrentHashMap<>(); this.bannedRooms = new ConcurrentHashMap<>();
this.sourceStacksByTriggerKey = new ConcurrentHashMap<>();
} }
/** /**
* Handle a wired event by finding and executing matching stacks. * Handle a wired event by finding and executing matching stacks.
* *
* @param event the event to handle * @param event the event to handle
* @return true if any stack was triggered (useful for SAY_SOMETHING to suppress message) * @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()) { if (room == null || !room.isLoaded()) {
return false; return false;
} }
int roomId = room.getId(); int roomId = room.getId();
// Check if room is banned from wired execution // Soft rate limiting to prevent rapid-fire event spam without banning whole rooms
if (isRoomBanned(roomId)) {
return false;
}
// Check rate limiting to prevent rapid-fire event spam (e.g., collision + chase loop)
if (isRateLimited(roomId, room, event.getType())) { if (isRateLimited(roomId, room, event.getType())) {
// Room has been banned, all events will be dropped
return false; return false;
} }
// Check and increment recursion depth to prevent infinite loops // Check and increment recursion depth to prevent infinite loops
int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0); int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0);
if (currentDepth >= MAX_RECURSION_DEPTH) { if (currentDepth >= MAX_RECURSION_DEPTH) {
@@ -152,7 +150,7 @@ public final class WiredEngine {
return false; return false;
} }
roomRecursionDepth.put(roomId, currentDepth + 1); roomRecursionDepth.put(roomId, currentDepth + 1);
try { try {
return handleEventInternal(event, room); return handleEventInternal(event, room);
} finally { } 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<WiredStack> 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<WiredStack> getStacksForSourceItem(Room room, WiredEvent.Type eventType, int sourceItemId) {
String cacheKey = room.getId() + ":" + eventType.name() + ":" + sourceItemId;
List<WiredStack> cached = sourceStacksByTriggerKey.get(cacheKey);
if (cached != null) {
return cached;
}
List<WiredStack> allStacks = index.getStacks(room, eventType);
if (allStacks.isEmpty()) {
sourceStacksByTriggerKey.put(cacheKey, Collections.emptyList());
return Collections.emptyList();
}
List<WiredStack> matching = new ArrayList<>();
for (WiredStack stack : allStacks) {
if (stack == null || stack.triggerItem() == null) {
continue;
}
if (stack.triggerItem().getId() == sourceItemId) {
matching.add(stack);
}
}
List<WiredStack> result = matching.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(matching);
sourceStacksByTriggerKey.put(cacheKey, result);
return result;
}
/** /**
* Internal event handling after recursion check. * Internal event handling after recursion check.
*/ */
@@ -232,12 +352,12 @@ public final class WiredEngine {
// Initial step for trigger // Initial step for trigger
state.step(); state.step();
debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})", debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})",
event.getType(), event.getType(),
stack.triggerItem() != null ? stack.triggerItem().getId() : "null", stack.triggerItem() != null ? stack.triggerItem().getId() : "null",
stack.conditions().size(), stack.conditions().size(),
stack.effects().size()); stack.effects().size());
// Run selectors before conditions so targets are available // Run selectors before conditions so targets are available
List<InteractionWiredEffect> executedSelectors = Collections.emptyList(); List<InteractionWiredEffect> executedSelectors = Collections.emptyList();
@@ -409,11 +529,11 @@ public final class WiredEngine {
*/ */
private void executeEffects(WiredStack stack, WiredContext ctx, long currentTime) { private void executeEffects(WiredStack stack, WiredContext ctx, long currentTime) {
List<IWiredEffect> effects = stack.effects(); List<IWiredEffect> effects = stack.effects();
if (effects.isEmpty()) { if (effects.isEmpty()) {
return; return;
} }
// Selectors already executed before conditions; only run regular effects here // Selectors already executed before conditions; only run regular effects here
List<IWiredEffect> regulars = new ArrayList<>(); List<IWiredEffect> regulars = new ArrayList<>();
for (IWiredEffect e : effects) { for (IWiredEffect e : effects) {
@@ -484,7 +604,7 @@ public final class WiredEngine {
ctx.state().step(); ctx.state().step();
try { try {
effect.execute(ctx); effect.execute(ctx);
// Activate box animation after execution // Activate box animation after execution
if (effect instanceof InteractionWiredEffect) { if (effect instanceof InteractionWiredEffect) {
InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect;
@@ -599,7 +719,7 @@ public final class WiredEngine {
Collections.shuffle(result, Emulator.getRandom()); Collections.shuffle(result, Emulator.getRandom());
return new ArrayList<>(result.subList(0, limit)); return new ArrayList<>(result.subList(0, limit));
} }
/** /**
* Schedule a delayed effect execution. * Schedule a delayed effect execution.
*/ */
@@ -610,15 +730,15 @@ public final class WiredEngine {
long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger); long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger);
Room room = ctx.room(); Room room = ctx.room();
RoomUnit actor = ctx.actor().orElse(null); RoomUnit actor = ctx.actor().orElse(null);
Emulator.getThreading().run(() -> { Emulator.getThreading().run(() -> {
if (!room.isLoaded() || room.getHabbos().isEmpty()) { if (!room.isLoaded() || room.getHabbos().isEmpty()) {
return; return;
} }
try { try {
effect.execute(ctx); effect.execute(ctx);
// Activate box animation after execution // Activate box animation after execution
if (effect instanceof InteractionWiredEffect) { if (effect instanceof InteractionWiredEffect) {
InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect;
@@ -753,14 +873,14 @@ public final class WiredEngine {
* Get the next unseen index for round-robin selection. * Get the next unseen index for round-robin selection.
*/ */
private int getNextUnseenIndex(WiredStack stack, int effectCount) { private int getNextUnseenIndex(WiredStack stack, int effectCount) {
String key = stack.triggerItem() != null String key = stack.triggerItem() != null
? String.valueOf(stack.triggerItem().getId()) ? String.valueOf(stack.triggerItem().getId())
: "default"; : "default";
int current = unseenIndices.getOrDefault(key, -1); int current = unseenIndices.getOrDefault(key, -1);
int next = (current + 1) % effectCount; int next = (current + 1) % effectCount;
unseenIndices.put(key, next); unseenIndices.put(key, next);
return next; return next;
} }
@@ -773,7 +893,7 @@ public final class WiredEngine {
// This event is checked for cancellation // This event is checked for cancellation
THashSet<InteractionWiredEffect> legacyEffects = new THashSet<>(); THashSet<InteractionWiredEffect> legacyEffects = new THashSet<>();
THashSet<InteractionWiredCondition> legacyConditions = new THashSet<>(); THashSet<InteractionWiredCondition> legacyConditions = new THashSet<>();
// Extract effects (all effects should now implement both interfaces) // Extract effects (all effects should now implement both interfaces)
for (IWiredEffect eff : stack.effects()) { for (IWiredEffect eff : stack.effects()) {
if (eff instanceof InteractionWiredEffect) { if (eff instanceof InteractionWiredEffect) {
@@ -785,7 +905,7 @@ public final class WiredEngine {
legacyConditions.add((InteractionWiredCondition) cond); legacyConditions.add((InteractionWiredCondition) cond);
} }
} }
WiredStackTriggeredEvent triggeredEvent = new WiredStackTriggeredEvent( WiredStackTriggeredEvent triggeredEvent = new WiredStackTriggeredEvent(
event.getRoom(), event.getRoom(),
event.getActor().orElse(null), event.getActor().orElse(null),
@@ -793,7 +913,7 @@ public final class WiredEngine {
legacyEffects, legacyEffects,
legacyConditions legacyConditions
); );
return !Emulator.getPluginManager().fireEvent(triggeredEvent).isCancelled(); return !Emulator.getPluginManager().fireEvent(triggeredEvent).isCancelled();
} }
return true; return true;
@@ -806,7 +926,7 @@ public final class WiredEngine {
if (stack.triggerItem() instanceof InteractionWiredTrigger) { if (stack.triggerItem() instanceof InteractionWiredTrigger) {
THashSet<InteractionWiredEffect> legacyEffects = new THashSet<>(); THashSet<InteractionWiredEffect> legacyEffects = new THashSet<>();
THashSet<InteractionWiredCondition> legacyConditions = new THashSet<>(); THashSet<InteractionWiredCondition> legacyConditions = new THashSet<>();
for (IWiredEffect eff : stack.effects()) { for (IWiredEffect eff : stack.effects()) {
if (eff instanceof InteractionWiredEffect) { if (eff instanceof InteractionWiredEffect) {
legacyEffects.add((InteractionWiredEffect) eff); legacyEffects.add((InteractionWiredEffect) eff);
@@ -817,7 +937,7 @@ public final class WiredEngine {
legacyConditions.add((InteractionWiredCondition) cond); legacyConditions.add((InteractionWiredCondition) cond);
} }
} }
Emulator.getPluginManager().fireEvent(new WiredStackExecutedEvent( Emulator.getPluginManager().fireEvent(new WiredStackExecutedEvent(
event.getRoom(), event.getRoom(),
event.getActor().orElse(null), event.getActor().orElse(null),
@@ -832,10 +952,16 @@ public final class WiredEngine {
* Log a debug message if debug mode is enabled. * Log a debug message if debug mode is enabled.
*/ */
private void debug(Room room, String format, Object... args) { private void debug(Room room, String format, Object... args) {
if (WiredManager.isDebugEnabled()) { if (!WiredManager.isDebugEnabled()) {
String message = String.format(format.replace("{}", "%s"), args); return;
LOGGER.info("[WiredEngine][Room {}] {}", room.getId(), message);
} }
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) { if (triggerItem == null || room.getRoomSpecialTypes() == null) {
return; return;
} }
THashSet<InteractionWiredExtra> extras = room.getRoomSpecialTypes().getExtras( THashSet<InteractionWiredExtra> extras = room.getRoomSpecialTypes().getExtras(
triggerItem.getX(), triggerItem.getY()); triggerItem.getX(), triggerItem.getY());
if (extras != null) { if (extras != null) {
for (InteractionWiredExtra extra : extras) { for (InteractionWiredExtra extra : extras) {
extra.activateBox(room, roomUnit, millis); extra.activateBox(room, roomUnit, millis);
@@ -926,7 +1052,7 @@ public final class WiredEngine {
public void clearUnseenCache() { public void clearUnseenCache() {
unseenIndices.clear(); unseenIndices.clear();
} }
/** /**
* Clear recursion tracking for a specific room. * Clear recursion tracking for a specific room.
* Should be called when a room is unloaded. * Should be called when a room is unloaded.
@@ -935,14 +1061,14 @@ public final class WiredEngine {
public void clearRoomRecursionDepth(int roomId) { public void clearRoomRecursionDepth(int roomId) {
roomRecursionDepth.remove(roomId); roomRecursionDepth.remove(roomId);
} }
/** /**
* Clear all recursion tracking. * Clear all recursion tracking.
*/ */
public void clearAllRecursionDepth() { public void clearAllRecursionDepth() {
roomRecursionDepth.clear(); roomRecursionDepth.clear();
} }
/** /**
* Get the current recursion depth for a room (for debugging). * Get the current recursion depth for a room (for debugging).
* @param roomId the room ID * @param roomId the room ID
@@ -951,7 +1077,7 @@ public final class WiredEngine {
public int getRecursionDepth(int roomId) { public int getRecursionDepth(int roomId) {
return roomRecursionDepth.getOrDefault(roomId, 0); return roomRecursionDepth.getOrDefault(roomId, 0);
} }
/** /**
* Clear rate limiters for a specific room. * Clear rate limiters for a specific room.
* Should be called when a room is unloaded. * Should be called when a room is unloaded.
@@ -961,83 +1087,81 @@ public final class WiredEngine {
String prefix = roomId + ":"; String prefix = roomId + ":";
eventRateLimiters.keySet().removeIf(key -> key.startsWith(prefix)); 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. * Clear room ban for a specific room.
* Should be called when a room is unloaded.
* @param roomId the room ID * @param roomId the room ID
*/ */
public void clearRoomBan(int roomId) { public void clearRoomBan(int roomId) {
bannedRooms.remove(roomId); // no-op
} }
/** /**
* Check if a room is currently banned from wired execution. * Check if a room is currently banned from wired execution.
* @param roomId the room ID * @param roomId the room ID
* @return true if wired is banned in this room * @return true if wired is banned in this room
*/ */
private boolean isRoomBanned(int roomId) { private boolean isRoomBanned(int roomId) {
Long banExpiry = bannedRooms.get(roomId); return false;
if (banExpiry == null) {
return false;
}
if (System.currentTimeMillis() >= banExpiry) {
// Ban expired, remove it
bannedRooms.remove(roomId);
return false;
}
return true;
} }
/** /**
* Ban wired execution in a room for WIRED_BAN_DURATION_MS. * Ban wired execution in a room.
* Sends alerts to all users in the room and a scripter alert to staff.
* @param roomId the room ID * @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) { private void banRoom(int roomId, Room room) {
long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS; // no-op
bannedRooms.put(roomId, banExpiry);
long banMinutes = WIRED_BAN_DURATION_MS / 60000;
// Send alert to all users in the room
String roomAlertMessage = Emulator.getTexts().getValue("wired.abuse.room.alert")
.replace("%minutes%", String.valueOf(banMinutes));
room.sendComposer(new GenericAlertComposer(roomAlertMessage).compose());
// Send scripter bubble alert to staff with room link
THashMap<String, String> keys = new THashMap<>();
keys.put("title", Emulator.getTexts().getValue("wired.abuse.staff.title"));
keys.put("message", Emulator.getTexts().getValue("wired.abuse.staff.message")
.replace("%roomname%", room.getName())
.replace("%owner%", room.getOwnerName())
.replace("%minutes%", String.valueOf(banMinutes)));
keys.put("linkUrl", "event:navigator/goto/" + roomId);
keys.put("linkTitle", Emulator.getTexts().getValue("wired.abuse.staff.link"));
Emulator.getGameEnvironment().getHabboManager().sendPacketToHabbosWithPermission(
new BubbleAlertComposer("admin.staffalert", keys).compose(),
"acc_modtool_room_info"
);
LOGGER.warn("Wired abuse detected in room {} ({}). Owner: {}. Wired banned for {} minutes.",
roomId, room.getName(), room.getOwnerName(), banMinutes);
} }
/** /**
* Check if an event should be rate-limited. * 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 roomId the room ID
* @param room the room object (for sending alerts if banned) * @param room the room object
* @param eventType the event type * @param eventType the event type
* @return true if the event should be blocked due to rate limiting * @return true if the event should be blocked due to rate limiting
*/ */
private boolean isRateLimited(int roomId, Room room, WiredEvent.Type eventType) { private boolean isRateLimited(int roomId, Room room, WiredEvent.Type eventType) {
String key = roomId + ":" + eventType.name(); String key = roomId + ":" + eventType.name();
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
EventRateTracker tracker = eventRateLimiters.compute(key, (k, existing) -> { EventRateTracker tracker = eventRateLimiters.compute(key, (k, existing) -> {
if (existing == null) { if (existing == null) {
return new EventRateTracker(now); return new EventRateTracker(now);
@@ -1045,51 +1169,46 @@ public final class WiredEngine {
existing.recordEvent(now); existing.recordEvent(now);
return existing; return existing;
}); });
boolean limited = tracker.isRateLimited(now); boolean limited = tracker.isRateLimited(now);
if (limited && tracker.shouldBan(now)) { if (limited && tracker.shouldBan(now)) {
// First time hitting limit in this suppression window - ban the room LOGGER.warn("Soft wired rate limit in room {} for event {}. Count in current window exceeded.",
banRoom(roomId, room); roomId, eventType);
} }
return limited; return limited;
} }
/** /**
* Tracks event rate for a specific room + event type combination. * Tracks event rate for a specific room + event type combination.
*/ */
private static final class EventRateTracker { private static final class EventRateTracker {
private long windowStart; private long windowStart;
private int eventCount; private int eventCount;
private boolean banned; private boolean warned;
EventRateTracker(long now) { EventRateTracker(long now) {
this.windowStart = now; this.windowStart = now;
this.eventCount = 1; this.eventCount = 1;
this.banned = false; this.warned = false;
} }
synchronized void recordEvent(long now) { synchronized void recordEvent(long now) {
// Reset window if expired
if (now - windowStart > RATE_LIMIT_WINDOW_MS) { if (now - windowStart > RATE_LIMIT_WINDOW_MS) {
windowStart = now; windowStart = now;
eventCount = 1; eventCount = 1;
// Don't reset banned here - room ban is checked separately warned = false;
} else { } else {
eventCount++; eventCount++;
} }
} }
synchronized boolean isRateLimited(long now) { synchronized boolean isRateLimited(long now) {
return eventCount > MAX_EVENTS_PER_WINDOW; 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) { synchronized boolean shouldBan(long now) {
if (eventCount > MAX_EVENTS_PER_WINDOW && !banned) { if (eventCount > MAX_EVENTS_PER_WINDOW && !warned) {
banned = true; warned = true;
return true; return true;
} }
return false; return false;
@@ -46,7 +46,7 @@ import java.sql.SQLException;
* wired engine. It provides static methods for triggering events and manages * wired engine. It provides static methods for triggering events and manages
* the lifecycle of the engine. * the lifecycle of the engine.
* </p> * </p>
* *
* <h3>Configuration Options:</h3> * <h3>Configuration Options:</h3>
* <ul> * <ul>
* <li>{@code wired.engine.enabled} - Enable new engine (parallel mode)</li> * <li>{@code wired.engine.enabled} - Enable new engine (parallel mode)</li>
@@ -54,7 +54,7 @@ import java.sql.SQLException;
* <li>{@code wired.engine.maxStepsPerStack} - Loop protection limit</li> * <li>{@code wired.engine.maxStepsPerStack} - Loop protection limit</li>
* <li>{@code wired.engine.debug} - Verbose logging</li> * <li>{@code wired.engine.debug} - Verbose logging</li>
* </ul> * </ul>
* *
* <h3>Migration Strategy:</h3> * <h3>Migration Strategy:</h3>
* <ol> * <ol>
* <li>Set {@code wired.engine.enabled=true} to run both engines in parallel</li> * <li>Set {@code wired.engine.enabled=true} to run both engines in parallel</li>
@@ -62,7 +62,7 @@ import java.sql.SQLException;
* <li>Set {@code wired.engine.exclusive=true} to disable legacy engine</li> * <li>Set {@code wired.engine.exclusive=true} to disable legacy engine</li>
* <li>Full migration complete - WiredManager is now the only wired engine</li> * <li>Full migration complete - WiredManager is now the only wired engine</li>
* </ol> * </ol>
* *
* @see WiredEngine * @see WiredEngine
* @see WiredEvents * @see WiredEvents
*/ */
@@ -86,10 +86,10 @@ public final class WiredManager {
/** The singleton engine instance */ /** The singleton engine instance */
private static volatile WiredEngine engine; private static volatile WiredEngine engine;
/** The stack index */ /** The stack index */
private static volatile RoomWiredStackIndex stackIndex; private static volatile RoomWiredStackIndex stackIndex;
/** Whether the engine is initialized */ /** Whether the engine is initialized */
private static volatile boolean initialized = false; private static volatile boolean initialized = false;
private WiredManager() { private WiredManager() {
@@ -119,7 +119,7 @@ public final class WiredManager {
boolean enabled = Emulator.getConfig().getBoolean(CONFIG_ENABLED, DEFAULT_ENABLED); boolean enabled = Emulator.getConfig().getBoolean(CONFIG_ENABLED, DEFAULT_ENABLED);
int maxSteps = Emulator.getConfig().getInt(CONFIG_MAX_STEPS, DEFAULT_MAX_STEPS); int maxSteps = Emulator.getConfig().getInt(CONFIG_MAX_STEPS, DEFAULT_MAX_STEPS);
boolean debug = Emulator.getConfig().getBoolean(CONFIG_DEBUG, false); boolean debug = Emulator.getConfig().getBoolean(CONFIG_DEBUG, false);
// Load additional configuration // Load additional configuration
MAXIMUM_FURNI_SELECTION = Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5); MAXIMUM_FURNI_SELECTION = Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5);
TELEPORT_DELAY = Emulator.getConfig().getInt("wired.effect.teleport.delay", 500); TELEPORT_DELAY = Emulator.getConfig().getInt("wired.effect.teleport.delay", 500);
@@ -133,13 +133,13 @@ public final class WiredManager {
stackIndex = new RoomWiredStackIndex(); stackIndex = new RoomWiredStackIndex();
WiredServices services = DefaultWiredServices.getInstance(); WiredServices services = DefaultWiredServices.getInstance();
engine = new WiredEngine(services, stackIndex, maxSteps); engine = new WiredEngine(services, stackIndex, maxSteps);
// Start the centralized tick service (50ms interval) // Start the centralized tick service (50ms interval)
WiredTickService.getInstance().start(); WiredTickService.getInstance().start();
initialized = true; initialized = true;
LOGGER.info("Wired Manager initialized - enabled: {}, maxSteps: {}, debug: {}", LOGGER.info("Wired Manager initialized - enabled: {}, maxSteps: {}, debug: {}",
enabled, maxSteps, debug); enabled, maxSteps, debug);
} }
@@ -153,16 +153,16 @@ public final class WiredManager {
} }
LOGGER.info("Shutting down Wired Manager..."); LOGGER.info("Shutting down Wired Manager...");
// Stop the tick service first // Stop the tick service first
WiredTickService.getInstance().stop(); WiredTickService.getInstance().stop();
if (stackIndex != null) { if (stackIndex != null) {
stackIndex.clearAll(); stackIndex.clearAll();
} }
if (engine != null) { if (engine != null) {
engine.clearUnseenCache(); engine.clearAllExecutionCaches();
} }
initialized = false; initialized = false;
@@ -212,10 +212,22 @@ public final class WiredManager {
if (!isEnabled() || engine == null) { if (!isEnabled() || engine == null) {
return false; return false;
} }
return engine.handleEvent(event); 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. * Trigger when a user walks onto furniture.
*/ */
@@ -223,7 +235,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null || item == null) { if (!isEnabled() || room == null || user == null || item == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.userWalksOn(room, user, item); WiredEvent event = WiredEvents.userWalksOn(room, user, item);
return handleEvent(event); return handleEvent(event);
} }
@@ -235,7 +247,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null || item == null) { if (!isEnabled() || room == null || user == null || item == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.userWalksOff(room, user, item); WiredEvent event = WiredEvents.userWalksOff(room, user, item);
return handleEvent(event); return handleEvent(event);
} }
@@ -311,7 +323,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) { if (!isEnabled() || room == null || user == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.userSays(room, user, message); WiredEvent event = WiredEvents.userSays(room, user, message);
return handleEvent(event); return handleEvent(event);
} }
@@ -332,7 +344,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) { if (!isEnabled() || room == null || user == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.userEntersRoom(room, user); WiredEvent event = WiredEvents.userEntersRoom(room, user);
return handleEvent(event); return handleEvent(event);
} }
@@ -356,7 +368,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || item == null) { if (!isEnabled() || room == null || item == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.furniStateChanged(room, user, item); WiredEvent event = WiredEvents.furniStateChanged(room, user, item);
return handleEvent(event); return handleEvent(event);
} }
@@ -365,24 +377,24 @@ public final class WiredManager {
* Trigger a timer tick. * Trigger a timer tick.
*/ */
public static boolean triggerTimerTick(Room room, HabboItem timerItem) { public static boolean triggerTimerTick(Room room, HabboItem timerItem) {
if (!isEnabled() || room == null) { if (!isEnabled() || room == null || timerItem == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.timerTick(room, timerItem); WiredEvent event = WiredEvents.timerTick(room, timerItem);
return handleEvent(event); return handleEventForSourceItem(event, timerItem);
} }
/** /**
* Trigger a periodic timer. * Trigger a periodic timer.
*/ */
public static boolean triggerTimerRepeat(Room room, HabboItem timerItem) { public static boolean triggerTimerRepeat(Room room, HabboItem timerItem) {
if (!isEnabled() || room == null) { if (!isEnabled() || room == null || timerItem == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.timerRepeat(room, timerItem); WiredEvent event = WiredEvents.timerRepeat(room, timerItem);
return handleEvent(event); return handleEventForSourceItem(event, timerItem);
} }
public static boolean triggerClockCounter(Room room, HabboItem counterItem) { public static boolean triggerClockCounter(Room room, HabboItem counterItem) {
@@ -391,31 +403,31 @@ public final class WiredManager {
} }
WiredEvent event = WiredEvents.clockCounter(room, counterItem); WiredEvent event = WiredEvents.clockCounter(room, counterItem);
return handleEvent(event); return handleEventForSourceItem(event, counterItem);
} }
/** /**
* Trigger a long periodic timer. * Trigger a long periodic timer.
*/ */
public static boolean triggerTimerRepeatLong(Room room, HabboItem timerItem) { public static boolean triggerTimerRepeatLong(Room room, HabboItem timerItem) {
if (!isEnabled() || room == null) { if (!isEnabled() || room == null || timerItem == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.timerRepeatLong(room, timerItem); WiredEvent event = WiredEvents.timerRepeatLong(room, timerItem);
return handleEvent(event); return handleEventForSourceItem(event, timerItem);
} }
/** /**
* Trigger a short periodic timer. * Trigger a short periodic timer.
*/ */
public static boolean triggerTimerRepeatShort(Room room, HabboItem timerItem) { public static boolean triggerTimerRepeatShort(Room room, HabboItem timerItem) {
if (!isEnabled() || room == null) { if (!isEnabled() || room == null || timerItem == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.timerRepeatShort(room, timerItem); 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) { if (!isEnabled() || room == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.gameStarts(room); WiredEvent event = WiredEvents.gameStarts(room);
return handleEvent(event); return handleEvent(event);
} }
@@ -437,7 +449,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) { if (!isEnabled() || room == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.gameEnds(room); WiredEvent event = WiredEvents.gameEnds(room);
return handleEvent(event); return handleEvent(event);
} }
@@ -449,7 +461,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || botUnit == null) { if (!isEnabled() || room == null || botUnit == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.botCollision(room, botUnit); WiredEvent event = WiredEvents.botCollision(room, botUnit);
return handleEvent(event); return handleEvent(event);
} }
@@ -461,7 +473,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || botUnit == null) { if (!isEnabled() || room == null || botUnit == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.botReachedFurni(room, botUnit, item); WiredEvent event = WiredEvents.botReachedFurni(room, botUnit, item);
return handleEvent(event); return handleEvent(event);
} }
@@ -473,7 +485,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || botUnit == null) { if (!isEnabled() || room == null || botUnit == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.botReachedHabbo(room, botUnit, targetUser); WiredEvent event = WiredEvents.botReachedHabbo(room, botUnit, targetUser);
return handleEvent(event); return handleEvent(event);
} }
@@ -489,7 +501,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) { if (!isEnabled() || room == null || user == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.scoreAchieved(room, user, score, scoreAdded); WiredEvent event = WiredEvents.scoreAchieved(room, user, score, scoreAdded);
return handleEvent(event); return handleEvent(event);
} }
@@ -501,7 +513,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) { if (!isEnabled() || room == null || user == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.userIdles(room, user); WiredEvent event = WiredEvents.userIdles(room, user);
return handleEvent(event); return handleEvent(event);
} }
@@ -513,7 +525,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) { if (!isEnabled() || room == null || user == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.userUnidles(room, user); WiredEvent event = WiredEvents.userUnidles(room, user);
return handleEvent(event); return handleEvent(event);
} }
@@ -525,7 +537,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) { if (!isEnabled() || room == null || user == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.userStartsDancing(room, user); WiredEvent event = WiredEvents.userStartsDancing(room, user);
return handleEvent(event); return handleEvent(event);
} }
@@ -537,7 +549,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) { if (!isEnabled() || room == null || user == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.userStopsDancing(room, user); WiredEvent event = WiredEvents.userStopsDancing(room, user);
return handleEvent(event); return handleEvent(event);
} }
@@ -549,7 +561,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) { if (!isEnabled() || room == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.teamWins(room, user); WiredEvent event = WiredEvents.teamWins(room, user);
return handleEvent(event); return handleEvent(event);
} }
@@ -561,7 +573,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) { if (!isEnabled() || room == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.teamLoses(room, user); WiredEvent event = WiredEvents.teamLoses(room, user);
return handleEvent(event); return handleEvent(event);
} }
@@ -574,7 +586,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) { if (!isEnabled() || room == null) {
return false; return false;
} }
WiredEvent event = WiredEvents.fromLegacy(triggerType, room, roomUnit, stuff); WiredEvent event = WiredEvents.fromLegacy(triggerType, room, roomUnit, stuff);
return handleEvent(event); return handleEvent(event);
} }
@@ -586,11 +598,20 @@ public final class WiredManager {
* Call this when wired items are added/removed/moved. * Call this when wired items are added/removed/moved.
*/ */
public static void invalidateRoom(Room room) { public static void invalidateRoom(Room room) {
if (stackIndex != null && room != null) { if (room == null) {
return;
}
if (stackIndex != null) {
stackIndex.invalidateAll(room); 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) { if (stackIndex != null && room != null && tile != null) {
stackIndex.invalidate(room, tile); stackIndex.invalidate(room, tile);
} }
if (engine != null && room != null) {
engine.clearRoomSourceStackCache(room.getId());
}
} }
/** /**
* Rebuild the wired index for a room. * Rebuild the wired index for a room.
*/ */
public static void rebuildRoom(Room 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); 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 */ /** Maximum number of furniture items that can be selected in a single wired component */
public static int MAXIMUM_FURNI_SELECTION = 5; public static int MAXIMUM_FURNI_SELECTION = 5;
/** Delay in milliseconds between teleport executions */ /** Delay in milliseconds between teleport executions */
public static int TELEPORT_DELAY = 500; public static int TELEPORT_DELAY = 500;
// ========== Debug Mode ========== // ========== Debug Mode ==========
/** Debug mode - when enabled, logs detailed wired execution flow */ /** Debug mode - when enabled, logs detailed wired execution flow */
private static boolean debugEnabled = false; private static boolean debugEnabled = false;
/** /**
* Enables or disables wired debug mode. * Enables or disables wired debug mode.
* When enabled, detailed execution logs are written to help troubleshoot wired stacks. * When enabled, detailed execution logs are written to help troubleshoot wired stacks.
* *
* @param enabled true to enable debug logging, false to disable * @param enabled true to enable debug logging, false to disable
*/ */
public static void setDebugEnabled(boolean enabled) { public static void setDebugEnabled(boolean enabled) {
@@ -637,19 +670,19 @@ public final class WiredManager {
LOGGER.info("Wired debug mode ENABLED"); LOGGER.info("Wired debug mode ENABLED");
} }
} }
/** /**
* Checks if wired debug mode is enabled. * Checks if wired debug mode is enabled.
* *
* @return true if debug mode is active * @return true if debug mode is active
*/ */
public static boolean isDebugEnabled() { public static boolean isDebugEnabled() {
return debugEnabled; return debugEnabled;
} }
/** /**
* Logs a debug message if debug mode is enabled. * Logs a debug message if debug mode is enabled.
* *
* @param message the message to log * @param message the message to log
* @param args optional format arguments * @param args optional format arguments
*/ */
@@ -660,7 +693,7 @@ public final class WiredManager {
} }
// ========== JSON Utilities ========== // ========== JSON Utilities ==========
private static GsonBuilder gsonBuilder = null; private static GsonBuilder gsonBuilder = null;
private static Gson cachedGson = null; private static Gson cachedGson = null;
@@ -670,12 +703,12 @@ public final class WiredManager {
} }
return gsonBuilder; return gsonBuilder;
} }
/** /**
* Gets a cached Gson instance. This is more efficient than calling * Gets a cached Gson instance. This is more efficient than calling
* getGsonBuilder().create() multiple times, as Gson instances are thread-safe * getGsonBuilder().create() multiple times, as Gson instances are thread-safe
* and can be reused. * and can be reused.
* *
* @return a cached Gson instance * @return a cached Gson instance
*/ */
public static Gson getGson() { public static Gson getGson() {
@@ -686,50 +719,53 @@ public final class WiredManager {
} }
// ========== Tick Service Integration ========== // ========== Tick Service Integration ==========
/** /**
* Registers a tickable wired item with the centralized tick service. * Registers a tickable wired item with the centralized tick service.
* <p> * <p>
* Call this when a time-based wired trigger is placed in a room or when * Call this when a time-based wired trigger is placed in a room or when
* a room is loaded. * a room is loaded.
* </p> * </p>
* *
* @param room the room the item is in * @param room the room the item is in
* @param tickable the tickable item (e.g., WiredTriggerRepeater) * @param tickable the tickable item (e.g., WiredTriggerRepeater)
*/ */
public static void registerTickable(Room room, WiredTickable tickable) { public static void registerTickable(Room room, WiredTickable tickable) {
WiredTickService.getInstance().register(room, tickable); WiredTickService.getInstance().register(room, tickable);
} }
/** /**
* Unregisters a tickable wired item from the tick service. * Unregisters a tickable wired item from the tick service.
* <p> * <p>
* Call this when a time-based wired trigger is picked up or when * Call this when a time-based wired trigger is picked up or when
* a room is unloaded. * a room is unloaded.
* </p> * </p>
* *
* @param room the room the item was in * @param room the room the item was in
* @param tickable the tickable item * @param tickable the tickable item
*/ */
public static void unregisterTickable(Room room, WiredTickable tickable) { public static void unregisterTickable(Room room, WiredTickable tickable) {
WiredTickService.getInstance().unregister(room, tickable); WiredTickService.getInstance().unregister(room, tickable);
} }
/** /**
* Unregisters all tickables for a room. * Unregisters all tickables for a room.
* <p> * <p>
* Call this when a room is unloaded to clean up all tick registrations. * Call this when a room is unloaded to clean up all tick registrations.
* </p> * </p>
* *
* @param room the room * @param room the room
*/ */
public static void unregisterRoomTickables(Room room) { public static void unregisterRoomTickables(Room room) {
WiredTickService.getInstance().unregisterRoom(room); WiredTickService.getInstance().unregisterRoom(room);
if (engine != null && room != null) {
engine.clearRoomExecutionCaches(room.getId());
}
} }
/** /**
* Gets the tick service instance. * Gets the tick service instance.
* *
* @return the WiredTickService * @return the WiredTickService
*/ */
public static WiredTickService getTickService() { public static WiredTickService getTickService() {
@@ -771,7 +807,7 @@ public final class WiredManager {
* <p> * <p>
* This uses the new tick service for managing timer resets. * This uses the new tick service for managing timer resets.
* </p> * </p>
* *
* @param room the room * @param room the room
*/ */
public static void resetTimers(Room room) { public static void resetTimers(Room room) {
@@ -804,9 +840,9 @@ public final class WiredManager {
if (item instanceof InteractionWiredEffect && !(item instanceof WiredEffectTriggerStacks)) { if (item instanceof InteractionWiredEffect && !(item instanceof WiredEffectTriggerStacks)) {
InteractionWiredEffect effect = (InteractionWiredEffect) item; InteractionWiredEffect effect = (InteractionWiredEffect) item;
WiredEvent event = WiredEvent.builder(WiredEvent.Type.CUSTOM, room) WiredEvent event = WiredEvent.builder(WiredEvent.Type.CUSTOM, room)
.actor(roomUnit) .actor(roomUnit)
.callStackDepth(callStackDepth) .callStackDepth(callStackDepth)
.build(); .build();
WiredContext ctx = new WiredContext(event, effect, DefaultWiredServices.getInstance(), new WiredState(100)); WiredContext ctx = new WiredContext(event, effect, DefaultWiredServices.getInstance(), new WiredState(100));
effect.execute(ctx); effect.execute(ctx);
effect.setCooldown(millis); effect.setCooldown(millis);
@@ -823,12 +859,12 @@ public final class WiredManager {
/** /**
* Asynchronously drops/deletes all rewards given by a specific wired item. * Asynchronously drops/deletes all rewards given by a specific wired item.
* Used when a wired reward box is picked up or reset. * Used when a wired reward box is picked up or reset.
* *
* @param wiredId The ID of the wired item whose rewards should be deleted * @param wiredId The ID of the wired item whose rewards should be deleted
*/ */
public static void dropRewards(int wiredId) { public static void dropRewards(int wiredId) {
Emulator.getThreading().run(() -> { 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 = ?")) { PreparedStatement statement = connection.prepareStatement("DELETE FROM wired_rewards_given WHERE wired_item = ?")) {
statement.setInt(1, wiredId); statement.setInt(1, wiredId);
statement.execute(); statement.execute();
@@ -1066,4 +1102,3 @@ public final class WiredManager {
return false; return false;
} }
} }
@@ -9,133 +9,110 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
/** /**
* Centralized tick service for all wired timing operations. * Centralized tick service for all wired timing operations.
* <p> *
* This service runs a single 50ms tick loop that processes all registered * <p>This version keeps a single global tick clock, but distributes room processing
* {@link WiredTickable} items across all rooms. This replaces the old * across multiple single-threaded shard workers. A room is always processed on the
* per-room 500ms cycle approach and provides: * same shard, preserving in-room order while preventing one heavy room from delaying
* </p> * all other rooms.</p>
*
* <ul>
* <li>Higher resolution timing (50ms vs 500ms)</li>
* <li>Centralized management - single thread for all rooms</li>
* <li>Proper room lifecycle handling</li>
* <li>Efficient registration/unregistration</li>
* </ul>
*
* <h3>Architecture:</h3>
* <pre>
* WiredTickService (singleton)
* └── ScheduledExecutorService (50ms tick)
* └── For each room with tickables:
* └── For each WiredTickable:
* └── onWiredTick(room, currentTime)
* </pre>
*
* <h3>Thread Safety:</h3>
* All collections are thread-safe. The tick loop catches and logs exceptions
* to prevent one bad item from crashing the entire service.
*
* @see WiredTickable
*/ */
public final class WiredTickService { public final class WiredTickService {
private static final Logger LOGGER = LoggerFactory.getLogger(WiredTickService.class); private static final Logger LOGGER = LoggerFactory.getLogger(WiredTickService.class);
/** Default tick interval in milliseconds */
public static final int DEFAULT_TICK_INTERVAL_MS = 50; 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; public static final int MIN_TICK_INTERVAL_MS = 10;
/** Maximum allowed tick interval */
public static final int MAX_TICK_INTERVAL_MS = 500; 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; private static volatile WiredTickService instance;
/** The configured tick interval in milliseconds */
private int tickIntervalMs = DEFAULT_TICK_INTERVAL_MS; private int tickIntervalMs = DEFAULT_TICK_INTERVAL_MS;
/** Whether debug logging is enabled */
private boolean debugEnabled = false; private boolean debugEnabled = false;
/** Thread priority for the tick service */
private int threadPriority = Thread.NORM_PRIORITY + 1; private int threadPriority = Thread.NORM_PRIORITY + 1;
private int workerCount = DEFAULT_WORKER_COUNT;
/**
* Global tick counter - increments every tick. /** Global logical tick counter shared by every shard. */
* All repeaters use this to stay synchronized. private final AtomicLong tickCount = new AtomicLong(0);
* Repeaters fire when (tickCount * tickIntervalMs) % repeatTime == 0
*/ /** Schedules the global logical ticks. */
private volatile long tickCount = 0; private ScheduledExecutorService coordinator;
/** The scheduled executor for the tick loop */ /** One single-thread executor per shard, preserving order inside the shard. */
private ScheduledExecutorService scheduler; private ExecutorService[] shardExecutors;
/** The scheduled future for the tick task */ /** Highest logical tick requested for each shard. */
private ScheduledFuture<?> tickTask; private AtomicLong[] shardRequestedTicks;
/** Map of room ID to set of registered tickables */ /** 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<Integer, Set<WiredTickable>> roomTickables; private final ConcurrentHashMap<Integer, Set<WiredTickable>> roomTickables;
/** Whether the service is running */
private final AtomicBoolean running; private final AtomicBoolean running;
/**
* Private constructor for singleton.
*/
private WiredTickService() { private WiredTickService() {
this.roomTickables = new ConcurrentHashMap<>(); this.roomTickables = new ConcurrentHashMap<>();
this.running = new AtomicBoolean(false); this.running = new AtomicBoolean(false);
} }
/**
* Loads configuration from emulator settings.
*/
private void loadConfiguration() { private void loadConfiguration() {
// Load tick interval
int configuredInterval = Emulator.getConfig().getInt("wired.tick.interval.ms", DEFAULT_TICK_INTERVAL_MS); 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)); this.tickIntervalMs = Math.max(MIN_TICK_INTERVAL_MS, Math.min(MAX_TICK_INTERVAL_MS, configuredInterval));
if (configuredInterval != this.tickIntervalMs) { if (configuredInterval != this.tickIntervalMs) {
LOGGER.warn("wired.tick.interval.ms value {} is out of range [{}-{}], using {}", LOGGER.warn(
configuredInterval, MIN_TICK_INTERVAL_MS, MAX_TICK_INTERVAL_MS, this.tickIntervalMs); "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); 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); 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)); 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() { public int getTickIntervalMs() {
return tickIntervalMs; return tickIntervalMs;
} }
/**
* Checks if debug logging is enabled.
*
* @return true if debug is enabled
*/
public boolean isDebugEnabled() { public boolean isDebugEnabled() {
return debugEnabled; return debugEnabled;
} }
/** public int getWorkerCount() {
* Gets the singleton instance. return workerCount;
* }
* @return the WiredTickService instance
*/
public static WiredTickService getInstance() { public static WiredTickService getInstance() {
if (instance == null) { if (instance == null) {
synchronized (WiredTickService.class) { synchronized (WiredTickService.class) {
@@ -146,150 +123,158 @@ public final class WiredTickService {
} }
return instance; return instance;
} }
/**
* Starts the tick service.
* <p>
* Should be called during emulator startup after WiredManager.initialize().
* </p>
*/
public synchronized void start() { public synchronized void start() {
if (running.get()) { if (running.get()) {
LOGGER.warn("WiredTickService already running"); LOGGER.warn("WiredTickService already running");
return; return;
} }
// Load configuration from emulator settings
loadConfiguration(); loadConfiguration();
LOGGER.info("Starting WiredTickService with {}ms tick interval (debug={}, priority={})...", LOGGER.info(
tickIntervalMs, debugEnabled, threadPriority); "Starting WiredTickService with {}ms tick interval (workers={}, debug={}, priority={})...",
tickIntervalMs,
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> { workerCount,
Thread t = new Thread(r, "WiredTickService"); debugEnabled,
threadPriority
);
this.coordinator = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "WiredTickCoordinator");
t.setDaemon(true); t.setDaemon(true);
t.setPriority(threadPriority); t.setPriority(threadPriority);
return t; return t;
}); });
this.tickTask = scheduler.scheduleAtFixedRate( this.shardExecutors = new ExecutorService[workerCount];
this::tick, this.shardRequestedTicks = new AtomicLong[workerCount];
tickIntervalMs, this.shardProcessedTicks = new AtomicLong[workerCount];
tickIntervalMs, this.shardScheduled = new AtomicBoolean[workerCount];
TimeUnit.MILLISECONDS
); 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); 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"); LOGGER.info("WiredTickService started successfully");
} }
/**
* Stops the tick service.
* <p>
* Should be called during emulator shutdown.
* </p>
*/
public synchronized void stop() { public synchronized void stop() {
if (!running.get()) { if (!running.get()) {
return; return;
} }
LOGGER.info("Stopping WiredTickService..."); LOGGER.info("Stopping WiredTickService...");
running.set(false); running.set(false);
if (tickTask != null) { if (coordinator != null) {
tickTask.cancel(false); coordinator.shutdown();
tickTask = null;
}
if (scheduler != null) {
scheduler.shutdown();
try { try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { if (!coordinator.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow(); coordinator.shutdownNow();
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
scheduler.shutdownNow(); coordinator.shutdownNow();
Thread.currentThread().interrupt(); 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(); roomTickables.clear();
LOGGER.info("WiredTickService stopped"); LOGGER.info("WiredTickService stopped");
} }
/**
* Checks if the service is running.
*
* @return true if running
*/
public boolean isRunning() { public boolean isRunning() {
return running.get(); return running.get();
} }
/**
* Registers a tickable item with the service.
* <p>
* The item will start receiving {@link WiredTickable#onWiredTick} calls
* on the next tick cycle.
* </p>
*
* @param room the room the item is in
* @param tickable the tickable item
*/
public void register(Room room, WiredTickable tickable) { public void register(Room room, WiredTickable tickable) {
if (room == null || tickable == null) { if (room == null || tickable == null) {
return; return;
} }
int roomId = room.getId(); int roomId = room.getId();
Set<WiredTickable> tickables = roomTickables.computeIfAbsent( Set<WiredTickable> tickables = roomTickables.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet());
roomId,
k -> ConcurrentHashMap.newKeySet()
);
if (tickables.add(tickable)) { if (tickables.add(tickable)) {
tickable.onRegistered(room, System.currentTimeMillis()); 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) { public void unregister(Room room, WiredTickable tickable) {
if (room == null || tickable == null) { if (room == null || tickable == null) {
return; return;
} }
int roomId = room.getId(); int roomId = room.getId();
Set<WiredTickable> tickables = roomTickables.get(roomId); Set<WiredTickable> tickables = roomTickables.get(roomId);
if (tickables != null) { if (tickables != null) {
if (tickables.remove(tickable)) { if (tickables.remove(tickable)) {
tickable.onUnregistered(room); tickable.onUnregistered(room);
} }
// Clean up empty sets
if (tickables.isEmpty()) { if (tickables.isEmpty()) {
roomTickables.remove(roomId); 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) { public void unregister(int roomId, int tickableId) {
Set<WiredTickable> tickables = roomTickables.get(roomId); Set<WiredTickable> tickables = roomTickables.get(roomId);
if (tickables != null) { if (tickables != null) {
tickables.removeIf(t -> { tickables.removeIf(t -> {
if (t.getId() == tickableId) { if (t.getId() == tickableId) {
@@ -301,162 +286,240 @@ public final class WiredTickService {
} }
return false; return false;
}); });
if (tickables.isEmpty()) { if (tickables.isEmpty()) {
roomTickables.remove(roomId); roomTickables.remove(roomId);
} }
} }
} }
/**
* Unregisters all tickables for a room.
* <p>
* Should be called when a room is unloaded.
* </p>
*
* @param room the room
*/
public void unregisterRoom(Room room) { public void unregisterRoom(Room room) {
if (room == null) { if (room == null) {
return; return;
} }
Set<WiredTickable> tickables = roomTickables.remove(room.getId()); Set<WiredTickable> tickables = roomTickables.remove(room.getId());
if (tickables != null) { if (tickables != null) {
for (WiredTickable tickable : tickables) { WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
tickable.onUnregistered(room); 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) { public void resetRoomTimers(Room room) {
if (room == null) { if (room == null) {
return; return;
} }
Set<WiredTickable> tickables = roomTickables.get(room.getId()); Set<WiredTickable> tickables = roomTickables.get(room.getId());
if (tickables != null) { if (tickables != null) {
for (WiredTickable tickable : tickables) { WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
for (WiredTickable tickable : snapshot) {
try { try {
tickable.resetTimer(); if (tickable != null) {
} catch (Exception e) { tickable.resetTimer();
LOGGER.error("Error resetting timer for tickable {} in room {}", }
tickable.getId(), room.getId(), e); } 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) { public int getTickableCount(int roomId) {
Set<WiredTickable> tickables = roomTickables.get(roomId); Set<WiredTickable> tickables = roomTickables.get(roomId);
return tickables != null ? tickables.size() : 0; return tickables != null ? tickables.size() : 0;
} }
/**
* Gets the total count of registered tickables across all rooms.
*
* @return the total count
*/
public int getTotalTickableCount() { public int getTotalTickableCount() {
return roomTickables.values().stream() return roomTickables.values().stream().mapToInt(Set::size).sum();
.mapToInt(Set::size)
.sum();
} }
/**
* Gets the count of rooms with registered tickables.
*
* @return the room count
*/
public int getActiveRoomCount() { public int getActiveRoomCount() {
return roomTickables.size(); return roomTickables.size();
} }
/** public long getTickCount() {
* The main tick loop. return tickCount.get();
* <p> }
* Called at the configured interval by the scheduler. Processes all registered tickables
* across all rooms. private void dispatchTick() {
* </p>
*/
private void tick() {
if (!running.get() || Emulator.isShuttingDown) { if (!running.get() || Emulator.isShuttingDown) {
return; return;
} }
// Increment global tick counter long currentTick = tickCount.incrementAndGet();
tickCount++;
for (int shardIndex = 0; shardIndex < workerCount; shardIndex++) {
long startTime = System.currentTimeMillis(); shardRequestedTicks[shardIndex].set(currentTick);
int tickablesProcessed = 0; 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<Integer, Set<WiredTickable>> entry : roomTickables.entrySet()) { for (Map.Entry<Integer, Set<WiredTickable>> entry : roomTickables.entrySet()) {
int roomId = entry.getKey(); int roomId = entry.getKey();
Set<WiredTickable> tickables = entry.getValue(); if (getShardIndex(roomId) != shardIndex) {
if (tickables.isEmpty()) {
continue; continue;
} }
// Get the room - skip if not loaded Set<WiredTickable> tickables = entry.getValue();
if (tickables == null || tickables.isEmpty()) {
continue;
}
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId); Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
if (room == null || !room.isLoaded()) { if (room == null || !room.isLoaded()) {
continue; continue;
} }
// Skip if room is empty (optimization)
if (room.getCurrentHabbos().isEmpty() && room.getCurrentBots().isEmpty()) { if (room.getCurrentHabbos().isEmpty() && room.getCurrentBots().isEmpty()) {
continue; continue;
} }
// Process each tickable long roomStart = System.currentTimeMillis();
for (WiredTickable tickable : tickables) { 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 { try {
// Verify item still belongs to this room
if (tickable.getRoomId() != roomId) { if (tickable.getRoomId() != roomId) {
// Item moved to another room, unregister it unregister(roomId, tickable.getId());
tickables.remove(tickable);
continue; continue;
} }
// Pass global tick count - all tickables see the same counter tickable.onWiredTick(room, currentTick, tickIntervalMs);
// This keeps repeaters with the same interval perfectly synchronized processedTickables++;
tickable.onWiredTick(room, tickCount, tickIntervalMs);
tickablesProcessed++; long tickableDuration = System.currentTimeMillis() - tickableStart;
} catch (Exception e) { if (tickableDuration > SLOW_TICKABLE_THRESHOLD_MS) {
LOGGER.error("Error in wired tick for tickable {} in room {}: {}", LOGGER.warn(
tickable.getId(), roomId, e.getMessage(), e); "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 long shardDuration = System.currentTimeMillis() - shardStart;
if (debugEnabled && tickablesProcessed > 0) { if (shardDuration > SLOW_SHARD_THRESHOLD_MS) {
LOGGER.debug("Wired tick #{} completed: {} tickables processed in {}ms", LOGGER.warn(
tickCount, tickablesProcessed, System.currentTimeMillis() - startTime); "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
);
} }
} }
/** private int getShardIndex(int roomId) {
* Gets the current global tick count. return Math.floorMod(roomId, workerCount);
*
* @return the tick count
*/
public long getTickCount() {
return tickCount;
} }
} }