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
Merge branch 'dev' of https://github.com/duckietm/Arcturus-Morningstar-Extended into dev
This commit is contained in:
@@ -0,0 +1 @@
|
||||
INSERT INTO emulator_settings (`key`, `value`) VALUES ('wired.tick.workers', '6');
|
||||
+2
-1
@@ -90,6 +90,7 @@ public class InteractionWiredHighscore extends HabboItem {
|
||||
try {
|
||||
int state = Integer.parseInt(this.getExtradata());
|
||||
this.setExtradata(Math.abs(state - 1) + "");
|
||||
this.needsUpdate(true);
|
||||
room.updateItem(this);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Caught exception", e);
|
||||
@@ -150,4 +151,4 @@ public class InteractionWiredHighscore extends HabboItem {
|
||||
public void reloadData() {
|
||||
this.data = Emulator.getGameEnvironment().getItemManager().getHighscoreManager().getHighscoreRowsForItem(this.getId(), this.clearType, this.scoreType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
* It receives {@link WiredEvent} objects, finds matching stacks via {@link WiredStackIndex},
|
||||
* evaluates conditions, and executes effects.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* <h3>Execution Flow:</h3>
|
||||
* <ol>
|
||||
* <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>Handle delays for timed effects</li>
|
||||
* </ol>
|
||||
*
|
||||
*
|
||||
* <h3>Safety Features:</h3>
|
||||
* <ul>
|
||||
* <li>Step limits via {@link WiredState} prevent infinite loops</li>
|
||||
* <li>Effect cooldowns prevent rapid re-triggering</li>
|
||||
* <li>Exceptions are caught and logged, not propagated</li>
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
* @see WiredEvent
|
||||
* @see WiredContext
|
||||
* @see WiredStackIndex
|
||||
@@ -64,38 +64,41 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
public final class WiredEngine {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WiredEngine.class);
|
||||
|
||||
|
||||
/** Maximum recursion depth to prevent infinite loops (e.g., collision + chase) */
|
||||
public static int MAX_RECURSION_DEPTH = 10;
|
||||
|
||||
|
||||
/** Maximum events of same type per room within rate limit window before banning */
|
||||
public static int MAX_EVENTS_PER_WINDOW = 100;
|
||||
|
||||
|
||||
/** Time window for counting rapid events (milliseconds) */
|
||||
public static long RATE_LIMIT_WINDOW_MS = 10000;
|
||||
|
||||
|
||||
/** Duration to ban wired execution in a room after abuse detected (milliseconds) */
|
||||
public static long WIRED_BAN_DURATION_MS = 600000;
|
||||
|
||||
private final WiredServices services;
|
||||
private final WiredStackIndex index;
|
||||
private final int maxStepsPerStack;
|
||||
|
||||
|
||||
/** Track unseen effect indices per room+tile for round-robin selection */
|
||||
private final ConcurrentHashMap<String, Integer> unseenIndices;
|
||||
|
||||
|
||||
/** Track recursion depth per room to prevent infinite loops */
|
||||
private final ConcurrentHashMap<Integer, Integer> roomRecursionDepth;
|
||||
|
||||
|
||||
/** Track event timestamps per room+eventType for rate limiting: key = "roomId:eventType" */
|
||||
private final ConcurrentHashMap<String, EventRateTracker> eventRateLimiters;
|
||||
|
||||
|
||||
/** Track rooms that are banned from wired execution: roomId -> ban expiry timestamp */
|
||||
private final ConcurrentHashMap<Integer, Long> bannedRooms;
|
||||
|
||||
/** Cache room+eventType+sourceItemId -> matching stacks for source-triggered timer events */
|
||||
private final ConcurrentHashMap<String, List<WiredStack>> sourceStacksByTriggerKey;
|
||||
|
||||
/**
|
||||
* Create a new wired engine.
|
||||
*
|
||||
*
|
||||
* @param services the services for performing side effects
|
||||
* @param index the stack index for finding matching stacks
|
||||
* @param maxStepsPerStack maximum steps per stack execution (loop protection)
|
||||
@@ -104,7 +107,7 @@ public final class WiredEngine {
|
||||
if (services == null) throw new IllegalArgumentException("Services cannot be null");
|
||||
if (index == null) throw new IllegalArgumentException("Index cannot be null");
|
||||
if (maxStepsPerStack <= 0) throw new IllegalArgumentException("Max steps must be positive");
|
||||
|
||||
|
||||
this.services = services;
|
||||
this.index = index;
|
||||
this.maxStepsPerStack = maxStepsPerStack;
|
||||
@@ -112,11 +115,12 @@ public final class WiredEngine {
|
||||
this.roomRecursionDepth = new ConcurrentHashMap<>();
|
||||
this.eventRateLimiters = new ConcurrentHashMap<>();
|
||||
this.bannedRooms = new ConcurrentHashMap<>();
|
||||
this.sourceStacksByTriggerKey = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a wired event by finding and executing matching stacks.
|
||||
*
|
||||
*
|
||||
* @param event the event to handle
|
||||
* @return true if any stack was triggered (useful for SAY_SOMETHING to suppress message)
|
||||
*/
|
||||
@@ -129,20 +133,14 @@ public final class WiredEngine {
|
||||
if (room == null || !room.isLoaded()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
int roomId = room.getId();
|
||||
|
||||
// Check if room is banned from wired execution
|
||||
if (isRoomBanned(roomId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check rate limiting to prevent rapid-fire event spam (e.g., collision + chase loop)
|
||||
|
||||
// Soft rate limiting to prevent rapid-fire event spam without banning whole rooms
|
||||
if (isRateLimited(roomId, room, event.getType())) {
|
||||
// Room has been banned, all events will be dropped
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Check and increment recursion depth to prevent infinite loops
|
||||
int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0);
|
||||
if (currentDepth >= MAX_RECURSION_DEPTH) {
|
||||
@@ -152,7 +150,7 @@ public final class WiredEngine {
|
||||
return false;
|
||||
}
|
||||
roomRecursionDepth.put(roomId, currentDepth + 1);
|
||||
|
||||
|
||||
try {
|
||||
return handleEventInternal(event, room);
|
||||
} finally {
|
||||
@@ -165,7 +163,129 @@ public final class WiredEngine {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle a wired event when the source trigger item is already known.
|
||||
* This is mainly used by timed wired triggers to avoid scanning unrelated stacks.
|
||||
*
|
||||
* @param event the event to handle
|
||||
* @param sourceItemId the trigger item id that originated the event
|
||||
* @return true if any matching stack was triggered
|
||||
*/
|
||||
public boolean handleEventForSourceItem(WiredEvent event, int sourceItemId) {
|
||||
if (event == null || sourceItemId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Room room = event.getRoom();
|
||||
if (room == null || !room.isLoaded()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int roomId = room.getId();
|
||||
|
||||
if (isRateLimited(roomId, room, event.getType())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0);
|
||||
if (currentDepth >= MAX_RECURSION_DEPTH) {
|
||||
LOGGER.warn("Wired recursion limit reached in room {} (depth: {}). " +
|
||||
"Possible infinite loop detected (source item execution). Aborting.", roomId, currentDepth);
|
||||
debug(room, "RECURSION LIMIT REACHED - aborting source-item execution");
|
||||
return false;
|
||||
}
|
||||
roomRecursionDepth.put(roomId, currentDepth + 1);
|
||||
|
||||
try {
|
||||
return handleEventForSourceItemInternal(event, room, sourceItemId);
|
||||
} finally {
|
||||
int newDepth = roomRecursionDepth.getOrDefault(roomId, 1) - 1;
|
||||
if (newDepth <= 0) {
|
||||
roomRecursionDepth.remove(roomId);
|
||||
} else {
|
||||
roomRecursionDepth.put(roomId, newDepth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal event handling optimized for a known source trigger item.
|
||||
*/
|
||||
private boolean handleEventForSourceItemInternal(WiredEvent event, Room room, int sourceItemId) {
|
||||
List<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.
|
||||
*/
|
||||
@@ -232,12 +352,12 @@ public final class WiredEngine {
|
||||
|
||||
// Initial step for trigger
|
||||
state.step();
|
||||
|
||||
debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})",
|
||||
event.getType(),
|
||||
stack.triggerItem() != null ? stack.triggerItem().getId() : "null",
|
||||
stack.conditions().size(),
|
||||
stack.effects().size());
|
||||
|
||||
debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})",
|
||||
event.getType(),
|
||||
stack.triggerItem() != null ? stack.triggerItem().getId() : "null",
|
||||
stack.conditions().size(),
|
||||
stack.effects().size());
|
||||
|
||||
// Run selectors before conditions so targets are available
|
||||
List<InteractionWiredEffect> executedSelectors = Collections.emptyList();
|
||||
@@ -409,11 +529,11 @@ public final class WiredEngine {
|
||||
*/
|
||||
private void executeEffects(WiredStack stack, WiredContext ctx, long currentTime) {
|
||||
List<IWiredEffect> effects = stack.effects();
|
||||
|
||||
|
||||
if (effects.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Selectors already executed before conditions; only run regular effects here
|
||||
List<IWiredEffect> regulars = new ArrayList<>();
|
||||
for (IWiredEffect e : effects) {
|
||||
@@ -484,7 +604,7 @@ public final class WiredEngine {
|
||||
ctx.state().step();
|
||||
try {
|
||||
effect.execute(ctx);
|
||||
|
||||
|
||||
// Activate box animation after execution
|
||||
if (effect instanceof InteractionWiredEffect) {
|
||||
InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect;
|
||||
@@ -599,7 +719,7 @@ public final class WiredEngine {
|
||||
Collections.shuffle(result, Emulator.getRandom());
|
||||
return new ArrayList<>(result.subList(0, limit));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Schedule a delayed effect execution.
|
||||
*/
|
||||
@@ -610,15 +730,15 @@ public final class WiredEngine {
|
||||
long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger);
|
||||
Room room = ctx.room();
|
||||
RoomUnit actor = ctx.actor().orElse(null);
|
||||
|
||||
|
||||
Emulator.getThreading().run(() -> {
|
||||
if (!room.isLoaded() || room.getHabbos().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
effect.execute(ctx);
|
||||
|
||||
|
||||
// Activate box animation after execution
|
||||
if (effect instanceof InteractionWiredEffect) {
|
||||
InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect;
|
||||
@@ -753,14 +873,14 @@ public final class WiredEngine {
|
||||
* Get the next unseen index for round-robin selection.
|
||||
*/
|
||||
private int getNextUnseenIndex(WiredStack stack, int effectCount) {
|
||||
String key = stack.triggerItem() != null
|
||||
String key = stack.triggerItem() != null
|
||||
? String.valueOf(stack.triggerItem().getId())
|
||||
: "default";
|
||||
|
||||
|
||||
int current = unseenIndices.getOrDefault(key, -1);
|
||||
int next = (current + 1) % effectCount;
|
||||
unseenIndices.put(key, next);
|
||||
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -773,7 +893,7 @@ public final class WiredEngine {
|
||||
// This event is checked for cancellation
|
||||
THashSet<InteractionWiredEffect> legacyEffects = new THashSet<>();
|
||||
THashSet<InteractionWiredCondition> legacyConditions = new THashSet<>();
|
||||
|
||||
|
||||
// Extract effects (all effects should now implement both interfaces)
|
||||
for (IWiredEffect eff : stack.effects()) {
|
||||
if (eff instanceof InteractionWiredEffect) {
|
||||
@@ -785,7 +905,7 @@ public final class WiredEngine {
|
||||
legacyConditions.add((InteractionWiredCondition) cond);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
WiredStackTriggeredEvent triggeredEvent = new WiredStackTriggeredEvent(
|
||||
event.getRoom(),
|
||||
event.getActor().orElse(null),
|
||||
@@ -793,7 +913,7 @@ public final class WiredEngine {
|
||||
legacyEffects,
|
||||
legacyConditions
|
||||
);
|
||||
|
||||
|
||||
return !Emulator.getPluginManager().fireEvent(triggeredEvent).isCancelled();
|
||||
}
|
||||
return true;
|
||||
@@ -806,7 +926,7 @@ public final class WiredEngine {
|
||||
if (stack.triggerItem() instanceof InteractionWiredTrigger) {
|
||||
THashSet<InteractionWiredEffect> legacyEffects = new THashSet<>();
|
||||
THashSet<InteractionWiredCondition> legacyConditions = new THashSet<>();
|
||||
|
||||
|
||||
for (IWiredEffect eff : stack.effects()) {
|
||||
if (eff instanceof InteractionWiredEffect) {
|
||||
legacyEffects.add((InteractionWiredEffect) eff);
|
||||
@@ -817,7 +937,7 @@ public final class WiredEngine {
|
||||
legacyConditions.add((InteractionWiredCondition) cond);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Emulator.getPluginManager().fireEvent(new WiredStackExecutedEvent(
|
||||
event.getRoom(),
|
||||
event.getActor().orElse(null),
|
||||
@@ -832,10 +952,16 @@ public final class WiredEngine {
|
||||
* Log a debug message if debug mode is enabled.
|
||||
*/
|
||||
private void debug(Room room, String format, Object... args) {
|
||||
if (WiredManager.isDebugEnabled()) {
|
||||
String message = String.format(format.replace("{}", "%s"), args);
|
||||
LOGGER.info("[WiredEngine][Room {}] {}", room.getId(), message);
|
||||
if (!WiredManager.isDebugEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LOGGER.isDebugEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String message = String.format(format.replace("{}", "%s"), args);
|
||||
LOGGER.debug("[WiredEngine][Room {}] {}", room.getId(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -845,10 +971,10 @@ public final class WiredEngine {
|
||||
if (triggerItem == null || room.getRoomSpecialTypes() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
THashSet<InteractionWiredExtra> extras = room.getRoomSpecialTypes().getExtras(
|
||||
triggerItem.getX(), triggerItem.getY());
|
||||
|
||||
|
||||
if (extras != null) {
|
||||
for (InteractionWiredExtra extra : extras) {
|
||||
extra.activateBox(room, roomUnit, millis);
|
||||
@@ -926,7 +1052,7 @@ public final class WiredEngine {
|
||||
public void clearUnseenCache() {
|
||||
unseenIndices.clear();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear recursion tracking for a specific room.
|
||||
* Should be called when a room is unloaded.
|
||||
@@ -935,14 +1061,14 @@ public final class WiredEngine {
|
||||
public void clearRoomRecursionDepth(int roomId) {
|
||||
roomRecursionDepth.remove(roomId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear all recursion tracking.
|
||||
*/
|
||||
public void clearAllRecursionDepth() {
|
||||
roomRecursionDepth.clear();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the current recursion depth for a room (for debugging).
|
||||
* @param roomId the room ID
|
||||
@@ -951,7 +1077,7 @@ public final class WiredEngine {
|
||||
public int getRecursionDepth(int roomId) {
|
||||
return roomRecursionDepth.getOrDefault(roomId, 0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear rate limiters for a specific room.
|
||||
* Should be called when a room is unloaded.
|
||||
@@ -961,83 +1087,81 @@ public final class WiredEngine {
|
||||
String prefix = roomId + ":";
|
||||
eventRateLimiters.keySet().removeIf(key -> key.startsWith(prefix));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear cached source-stack lookups for a specific room.
|
||||
* @param roomId the room ID
|
||||
*/
|
||||
public void clearRoomSourceStackCache(int roomId) {
|
||||
String prefix = roomId + ":";
|
||||
sourceStacksByTriggerKey.keySet().removeIf(key -> key.startsWith(prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached source-stack lookups.
|
||||
*/
|
||||
public void clearAllSourceStackCache() {
|
||||
sourceStacksByTriggerKey.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all execution-related caches for a specific room.
|
||||
* @param roomId the room ID
|
||||
*/
|
||||
public void clearRoomExecutionCaches(int roomId) {
|
||||
clearRoomRecursionDepth(roomId);
|
||||
clearRoomRateLimiters(roomId);
|
||||
clearRoomSourceStackCache(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all execution-related caches.
|
||||
*/
|
||||
public void clearAllExecutionCaches() {
|
||||
clearAllRecursionDepth();
|
||||
eventRateLimiters.clear();
|
||||
clearAllSourceStackCache();
|
||||
clearUnseenCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear room ban for a specific room.
|
||||
* Should be called when a room is unloaded.
|
||||
* @param roomId the room ID
|
||||
*/
|
||||
public void clearRoomBan(int roomId) {
|
||||
bannedRooms.remove(roomId);
|
||||
// no-op
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if a room is currently banned from wired execution.
|
||||
* @param roomId the room ID
|
||||
* @return true if wired is banned in this room
|
||||
*/
|
||||
private boolean isRoomBanned(int roomId) {
|
||||
Long banExpiry = bannedRooms.get(roomId);
|
||||
if (banExpiry == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() >= banExpiry) {
|
||||
// Ban expired, remove it
|
||||
bannedRooms.remove(roomId);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ban wired execution in a room for WIRED_BAN_DURATION_MS.
|
||||
* Sends alerts to all users in the room and a scripter alert to staff.
|
||||
* Ban wired execution in a room.
|
||||
* @param roomId the room ID
|
||||
* @param room the room object (for sending alerts)
|
||||
* @param room the room object
|
||||
*/
|
||||
private void banRoom(int roomId, Room room) {
|
||||
long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS;
|
||||
bannedRooms.put(roomId, banExpiry);
|
||||
|
||||
long banMinutes = WIRED_BAN_DURATION_MS / 60000;
|
||||
|
||||
// Send alert to all users in the room
|
||||
String roomAlertMessage = Emulator.getTexts().getValue("wired.abuse.room.alert")
|
||||
.replace("%minutes%", String.valueOf(banMinutes));
|
||||
room.sendComposer(new GenericAlertComposer(roomAlertMessage).compose());
|
||||
|
||||
// Send scripter bubble alert to staff with room link
|
||||
THashMap<String, String> keys = new THashMap<>();
|
||||
keys.put("title", Emulator.getTexts().getValue("wired.abuse.staff.title"));
|
||||
keys.put("message", Emulator.getTexts().getValue("wired.abuse.staff.message")
|
||||
.replace("%roomname%", room.getName())
|
||||
.replace("%owner%", room.getOwnerName())
|
||||
.replace("%minutes%", String.valueOf(banMinutes)));
|
||||
keys.put("linkUrl", "event:navigator/goto/" + roomId);
|
||||
keys.put("linkTitle", Emulator.getTexts().getValue("wired.abuse.staff.link"));
|
||||
Emulator.getGameEnvironment().getHabboManager().sendPacketToHabbosWithPermission(
|
||||
new BubbleAlertComposer("admin.staffalert", keys).compose(),
|
||||
"acc_modtool_room_info"
|
||||
);
|
||||
|
||||
LOGGER.warn("Wired abuse detected in room {} ({}). Owner: {}. Wired banned for {} minutes.",
|
||||
roomId, room.getName(), room.getOwnerName(), banMinutes);
|
||||
// no-op
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if an event should be rate-limited.
|
||||
* If rate limit exceeded, bans the room and sends alerts.
|
||||
* Uses a soft limiter only, without banning rooms.
|
||||
* @param roomId the room ID
|
||||
* @param room the room object (for sending alerts if banned)
|
||||
* @param room the room object
|
||||
* @param eventType the event type
|
||||
* @return true if the event should be blocked due to rate limiting
|
||||
*/
|
||||
private boolean isRateLimited(int roomId, Room room, WiredEvent.Type eventType) {
|
||||
String key = roomId + ":" + eventType.name();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
|
||||
EventRateTracker tracker = eventRateLimiters.compute(key, (k, existing) -> {
|
||||
if (existing == null) {
|
||||
return new EventRateTracker(now);
|
||||
@@ -1045,51 +1169,46 @@ public final class WiredEngine {
|
||||
existing.recordEvent(now);
|
||||
return existing;
|
||||
});
|
||||
|
||||
|
||||
boolean limited = tracker.isRateLimited(now);
|
||||
if (limited && tracker.shouldBan(now)) {
|
||||
// First time hitting limit in this suppression window - ban the room
|
||||
banRoom(roomId, room);
|
||||
LOGGER.warn("Soft wired rate limit in room {} for event {}. Count in current window exceeded.",
|
||||
roomId, eventType);
|
||||
}
|
||||
return limited;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tracks event rate for a specific room + event type combination.
|
||||
*/
|
||||
private static final class EventRateTracker {
|
||||
private long windowStart;
|
||||
private int eventCount;
|
||||
private boolean banned;
|
||||
|
||||
private boolean warned;
|
||||
|
||||
EventRateTracker(long now) {
|
||||
this.windowStart = now;
|
||||
this.eventCount = 1;
|
||||
this.banned = false;
|
||||
this.warned = false;
|
||||
}
|
||||
|
||||
|
||||
synchronized void recordEvent(long now) {
|
||||
// Reset window if expired
|
||||
if (now - windowStart > RATE_LIMIT_WINDOW_MS) {
|
||||
windowStart = now;
|
||||
eventCount = 1;
|
||||
// Don't reset banned here - room ban is checked separately
|
||||
warned = false;
|
||||
} else {
|
||||
eventCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
synchronized boolean isRateLimited(long now) {
|
||||
return eventCount > MAX_EVENTS_PER_WINDOW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is the first time we've hit the limit (to trigger ban).
|
||||
* Returns true only once per suppression window.
|
||||
*/
|
||||
|
||||
synchronized boolean shouldBan(long now) {
|
||||
if (eventCount > MAX_EVENTS_PER_WINDOW && !banned) {
|
||||
banned = true;
|
||||
if (eventCount > MAX_EVENTS_PER_WINDOW && !warned) {
|
||||
warned = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -46,7 +46,7 @@ import java.sql.SQLException;
|
||||
* wired engine. It provides static methods for triggering events and manages
|
||||
* the lifecycle of the engine.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* <h3>Configuration Options:</h3>
|
||||
* <ul>
|
||||
* <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.debug} - Verbose logging</li>
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
* <h3>Migration Strategy:</h3>
|
||||
* <ol>
|
||||
* <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>Full migration complete - WiredManager is now the only wired engine</li>
|
||||
* </ol>
|
||||
*
|
||||
*
|
||||
* @see WiredEngine
|
||||
* @see WiredEvents
|
||||
*/
|
||||
@@ -86,10 +86,10 @@ public final class WiredManager {
|
||||
|
||||
/** The singleton engine instance */
|
||||
private static volatile WiredEngine engine;
|
||||
|
||||
|
||||
/** The stack index */
|
||||
private static volatile RoomWiredStackIndex stackIndex;
|
||||
|
||||
|
||||
/** Whether the engine is initialized */
|
||||
private static volatile boolean initialized = false;
|
||||
private WiredManager() {
|
||||
@@ -119,7 +119,7 @@ public final class WiredManager {
|
||||
boolean enabled = Emulator.getConfig().getBoolean(CONFIG_ENABLED, DEFAULT_ENABLED);
|
||||
int maxSteps = Emulator.getConfig().getInt(CONFIG_MAX_STEPS, DEFAULT_MAX_STEPS);
|
||||
boolean debug = Emulator.getConfig().getBoolean(CONFIG_DEBUG, false);
|
||||
|
||||
|
||||
// Load additional configuration
|
||||
MAXIMUM_FURNI_SELECTION = Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5);
|
||||
TELEPORT_DELAY = Emulator.getConfig().getInt("wired.effect.teleport.delay", 500);
|
||||
@@ -133,13 +133,13 @@ public final class WiredManager {
|
||||
stackIndex = new RoomWiredStackIndex();
|
||||
WiredServices services = DefaultWiredServices.getInstance();
|
||||
engine = new WiredEngine(services, stackIndex, maxSteps);
|
||||
|
||||
|
||||
// Start the centralized tick service (50ms interval)
|
||||
WiredTickService.getInstance().start();
|
||||
|
||||
initialized = true;
|
||||
|
||||
LOGGER.info("Wired Manager initialized - enabled: {}, maxSteps: {}, debug: {}",
|
||||
|
||||
LOGGER.info("Wired Manager initialized - enabled: {}, maxSteps: {}, debug: {}",
|
||||
enabled, maxSteps, debug);
|
||||
}
|
||||
|
||||
@@ -153,16 +153,16 @@ public final class WiredManager {
|
||||
}
|
||||
|
||||
LOGGER.info("Shutting down Wired Manager...");
|
||||
|
||||
|
||||
// Stop the tick service first
|
||||
WiredTickService.getInstance().stop();
|
||||
|
||||
|
||||
if (stackIndex != null) {
|
||||
stackIndex.clearAll();
|
||||
}
|
||||
|
||||
|
||||
if (engine != null) {
|
||||
engine.clearUnseenCache();
|
||||
engine.clearAllExecutionCaches();
|
||||
}
|
||||
|
||||
initialized = false;
|
||||
@@ -212,10 +212,22 @@ public final class WiredManager {
|
||||
if (!isEnabled() || engine == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return engine.handleEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a wired event using the new engine when the source trigger item is already known.
|
||||
* Used by timed wired to avoid scanning unrelated stacks.
|
||||
*/
|
||||
private static boolean handleEventForSourceItem(WiredEvent event, HabboItem sourceItem) {
|
||||
if (!isEnabled() || engine == null || event == null || sourceItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return engine.handleEventForSourceItem(event, sourceItem.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger when a user walks onto furniture.
|
||||
*/
|
||||
@@ -223,7 +235,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null || user == null || item == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.userWalksOn(room, user, item);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -235,7 +247,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null || user == null || item == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.userWalksOff(room, user, item);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -311,7 +323,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null || user == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.userSays(room, user, message);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -332,7 +344,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null || user == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.userEntersRoom(room, user);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -356,7 +368,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null || item == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.furniStateChanged(room, user, item);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -365,24 +377,24 @@ public final class WiredManager {
|
||||
* Trigger a timer tick.
|
||||
*/
|
||||
public static boolean triggerTimerTick(Room room, HabboItem timerItem) {
|
||||
if (!isEnabled() || room == null) {
|
||||
if (!isEnabled() || room == null || timerItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.timerTick(room, timerItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, timerItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a periodic timer.
|
||||
*/
|
||||
public static boolean triggerTimerRepeat(Room room, HabboItem timerItem) {
|
||||
if (!isEnabled() || room == null) {
|
||||
if (!isEnabled() || room == null || timerItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.timerRepeat(room, timerItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, timerItem);
|
||||
}
|
||||
|
||||
public static boolean triggerClockCounter(Room room, HabboItem counterItem) {
|
||||
@@ -391,31 +403,31 @@ public final class WiredManager {
|
||||
}
|
||||
|
||||
WiredEvent event = WiredEvents.clockCounter(room, counterItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, counterItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a long periodic timer.
|
||||
*/
|
||||
public static boolean triggerTimerRepeatLong(Room room, HabboItem timerItem) {
|
||||
if (!isEnabled() || room == null) {
|
||||
if (!isEnabled() || room == null || timerItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WiredEvent event = WiredEvents.timerRepeatLong(room, timerItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, timerItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a short periodic timer.
|
||||
*/
|
||||
public static boolean triggerTimerRepeatShort(Room room, HabboItem timerItem) {
|
||||
if (!isEnabled() || room == null) {
|
||||
if (!isEnabled() || room == null || timerItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WiredEvent event = WiredEvents.timerRepeatShort(room, timerItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, timerItem);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -425,7 +437,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.gameStarts(room);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -437,7 +449,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.gameEnds(room);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -449,7 +461,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null || botUnit == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.botCollision(room, botUnit);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -461,7 +473,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null || botUnit == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.botReachedFurni(room, botUnit, item);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -473,7 +485,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null || botUnit == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.botReachedHabbo(room, botUnit, targetUser);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -489,7 +501,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null || user == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.scoreAchieved(room, user, score, scoreAdded);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -501,7 +513,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null || user == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.userIdles(room, user);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -513,7 +525,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null || user == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.userUnidles(room, user);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -525,7 +537,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null || user == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.userStartsDancing(room, user);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -537,7 +549,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null || user == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.userStopsDancing(room, user);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -549,7 +561,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.teamWins(room, user);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -561,7 +573,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.teamLoses(room, user);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -574,7 +586,7 @@ public final class WiredManager {
|
||||
if (!isEnabled() || room == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
WiredEvent event = WiredEvents.fromLegacy(triggerType, room, roomUnit, stuff);
|
||||
return handleEvent(event);
|
||||
}
|
||||
@@ -586,11 +598,20 @@ public final class WiredManager {
|
||||
* Call this when wired items are added/removed/moved.
|
||||
*/
|
||||
public static void invalidateRoom(Room room) {
|
||||
if (stackIndex != null && room != null) {
|
||||
if (room == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stackIndex != null) {
|
||||
stackIndex.invalidateAll(room);
|
||||
if (debugEnabled) {
|
||||
LOGGER.info("[Wired] Cache invalidated for room {}", room.getId());
|
||||
}
|
||||
}
|
||||
|
||||
if (engine != null) {
|
||||
engine.clearRoomExecutionCaches(room.getId());
|
||||
}
|
||||
|
||||
if (debugEnabled) {
|
||||
LOGGER.info("[Wired] Cache invalidated for room {}", room.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,13 +622,25 @@ public final class WiredManager {
|
||||
if (stackIndex != null && room != null && tile != null) {
|
||||
stackIndex.invalidate(room, tile);
|
||||
}
|
||||
|
||||
if (engine != null && room != null) {
|
||||
engine.clearRoomSourceStackCache(room.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the wired index for a room.
|
||||
*/
|
||||
public static void rebuildRoom(Room room) {
|
||||
if (stackIndex != null && room != null) {
|
||||
if (room == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine != null) {
|
||||
engine.clearRoomExecutionCaches(room.getId());
|
||||
}
|
||||
|
||||
if (stackIndex != null) {
|
||||
stackIndex.rebuild(room);
|
||||
}
|
||||
}
|
||||
@@ -616,19 +649,19 @@ public final class WiredManager {
|
||||
|
||||
/** Maximum number of furniture items that can be selected in a single wired component */
|
||||
public static int MAXIMUM_FURNI_SELECTION = 5;
|
||||
|
||||
|
||||
/** Delay in milliseconds between teleport executions */
|
||||
public static int TELEPORT_DELAY = 500;
|
||||
|
||||
// ========== Debug Mode ==========
|
||||
|
||||
|
||||
/** Debug mode - when enabled, logs detailed wired execution flow */
|
||||
private static boolean debugEnabled = false;
|
||||
|
||||
/**
|
||||
* Enables or disables wired debug mode.
|
||||
* When enabled, detailed execution logs are written to help troubleshoot wired stacks.
|
||||
*
|
||||
*
|
||||
* @param enabled true to enable debug logging, false to disable
|
||||
*/
|
||||
public static void setDebugEnabled(boolean enabled) {
|
||||
@@ -637,19 +670,19 @@ public final class WiredManager {
|
||||
LOGGER.info("Wired debug mode ENABLED");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if wired debug mode is enabled.
|
||||
*
|
||||
*
|
||||
* @return true if debug mode is active
|
||||
*/
|
||||
public static boolean isDebugEnabled() {
|
||||
return debugEnabled;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Logs a debug message if debug mode is enabled.
|
||||
*
|
||||
*
|
||||
* @param message the message to log
|
||||
* @param args optional format arguments
|
||||
*/
|
||||
@@ -660,7 +693,7 @@ public final class WiredManager {
|
||||
}
|
||||
|
||||
// ========== JSON Utilities ==========
|
||||
|
||||
|
||||
private static GsonBuilder gsonBuilder = null;
|
||||
private static Gson cachedGson = null;
|
||||
|
||||
@@ -670,12 +703,12 @@ public final class WiredManager {
|
||||
}
|
||||
return gsonBuilder;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets a cached Gson instance. This is more efficient than calling
|
||||
* getGsonBuilder().create() multiple times, as Gson instances are thread-safe
|
||||
* and can be reused.
|
||||
*
|
||||
*
|
||||
* @return a cached Gson instance
|
||||
*/
|
||||
public static Gson getGson() {
|
||||
@@ -686,50 +719,53 @@ public final class WiredManager {
|
||||
}
|
||||
|
||||
// ========== Tick Service Integration ==========
|
||||
|
||||
|
||||
/**
|
||||
* Registers a tickable wired item with the centralized tick service.
|
||||
* <p>
|
||||
* Call this when a time-based wired trigger is placed in a room or when
|
||||
* a room is loaded.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* @param room the room the item is in
|
||||
* @param tickable the tickable item (e.g., WiredTriggerRepeater)
|
||||
*/
|
||||
public static void registerTickable(Room room, WiredTickable tickable) {
|
||||
WiredTickService.getInstance().register(room, tickable);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unregisters a tickable wired item from the tick service.
|
||||
* <p>
|
||||
* Call this when a time-based wired trigger is picked up or when
|
||||
* a room is unloaded.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* @param room the room the item was in
|
||||
* @param tickable the tickable item
|
||||
*/
|
||||
public static void unregisterTickable(Room room, WiredTickable tickable) {
|
||||
WiredTickService.getInstance().unregister(room, tickable);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unregisters all tickables for a room.
|
||||
* <p>
|
||||
* Call this when a room is unloaded to clean up all tick registrations.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* @param room the room
|
||||
*/
|
||||
public static void unregisterRoomTickables(Room room) {
|
||||
WiredTickService.getInstance().unregisterRoom(room);
|
||||
if (engine != null && room != null) {
|
||||
engine.clearRoomExecutionCaches(room.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the tick service instance.
|
||||
*
|
||||
*
|
||||
* @return the WiredTickService
|
||||
*/
|
||||
public static WiredTickService getTickService() {
|
||||
@@ -771,7 +807,7 @@ public final class WiredManager {
|
||||
* <p>
|
||||
* This uses the new tick service for managing timer resets.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* @param room the room
|
||||
*/
|
||||
public static void resetTimers(Room room) {
|
||||
@@ -804,9 +840,9 @@ public final class WiredManager {
|
||||
if (item instanceof InteractionWiredEffect && !(item instanceof WiredEffectTriggerStacks)) {
|
||||
InteractionWiredEffect effect = (InteractionWiredEffect) item;
|
||||
WiredEvent event = WiredEvent.builder(WiredEvent.Type.CUSTOM, room)
|
||||
.actor(roomUnit)
|
||||
.callStackDepth(callStackDepth)
|
||||
.build();
|
||||
.actor(roomUnit)
|
||||
.callStackDepth(callStackDepth)
|
||||
.build();
|
||||
WiredContext ctx = new WiredContext(event, effect, DefaultWiredServices.getInstance(), new WiredState(100));
|
||||
effect.execute(ctx);
|
||||
effect.setCooldown(millis);
|
||||
@@ -823,12 +859,12 @@ public final class WiredManager {
|
||||
/**
|
||||
* Asynchronously drops/deletes all rewards given by a specific wired item.
|
||||
* Used when a wired reward box is picked up or reset.
|
||||
*
|
||||
*
|
||||
* @param wiredId The ID of the wired item whose rewards should be deleted
|
||||
*/
|
||||
public static void dropRewards(int wiredId) {
|
||||
Emulator.getThreading().run(() -> {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("DELETE FROM wired_rewards_given WHERE wired_item = ?")) {
|
||||
statement.setInt(1, wiredId);
|
||||
statement.execute();
|
||||
@@ -1066,4 +1102,3 @@ public final class WiredManager {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,133 +9,110 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Centralized tick service for all wired timing operations.
|
||||
* <p>
|
||||
* This service runs a single 50ms tick loop that processes all registered
|
||||
* {@link WiredTickable} items across all rooms. This replaces the old
|
||||
* per-room 500ms cycle approach and provides:
|
||||
* </p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>Higher resolution timing (50ms vs 500ms)</li>
|
||||
* <li>Centralized management - single thread for all rooms</li>
|
||||
* <li>Proper room lifecycle handling</li>
|
||||
* <li>Efficient registration/unregistration</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>Architecture:</h3>
|
||||
* <pre>
|
||||
* WiredTickService (singleton)
|
||||
* └── ScheduledExecutorService (50ms tick)
|
||||
* └── For each room with tickables:
|
||||
* └── For each WiredTickable:
|
||||
* └── onWiredTick(room, currentTime)
|
||||
* </pre>
|
||||
*
|
||||
* <h3>Thread Safety:</h3>
|
||||
* All collections are thread-safe. The tick loop catches and logs exceptions
|
||||
* to prevent one bad item from crashing the entire service.
|
||||
*
|
||||
* @see WiredTickable
|
||||
*
|
||||
* <p>This version keeps a single global tick clock, but distributes room processing
|
||||
* across multiple single-threaded shard workers. A room is always processed on the
|
||||
* same shard, preserving in-room order while preventing one heavy room from delaying
|
||||
* all other rooms.</p>
|
||||
*/
|
||||
public final class WiredTickService {
|
||||
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WiredTickService.class);
|
||||
|
||||
/** Default tick interval in milliseconds */
|
||||
|
||||
public static final int DEFAULT_TICK_INTERVAL_MS = 50;
|
||||
|
||||
/** Minimum allowed tick interval (prevents CPU overload) */
|
||||
public static final int MIN_TICK_INTERVAL_MS = 10;
|
||||
|
||||
/** Maximum allowed tick interval */
|
||||
public static final int MAX_TICK_INTERVAL_MS = 500;
|
||||
|
||||
/** Singleton instance */
|
||||
|
||||
public static final int DEFAULT_WORKER_COUNT = Math.max(2, Math.min(8, Runtime.getRuntime().availableProcessors()));
|
||||
public static final int MIN_WORKER_COUNT = 1;
|
||||
public static final int MAX_WORKER_COUNT = 32;
|
||||
|
||||
public static final long SLOW_TICKABLE_THRESHOLD_MS = 100L;
|
||||
public static final long SLOW_ROOM_THRESHOLD_MS = 50L;
|
||||
public static final long SLOW_SHARD_THRESHOLD_MS = 250L;
|
||||
|
||||
private static volatile WiredTickService instance;
|
||||
|
||||
/** The configured tick interval in milliseconds */
|
||||
|
||||
private int tickIntervalMs = DEFAULT_TICK_INTERVAL_MS;
|
||||
|
||||
/** Whether debug logging is enabled */
|
||||
private boolean debugEnabled = false;
|
||||
|
||||
/** Thread priority for the tick service */
|
||||
private int threadPriority = Thread.NORM_PRIORITY + 1;
|
||||
|
||||
/**
|
||||
* Global tick counter - increments every tick.
|
||||
* All repeaters use this to stay synchronized.
|
||||
* Repeaters fire when (tickCount * tickIntervalMs) % repeatTime == 0
|
||||
*/
|
||||
private volatile long tickCount = 0;
|
||||
|
||||
/** The scheduled executor for the tick loop */
|
||||
private ScheduledExecutorService scheduler;
|
||||
|
||||
/** The scheduled future for the tick task */
|
||||
private ScheduledFuture<?> tickTask;
|
||||
|
||||
/** Map of room ID to set of registered tickables */
|
||||
private int workerCount = DEFAULT_WORKER_COUNT;
|
||||
|
||||
/** Global logical tick counter shared by every shard. */
|
||||
private final AtomicLong tickCount = new AtomicLong(0);
|
||||
|
||||
/** Schedules the global logical ticks. */
|
||||
private ScheduledExecutorService coordinator;
|
||||
|
||||
/** One single-thread executor per shard, preserving order inside the shard. */
|
||||
private ExecutorService[] shardExecutors;
|
||||
|
||||
/** Highest logical tick requested for each shard. */
|
||||
private AtomicLong[] shardRequestedTicks;
|
||||
|
||||
/** Last logical tick fully processed by each shard. */
|
||||
private AtomicLong[] shardProcessedTicks;
|
||||
|
||||
/** Whether a shard worker loop is currently scheduled/running. */
|
||||
private AtomicBoolean[] shardScheduled;
|
||||
|
||||
private final ConcurrentHashMap<Integer, Set<WiredTickable>> roomTickables;
|
||||
|
||||
/** Whether the service is running */
|
||||
private final AtomicBoolean running;
|
||||
|
||||
/**
|
||||
* Private constructor for singleton.
|
||||
*/
|
||||
|
||||
private WiredTickService() {
|
||||
this.roomTickables = new ConcurrentHashMap<>();
|
||||
this.running = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads configuration from emulator settings.
|
||||
*/
|
||||
|
||||
private void loadConfiguration() {
|
||||
// Load tick interval
|
||||
int configuredInterval = Emulator.getConfig().getInt("wired.tick.interval.ms", DEFAULT_TICK_INTERVAL_MS);
|
||||
this.tickIntervalMs = Math.max(MIN_TICK_INTERVAL_MS, Math.min(MAX_TICK_INTERVAL_MS, configuredInterval));
|
||||
|
||||
|
||||
if (configuredInterval != this.tickIntervalMs) {
|
||||
LOGGER.warn("wired.tick.interval.ms value {} is out of range [{}-{}], using {}",
|
||||
configuredInterval, MIN_TICK_INTERVAL_MS, MAX_TICK_INTERVAL_MS, this.tickIntervalMs);
|
||||
LOGGER.warn(
|
||||
"wired.tick.interval.ms value {} is out of range [{}-{}], using {}",
|
||||
configuredInterval,
|
||||
MIN_TICK_INTERVAL_MS,
|
||||
MAX_TICK_INTERVAL_MS,
|
||||
this.tickIntervalMs
|
||||
);
|
||||
}
|
||||
|
||||
// Load debug flag
|
||||
|
||||
this.debugEnabled = Emulator.getConfig().getBoolean("wired.tick.debug", false);
|
||||
|
||||
// Load thread priority
|
||||
|
||||
int configuredPriority = Emulator.getConfig().getInt("wired.tick.thread.priority", Thread.NORM_PRIORITY + 1);
|
||||
this.threadPriority = Math.max(Thread.MIN_PRIORITY, Math.min(Thread.MAX_PRIORITY, configuredPriority));
|
||||
|
||||
int configuredWorkers = Emulator.getConfig().getInt("wired.tick.workers", DEFAULT_WORKER_COUNT);
|
||||
this.workerCount = Math.max(MIN_WORKER_COUNT, Math.min(MAX_WORKER_COUNT, configuredWorkers));
|
||||
|
||||
if (configuredWorkers != this.workerCount) {
|
||||
LOGGER.warn(
|
||||
"wired.tick.workers value {} is out of range [{}-{}], using {}",
|
||||
configuredWorkers,
|
||||
MIN_WORKER_COUNT,
|
||||
MAX_WORKER_COUNT,
|
||||
this.workerCount
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configured tick interval in milliseconds.
|
||||
*
|
||||
* @return the tick interval
|
||||
*/
|
||||
|
||||
public int getTickIntervalMs() {
|
||||
return tickIntervalMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if debug logging is enabled.
|
||||
*
|
||||
* @return true if debug is enabled
|
||||
*/
|
||||
|
||||
public boolean isDebugEnabled() {
|
||||
return debugEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance.
|
||||
*
|
||||
* @return the WiredTickService instance
|
||||
*/
|
||||
|
||||
public int getWorkerCount() {
|
||||
return workerCount;
|
||||
}
|
||||
|
||||
public static WiredTickService getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (WiredTickService.class) {
|
||||
@@ -146,150 +123,158 @@ public final class WiredTickService {
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the tick service.
|
||||
* <p>
|
||||
* Should be called during emulator startup after WiredManager.initialize().
|
||||
* </p>
|
||||
*/
|
||||
|
||||
public synchronized void start() {
|
||||
if (running.get()) {
|
||||
LOGGER.warn("WiredTickService already running");
|
||||
return;
|
||||
}
|
||||
|
||||
// Load configuration from emulator settings
|
||||
|
||||
loadConfiguration();
|
||||
|
||||
LOGGER.info("Starting WiredTickService with {}ms tick interval (debug={}, priority={})...",
|
||||
tickIntervalMs, debugEnabled, threadPriority);
|
||||
|
||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "WiredTickService");
|
||||
|
||||
LOGGER.info(
|
||||
"Starting WiredTickService with {}ms tick interval (workers={}, debug={}, priority={})...",
|
||||
tickIntervalMs,
|
||||
workerCount,
|
||||
debugEnabled,
|
||||
threadPriority
|
||||
);
|
||||
|
||||
this.coordinator = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "WiredTickCoordinator");
|
||||
t.setDaemon(true);
|
||||
t.setPriority(threadPriority);
|
||||
return t;
|
||||
});
|
||||
|
||||
this.tickTask = scheduler.scheduleAtFixedRate(
|
||||
this::tick,
|
||||
tickIntervalMs,
|
||||
tickIntervalMs,
|
||||
TimeUnit.MILLISECONDS
|
||||
);
|
||||
|
||||
|
||||
this.shardExecutors = new ExecutorService[workerCount];
|
||||
this.shardRequestedTicks = new AtomicLong[workerCount];
|
||||
this.shardProcessedTicks = new AtomicLong[workerCount];
|
||||
this.shardScheduled = new AtomicBoolean[workerCount];
|
||||
|
||||
for (int i = 0; i < workerCount; i++) {
|
||||
final int shardIndex = i;
|
||||
this.shardExecutors[i] = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "WiredTickShard-" + shardIndex);
|
||||
t.setDaemon(true);
|
||||
t.setPriority(threadPriority);
|
||||
return t;
|
||||
});
|
||||
this.shardRequestedTicks[i] = new AtomicLong(0L);
|
||||
this.shardProcessedTicks[i] = new AtomicLong(0L);
|
||||
this.shardScheduled[i] = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
this.tickCount.set(0L);
|
||||
running.set(true);
|
||||
|
||||
this.coordinator.scheduleAtFixedRate(
|
||||
() -> {
|
||||
try {
|
||||
dispatchTick();
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error("WiredTickService fatal coordinator error", t);
|
||||
}
|
||||
},
|
||||
tickIntervalMs,
|
||||
tickIntervalMs,
|
||||
TimeUnit.MILLISECONDS
|
||||
);
|
||||
|
||||
LOGGER.info("WiredTickService started successfully");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the tick service.
|
||||
* <p>
|
||||
* Should be called during emulator shutdown.
|
||||
* </p>
|
||||
*/
|
||||
|
||||
public synchronized void stop() {
|
||||
if (!running.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
LOGGER.info("Stopping WiredTickService...");
|
||||
|
||||
running.set(false);
|
||||
|
||||
if (tickTask != null) {
|
||||
tickTask.cancel(false);
|
||||
tickTask = null;
|
||||
}
|
||||
|
||||
if (scheduler != null) {
|
||||
scheduler.shutdown();
|
||||
|
||||
if (coordinator != null) {
|
||||
coordinator.shutdown();
|
||||
try {
|
||||
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
scheduler.shutdownNow();
|
||||
if (!coordinator.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
coordinator.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
scheduler.shutdownNow();
|
||||
coordinator.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
scheduler = null;
|
||||
coordinator = null;
|
||||
}
|
||||
|
||||
|
||||
if (shardExecutors != null) {
|
||||
for (ExecutorService executor : shardExecutors) {
|
||||
if (executor != null) {
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
for (ExecutorService executor : shardExecutors) {
|
||||
if (executor == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
executor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shardExecutors = null;
|
||||
shardRequestedTicks = null;
|
||||
shardProcessedTicks = null;
|
||||
shardScheduled = null;
|
||||
|
||||
roomTickables.clear();
|
||||
LOGGER.info("WiredTickService stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the service is running.
|
||||
*
|
||||
* @return true if running
|
||||
*/
|
||||
|
||||
public boolean isRunning() {
|
||||
return running.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a tickable item with the service.
|
||||
* <p>
|
||||
* The item will start receiving {@link WiredTickable#onWiredTick} calls
|
||||
* on the next tick cycle.
|
||||
* </p>
|
||||
*
|
||||
* @param room the room the item is in
|
||||
* @param tickable the tickable item
|
||||
*/
|
||||
|
||||
public void register(Room room, WiredTickable tickable) {
|
||||
if (room == null || tickable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
int roomId = room.getId();
|
||||
Set<WiredTickable> tickables = roomTickables.computeIfAbsent(
|
||||
roomId,
|
||||
k -> ConcurrentHashMap.newKeySet()
|
||||
);
|
||||
|
||||
Set<WiredTickable> tickables = roomTickables.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet());
|
||||
|
||||
if (tickables.add(tickable)) {
|
||||
tickable.onRegistered(room, System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a tickable item from the service.
|
||||
*
|
||||
* @param room the room the item was in
|
||||
* @param tickable the tickable item
|
||||
*/
|
||||
|
||||
public void unregister(Room room, WiredTickable tickable) {
|
||||
if (room == null || tickable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
int roomId = room.getId();
|
||||
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
||||
|
||||
|
||||
if (tickables != null) {
|
||||
if (tickables.remove(tickable)) {
|
||||
tickable.onUnregistered(room);
|
||||
}
|
||||
|
||||
// Clean up empty sets
|
||||
|
||||
if (tickables.isEmpty()) {
|
||||
roomTickables.remove(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a tickable by ID.
|
||||
*
|
||||
* @param roomId the room ID
|
||||
* @param tickableId the tickable item ID
|
||||
*/
|
||||
|
||||
public void unregister(int roomId, int tickableId) {
|
||||
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
||||
|
||||
|
||||
if (tickables != null) {
|
||||
tickables.removeIf(t -> {
|
||||
if (t.getId() == tickableId) {
|
||||
@@ -301,162 +286,240 @@ public final class WiredTickService {
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
if (tickables.isEmpty()) {
|
||||
roomTickables.remove(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters all tickables for a room.
|
||||
* <p>
|
||||
* Should be called when a room is unloaded.
|
||||
* </p>
|
||||
*
|
||||
* @param room the room
|
||||
*/
|
||||
|
||||
public void unregisterRoom(Room room) {
|
||||
if (room == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Set<WiredTickable> tickables = roomTickables.remove(room.getId());
|
||||
|
||||
|
||||
if (tickables != null) {
|
||||
for (WiredTickable tickable : tickables) {
|
||||
tickable.onUnregistered(room);
|
||||
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
|
||||
for (WiredTickable tickable : snapshot) {
|
||||
try {
|
||||
if (tickable != null) {
|
||||
tickable.onUnregistered(room);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error(
|
||||
"Error unregistering tickable {} from room {}",
|
||||
tickable != null ? tickable.getId() : -1,
|
||||
room.getId(),
|
||||
t
|
||||
);
|
||||
}
|
||||
}
|
||||
LOGGER.debug("Unregistered {} tickables from room {}", tickables.size(), room.getId());
|
||||
LOGGER.debug("Unregistered {} tickables from room {}", snapshot.length, room.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all timers in a room.
|
||||
*
|
||||
* @param room the room
|
||||
*/
|
||||
|
||||
public void resetRoomTimers(Room room) {
|
||||
if (room == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Set<WiredTickable> tickables = roomTickables.get(room.getId());
|
||||
|
||||
|
||||
if (tickables != null) {
|
||||
for (WiredTickable tickable : tickables) {
|
||||
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
|
||||
for (WiredTickable tickable : snapshot) {
|
||||
try {
|
||||
tickable.resetTimer();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error resetting timer for tickable {} in room {}",
|
||||
tickable.getId(), room.getId(), e);
|
||||
if (tickable != null) {
|
||||
tickable.resetTimer();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error(
|
||||
"Error resetting timer for tickable {} in room {}",
|
||||
tickable != null ? tickable.getId() : -1,
|
||||
room.getId(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of registered tickables for a room.
|
||||
*
|
||||
* @param roomId the room ID
|
||||
* @return the count
|
||||
*/
|
||||
|
||||
public int getTickableCount(int roomId) {
|
||||
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
||||
return tickables != null ? tickables.size() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total count of registered tickables across all rooms.
|
||||
*
|
||||
* @return the total count
|
||||
*/
|
||||
|
||||
public int getTotalTickableCount() {
|
||||
return roomTickables.values().stream()
|
||||
.mapToInt(Set::size)
|
||||
.sum();
|
||||
return roomTickables.values().stream().mapToInt(Set::size).sum();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of rooms with registered tickables.
|
||||
*
|
||||
* @return the room count
|
||||
*/
|
||||
|
||||
public int getActiveRoomCount() {
|
||||
return roomTickables.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* The main tick loop.
|
||||
* <p>
|
||||
* Called at the configured interval by the scheduler. Processes all registered tickables
|
||||
* across all rooms.
|
||||
* </p>
|
||||
*/
|
||||
private void tick() {
|
||||
|
||||
public long getTickCount() {
|
||||
return tickCount.get();
|
||||
}
|
||||
|
||||
private void dispatchTick() {
|
||||
if (!running.get() || Emulator.isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment global tick counter
|
||||
tickCount++;
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
int tickablesProcessed = 0;
|
||||
|
||||
|
||||
long currentTick = tickCount.incrementAndGet();
|
||||
|
||||
for (int shardIndex = 0; shardIndex < workerCount; shardIndex++) {
|
||||
shardRequestedTicks[shardIndex].set(currentTick);
|
||||
scheduleShardIfNeeded(shardIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleShardIfNeeded(int shardIndex) {
|
||||
if (!running.get() || shardExecutors == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shardScheduled[shardIndex].compareAndSet(false, true)) {
|
||||
shardExecutors[shardIndex].execute(() -> runShardLoop(shardIndex));
|
||||
}
|
||||
}
|
||||
|
||||
private void runShardLoop(int shardIndex) {
|
||||
try {
|
||||
while (running.get() && !Emulator.isShuttingDown) {
|
||||
long nextTick = shardProcessedTicks[shardIndex].get() + 1L;
|
||||
long requestedTick = shardRequestedTicks[shardIndex].get();
|
||||
|
||||
if (nextTick > requestedTick) {
|
||||
break;
|
||||
}
|
||||
|
||||
processShardTick(shardIndex, nextTick);
|
||||
shardProcessedTicks[shardIndex].set(nextTick);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error("Fatal error in WiredTick shard {}", shardIndex, t);
|
||||
} finally {
|
||||
shardScheduled[shardIndex].set(false);
|
||||
if (running.get() && shardProcessedTicks[shardIndex].get() < shardRequestedTicks[shardIndex].get()) {
|
||||
scheduleShardIfNeeded(shardIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processShardTick(int shardIndex, long currentTick) {
|
||||
long shardStart = System.currentTimeMillis();
|
||||
int processedTickables = 0;
|
||||
int processedRooms = 0;
|
||||
|
||||
for (Map.Entry<Integer, Set<WiredTickable>> entry : roomTickables.entrySet()) {
|
||||
int roomId = entry.getKey();
|
||||
Set<WiredTickable> tickables = entry.getValue();
|
||||
|
||||
if (tickables.isEmpty()) {
|
||||
if (getShardIndex(roomId) != shardIndex) {
|
||||
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);
|
||||
if (room == null || !room.isLoaded()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if room is empty (optimization)
|
||||
|
||||
if (room.getCurrentHabbos().isEmpty() && room.getCurrentBots().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process each tickable
|
||||
for (WiredTickable tickable : tickables) {
|
||||
|
||||
long roomStart = System.currentTimeMillis();
|
||||
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
|
||||
if (snapshot.length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
processedRooms++;
|
||||
|
||||
for (WiredTickable tickable : snapshot) {
|
||||
long tickableStart = System.currentTimeMillis();
|
||||
|
||||
if (tickable == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify item still belongs to this room
|
||||
if (tickable.getRoomId() != roomId) {
|
||||
// Item moved to another room, unregister it
|
||||
tickables.remove(tickable);
|
||||
unregister(roomId, tickable.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pass global tick count - all tickables see the same counter
|
||||
// This keeps repeaters with the same interval perfectly synchronized
|
||||
tickable.onWiredTick(room, tickCount, tickIntervalMs);
|
||||
tickablesProcessed++;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error in wired tick for tickable {} in room {}: {}",
|
||||
tickable.getId(), roomId, e.getMessage(), e);
|
||||
|
||||
tickable.onWiredTick(room, currentTick, tickIntervalMs);
|
||||
processedTickables++;
|
||||
|
||||
long tickableDuration = System.currentTimeMillis() - tickableStart;
|
||||
if (tickableDuration > SLOW_TICKABLE_THRESHOLD_MS) {
|
||||
LOGGER.warn(
|
||||
"Slow wired tickable: shard={}, room={}, tick={}, tickableId={}, class={}, took={}ms",
|
||||
shardIndex,
|
||||
roomId,
|
||||
currentTick,
|
||||
tickable.getId(),
|
||||
tickable.getClass().getName(),
|
||||
tickableDuration
|
||||
);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
long tickableDuration = System.currentTimeMillis() - tickableStart;
|
||||
LOGGER.error(
|
||||
"Error in wired tick for tickable {} in room {} after {}ms",
|
||||
tickable.getId(),
|
||||
roomId,
|
||||
tickableDuration,
|
||||
t
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
long roomDuration = System.currentTimeMillis() - roomStart;
|
||||
if (roomDuration > SLOW_ROOM_THRESHOLD_MS) {
|
||||
LOGGER.warn(
|
||||
"Slow wired room tick: shard={}, room={}, tick={}, tickables={}, took={}ms",
|
||||
shardIndex,
|
||||
roomId,
|
||||
currentTick,
|
||||
snapshot.length,
|
||||
roomDuration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Debug logging if enabled
|
||||
if (debugEnabled && tickablesProcessed > 0) {
|
||||
LOGGER.debug("Wired tick #{} completed: {} tickables processed in {}ms",
|
||||
tickCount, tickablesProcessed, System.currentTimeMillis() - startTime);
|
||||
|
||||
long shardDuration = System.currentTimeMillis() - shardStart;
|
||||
if (shardDuration > SLOW_SHARD_THRESHOLD_MS) {
|
||||
LOGGER.warn(
|
||||
"Slow wired shard tick: shard={}, tick={}, rooms={}, tickables={}, took={}ms",
|
||||
shardIndex,
|
||||
currentTick,
|
||||
processedRooms,
|
||||
processedTickables,
|
||||
shardDuration
|
||||
);
|
||||
}
|
||||
|
||||
if (debugEnabled && processedTickables > 0) {
|
||||
LOGGER.debug(
|
||||
"Wired shard tick completed: shard={}, tick={}, rooms={}, tickables={}, took={}ms",
|
||||
shardIndex,
|
||||
currentTick,
|
||||
processedRooms,
|
||||
processedTickables,
|
||||
shardDuration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current global tick count.
|
||||
*
|
||||
* @return the tick count
|
||||
*/
|
||||
public long getTickCount() {
|
||||
return tickCount;
|
||||
|
||||
private int getShardIndex(int roomId) {
|
||||
return Math.floorMod(roomId, workerCount);
|
||||
}
|
||||
}
|
||||
|
||||
+194
-42
@@ -44,15 +44,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
LOGGER.error("DEBUG GIFT: entered CatalogBuyItemAsGiftEvent.handle()");
|
||||
|
||||
if (Emulator.getIntUnixTimestamp() - this.client.getHabbo().getHabboStats().lastGiftTimestamp >= CatalogManager.PURCHASE_COOLDOWN) {
|
||||
this.client.getHabbo().getHabboStats().lastGiftTimestamp = Emulator.getIntUnixTimestamp();
|
||||
|
||||
if (ShutdownEmulator.timestamp > 0) {
|
||||
LOGGER.error("DEBUG GIFT: emulator closing");
|
||||
this.client.sendResponse(new HotelWillCloseInMinutesComposer((ShutdownEmulator.timestamp - Emulator.getIntUnixTimestamp()) / 60));
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.client.getHabbo().getHabboStats().isPurchasingFurniture) {
|
||||
LOGGER.error("DEBUG GIFT: isPurchasingFurniture already true");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
} else {
|
||||
@@ -60,7 +65,6 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
int pageId = this.packet.readInt();
|
||||
int itemId = this.packet.readInt();
|
||||
String extraData = this.packet.readString();
|
||||
@@ -71,14 +75,22 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
int ribbonId = this.packet.readInt();
|
||||
boolean showName = this.packet.readBoolean();
|
||||
|
||||
LOGGER.error(
|
||||
"DEBUG GIFT: pageId={}, itemId={}, extraData={}, username={}, spriteId={}, color={}, ribbonId={}, showName={}, message={}",
|
||||
pageId, itemId, extraData, username, spriteId, color, ribbonId, showName, message
|
||||
);
|
||||
|
||||
int userId = 0;
|
||||
|
||||
if (!Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId) && !Emulator.getGameEnvironment().getCatalogManager().giftFurnis.containsKey(spriteId)) {
|
||||
if (!Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId)
|
||||
&& !Emulator.getGameEnvironment().getCatalogManager().giftFurnis.containsKey(spriteId)) {
|
||||
LOGGER.error("DEBUG GIFT: invalid spriteId for gift wrapper/furni -> {}", spriteId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!GiftConfigurationComposer.BOX_TYPES.contains(color) || !GiftConfigurationComposer.RIBBON_TYPES.contains(ribbonId)) {
|
||||
LOGGER.error("DEBUG GIFT: invalid color/ribbon -> color={}, ribbonId={}", color, ribbonId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
@@ -89,10 +101,12 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
Integer iItemId = Emulator.getGameEnvironment().getCatalogManager().giftWrappers.get(spriteId);
|
||||
|
||||
if (iItemId == null)
|
||||
if (iItemId == null) {
|
||||
iItemId = Emulator.getGameEnvironment().getCatalogManager().giftFurnis.get(spriteId);
|
||||
}
|
||||
|
||||
if (iItemId == null) {
|
||||
LOGGER.error("DEBUG GIFT: iItemId null for spriteId={}", spriteId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
@@ -100,9 +114,15 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
Item giftItem = Emulator.getGameEnvironment().getItemManager().getItem(iItemId);
|
||||
|
||||
if (giftItem == null) {
|
||||
giftItem = Emulator.getGameEnvironment().getItemManager().getItem((Integer) Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray()[Emulator.getRandom().nextInt(Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size())]);
|
||||
LOGGER.error("DEBUG GIFT: direct giftItem null, trying random fallback. iItemId={}", iItemId);
|
||||
giftItem = Emulator.getGameEnvironment().getItemManager().getItem(
|
||||
(Integer) Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray()[
|
||||
Emulator.getRandom().nextInt(Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size())
|
||||
]
|
||||
);
|
||||
|
||||
if (giftItem == null) {
|
||||
LOGGER.error("DEBUG GIFT: fallback giftItem also null");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
@@ -112,6 +132,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
|
||||
|
||||
if (habbo == null) {
|
||||
LOGGER.error("DEBUG GIFT: target user not online, checking DB -> {}", username);
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE username = ?")) {
|
||||
statement.setString(1, username);
|
||||
|
||||
@@ -128,6 +149,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
|
||||
if (userId == 0) {
|
||||
LOGGER.error("DEBUG GIFT: receiver not found -> {}", username);
|
||||
this.client.sendResponse(new GiftReceiverNotFoundComposer());
|
||||
return;
|
||||
}
|
||||
@@ -135,11 +157,17 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId);
|
||||
|
||||
if (page == null) {
|
||||
LOGGER.error("DEBUG GIFT: page null -> {}", pageId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (page.getRank() > this.client.getHabbo().getHabboInfo().getRank().getId() || !page.isEnabled() || !page.isVisible()) {
|
||||
LOGGER.error("DEBUG GIFT: page access denied. pageRank={}, userRank={}, enabled={}, visible={}",
|
||||
page.getRank(),
|
||||
this.client.getHabbo().getHabboInfo().getRank().getId(),
|
||||
page.isEnabled(),
|
||||
page.isVisible());
|
||||
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
|
||||
return;
|
||||
}
|
||||
@@ -147,17 +175,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
CatalogItem item = page.getCatalogItem(itemId);
|
||||
|
||||
if (item == null) {
|
||||
LOGGER.error("DEBUG GIFT: catalog item null -> {}", itemId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.isClubOnly() && !this.client.getHabbo().getHabboStats().hasActiveClub()) {
|
||||
LOGGER.error("DEBUG GIFT: item requires club -> itemId={}", itemId);
|
||||
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.REQUIRES_CLUB));
|
||||
return;
|
||||
}
|
||||
|
||||
for (Item baseItem : item.getBaseItems()) {
|
||||
if (!baseItem.allowGift()) {
|
||||
LOGGER.error("DEBUG GIFT: base item not giftable -> baseItemId={}, name={}", baseItem.getId(), baseItem.getName());
|
||||
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
|
||||
return;
|
||||
}
|
||||
@@ -165,6 +196,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
if (item.isLimited()) {
|
||||
if (item.getLimitedStack() == item.getLimitedSells()) {
|
||||
LOGGER.error("DEBUG GIFT: LTD sold out -> itemId={}", itemId);
|
||||
this.client.sendResponse(new AlertLimitedSoldOutComposer());
|
||||
return;
|
||||
}
|
||||
@@ -173,7 +205,14 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
int totalCredits = item.getCredits();
|
||||
int totalPoints = item.getPoints();
|
||||
|
||||
if(totalCredits > this.client.getHabbo().getHabboInfo().getCredits() || totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType())) {
|
||||
if (totalCredits > this.client.getHabbo().getHabboInfo().getCredits()
|
||||
|| totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType())) {
|
||||
LOGGER.error("DEBUG GIFT: not enough currency. creditsNeeded={}, creditsHave={}, pointsNeeded={}, pointsHave={}, pointsType={}",
|
||||
totalCredits,
|
||||
this.client.getHabbo().getHabboInfo().getCredits(),
|
||||
totalPoints,
|
||||
this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType()),
|
||||
item.getPointsType());
|
||||
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
|
||||
return;
|
||||
}
|
||||
@@ -181,23 +220,34 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
CatalogLimitedConfiguration limitedConfiguration = null;
|
||||
int limitedStack = 0;
|
||||
int limitedNumber = 0;
|
||||
|
||||
if (item.isLimited()) {
|
||||
if (Emulator.getGameEnvironment().getCatalogManager().getLimitedConfig(item).available() == 0) {
|
||||
LOGGER.error("DEBUG GIFT: LTD available=0 -> itemId={}", itemId);
|
||||
this.client.sendResponse(new AlertLimitedSoldOutComposer());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check daily LTD limits for the buyer (sender of the gift)
|
||||
if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) {
|
||||
int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total");
|
||||
if (this.client.getHabbo().getHabboStats().totalLtds() >= ltdLimit) {
|
||||
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total").replace("%itemname%", item.getBaseItems().iterator().next().getFullName()).replace("%limit%", ltdLimit + ""));
|
||||
LOGGER.error("DEBUG GIFT: sender reached daily total LTD limit");
|
||||
this.client.getHabbo().alert(
|
||||
Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total")
|
||||
.replace("%itemname%", item.getBaseItems().iterator().next().getFullName())
|
||||
.replace("%limit%", ltdLimit + "")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item");
|
||||
if (this.client.getHabbo().getHabboStats().totalLtds(item.getId()) >= ltdLimit) {
|
||||
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item").replace("%itemname%", item.getBaseItems().iterator().next().getFullName()).replace("%limit%", ltdLimit + ""));
|
||||
LOGGER.error("DEBUG GIFT: sender reached daily LTD item limit");
|
||||
this.client.getHabbo().alert(
|
||||
Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item")
|
||||
.replace("%itemname%", item.getBaseItems().iterator().next().getFullName())
|
||||
.replace("%limit%", ltdLimit + "")
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -210,8 +260,6 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
limitedNumber = limitedConfiguration.getNumber();
|
||||
limitedStack = limitedConfiguration.getTotalSet();
|
||||
|
||||
// Log the LTD purchase for daily limits
|
||||
this.client.getHabbo().getHabboStats().addLtdLog(item.getId(), Emulator.getIntUnixTimestamp());
|
||||
}
|
||||
|
||||
@@ -229,6 +277,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) as c FROM users_badges WHERE user_id = ? AND badge_code LIKE ?")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setString(2, baseItem.getName());
|
||||
|
||||
try (ResultSet rSet = statement.executeQuery()) {
|
||||
if (rSet.next()) {
|
||||
c = rSet.getInt("c");
|
||||
@@ -244,17 +293,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
|
||||
if (badgeFound) {
|
||||
LOGGER.error("DEBUG GIFT: receiver already has badge");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.ALREADY_HAVE_BADGE));
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.getAmount() > 1 || item.getBaseItems().size() > 1) {
|
||||
LOGGER.error("DEBUG GIFT: unsupported multi amount/baseItems. amount={}, baseItems={}", item.getAmount(), item.getBaseItems().size());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
for (Item baseItem : item.getBaseItems()) {
|
||||
if (item.getItemAmount(baseItem.getId()) > 1) {
|
||||
LOGGER.error("DEBUG GIFT: unsupported item amount > 1 for baseItemId={}", baseItem.getId());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
@@ -278,37 +330,88 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
badgeFound = true;
|
||||
}
|
||||
} else if (item.getName().startsWith("rentable_bot_")) {
|
||||
LOGGER.error("DEBUG GIFT: rentable bot gifts not supported");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
} else if (Item.isPet(baseItem)) {
|
||||
LOGGER.error("DEBUG GIFT: pet gifts not supported");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
} else {
|
||||
if (baseItem.getInteractionType().getType() == InteractionTrophy.class || baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class) {
|
||||
if (baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class && habbo != null && !habbo.getClient().getHabbo().getInventory().getBadgesComponent().hasBadge(extraData)) {
|
||||
ScripterManager.scripterDetected(habbo.getClient(), Emulator.getTexts().getValue("scripter.warning.catalog.badge_display").replace("%username%", habbo.getClient().getHabbo().getHabboInfo().getUsername()).replace("%badge%", extraData));
|
||||
if (baseItem.getInteractionType().getType() == InteractionTrophy.class
|
||||
|| baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class) {
|
||||
if (baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class
|
||||
&& habbo != null
|
||||
&& !habbo.getClient().getHabbo().getInventory().getBadgesComponent().hasBadge(extraData)) {
|
||||
ScripterManager.scripterDetected(
|
||||
habbo.getClient(),
|
||||
Emulator.getTexts().getValue("scripter.warning.catalog.badge_display")
|
||||
.replace("%username%", habbo.getClient().getHabbo().getHabboInfo().getUsername())
|
||||
.replace("%badge%", extraData)
|
||||
);
|
||||
extraData = "UMAD";
|
||||
}
|
||||
|
||||
extraData = this.client.getHabbo().getHabboInfo().getUsername() + (char) 9 + Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "-" + (Calendar.getInstance().get(Calendar.MONTH) + 1) + "-" + Calendar.getInstance().get(Calendar.YEAR) + (char) 9 + extraData;
|
||||
extraData = this.client.getHabbo().getHabboInfo().getUsername()
|
||||
+ (char) 9
|
||||
+ Calendar.getInstance().get(Calendar.DAY_OF_MONTH)
|
||||
+ "-"
|
||||
+ (Calendar.getInstance().get(Calendar.MONTH) + 1)
|
||||
+ "-"
|
||||
+ Calendar.getInstance().get(Calendar.YEAR)
|
||||
+ (char) 9
|
||||
+ extraData;
|
||||
}
|
||||
|
||||
if (baseItem.getInteractionType().getType() == InteractionTeleport.class || baseItem.getInteractionType().getType() == InteractionTeleportTile.class) {
|
||||
HabboItem teleportOne = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
|
||||
HabboItem teleportTwo = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
|
||||
if (baseItem.getInteractionType().getType() == InteractionTeleport.class
|
||||
|| baseItem.getInteractionType().getType() == InteractionTeleportTile.class) {
|
||||
|
||||
HabboItem teleportOne = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
|
||||
HabboItem teleportTwo = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
|
||||
|
||||
if (teleportOne == null || teleportTwo == null) {
|
||||
LOGGER.error("DEBUG GIFT: teleport creation failed. baseItemId={}, teleportOneNull={}, teleportTwoNull={}",
|
||||
baseItem.getId(), teleportOne == null, teleportTwo == null);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
Emulator.getGameEnvironment().getItemManager().insertTeleportPair(teleportOne.getId(), teleportTwo.getId());
|
||||
itemsList.add(teleportOne);
|
||||
itemsList.add(teleportTwo);
|
||||
|
||||
} else if (baseItem.getInteractionType().getType() == InteractionHopper.class) {
|
||||
HabboItem hopper = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedNumber, limitedNumber, extraData);
|
||||
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedNumber, limitedNumber, extraData);
|
||||
|
||||
Emulator.getGameEnvironment().getItemManager().insertHopper(hopper);
|
||||
if (habboItem == null) {
|
||||
LOGGER.error("DEBUG GIFT: hopper creation failed. baseItemId={}", baseItem.getId());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
itemsList.add(hopper);
|
||||
} else if (baseItem.getInteractionType().getType() == InteractionGuildFurni.class || baseItem.getInteractionType().getType() == InteractionGuildGate.class) {
|
||||
InteractionGuildFurni habboItem = (InteractionGuildFurni) Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
|
||||
Emulator.getGameEnvironment().getItemManager().insertHopper(habboItem);
|
||||
itemsList.add(habboItem);
|
||||
|
||||
} else if (baseItem.getInteractionType().getType() == InteractionGuildFurni.class
|
||||
|| baseItem.getInteractionType().getType() == InteractionGuildGate.class) {
|
||||
HabboItem createdItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
|
||||
|
||||
if (createdItem == null) {
|
||||
LOGGER.error("DEBUG GIFT: guild item creation failed. baseItemId={}", baseItem.getId());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(createdItem instanceof InteractionGuildFurni)) {
|
||||
LOGGER.error("DEBUG GIFT: created guild item has wrong class -> {}", createdItem.getClass().getName());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
InteractionGuildFurni habboItem = (InteractionGuildFurni) createdItem;
|
||||
habboItem.setExtradata("");
|
||||
habboItem.needsUpdate(true);
|
||||
|
||||
int guildId;
|
||||
try {
|
||||
guildId = Integer.parseInt(extraData);
|
||||
@@ -317,15 +420,24 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
Emulator.getThreading().run(habboItem);
|
||||
Emulator.getGameEnvironment().getGuildManager().setGuild(habboItem, guildId);
|
||||
itemsList.add(habboItem);
|
||||
} else {
|
||||
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
|
||||
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
|
||||
|
||||
if (habboItem == null) {
|
||||
LOGGER.error("DEBUG GIFT: normal item creation failed. baseItemId={}, baseItemName={}", baseItem.getId(), baseItem.getName());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
itemsList.add(habboItem);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOGGER.error("DEBUG GIFT: avatar_effect not supported");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
this.client.sendResponse(new GenericAlertComposer(Emulator.getTexts().getValue("error.catalog.buy.not_yet")));
|
||||
return;
|
||||
@@ -333,48 +445,85 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder giftData = new StringBuilder(itemsList.size() + "\t");
|
||||
|
||||
for (HabboItem i : itemsList) {
|
||||
giftData.append(i.getId()).append("\t");
|
||||
}
|
||||
|
||||
giftData.append(color).append("\t").append(ribbonId).append("\t").append(showName ? "1" : "0").append("\t").append(message.replace("\t", "")).append("\t").append(this.client.getHabbo().getHabboInfo().getUsername()).append("\t").append(this.client.getHabbo().getHabboInfo().getLook());
|
||||
|
||||
HabboItem gift = Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, giftData.toString(), 0, 0);
|
||||
|
||||
if (gift == null) {
|
||||
if (itemsList.isEmpty()) {
|
||||
LOGGER.error("DEBUG GIFT: itemsList empty before giftData");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder giftData = new StringBuilder(itemsList.size() + "\t");
|
||||
|
||||
for (HabboItem i : itemsList) {
|
||||
if (i == null) {
|
||||
LOGGER.error("DEBUG GIFT: null HabboItem detected inside itemsList");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
giftData.append(i.getId()).append("\t");
|
||||
}
|
||||
|
||||
giftData.append(color)
|
||||
.append("\t")
|
||||
.append(ribbonId)
|
||||
.append("\t")
|
||||
.append(showName ? "1" : "0")
|
||||
.append("\t")
|
||||
.append(message.replace("\t", ""))
|
||||
.append("\t")
|
||||
.append(this.client.getHabbo().getHabboInfo().getUsername())
|
||||
.append("\t")
|
||||
.append(this.client.getHabbo().getHabboInfo().getLook());
|
||||
|
||||
HabboItem gift = Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, giftData.toString(), 0, 0);
|
||||
|
||||
if (gift == null) {
|
||||
LOGGER.error("DEBUG GIFT: createGift returned null");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark limited items as sold in the database to prevent duplication after catalog reload
|
||||
if (limitedConfiguration != null) {
|
||||
for (HabboItem itm : itemsList) {
|
||||
if (itm == null) {
|
||||
LOGGER.error("DEBUG GIFT: null item before limitedSold()");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
limitedConfiguration.limitedSold(item.getId(), this.client.getHabbo(), itm);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.client.getHabbo().getHabboInfo().getId() != userId) {
|
||||
AchievementManager.progressAchievement(this.client.getHabbo(), Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftGiver"));
|
||||
AchievementManager.progressAchievement(
|
||||
this.client.getHabbo(),
|
||||
Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftGiver")
|
||||
);
|
||||
}
|
||||
|
||||
if (habbo != null) {
|
||||
habbo.getClient().sendResponse(new AddHabboItemComposer(gift));
|
||||
habbo.getClient().getHabbo().getInventory().getItemsComponent().addItem(gift);
|
||||
habbo.getClient().sendResponse(new InventoryRefreshComposer());
|
||||
|
||||
THashMap<String, String> keys = new THashMap<>();
|
||||
keys.put("display", "BUBBLE");
|
||||
keys.put("image", "${image.library.url}notifications/gift.gif");
|
||||
keys.put("message", Emulator.getTexts().getValue("generic.gift.received.anonymous"));
|
||||
|
||||
if (showName) {
|
||||
keys.put("message", Emulator.getTexts().getValue("generic.gift.received").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
|
||||
keys.put("message", Emulator.getTexts().getValue("generic.gift.received")
|
||||
.replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
|
||||
}
|
||||
|
||||
habbo.getClient().sendResponse(new BubbleAlertComposer(BubbleAlertKeys.RECEIVED_BADGE.key, keys));
|
||||
}
|
||||
|
||||
if (this.client.getHabbo().getHabboInfo().getId() != userId) {
|
||||
AchievementManager.progressAchievement(userId, Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftReceiver"));
|
||||
AchievementManager.progressAchievement(
|
||||
userId,
|
||||
Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftReceiver")
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) {
|
||||
@@ -382,6 +531,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
this.client.getHabbo().giveCredits(-totalCredits);
|
||||
}
|
||||
}
|
||||
|
||||
if (totalPoints > 0) {
|
||||
if (item.getPointsType() == 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_PIXELS)) {
|
||||
this.client.getHabbo().givePixels(-totalPoints);
|
||||
@@ -390,16 +540,18 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.error("DEBUG GIFT: success sending PurchaseOKComposer");
|
||||
this.client.sendResponse(new PurchaseOKComposer(item));
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Exception caught", e);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Exception caught", e);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
} finally {
|
||||
this.client.getHabbo().getHabboStats().isPurchasingFurniture = false;
|
||||
}
|
||||
} else {
|
||||
LOGGER.error("DEBUG GIFT: cooldown blocked purchase");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
-3
@@ -26,9 +26,16 @@ public class RoomUserWalkEvent extends MessageHandler {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RoomUserWalkEvent.class);
|
||||
public static final String CONTROL_KEY = "control";
|
||||
|
||||
private static final String WALK_FLOOD_COUNT_KEY = "__walkFloodCount";
|
||||
private static final String WALK_FLOOD_WINDOW_KEY = "__walkFloodWindow";
|
||||
private static final String WALK_LAST_X_KEY = "__walkLastX";
|
||||
private static final String WALK_LAST_Y_KEY = "__walkLastY";
|
||||
|
||||
private static final int MAX_WALKS_PER_SECOND = 15;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return Emulator.getConfig().getInt("pathfinder.click.delay", 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -37,8 +44,43 @@ public class RoomUserWalkEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
int x = this.packet.readInt(); // Position X
|
||||
int y = this.packet.readInt(); // Position Y
|
||||
int x = this.packet.readInt();
|
||||
int y = this.packet.readInt();
|
||||
|
||||
RoomUnit unit = this.client.getHabbo().getRoomUnit();
|
||||
if (unit != null) {
|
||||
long now = System.currentTimeMillis();
|
||||
Object windowObj = unit.getCacheable().get(WALK_FLOOD_WINDOW_KEY);
|
||||
Object countObj = unit.getCacheable().get(WALK_FLOOD_COUNT_KEY);
|
||||
|
||||
long windowStart = (windowObj instanceof Long) ? (Long) windowObj : 0L;
|
||||
int count = (countObj instanceof Integer) ? (Integer) countObj : 0;
|
||||
|
||||
if (now - windowStart > 1000) {
|
||||
// New 1-second window
|
||||
windowStart = now;
|
||||
count = 0;
|
||||
}
|
||||
|
||||
count++;
|
||||
unit.getCacheable().put(WALK_FLOOD_WINDOW_KEY, windowStart);
|
||||
unit.getCacheable().put(WALK_FLOOD_COUNT_KEY, count);
|
||||
|
||||
if (count > MAX_WALKS_PER_SECOND) {
|
||||
unit.getCacheable().put(WALK_LAST_X_KEY, x);
|
||||
unit.getCacheable().put(WALK_LAST_Y_KEY, y);
|
||||
return;
|
||||
}
|
||||
|
||||
Object lastX = unit.getCacheable().get(WALK_LAST_X_KEY);
|
||||
Object lastY = unit.getCacheable().get(WALK_LAST_Y_KEY);
|
||||
if (lastX != null && lastY != null) {
|
||||
x = (Integer) lastX;
|
||||
y = (Integer) lastY;
|
||||
unit.getCacheable().remove(WALK_LAST_X_KEY);
|
||||
unit.getCacheable().remove(WALK_LAST_Y_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
Habbo habbo = getControlledHabbo();
|
||||
if (habbo == null) {
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user