You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-20 07:26:18 +00:00
@@ -0,0 +1 @@
|
|||||||
|
INSERT INTO emulator_settings (`key`, `value`) VALUES ('wired.tick.workers', '6');
|
||||||
+1
@@ -90,6 +90,7 @@ public class InteractionWiredHighscore extends HabboItem {
|
|||||||
try {
|
try {
|
||||||
int state = Integer.parseInt(this.getExtradata());
|
int state = Integer.parseInt(this.getExtradata());
|
||||||
this.setExtradata(Math.abs(state - 1) + "");
|
this.setExtradata(Math.abs(state - 1) + "");
|
||||||
|
this.needsUpdate(true);
|
||||||
room.updateItem(this);
|
room.updateItem(this);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error("Caught exception", e);
|
LOGGER.error("Caught exception", e);
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ public final class WiredEngine {
|
|||||||
/** 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.
|
||||||
*
|
*
|
||||||
@@ -112,6 +115,7 @@ 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<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,14 +136,8 @@ public final class WiredEngine {
|
|||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +164,128 @@ 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.
|
||||||
*/
|
*/
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -962,13 +1088,48 @@ public final class WiredEngine {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -977,60 +1138,23 @@ public final class WiredEngine {
|
|||||||
* @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);
|
|
||||||
if (banExpiry == null) {
|
|
||||||
return false;
|
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
|
||||||
*/
|
*/
|
||||||
@@ -1048,8 +1172,8 @@ public final class WiredEngine {
|
|||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -1060,20 +1184,19 @@ public final class WiredEngine {
|
|||||||
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++;
|
||||||
}
|
}
|
||||||
@@ -1083,13 +1206,9 @@ public final class WiredEngine {
|
|||||||
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;
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ public final class WiredManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (engine != null) {
|
if (engine != null) {
|
||||||
engine.clearUnseenCache();
|
engine.clearAllExecutionCaches();
|
||||||
}
|
}
|
||||||
|
|
||||||
initialized = false;
|
initialized = false;
|
||||||
@@ -216,6 +216,18 @@ public final class WiredManager {
|
|||||||
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.
|
||||||
*/
|
*/
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -586,13 +598,22 @@ 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 (engine != null) {
|
||||||
|
engine.clearRoomExecutionCaches(room.getId());
|
||||||
|
}
|
||||||
|
|
||||||
if (debugEnabled) {
|
if (debugEnabled) {
|
||||||
LOGGER.info("[Wired] Cache invalidated for room {}", room.getId());
|
LOGGER.info("[Wired] Cache invalidated for room {}", room.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidate the wired index for a specific tile.
|
* Invalidate the wired index for a specific tile.
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -725,6 +758,9 @@ public final class WiredManager {
|
|||||||
*/
|
*/
|
||||||
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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
|
|
||||||
* {@link WiredTickable} items across all rooms. This replaces the old
|
|
||||||
* per-room 500ms cycle approach and provides:
|
|
||||||
* </p>
|
|
||||||
*
|
*
|
||||||
* <ul>
|
* <p>This version keeps a single global tick clock, but distributes room processing
|
||||||
* <li>Higher resolution timing (50ms vs 500ms)</li>
|
* across multiple single-threaded shard workers. A room is always processed on the
|
||||||
* <li>Centralized management - single thread for all rooms</li>
|
* same shard, preserving in-room order while preventing one heavy room from delaying
|
||||||
* <li>Proper room lifecycle handling</li>
|
* all other rooms.</p>
|
||||||
* <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 logical tick counter shared by every shard. */
|
||||||
* Global tick counter - increments every tick.
|
private final AtomicLong tickCount = new AtomicLong(0);
|
||||||
* 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 */
|
/** Schedules the global logical ticks. */
|
||||||
private ScheduledExecutorService scheduler;
|
private ScheduledExecutorService coordinator;
|
||||||
|
|
||||||
/** The scheduled future for the tick task */
|
/** One single-thread executor per shard, preserving order inside the shard. */
|
||||||
private ScheduledFuture<?> tickTask;
|
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;
|
||||||
|
|
||||||
/** Map of room ID to set of registered tickables */
|
|
||||||
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) {
|
||||||
@@ -147,120 +124,135 @@ 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,
|
||||||
|
workerCount,
|
||||||
|
debugEnabled,
|
||||||
|
threadPriority
|
||||||
|
);
|
||||||
|
|
||||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
this.coordinator = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||||
Thread t = new Thread(r, "WiredTickService");
|
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];
|
||||||
|
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,
|
||||||
tickIntervalMs,
|
tickIntervalMs,
|
||||||
TimeUnit.MILLISECONDS
|
TimeUnit.MILLISECONDS
|
||||||
);
|
);
|
||||||
|
|
||||||
running.set(true);
|
|
||||||
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;
|
||||||
@@ -274,19 +266,12 @@ public final class WiredTickService {
|
|||||||
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);
|
||||||
|
|
||||||
@@ -308,14 +293,6 @@ public final class WiredTickService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
||||||
@@ -324,18 +301,25 @@ public final class WiredTickService {
|
|||||||
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]);
|
||||||
|
for (WiredTickable tickable : snapshot) {
|
||||||
|
try {
|
||||||
|
if (tickable != null) {
|
||||||
tickable.onUnregistered(room);
|
tickable.onUnregistered(room);
|
||||||
}
|
}
|
||||||
LOGGER.debug("Unregistered {} tickables from room {}", tickables.size(), room.getId());
|
} catch (Throwable t) {
|
||||||
|
LOGGER.error(
|
||||||
|
"Error unregistering tickable {} from room {}",
|
||||||
|
tickable != null ? tickable.getId() : -1,
|
||||||
|
room.getId(),
|
||||||
|
t
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
@@ -344,119 +328,198 @@ public final class WiredTickService {
|
|||||||
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 {
|
||||||
|
if (tickable != null) {
|
||||||
tickable.resetTimer();
|
tickable.resetTimer();
|
||||||
} catch (Exception e) {
|
}
|
||||||
LOGGER.error("Error resetting timer for tickable {} in room {}",
|
} catch (Throwable e) {
|
||||||
tickable.getId(), room.getId(), 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++;
|
|
||||||
|
|
||||||
long startTime = System.currentTimeMillis();
|
for (int shardIndex = 0; shardIndex < workerCount; shardIndex++) {
|
||||||
int tickablesProcessed = 0;
|
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<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) {
|
||||||
|
continue;
|
||||||
if (tickables.isEmpty()) {
|
}
|
||||||
|
|
||||||
|
Set<WiredTickable> tickables = entry.getValue();
|
||||||
|
if (tickables == null || tickables.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the room - skip if not loaded
|
|
||||||
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]);
|
||||||
try {
|
if (snapshot.length == 0) {
|
||||||
// Verify item still belongs to this room
|
|
||||||
if (tickable.getRoomId() != roomId) {
|
|
||||||
// Item moved to another room, unregister it
|
|
||||||
tickables.remove(tickable);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass global tick count - all tickables see the same counter
|
processedRooms++;
|
||||||
// This keeps repeaters with the same interval perfectly synchronized
|
|
||||||
tickable.onWiredTick(room, tickCount, tickIntervalMs);
|
for (WiredTickable tickable : snapshot) {
|
||||||
tickablesProcessed++;
|
long tickableStart = System.currentTimeMillis();
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.error("Error in wired tick for tickable {} in room {}: {}",
|
if (tickable == null) {
|
||||||
tickable.getId(), roomId, e.getMessage(), e);
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (tickable.getRoomId() != roomId) {
|
||||||
|
unregister(roomId, tickable.getId());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logging if enabled
|
long roomDuration = System.currentTimeMillis() - roomStart;
|
||||||
if (debugEnabled && tickablesProcessed > 0) {
|
if (roomDuration > SLOW_ROOM_THRESHOLD_MS) {
|
||||||
LOGGER.debug("Wired tick #{} completed: {} tickables processed in {}ms",
|
LOGGER.warn(
|
||||||
tickCount, tickablesProcessed, System.currentTimeMillis() - startTime);
|
"Slow wired room tick: shard={}, room={}, tick={}, tickables={}, took={}ms",
|
||||||
|
shardIndex,
|
||||||
|
roomId,
|
||||||
|
currentTick,
|
||||||
|
snapshot.length,
|
||||||
|
roomDuration
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
long shardDuration = System.currentTimeMillis() - shardStart;
|
||||||
* Gets the current global tick count.
|
if (shardDuration > SLOW_SHARD_THRESHOLD_MS) {
|
||||||
*
|
LOGGER.warn(
|
||||||
* @return the tick count
|
"Slow wired shard tick: shard={}, tick={}, rooms={}, tickables={}, took={}ms",
|
||||||
*/
|
shardIndex,
|
||||||
public long getTickCount() {
|
currentTick,
|
||||||
return tickCount;
|
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) {
|
||||||
|
return Math.floorMod(roomId, workerCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-3
@@ -26,9 +26,16 @@ public class RoomUserWalkEvent extends MessageHandler {
|
|||||||
private static final Logger LOGGER = LoggerFactory.getLogger(RoomUserWalkEvent.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(RoomUserWalkEvent.class);
|
||||||
public static final String CONTROL_KEY = "control";
|
public static final String CONTROL_KEY = "control";
|
||||||
|
|
||||||
|
private static final String WALK_FLOOD_COUNT_KEY = "__walkFloodCount";
|
||||||
|
private static final String WALK_FLOOD_WINDOW_KEY = "__walkFloodWindow";
|
||||||
|
private static final String WALK_LAST_X_KEY = "__walkLastX";
|
||||||
|
private static final String WALK_LAST_Y_KEY = "__walkLastY";
|
||||||
|
|
||||||
|
private static final int MAX_WALKS_PER_SECOND = 15;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getRatelimit() {
|
public int getRatelimit() {
|
||||||
return Emulator.getConfig().getInt("pathfinder.click.delay", 0);
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -37,8 +44,43 @@ public class RoomUserWalkEvent extends MessageHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int x = this.packet.readInt(); // Position X
|
int x = this.packet.readInt();
|
||||||
int y = this.packet.readInt(); // Position Y
|
int y = this.packet.readInt();
|
||||||
|
|
||||||
|
RoomUnit unit = this.client.getHabbo().getRoomUnit();
|
||||||
|
if (unit != null) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
Object windowObj = unit.getCacheable().get(WALK_FLOOD_WINDOW_KEY);
|
||||||
|
Object countObj = unit.getCacheable().get(WALK_FLOOD_COUNT_KEY);
|
||||||
|
|
||||||
|
long windowStart = (windowObj instanceof Long) ? (Long) windowObj : 0L;
|
||||||
|
int count = (countObj instanceof Integer) ? (Integer) countObj : 0;
|
||||||
|
|
||||||
|
if (now - windowStart > 1000) {
|
||||||
|
// New 1-second window
|
||||||
|
windowStart = now;
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
unit.getCacheable().put(WALK_FLOOD_WINDOW_KEY, windowStart);
|
||||||
|
unit.getCacheable().put(WALK_FLOOD_COUNT_KEY, count);
|
||||||
|
|
||||||
|
if (count > MAX_WALKS_PER_SECOND) {
|
||||||
|
unit.getCacheable().put(WALK_LAST_X_KEY, x);
|
||||||
|
unit.getCacheable().put(WALK_LAST_Y_KEY, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object lastX = unit.getCacheable().get(WALK_LAST_X_KEY);
|
||||||
|
Object lastY = unit.getCacheable().get(WALK_LAST_Y_KEY);
|
||||||
|
if (lastX != null && lastY != null) {
|
||||||
|
x = (Integer) lastX;
|
||||||
|
y = (Integer) lastY;
|
||||||
|
unit.getCacheable().remove(WALK_LAST_X_KEY);
|
||||||
|
unit.getCacheable().remove(WALK_LAST_Y_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Habbo habbo = getControlledHabbo();
|
Habbo habbo = getControlledHabbo();
|
||||||
if (habbo == null) {
|
if (habbo == null) {
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user