You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
🆙 Wired Core updates
Thanks to Migueg
This commit is contained in:
@@ -0,0 +1 @@
|
||||
INSERT INTO emulator_settings (`key`, `value`) VALUES ('wired.tick.workers', '6');
|
||||
@@ -93,6 +93,9 @@ public final class WiredEngine {
|
||||
/** Track rooms that are banned from wired execution: roomId -> ban expiry timestamp */
|
||||
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.
|
||||
*
|
||||
@@ -112,6 +115,7 @@ public final class WiredEngine {
|
||||
this.roomRecursionDepth = new ConcurrentHashMap<>();
|
||||
this.eventRateLimiters = new ConcurrentHashMap<>();
|
||||
this.bannedRooms = new ConcurrentHashMap<>();
|
||||
this.sourceStacksByTriggerKey = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,14 +136,8 @@ public final class WiredEngine {
|
||||
|
||||
int roomId = room.getId();
|
||||
|
||||
// Check if room is banned from wired execution
|
||||
if (isRoomBanned(roomId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check rate limiting to prevent rapid-fire event spam (e.g., collision + chase loop)
|
||||
// Soft rate limiting to prevent rapid-fire event spam without banning whole rooms
|
||||
if (isRateLimited(roomId, room, event.getType())) {
|
||||
// Room has been banned, all events will be dropped
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -832,10 +952,16 @@ public final class WiredEngine {
|
||||
* Log a debug message if debug mode is enabled.
|
||||
*/
|
||||
private void debug(Room room, String format, Object... args) {
|
||||
if (WiredManager.isDebugEnabled()) {
|
||||
String message = String.format(format.replace("{}", "%s"), args);
|
||||
LOGGER.info("[WiredEngine][Room {}] {}", room.getId(), message);
|
||||
if (!WiredManager.isDebugEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LOGGER.isDebugEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String message = String.format(format.replace("{}", "%s"), args);
|
||||
LOGGER.debug("[WiredEngine][Room {}] {}", room.getId(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -962,13 +1088,48 @@ public final class WiredEngine {
|
||||
eventRateLimiters.keySet().removeIf(key -> key.startsWith(prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached source-stack lookups for a specific room.
|
||||
* @param roomId the room ID
|
||||
*/
|
||||
public void clearRoomSourceStackCache(int roomId) {
|
||||
String prefix = roomId + ":";
|
||||
sourceStacksByTriggerKey.keySet().removeIf(key -> key.startsWith(prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached source-stack lookups.
|
||||
*/
|
||||
public void clearAllSourceStackCache() {
|
||||
sourceStacksByTriggerKey.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all execution-related caches for a specific room.
|
||||
* @param roomId the room ID
|
||||
*/
|
||||
public void clearRoomExecutionCaches(int roomId) {
|
||||
clearRoomRecursionDepth(roomId);
|
||||
clearRoomRateLimiters(roomId);
|
||||
clearRoomSourceStackCache(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all execution-related caches.
|
||||
*/
|
||||
public void clearAllExecutionCaches() {
|
||||
clearAllRecursionDepth();
|
||||
eventRateLimiters.clear();
|
||||
clearAllSourceStackCache();
|
||||
clearUnseenCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear room ban for a specific room.
|
||||
* Should be called when a room is unloaded.
|
||||
* @param roomId the room ID
|
||||
*/
|
||||
public void clearRoomBan(int roomId) {
|
||||
bannedRooms.remove(roomId);
|
||||
// no-op
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -977,60 +1138,23 @@ public final class WiredEngine {
|
||||
* @return true if wired is banned in this room
|
||||
*/
|
||||
private boolean isRoomBanned(int roomId) {
|
||||
Long banExpiry = bannedRooms.get(roomId);
|
||||
if (banExpiry == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() >= banExpiry) {
|
||||
// Ban expired, remove it
|
||||
bannedRooms.remove(roomId);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ban wired execution in a room for WIRED_BAN_DURATION_MS.
|
||||
* Sends alerts to all users in the room and a scripter alert to staff.
|
||||
* Ban wired execution in a room.
|
||||
* @param roomId the room ID
|
||||
* @param room the room object (for sending alerts)
|
||||
* @param room the room object
|
||||
*/
|
||||
private void banRoom(int roomId, Room room) {
|
||||
long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS;
|
||||
bannedRooms.put(roomId, banExpiry);
|
||||
|
||||
long banMinutes = WIRED_BAN_DURATION_MS / 60000;
|
||||
|
||||
// Send alert to all users in the room
|
||||
String roomAlertMessage = Emulator.getTexts().getValue("wired.abuse.room.alert")
|
||||
.replace("%minutes%", String.valueOf(banMinutes));
|
||||
room.sendComposer(new GenericAlertComposer(roomAlertMessage).compose());
|
||||
|
||||
// Send scripter bubble alert to staff with room link
|
||||
THashMap<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);
|
||||
// no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an event should be rate-limited.
|
||||
* If rate limit exceeded, bans the room and sends alerts.
|
||||
* Uses a soft limiter only, without banning rooms.
|
||||
* @param roomId the room ID
|
||||
* @param room the room object (for sending alerts if banned)
|
||||
* @param room the room object
|
||||
* @param eventType the event type
|
||||
* @return true if the event should be blocked due to rate limiting
|
||||
*/
|
||||
@@ -1048,8 +1172,8 @@ public final class WiredEngine {
|
||||
|
||||
boolean limited = tracker.isRateLimited(now);
|
||||
if (limited && tracker.shouldBan(now)) {
|
||||
// First time hitting limit in this suppression window - ban the room
|
||||
banRoom(roomId, room);
|
||||
LOGGER.warn("Soft wired rate limit in room {} for event {}. Count in current window exceeded.",
|
||||
roomId, eventType);
|
||||
}
|
||||
return limited;
|
||||
}
|
||||
@@ -1060,20 +1184,19 @@ public final class WiredEngine {
|
||||
private static final class EventRateTracker {
|
||||
private long windowStart;
|
||||
private int eventCount;
|
||||
private boolean banned;
|
||||
private boolean warned;
|
||||
|
||||
EventRateTracker(long now) {
|
||||
this.windowStart = now;
|
||||
this.eventCount = 1;
|
||||
this.banned = false;
|
||||
this.warned = false;
|
||||
}
|
||||
|
||||
synchronized void recordEvent(long now) {
|
||||
// Reset window if expired
|
||||
if (now - windowStart > RATE_LIMIT_WINDOW_MS) {
|
||||
windowStart = now;
|
||||
eventCount = 1;
|
||||
// Don't reset banned here - room ban is checked separately
|
||||
warned = false;
|
||||
} else {
|
||||
eventCount++;
|
||||
}
|
||||
@@ -1083,13 +1206,9 @@ public final class WiredEngine {
|
||||
return eventCount > MAX_EVENTS_PER_WINDOW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is the first time we've hit the limit (to trigger ban).
|
||||
* Returns true only once per suppression window.
|
||||
*/
|
||||
synchronized boolean shouldBan(long now) {
|
||||
if (eventCount > MAX_EVENTS_PER_WINDOW && !banned) {
|
||||
banned = true;
|
||||
if (eventCount > MAX_EVENTS_PER_WINDOW && !warned) {
|
||||
warned = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -162,7 +162,7 @@ public final class WiredManager {
|
||||
}
|
||||
|
||||
if (engine != null) {
|
||||
engine.clearUnseenCache();
|
||||
engine.clearAllExecutionCaches();
|
||||
}
|
||||
|
||||
initialized = false;
|
||||
@@ -216,6 +216,18 @@ public final class WiredManager {
|
||||
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.
|
||||
*/
|
||||
@@ -365,24 +377,24 @@ public final class WiredManager {
|
||||
* Trigger a timer tick.
|
||||
*/
|
||||
public static boolean triggerTimerTick(Room room, HabboItem timerItem) {
|
||||
if (!isEnabled() || room == null) {
|
||||
if (!isEnabled() || room == null || timerItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WiredEvent event = WiredEvents.timerTick(room, timerItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, timerItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a periodic timer.
|
||||
*/
|
||||
public static boolean triggerTimerRepeat(Room room, HabboItem timerItem) {
|
||||
if (!isEnabled() || room == null) {
|
||||
if (!isEnabled() || room == null || timerItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WiredEvent event = WiredEvents.timerRepeat(room, timerItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, timerItem);
|
||||
}
|
||||
|
||||
public static boolean triggerClockCounter(Room room, HabboItem counterItem) {
|
||||
@@ -391,31 +403,31 @@ public final class WiredManager {
|
||||
}
|
||||
|
||||
WiredEvent event = WiredEvents.clockCounter(room, counterItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, counterItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a long periodic timer.
|
||||
*/
|
||||
public static boolean triggerTimerRepeatLong(Room room, HabboItem timerItem) {
|
||||
if (!isEnabled() || room == null) {
|
||||
if (!isEnabled() || room == null || timerItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WiredEvent event = WiredEvents.timerRepeatLong(room, timerItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, timerItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a short periodic timer.
|
||||
*/
|
||||
public static boolean triggerTimerRepeatShort(Room room, HabboItem timerItem) {
|
||||
if (!isEnabled() || room == null) {
|
||||
if (!isEnabled() || room == null || timerItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WiredEvent event = WiredEvents.timerRepeatShort(room, timerItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, timerItem);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -586,13 +598,22 @@ public final class WiredManager {
|
||||
* Call this when wired items are added/removed/moved.
|
||||
*/
|
||||
public static void invalidateRoom(Room room) {
|
||||
if (stackIndex != null && room != null) {
|
||||
if (room == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stackIndex != null) {
|
||||
stackIndex.invalidateAll(room);
|
||||
}
|
||||
|
||||
if (engine != null) {
|
||||
engine.clearRoomExecutionCaches(room.getId());
|
||||
}
|
||||
|
||||
if (debugEnabled) {
|
||||
LOGGER.info("[Wired] Cache invalidated for room {}", room.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the wired index for a specific tile.
|
||||
@@ -601,13 +622,25 @@ public final class WiredManager {
|
||||
if (stackIndex != null && room != null && tile != null) {
|
||||
stackIndex.invalidate(room, tile);
|
||||
}
|
||||
|
||||
if (engine != null && room != null) {
|
||||
engine.clearRoomSourceStackCache(room.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the wired index for a room.
|
||||
*/
|
||||
public static void rebuildRoom(Room room) {
|
||||
if (stackIndex != null && room != null) {
|
||||
if (room == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine != null) {
|
||||
engine.clearRoomExecutionCaches(room.getId());
|
||||
}
|
||||
|
||||
if (stackIndex != null) {
|
||||
stackIndex.rebuild(room);
|
||||
}
|
||||
}
|
||||
@@ -725,6 +758,9 @@ public final class WiredManager {
|
||||
*/
|
||||
public static void unregisterRoomTickables(Room room) {
|
||||
WiredTickService.getInstance().unregisterRoom(room);
|
||||
if (engine != null && room != null) {
|
||||
engine.clearRoomExecutionCaches(room.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1066,4 +1102,3 @@ public final class WiredManager {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,133 +9,110 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Centralized tick service for all wired timing operations.
|
||||
* <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>
|
||||
* <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
|
||||
* <p>This version keeps a single global tick clock, but distributes room processing
|
||||
* across multiple single-threaded shard workers. A room is always processed on the
|
||||
* same shard, preserving in-room order while preventing one heavy room from delaying
|
||||
* all other rooms.</p>
|
||||
*/
|
||||
public final class WiredTickService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WiredTickService.class);
|
||||
|
||||
/** Default tick interval in milliseconds */
|
||||
public static final int DEFAULT_TICK_INTERVAL_MS = 50;
|
||||
|
||||
/** Minimum allowed tick interval (prevents CPU overload) */
|
||||
public static final int MIN_TICK_INTERVAL_MS = 10;
|
||||
|
||||
/** Maximum allowed tick interval */
|
||||
public static final int MAX_TICK_INTERVAL_MS = 500;
|
||||
|
||||
/** Singleton instance */
|
||||
public static final int DEFAULT_WORKER_COUNT = Math.max(2, Math.min(8, Runtime.getRuntime().availableProcessors()));
|
||||
public static final int MIN_WORKER_COUNT = 1;
|
||||
public static final int MAX_WORKER_COUNT = 32;
|
||||
|
||||
public static final long SLOW_TICKABLE_THRESHOLD_MS = 100L;
|
||||
public static final long SLOW_ROOM_THRESHOLD_MS = 50L;
|
||||
public static final long SLOW_SHARD_THRESHOLD_MS = 250L;
|
||||
|
||||
private static volatile WiredTickService instance;
|
||||
|
||||
/** The configured tick interval in milliseconds */
|
||||
private int tickIntervalMs = DEFAULT_TICK_INTERVAL_MS;
|
||||
|
||||
/** Whether debug logging is enabled */
|
||||
private boolean debugEnabled = false;
|
||||
|
||||
/** Thread priority for the tick service */
|
||||
private int threadPriority = Thread.NORM_PRIORITY + 1;
|
||||
private int workerCount = DEFAULT_WORKER_COUNT;
|
||||
|
||||
/**
|
||||
* Global tick counter - increments every tick.
|
||||
* All repeaters use this to stay synchronized.
|
||||
* Repeaters fire when (tickCount * tickIntervalMs) % repeatTime == 0
|
||||
*/
|
||||
private volatile long tickCount = 0;
|
||||
/** Global logical tick counter shared by every shard. */
|
||||
private final AtomicLong tickCount = new AtomicLong(0);
|
||||
|
||||
/** The scheduled executor for the tick loop */
|
||||
private ScheduledExecutorService scheduler;
|
||||
/** Schedules the global logical ticks. */
|
||||
private ScheduledExecutorService coordinator;
|
||||
|
||||
/** The scheduled future for the tick task */
|
||||
private ScheduledFuture<?> tickTask;
|
||||
/** One single-thread executor per shard, preserving order inside the shard. */
|
||||
private ExecutorService[] shardExecutors;
|
||||
|
||||
/** Highest logical tick requested for each shard. */
|
||||
private AtomicLong[] shardRequestedTicks;
|
||||
|
||||
/** Last logical tick fully processed by each shard. */
|
||||
private AtomicLong[] shardProcessedTicks;
|
||||
|
||||
/** Whether a shard worker loop is currently scheduled/running. */
|
||||
private AtomicBoolean[] shardScheduled;
|
||||
|
||||
/** Map of room ID to set of registered tickables */
|
||||
private final ConcurrentHashMap<Integer, Set<WiredTickable>> roomTickables;
|
||||
|
||||
/** Whether the service is running */
|
||||
private final AtomicBoolean running;
|
||||
|
||||
/**
|
||||
* Private constructor for singleton.
|
||||
*/
|
||||
private WiredTickService() {
|
||||
this.roomTickables = new ConcurrentHashMap<>();
|
||||
this.running = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads configuration from emulator settings.
|
||||
*/
|
||||
private void loadConfiguration() {
|
||||
// Load tick interval
|
||||
int configuredInterval = Emulator.getConfig().getInt("wired.tick.interval.ms", DEFAULT_TICK_INTERVAL_MS);
|
||||
this.tickIntervalMs = Math.max(MIN_TICK_INTERVAL_MS, Math.min(MAX_TICK_INTERVAL_MS, configuredInterval));
|
||||
|
||||
if (configuredInterval != this.tickIntervalMs) {
|
||||
LOGGER.warn("wired.tick.interval.ms value {} is out of range [{}-{}], using {}",
|
||||
configuredInterval, MIN_TICK_INTERVAL_MS, MAX_TICK_INTERVAL_MS, this.tickIntervalMs);
|
||||
LOGGER.warn(
|
||||
"wired.tick.interval.ms value {} is out of range [{}-{}], using {}",
|
||||
configuredInterval,
|
||||
MIN_TICK_INTERVAL_MS,
|
||||
MAX_TICK_INTERVAL_MS,
|
||||
this.tickIntervalMs
|
||||
);
|
||||
}
|
||||
|
||||
// Load debug flag
|
||||
this.debugEnabled = Emulator.getConfig().getBoolean("wired.tick.debug", false);
|
||||
|
||||
// Load thread priority
|
||||
int configuredPriority = Emulator.getConfig().getInt("wired.tick.thread.priority", Thread.NORM_PRIORITY + 1);
|
||||
this.threadPriority = Math.max(Thread.MIN_PRIORITY, Math.min(Thread.MAX_PRIORITY, configuredPriority));
|
||||
|
||||
int configuredWorkers = Emulator.getConfig().getInt("wired.tick.workers", DEFAULT_WORKER_COUNT);
|
||||
this.workerCount = Math.max(MIN_WORKER_COUNT, Math.min(MAX_WORKER_COUNT, configuredWorkers));
|
||||
|
||||
if (configuredWorkers != this.workerCount) {
|
||||
LOGGER.warn(
|
||||
"wired.tick.workers value {} is out of range [{}-{}], using {}",
|
||||
configuredWorkers,
|
||||
MIN_WORKER_COUNT,
|
||||
MAX_WORKER_COUNT,
|
||||
this.workerCount
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configured tick interval in milliseconds.
|
||||
*
|
||||
* @return the tick interval
|
||||
*/
|
||||
public int getTickIntervalMs() {
|
||||
return tickIntervalMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if debug logging is enabled.
|
||||
*
|
||||
* @return true if debug is enabled
|
||||
*/
|
||||
public boolean isDebugEnabled() {
|
||||
return debugEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance.
|
||||
*
|
||||
* @return the WiredTickService instance
|
||||
*/
|
||||
public int getWorkerCount() {
|
||||
return workerCount;
|
||||
}
|
||||
|
||||
public static WiredTickService getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (WiredTickService.class) {
|
||||
@@ -147,120 +124,135 @@ public final class WiredTickService {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the tick service.
|
||||
* <p>
|
||||
* Should be called during emulator startup after WiredManager.initialize().
|
||||
* </p>
|
||||
*/
|
||||
public synchronized void start() {
|
||||
if (running.get()) {
|
||||
LOGGER.warn("WiredTickService already running");
|
||||
return;
|
||||
}
|
||||
|
||||
// Load configuration from emulator settings
|
||||
loadConfiguration();
|
||||
|
||||
LOGGER.info("Starting WiredTickService with {}ms tick interval (debug={}, priority={})...",
|
||||
tickIntervalMs, debugEnabled, threadPriority);
|
||||
LOGGER.info(
|
||||
"Starting WiredTickService with {}ms tick interval (workers={}, debug={}, priority={})...",
|
||||
tickIntervalMs,
|
||||
workerCount,
|
||||
debugEnabled,
|
||||
threadPriority
|
||||
);
|
||||
|
||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "WiredTickService");
|
||||
this.coordinator = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "WiredTickCoordinator");
|
||||
t.setDaemon(true);
|
||||
t.setPriority(threadPriority);
|
||||
return t;
|
||||
});
|
||||
|
||||
this.tickTask = scheduler.scheduleAtFixedRate(
|
||||
this::tick,
|
||||
this.shardExecutors = new ExecutorService[workerCount];
|
||||
this.shardRequestedTicks = new AtomicLong[workerCount];
|
||||
this.shardProcessedTicks = new AtomicLong[workerCount];
|
||||
this.shardScheduled = new AtomicBoolean[workerCount];
|
||||
|
||||
for (int i = 0; i < workerCount; i++) {
|
||||
final int shardIndex = i;
|
||||
this.shardExecutors[i] = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "WiredTickShard-" + shardIndex);
|
||||
t.setDaemon(true);
|
||||
t.setPriority(threadPriority);
|
||||
return t;
|
||||
});
|
||||
this.shardRequestedTicks[i] = new AtomicLong(0L);
|
||||
this.shardProcessedTicks[i] = new AtomicLong(0L);
|
||||
this.shardScheduled[i] = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
this.tickCount.set(0L);
|
||||
running.set(true);
|
||||
|
||||
this.coordinator.scheduleAtFixedRate(
|
||||
() -> {
|
||||
try {
|
||||
dispatchTick();
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error("WiredTickService fatal coordinator error", t);
|
||||
}
|
||||
},
|
||||
tickIntervalMs,
|
||||
tickIntervalMs,
|
||||
TimeUnit.MILLISECONDS
|
||||
);
|
||||
|
||||
running.set(true);
|
||||
LOGGER.info("WiredTickService started successfully");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the tick service.
|
||||
* <p>
|
||||
* Should be called during emulator shutdown.
|
||||
* </p>
|
||||
*/
|
||||
public synchronized void stop() {
|
||||
if (!running.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.info("Stopping WiredTickService...");
|
||||
|
||||
running.set(false);
|
||||
|
||||
if (tickTask != null) {
|
||||
tickTask.cancel(false);
|
||||
tickTask = null;
|
||||
}
|
||||
|
||||
if (scheduler != null) {
|
||||
scheduler.shutdown();
|
||||
if (coordinator != null) {
|
||||
coordinator.shutdown();
|
||||
try {
|
||||
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
scheduler.shutdownNow();
|
||||
if (!coordinator.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
coordinator.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
scheduler.shutdownNow();
|
||||
coordinator.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
scheduler = null;
|
||||
coordinator = null;
|
||||
}
|
||||
|
||||
if (shardExecutors != null) {
|
||||
for (ExecutorService executor : shardExecutors) {
|
||||
if (executor != null) {
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
for (ExecutorService executor : shardExecutors) {
|
||||
if (executor == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
executor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shardExecutors = null;
|
||||
shardRequestedTicks = null;
|
||||
shardProcessedTicks = null;
|
||||
shardScheduled = null;
|
||||
|
||||
roomTickables.clear();
|
||||
LOGGER.info("WiredTickService stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the service is running.
|
||||
*
|
||||
* @return true if running
|
||||
*/
|
||||
public boolean isRunning() {
|
||||
return running.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a tickable item with the service.
|
||||
* <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) {
|
||||
if (room == null || tickable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = room.getId();
|
||||
Set<WiredTickable> tickables = roomTickables.computeIfAbsent(
|
||||
roomId,
|
||||
k -> ConcurrentHashMap.newKeySet()
|
||||
);
|
||||
Set<WiredTickable> tickables = roomTickables.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet());
|
||||
|
||||
if (tickables.add(tickable)) {
|
||||
tickable.onRegistered(room, System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a tickable item from the service.
|
||||
*
|
||||
* @param room the room the item was in
|
||||
* @param tickable the tickable item
|
||||
*/
|
||||
public void unregister(Room room, WiredTickable tickable) {
|
||||
if (room == null || tickable == null) {
|
||||
return;
|
||||
@@ -274,19 +266,12 @@ public final class WiredTickService {
|
||||
tickable.onUnregistered(room);
|
||||
}
|
||||
|
||||
// Clean up empty sets
|
||||
if (tickables.isEmpty()) {
|
||||
roomTickables.remove(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a tickable by ID.
|
||||
*
|
||||
* @param roomId the room ID
|
||||
* @param tickableId the tickable item ID
|
||||
*/
|
||||
public void unregister(int roomId, int tickableId) {
|
||||
Set<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) {
|
||||
if (room == null) {
|
||||
return;
|
||||
@@ -324,18 +301,25 @@ public final class WiredTickService {
|
||||
Set<WiredTickable> tickables = roomTickables.remove(room.getId());
|
||||
|
||||
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);
|
||||
}
|
||||
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) {
|
||||
if (room == null) {
|
||||
return;
|
||||
@@ -344,119 +328,198 @@ public final class WiredTickService {
|
||||
Set<WiredTickable> tickables = roomTickables.get(room.getId());
|
||||
|
||||
if (tickables != null) {
|
||||
for (WiredTickable tickable : tickables) {
|
||||
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
|
||||
for (WiredTickable tickable : snapshot) {
|
||||
try {
|
||||
if (tickable != null) {
|
||||
tickable.resetTimer();
|
||||
} catch (Exception e) {
|
||||
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) {
|
||||
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
||||
return tickables != null ? tickables.size() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total count of registered tickables across all rooms.
|
||||
*
|
||||
* @return the total count
|
||||
*/
|
||||
public int getTotalTickableCount() {
|
||||
return roomTickables.values().stream()
|
||||
.mapToInt(Set::size)
|
||||
.sum();
|
||||
return roomTickables.values().stream().mapToInt(Set::size).sum();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of rooms with registered tickables.
|
||||
*
|
||||
* @return the room count
|
||||
*/
|
||||
public int getActiveRoomCount() {
|
||||
return roomTickables.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* The main tick loop.
|
||||
* <p>
|
||||
* Called at the configured interval by the scheduler. Processes all registered tickables
|
||||
* across all rooms.
|
||||
* </p>
|
||||
*/
|
||||
private void tick() {
|
||||
public long getTickCount() {
|
||||
return tickCount.get();
|
||||
}
|
||||
|
||||
private void dispatchTick() {
|
||||
if (!running.get() || Emulator.isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment global tick counter
|
||||
tickCount++;
|
||||
long currentTick = tickCount.incrementAndGet();
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
int tickablesProcessed = 0;
|
||||
for (int shardIndex = 0; shardIndex < workerCount; shardIndex++) {
|
||||
shardRequestedTicks[shardIndex].set(currentTick);
|
||||
scheduleShardIfNeeded(shardIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleShardIfNeeded(int shardIndex) {
|
||||
if (!running.get() || shardExecutors == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shardScheduled[shardIndex].compareAndSet(false, true)) {
|
||||
shardExecutors[shardIndex].execute(() -> runShardLoop(shardIndex));
|
||||
}
|
||||
}
|
||||
|
||||
private void runShardLoop(int shardIndex) {
|
||||
try {
|
||||
while (running.get() && !Emulator.isShuttingDown) {
|
||||
long nextTick = shardProcessedTicks[shardIndex].get() + 1L;
|
||||
long requestedTick = shardRequestedTicks[shardIndex].get();
|
||||
|
||||
if (nextTick > requestedTick) {
|
||||
break;
|
||||
}
|
||||
|
||||
processShardTick(shardIndex, nextTick);
|
||||
shardProcessedTicks[shardIndex].set(nextTick);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error("Fatal error in WiredTick shard {}", shardIndex, t);
|
||||
} finally {
|
||||
shardScheduled[shardIndex].set(false);
|
||||
if (running.get() && shardProcessedTicks[shardIndex].get() < shardRequestedTicks[shardIndex].get()) {
|
||||
scheduleShardIfNeeded(shardIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processShardTick(int shardIndex, long currentTick) {
|
||||
long shardStart = System.currentTimeMillis();
|
||||
int processedTickables = 0;
|
||||
int processedRooms = 0;
|
||||
|
||||
for (Map.Entry<Integer, Set<WiredTickable>> entry : roomTickables.entrySet()) {
|
||||
int roomId = entry.getKey();
|
||||
Set<WiredTickable> tickables = entry.getValue();
|
||||
|
||||
if (tickables.isEmpty()) {
|
||||
if (getShardIndex(roomId) != shardIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Set<WiredTickable> tickables = entry.getValue();
|
||||
if (tickables == null || tickables.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the room - skip if not loaded
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
|
||||
if (room == null || !room.isLoaded()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if room is empty (optimization)
|
||||
if (room.getCurrentHabbos().isEmpty() && room.getCurrentBots().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process each tickable
|
||||
for (WiredTickable tickable : tickables) {
|
||||
try {
|
||||
// Verify item still belongs to this room
|
||||
if (tickable.getRoomId() != roomId) {
|
||||
// Item moved to another room, unregister it
|
||||
tickables.remove(tickable);
|
||||
long roomStart = System.currentTimeMillis();
|
||||
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
|
||||
if (snapshot.length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pass global tick count - all tickables see the same counter
|
||||
// This keeps repeaters with the same interval perfectly synchronized
|
||||
tickable.onWiredTick(room, tickCount, tickIntervalMs);
|
||||
tickablesProcessed++;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error in wired tick for tickable {} in room {}: {}",
|
||||
tickable.getId(), roomId, e.getMessage(), e);
|
||||
processedRooms++;
|
||||
|
||||
for (WiredTickable tickable : snapshot) {
|
||||
long tickableStart = System.currentTimeMillis();
|
||||
|
||||
if (tickable == null) {
|
||||
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
|
||||
if (debugEnabled && tickablesProcessed > 0) {
|
||||
LOGGER.debug("Wired tick #{} completed: {} tickables processed in {}ms",
|
||||
tickCount, tickablesProcessed, System.currentTimeMillis() - startTime);
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current global tick count.
|
||||
*
|
||||
* @return the tick count
|
||||
*/
|
||||
public long getTickCount() {
|
||||
return tickCount;
|
||||
long shardDuration = System.currentTimeMillis() - shardStart;
|
||||
if (shardDuration > SLOW_SHARD_THRESHOLD_MS) {
|
||||
LOGGER.warn(
|
||||
"Slow wired shard tick: shard={}, tick={}, rooms={}, tickables={}, took={}ms",
|
||||
shardIndex,
|
||||
currentTick,
|
||||
processedRooms,
|
||||
processedTickables,
|
||||
shardDuration
|
||||
);
|
||||
}
|
||||
|
||||
if (debugEnabled && processedTickables > 0) {
|
||||
LOGGER.debug(
|
||||
"Wired shard tick completed: shard={}, tick={}, rooms={}, tickables={}, took={}ms",
|
||||
shardIndex,
|
||||
currentTick,
|
||||
processedRooms,
|
||||
processedTickables,
|
||||
shardDuration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private int getShardIndex(int roomId) {
|
||||
return Math.floorMod(roomId, workerCount);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user