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