Merge pull request #45 from Lorenzune/feature/pr-20260325

Refine wired movement sync, extra behavior, and source handling
This commit is contained in:
DuckieTM
2026-03-25 07:38:32 +01:00
committed by GitHub
31 changed files with 1261 additions and 137 deletions
+1 -1
View File
@@ -8,5 +8,5 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="openjdk-23" project-jdk-type="JavaSDK" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="graalvm-jdk-21" project-jdk-type="JavaSDK" />
</project>
@@ -54,6 +54,8 @@ import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraAnimatio
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurni;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUser;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraMoveCarryUsers;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecuteInOrder;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecutionLimit;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraMovePhysics;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraMoveNoAnimation;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraOrEval;
@@ -349,6 +351,8 @@ public class ItemManager {
this.interactionsList.add(new ItemInteraction("wf_xtra_mov_no_animation", WiredExtraMoveNoAnimation.class));
this.interactionsList.add(new ItemInteraction("wf_xtra_anim_time", WiredExtraAnimationTime.class));
this.interactionsList.add(new ItemInteraction("wf_xtra_mov_physics", WiredExtraMovePhysics.class));
this.interactionsList.add(new ItemInteraction("wf_xtra_exec_in_order", WiredExtraExecuteInOrder.class));
this.interactionsList.add(new ItemInteraction("wf_xtra_execution_limit", WiredExtraExecutionLimit.class));
this.interactionsList.add(new ItemInteraction("wf_highscore", InteractionWiredHighscore.class));
@@ -162,14 +162,7 @@ public class WiredConditionActorDir extends InteractionWiredCondition {
}
private int normalizeUserSource(int value) {
switch (value) {
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
private int normalizeQuantifier(int value) {
@@ -170,14 +170,7 @@ public class WiredConditionGroupMember extends InteractionWiredCondition {
}
private int normalizeUserSource(int value) {
switch (value) {
case WiredSourceUtil.SOURCE_TRIGGER:
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
private int normalizeGroupType(int value) {
@@ -170,14 +170,7 @@ public class WiredConditionNotInGroup extends InteractionWiredCondition {
}
private int normalizeUserSource(int value) {
switch (value) {
case WiredSourceUtil.SOURCE_TRIGGER:
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
private int normalizeGroupType(int value) {
@@ -187,6 +187,10 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
}
private int normalizeSourceType(int group, int value) {
if (group == SOURCE_GROUP_USERS) {
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
switch (value) {
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
@@ -84,14 +84,7 @@ abstract class WiredConditionTeamGameBase extends InteractionWiredCondition {
}
protected int normalizeUserSource(int value) {
switch (value) {
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
protected int normalizePlacement(int value) {
@@ -304,25 +304,16 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition {
}
private int normalizePrimaryUserSource(int value) {
switch (value) {
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
private int normalizeCompareUserSource(int value) {
switch (value) {
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER:
case WiredSourceUtil.SOURCE_CLICKED_USER:
case SOURCE_SPECIFIED_USERNAME:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
}
@@ -185,14 +185,7 @@ public class WiredConditionUserPerformsAction extends InteractionWiredCondition
}
protected int normalizeUserSource(int value) {
switch (value) {
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
protected int normalizeSignId(int value) {
@@ -213,14 +213,7 @@ public class WiredEffectBotGiveHandItem extends InteractionWiredEffect {
}
private int normalizeUserSource(int value) {
switch (value) {
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
}
private int normalizeBotSource(int value) {
@@ -4,14 +4,22 @@ import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.rooms.FurnitureMovementError;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomTile;
import com.eu.habbo.habbohotel.rooms.RoomUnit;
import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.habbohotel.wired.WiredEffectType;
import com.eu.habbo.habbohotel.wired.core.WiredContext;
import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper;
import com.eu.habbo.messages.outgoing.rooms.WiredMovementsComposer;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class WiredEffectFurniToUser extends WiredEffectUserFurniBase {
public static final WiredEffectType type = WiredEffectType.FURNI_TO_USER;
@@ -27,27 +35,102 @@ public class WiredEffectFurniToUser extends WiredEffectUserFurniBase {
@Override
public void execute(WiredContext ctx) {
Room room = ctx.room();
HabboItem item = this.resolveLastItem(ctx);
List<HabboItem> items = new ArrayList<>(this.resolveItems(ctx));
Habbo habbo = this.resolveLastHabbo(room, ctx);
if (room == null || item == null || habbo == null || habbo.getRoomUnit() == null) {
if (room == null || habbo == null || habbo.getRoomUnit() == null) {
return;
}
RoomTile targetTile = habbo.getRoomUnit().getCurrentLocation();
items.removeIf(item -> item == null);
if (items.isEmpty()) {
return;
}
items.sort(Comparator
.comparingDouble(HabboItem::getZ)
.thenComparingInt(HabboItem::getId));
Map<Integer, Double> followerZOverrides = new HashMap<>();
for (HabboItem item : items) {
followerZOverrides.put(item.getId(), item.getZ());
}
RoomUnit roomUnit = habbo.getRoomUnit();
boolean hasActiveMoveStatus = roomUnit.hasStatus(RoomUnitStatus.MOVE);
long moveStatusTimestamp = hasActiveMoveStatus ? roomUnit.getMoveStatusTimestamp() : 0L;
if (roomUnit.isWalking()) {
for (HabboItem item : items) {
if (item == null) {
continue;
}
WiredMoveCarryHelper.registerUserFollower(room, this, item, roomUnit, followerZOverrides.get(item.getId()), ctx);
}
if (!hasActiveMoveStatus) {
return;
}
}
RoomTile targetTile = this.resolveTargetTile(habbo);
if (targetTile == null) {
return;
}
FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, item, targetTile, item.getRotation(), null, false, ctx);
if (error == FurnitureMovementError.NONE) {
return;
Integer animationDurationOverride = WiredMoveCarryHelper.hasNoAnimationExtra(room, this)
? null
: this.resolveFollowAnimationDuration(room, habbo, this);
int anchorType = hasActiveMoveStatus ? WiredMovementsComposer.FURNI_ANCHOR_USER : WiredMovementsComposer.FURNI_ANCHOR_NONE;
int anchorId = hasActiveMoveStatus ? roomUnit.getId() : 0;
if (hasActiveMoveStatus) {
int animationDuration = WiredMoveCarryHelper.resolveMoveStepDuration(roomUnit);
int animationElapsed = WiredMoveCarryHelper.resolveMoveStepElapsed(roomUnit);
for (HabboItem item : items) {
if (item == null || WiredMoveCarryHelper.isUserFollowerProcessed(roomUnit, item, moveStatusTimestamp)) {
continue;
}
Double targetZ = WiredMoveCarryHelper.resolveFollowerStackZ(room, item, targetTile, item.getRotation());
FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, item, targetTile, item.getRotation(), targetZ, null, false, ctx, animationDuration, animationElapsed, WiredMovementsComposer.FURNI_ANCHOR_USER, roomUnit.getId());
if (error != FurnitureMovementError.NONE) {
Double fallbackZ = followerZOverrides.get(item.getId());
if (fallbackZ != null) {
error = WiredMoveCarryHelper.moveFurni(room, this, item, targetTile, item.getRotation(), fallbackZ, null, false, ctx, animationDuration, animationElapsed, WiredMovementsComposer.FURNI_ANCHOR_USER, roomUnit.getId());
}
}
if (error == FurnitureMovementError.NONE) {
WiredMoveCarryHelper.markUserFollowerProcessed(roomUnit, item, moveStatusTimestamp);
}
}
}
if (item.getBaseItem().getStateCount() > 0) {
error = WiredMoveCarryHelper.moveFurni(room, this, item, targetTile, item.getRotation(), item.getZ(), null, false, ctx);
for (HabboItem item : items) {
if (item == null) {
continue;
}
if (hasActiveMoveStatus && WiredMoveCarryHelper.isUserFollowerProcessed(roomUnit, item, moveStatusTimestamp)) {
continue;
}
Double targetZ = WiredMoveCarryHelper.resolveFollowerStackZ(room, item, targetTile, item.getRotation());
FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, item, targetTile, item.getRotation(), targetZ, null, false, ctx, animationDurationOverride, null, anchorType, anchorId);
if (error == FurnitureMovementError.NONE) {
return;
continue;
}
Double fallbackZ = followerZOverrides.get(item.getId());
if (fallbackZ != null) {
WiredMoveCarryHelper.moveFurni(room, this, item, targetTile, item.getRotation(), fallbackZ, null, false, ctx, animationDurationOverride, null, anchorType, anchorId);
}
}
}
@@ -7,15 +7,19 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomTile;
import com.eu.habbo.habbohotel.rooms.RoomUnit;
import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.habbohotel.wired.WiredEffectType;
import com.eu.habbo.habbohotel.wired.core.WiredContext;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.incoming.wired.WiredSaveException;
import com.eu.habbo.messages.outgoing.rooms.WiredMovementsComposer;
import gnu.trove.procedure.TObjectProcedure;
import java.sql.ResultSet;
@@ -37,7 +41,7 @@ public abstract class WiredEffectUserFurniBase extends InteractionWiredEffect {
super(id, userId, item, extradata, limitedStack, limitedSells);
}
protected HabboItem resolveLastItem(WiredContext ctx) {
protected List<HabboItem> resolveItems(WiredContext ctx) {
Room room = ctx.room();
List<HabboItem> effectiveItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items);
@@ -47,6 +51,12 @@ public abstract class WiredEffectUserFurniBase extends InteractionWiredEffect {
|| room.getHabboItem(item.getId()) == null);
}
return effectiveItems;
}
protected HabboItem resolveLastItem(WiredContext ctx) {
List<HabboItem> effectiveItems = this.resolveItems(ctx);
if (effectiveItems.isEmpty()) {
return null;
}
@@ -90,6 +100,62 @@ public abstract class WiredEffectUserFurniBase extends InteractionWiredEffect {
return habbos;
}
protected RoomTile resolveTargetTile(Habbo habbo) {
if (habbo == null || habbo.getRoomUnit() == null) {
return null;
}
RoomUnit roomUnit = habbo.getRoomUnit();
RoomTile movingTile = this.resolveActiveMoveTile(roomUnit);
if (movingTile != null) {
return movingTile;
}
return roomUnit.getCurrentLocation();
}
private RoomTile resolveActiveMoveTile(RoomUnit roomUnit) {
if (roomUnit == null || roomUnit.getRoom() == null || roomUnit.getRoom().getLayout() == null) {
return null;
}
String moveStatus = roomUnit.getStatus(RoomUnitStatus.MOVE);
if (moveStatus != null && !moveStatus.isEmpty()) {
String[] parts = moveStatus.split(",");
if (parts.length >= 2) {
try {
return roomUnit.getRoom().getLayout().getTile(
Short.parseShort(parts[0]),
Short.parseShort(parts[1]));
} catch (NumberFormatException ignored) {
}
}
}
return null;
}
protected Integer resolveFollowAnimationDuration(Room room, Habbo habbo, HabboItem stackItem) {
if (room == null || habbo == null || habbo.getRoomUnit() == null || stackItem == null) {
return null;
}
RoomUnit roomUnit = habbo.getRoomUnit();
if (this.resolveActiveMoveTile(roomUnit) == null) {
return null;
}
long moveStatusTimestamp = roomUnit.getMoveStatusTimestamp();
if (moveStatusTimestamp <= 0L) {
return null;
}
int configuredDuration = WiredMoveCarryHelper.getAnimationDuration(room, stackItem, WiredMovementsComposer.DEFAULT_DURATION);
int remainingStepDuration = (int) Math.max(50L, WiredMovementsComposer.DEFAULT_DURATION - Math.max(0L, System.currentTimeMillis() - moveStatusTimestamp));
return Math.min(configuredDuration, remainingStepDuration);
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(
@@ -0,0 +1,78 @@
package com.eu.habbo.habbohotel.items.interactions.wired.extra;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomUnit;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.messages.ServerMessage;
import java.sql.ResultSet;
import java.sql.SQLException;
public class WiredExtraExecuteInOrder extends InteractionWiredExtra {
public static final int CODE = 64;
public WiredExtraExecuteInOrder(ResultSet set, Item baseItem) throws SQLException {
super(set, baseItem);
}
public WiredExtraExecuteInOrder(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
super(id, userId, item, extradata, limitedStack, limitedSells);
}
@Override
public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) {
return false;
}
@Override
public boolean saveData(WiredSettings settings, GameClient gameClient) {
return true;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData());
}
@Override
public void serializeWiredData(ServerMessage message, Room room) {
message.appendBoolean(false);
message.appendInt(0);
message.appendInt(0);
message.appendInt(this.getBaseItem().getSpriteId());
message.appendInt(this.getId());
message.appendString("");
message.appendInt(0);
message.appendInt(0);
message.appendInt(CODE);
message.appendInt(0);
message.appendInt(0);
}
@Override
public void loadWiredData(ResultSet set, Room room) throws SQLException {
this.onPickUp();
}
@Override
public void onPickUp() {
}
@Override
public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception {
}
@Override
public boolean hasConfiguration() {
return true;
}
static class JsonData {
}
}
@@ -0,0 +1,204 @@
package com.eu.habbo.habbohotel.items.interactions.wired.extra;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomTile;
import com.eu.habbo.habbohotel.rooms.RoomUnit;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.messages.ServerMessage;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayDeque;
import java.util.Deque;
public class WiredExtraExecutionLimit extends InteractionWiredExtra {
public static final int CODE = 65;
public static final int MIN_EXECUTIONS = 1;
public static final int MAX_EXECUTIONS = 100;
public static final int DEFAULT_EXECUTIONS = 1;
public static final int MIN_TIME_WINDOW_MS = 1000;
public static final int MAX_TIME_WINDOW_MS = 10000;
public static final int DEFAULT_TIME_WINDOW_MS = 1000;
public static final int TIME_WINDOW_STEP_MS = 500;
private final Deque<Long> recentExecutionTimestamps = new ArrayDeque<>();
private int maxExecutions = DEFAULT_EXECUTIONS;
private int timeWindowMs = DEFAULT_TIME_WINDOW_MS;
public WiredExtraExecutionLimit(ResultSet set, Item baseItem) throws SQLException {
super(set, baseItem);
}
public WiredExtraExecutionLimit(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
super(id, userId, item, extradata, limitedStack, limitedSells);
}
@Override
public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) {
return false;
}
@Override
public boolean saveData(WiredSettings settings, GameClient gameClient) {
int[] intParams = settings.getIntParams();
int nextExecutions = (intParams.length > 0) ? intParams[0] : this.maxExecutions;
int nextTimeWindowMs = (intParams.length > 1) ? intParams[1] : this.timeWindowMs;
this.maxExecutions = normalizeExecutions(nextExecutions);
this.timeWindowMs = normalizeTimeWindowMs(nextTimeWindowMs);
clearRuntimeState();
return true;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.maxExecutions, this.timeWindowMs));
}
@Override
public void serializeWiredData(ServerMessage message, Room room) {
message.appendBoolean(false);
message.appendInt(0);
message.appendInt(0);
message.appendInt(this.getBaseItem().getSpriteId());
message.appendInt(this.getId());
message.appendString("");
message.appendInt(2);
message.appendInt(this.maxExecutions);
message.appendInt(this.timeWindowMs);
message.appendInt(0);
message.appendInt(CODE);
message.appendInt(0);
message.appendInt(0);
}
@Override
public void loadWiredData(ResultSet set, Room room) throws SQLException {
this.onPickUp();
String wiredData = set.getString("wired_data");
if (wiredData == null || wiredData.isEmpty()) {
return;
}
if (wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
if (data != null) {
this.maxExecutions = normalizeExecutions(data.maxExecutions);
this.timeWindowMs = normalizeTimeWindowMs(data.timeWindowMs);
}
return;
}
String[] legacyData = wiredData.split(";");
try {
if (legacyData.length > 0) {
this.maxExecutions = normalizeExecutions(Integer.parseInt(legacyData[0]));
}
if (legacyData.length > 1) {
this.timeWindowMs = normalizeTimeWindowMs(Integer.parseInt(legacyData[1]));
}
} catch (NumberFormatException ignored) {
this.maxExecutions = DEFAULT_EXECUTIONS;
this.timeWindowMs = DEFAULT_TIME_WINDOW_MS;
}
}
@Override
public void onPickUp() {
this.maxExecutions = DEFAULT_EXECUTIONS;
this.timeWindowMs = DEFAULT_TIME_WINDOW_MS;
clearRuntimeState();
}
@Override
public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception {
}
@Override
public void onMove(Room room, RoomTile oldLocation, RoomTile newLocation) {
super.onMove(room, oldLocation, newLocation);
clearRuntimeState();
}
@Override
public boolean hasConfiguration() {
return true;
}
public boolean tryAcquireExecutionSlot(long timestamp) {
synchronized (this.recentExecutionTimestamps) {
pruneExpiredTimestamps(timestamp);
if (this.recentExecutionTimestamps.size() >= this.maxExecutions) {
return false;
}
this.recentExecutionTimestamps.addLast(timestamp);
return true;
}
}
public boolean canExecuteAt(long timestamp) {
synchronized (this.recentExecutionTimestamps) {
pruneExpiredTimestamps(timestamp);
return this.recentExecutionTimestamps.size() < this.maxExecutions;
}
}
public int getMaxExecutions() {
return this.maxExecutions;
}
public int getTimeWindowMs() {
return this.timeWindowMs;
}
public void clearRuntimeState() {
synchronized (this.recentExecutionTimestamps) {
this.recentExecutionTimestamps.clear();
}
}
private void pruneExpiredTimestamps(long timestamp) {
while (!this.recentExecutionTimestamps.isEmpty()
&& (timestamp - this.recentExecutionTimestamps.peekFirst()) >= this.timeWindowMs) {
this.recentExecutionTimestamps.removeFirst();
}
}
private static int normalizeExecutions(int value) {
return Math.max(MIN_EXECUTIONS, Math.min(MAX_EXECUTIONS, value));
}
private static int normalizeTimeWindowMs(int value) {
if (value < MIN_TIME_WINDOW_MS) {
return MIN_TIME_WINDOW_MS;
}
if (value > MAX_TIME_WINDOW_MS) {
return MAX_TIME_WINDOW_MS;
}
return Math.round(value / (float) TIME_WINDOW_STEP_MS) * TIME_WINDOW_STEP_MS;
}
static class JsonData {
int maxExecutions;
int timeWindowMs;
JsonData(int maxExecutions, int timeWindowMs) {
this.maxExecutions = maxExecutions;
this.timeWindowMs = timeWindowMs;
}
}
}
@@ -155,12 +155,20 @@ public class WiredTriggerAtSetTime extends InteractionWiredTrigger implements Wi
// Check if enough time has passed
if (this.accumulatedTime >= this.executeTime) {
if (this.getRoomId() != 0 && room.isLoaded()) {
long currentTime = System.currentTimeMillis();
if (!WiredManager.isTriggerExecutionAllowed(room, this, currentTime)) {
return;
}
this.hasFired = true;
this.accumulatedTime = 0;
WiredManager.triggerTimerTick(room, this);
return;
}
this.hasFired = true;
this.accumulatedTime = 0;
if (this.getRoomId() != 0 && room.isLoaded()) {
WiredManager.triggerTimerTick(room, this);
}
}
}
@@ -155,12 +155,20 @@ public class WiredTriggerAtTimeLong extends InteractionWiredTrigger implements W
// Check if enough time has passed
if (this.accumulatedTime >= this.executeTime) {
if (this.getRoomId() != 0 && room.isLoaded()) {
long currentTime = System.currentTimeMillis();
if (!WiredManager.isTriggerExecutionAllowed(room, this, currentTime)) {
return;
}
this.hasFired = true;
this.accumulatedTime = 0;
WiredManager.triggerTimerTick(room, this);
return;
}
this.hasFired = true;
this.accumulatedTime = 0;
if (this.getRoomId() != 0 && room.isLoaded()) {
WiredManager.triggerTimerTick(room, this);
}
}
}
@@ -144,7 +144,9 @@ public class WiredTriggerRepeater extends InteractionWiredTrigger implements Wir
// Fire when elapsed time is a multiple of repeatTime
if (elapsedMs % this.repeatTime == 0) {
if (this.getRoomId() != 0 && room.isLoaded()) {
long currentTime = System.currentTimeMillis();
if (this.getRoomId() != 0 && room.isLoaded()
&& WiredManager.isTriggerExecutionAllowed(room, this, currentTime)) {
WiredManager.triggerTimerRepeat(room, this);
}
}
@@ -138,7 +138,9 @@ public class WiredTriggerRepeaterLong extends InteractionWiredTrigger implements
// Fire when elapsed time is a multiple of repeat time
if (elapsedMs % this.repeatTime == 0) {
if (this.getRoomId() != 0 && room.isLoaded()) {
long currentTime = System.currentTimeMillis();
if (this.getRoomId() != 0 && room.isLoaded()
&& WiredManager.isTriggerExecutionAllowed(room, this, currentTime)) {
WiredManager.triggerTimerRepeatLong(room, this);
}
}
@@ -105,7 +105,9 @@ public class WiredTriggerRepeaterShort extends WiredTriggerRepeater {
long elapsedMs = tickCount * tickIntervalMs;
if (elapsedMs % this.repeatTime == 0) {
if (this.getRoomId() != 0 && room.isLoaded()) {
long currentTime = System.currentTimeMillis();
if (this.getRoomId() != 0 && room.isLoaded()
&& WiredManager.isTriggerExecutionAllowed(room, this, currentTime)) {
WiredManager.triggerTimerRepeatShort(room, this);
}
}
@@ -2032,6 +2032,10 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
this.messagingManager.sendComposer(message);
}
public void sendComposers(Collection<ServerMessage> messages) {
this.messagingManager.sendComposers(messages);
}
public void sendComposerToHabbosWithRights(ServerMessage message) {
this.messagingManager.sendComposerToHabbosWithRights(message);
}
@@ -10,6 +10,7 @@ import com.eu.habbo.habbohotel.users.DanceType;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.rooms.RoomAccessDeniedComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUnitIdleComposer;
@@ -123,7 +124,19 @@ public class RoomCycleManager {
// Send status updates
if (!updatedUnit.isEmpty()) {
this.room.sendComposer(new RoomUserStatusComposer(updatedUnit, true).compose());
ServerMessage statusComposer = new RoomUserStatusComposer(updatedUnit, true).compose();
WiredMoveCarryHelper.beginMovementCollection();
WiredMoveCarryHelper.processUserFollowers(this.room, updatedUnit);
ServerMessage wiredMovementsComposer = WiredMoveCarryHelper.finishMovementCollection();
if (wiredMovementsComposer != null) {
ArrayList<ServerMessage> batchedMessages = new ArrayList<>(2);
batchedMessages.add(statusComposer);
batchedMessages.add(wiredMovementsComposer);
this.room.sendComposers(batchedMessages);
} else {
this.room.sendComposer(statusComposer);
}
}
// Cycle trax manager
@@ -4,6 +4,9 @@ import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer;
import java.util.ArrayList;
import java.util.Collection;
/**
* Manages all messaging and communication within a room.
* Handles sending messages to Habbos, pet/bot chat, and alerts.
@@ -30,6 +33,34 @@ public class RoomMessagingManager {
}
}
public void sendComposers(Collection<ServerMessage> messages) {
if (messages == null || messages.isEmpty()) {
return;
}
ArrayList<ServerMessage> responses = new ArrayList<>();
for (ServerMessage message : messages) {
if (message == null) {
continue;
}
responses.add(message);
}
if (responses.isEmpty()) {
return;
}
for (Habbo habbo : this.room.getHabbos()) {
if (habbo.getClient() == null) {
continue;
}
habbo.getClient().sendResponses(new ArrayList<>(responses));
}
}
/**
* Sends a message to all Habbos with rights in the room.
*/
@@ -74,6 +74,7 @@ public class RoomUnit {
private int handItem;
private long handItemTimestamp;
private long lastRollerTime;
private long moveStatusTimestamp;
private int walkTimeOut;
private int effectId;
private int effectEndTimestamp;
@@ -104,6 +105,7 @@ public class RoomUnit {
this.goalLocation = null;
this.startLocation = this.currentLocation;
this.inRoom = false;
this.moveStatusTimestamp = 0L;
this.status.clear();
@@ -611,12 +613,16 @@ public class RoomUnit {
}
public void removeStatus(RoomUnitStatus key) {
if (key == RoomUnitStatus.MOVE) {
this.moveStatusTimestamp = 0L;
}
this.status.remove(key);
}
public void setStatus(RoomUnitStatus key, String value) {
if (key != null && value != null) {
if (key == RoomUnitStatus.MOVE) {
this.moveStatusTimestamp = System.currentTimeMillis();
WiredMoveCarryHelper.clearStatusComposerSuppression(this);
WiredUserMovementHelper.clearStatusComposerSuppression(this);
}
@@ -630,6 +636,7 @@ public class RoomUnit {
}
public void clearStatus() {
this.moveStatusTimestamp = 0L;
this.status.clear();
}
@@ -657,6 +664,10 @@ public class RoomUnit {
this.lastRollerTime = lastRollerTime;
}
public long getMoveStatusTimestamp() {
return this.moveStatusTimestamp;
}
/**
* Checks if enough time has passed since the last roller movement to allow rolling again.
* This prevents desync issues where the client hasn't finished the roller animation.
@@ -10,6 +10,8 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredTriggerReset;
import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectGiveReward;
import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectTriggerStacks;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecuteInOrder;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecutionLimit;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRandom;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUnseen;
import com.eu.habbo.habbohotel.rooms.Room;
@@ -27,6 +29,7 @@ import com.eu.habbo.plugin.events.furniture.wired.WiredConditionFailedEvent;
import com.eu.habbo.plugin.events.furniture.wired.WiredStackExecutedEvent;
import com.eu.habbo.plugin.events.furniture.wired.WiredStackTriggeredEvent;
import com.eu.habbo.plugin.events.users.UserWiredRewardReceived;
import com.eu.habbo.habbohotel.wired.core.WiredExecutionOrderUtil;
import com.google.gson.GsonBuilder;
import gnu.trove.set.hash.THashSet;
import org.slf4j.Logger;
@@ -37,7 +40,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
public class WiredHandler {
@@ -49,6 +52,11 @@ public class WiredHandler {
private static GsonBuilder gsonBuilder = null;
private static final class LegacyExecutionPlan {
private final LinkedHashSet<InteractionWiredEffect> effects = new LinkedHashSet<>();
private boolean executeInOrder = false;
}
public static boolean handle(WiredTriggerType triggerType, RoomUnit roomUnit, Room room, Object[] stuff) {
if (triggerType == WiredTriggerType.CUSTOM) return false;
@@ -72,7 +80,7 @@ public class WiredHandler {
return false;
long millis = System.currentTimeMillis();
THashSet<InteractionWiredEffect> effectsToExecute = new THashSet<InteractionWiredEffect>();
List<LegacyExecutionPlan> executionPlans = new ArrayList<>();
List<RoomTile> triggeredTiles = new ArrayList<>();
for (InteractionWiredTrigger trigger : triggers) {
@@ -81,10 +89,10 @@ public class WiredHandler {
if (triggeredTiles.contains(tile))
continue;
THashSet<InteractionWiredEffect> tEffectsToExecute = new THashSet<InteractionWiredEffect>();
LegacyExecutionPlan executionPlan = new LegacyExecutionPlan();
if (handle(trigger, roomUnit, room, stuff, tEffectsToExecute)) {
effectsToExecute.addAll(tEffectsToExecute);
if (handle(trigger, roomUnit, room, stuff, executionPlan)) {
executionPlans.add(executionPlan);
if (triggerType.equals(WiredTriggerType.SAY_SOMETHING))
talked = true;
@@ -93,8 +101,8 @@ public class WiredHandler {
}
}
for (InteractionWiredEffect effect : effectsToExecute) {
triggerEffect(effect, roomUnit, room, stuff, millis);
for (LegacyExecutionPlan executionPlan : executionPlans) {
triggerEffects(executionPlan.effects, roomUnit, room, stuff, millis, executionPlan.executeInOrder);
}
return talked;
@@ -119,7 +127,7 @@ public class WiredHandler {
return false;
long millis = System.currentTimeMillis();
THashSet<InteractionWiredEffect> effectsToExecute = new THashSet<InteractionWiredEffect>();
List<LegacyExecutionPlan> executionPlans = new ArrayList<>();
List<RoomTile> triggeredTiles = new ArrayList<>();
for (InteractionWiredTrigger trigger : triggers) {
@@ -130,44 +138,51 @@ public class WiredHandler {
if (triggeredTiles.contains(tile))
continue;
THashSet<InteractionWiredEffect> tEffectsToExecute = new THashSet<InteractionWiredEffect>();
LegacyExecutionPlan executionPlan = new LegacyExecutionPlan();
if (handle(trigger, roomUnit, room, stuff, tEffectsToExecute)) {
effectsToExecute.addAll(tEffectsToExecute);
if (handle(trigger, roomUnit, room, stuff, executionPlan)) {
executionPlans.add(executionPlan);
triggeredTiles.add(tile);
}
}
for (InteractionWiredEffect effect : effectsToExecute) {
triggerEffect(effect, roomUnit, room, stuff, millis);
for (LegacyExecutionPlan executionPlan : executionPlans) {
triggerEffects(executionPlan.effects, roomUnit, room, stuff, millis, executionPlan.executeInOrder);
}
return effectsToExecute.size() > 0;
return !executionPlans.isEmpty();
}
public static boolean handle(InteractionWiredTrigger trigger, final RoomUnit roomUnit, final Room room, final Object[] stuff) {
long millis = System.currentTimeMillis();
THashSet<InteractionWiredEffect> effectsToExecute = new THashSet<InteractionWiredEffect>();
LegacyExecutionPlan executionPlan = new LegacyExecutionPlan();
if(handle(trigger, roomUnit, room, stuff, effectsToExecute)) {
for (InteractionWiredEffect effect : effectsToExecute) {
triggerEffect(effect, roomUnit, room, stuff, millis);
}
if(handle(trigger, roomUnit, room, stuff, executionPlan)) {
triggerEffects(executionPlan.effects, roomUnit, room, stuff, millis, executionPlan.executeInOrder);
return true;
}
return false;
}
public static boolean handle(InteractionWiredTrigger trigger, final RoomUnit roomUnit, final Room room, final Object[] stuff, final THashSet<InteractionWiredEffect> effectsToExecute) {
private static boolean handle(InteractionWiredTrigger trigger, final RoomUnit roomUnit, final Room room, final Object[] stuff, final LegacyExecutionPlan executionPlan) {
long millis = System.currentTimeMillis();
int roomUnitId = roomUnit != null ? roomUnit.getId() : -1;
if (Emulator.isReady && ((Emulator.getConfig().getBoolean("wired.custom.enabled", false) && (trigger.canExecute(millis) || roomUnitId > -1) && trigger.userCanExecute(roomUnitId, millis)) || (!Emulator.getConfig().getBoolean("wired.custom.enabled", false) && trigger.canExecute(millis))) && trigger.execute(roomUnit, room, stuff)) {
trigger.activateBox(room, roomUnit, millis);
THashSet<InteractionWiredCondition> conditions = room.getRoomSpecialTypes().getConditions(trigger.getX(), trigger.getY());
THashSet<InteractionWiredEffect> effects = room.getRoomSpecialTypes().getEffects(trigger.getX(), trigger.getY());
if (Emulator.getPluginManager().fireEvent(new WiredStackTriggeredEvent(room, roomUnit, trigger, effects, conditions)).isCancelled())
return false;
THashSet<InteractionWiredExtra> extras = room.getRoomSpecialTypes().getExtras(trigger.getX(), trigger.getY());
WiredExtraExecutionLimit executionLimitExtra = null;
WiredExtraRandom randomExtra = null;
for (InteractionWiredExtra extra : extras) {
if (executionLimitExtra == null && extra instanceof WiredExtraExecutionLimit) {
executionLimitExtra = (WiredExtraExecutionLimit) extra;
}
if (randomExtra == null && extra instanceof WiredExtraRandom) {
randomExtra = (WiredExtraRandom) extra;
}
}
if (!conditions.isEmpty()) {
ArrayList<WiredConditionType> matchedConditions = new ArrayList<>(conditions.size());
@@ -187,39 +202,48 @@ public class WiredHandler {
}
}
if (executionLimitExtra != null && !executionLimitExtra.tryAcquireExecutionSlot(millis)) {
return false;
}
if (Emulator.getPluginManager().fireEvent(new WiredStackTriggeredEvent(room, roomUnit, trigger, effects, conditions)).isCancelled())
return false;
trigger.activateBox(room, roomUnit, millis);
trigger.setCooldown(millis);
boolean hasExtraUnseen = room.getRoomSpecialTypes().hasExtraType(trigger.getX(), trigger.getY(), WiredExtraUnseen.class);
THashSet<InteractionWiredExtra> extras = room.getRoomSpecialTypes().getExtras(trigger.getX(), trigger.getY());
WiredExtraRandom randomExtra = null;
boolean hasExtraExecuteInOrder = room.getRoomSpecialTypes().hasExtraType(trigger.getX(), trigger.getY(), WiredExtraExecuteInOrder.class);
for (InteractionWiredExtra extra : extras) {
extra.activateBox(room, roomUnit, millis);
if (randomExtra == null && extra instanceof WiredExtraRandom) {
randomExtra = (WiredExtraRandom) extra;
}
}
List<InteractionWiredEffect> effectList = new ArrayList<>(effects);
List<InteractionWiredEffect> effectList = (hasExtraUnseen || hasExtraExecuteInOrder)
? WiredExecutionOrderUtil.sort(effects)
: new ArrayList<>(effects);
if (randomExtra != null || hasExtraUnseen) {
Collections.shuffle(effectList);
}
executionPlan.executeInOrder = hasExtraExecuteInOrder;
if (hasExtraUnseen) {
for (InteractionWiredExtra extra : room.getRoomSpecialTypes().getExtras(trigger.getX(), trigger.getY())) {
if (extra instanceof WiredExtraUnseen) {
extra.setExtradata(extra.getExtradata().equals("1") ? "0" : "1");
InteractionWiredEffect effect = ((WiredExtraUnseen) extra).getUnseenEffect(effectList);
effectsToExecute.add(effect); // triggerEffect(effect, roomUnit, room, stuff, millis);
if (effect != null) {
executionPlan.effects.add(effect);
}
break;
}
}
} else if (randomExtra != null) {
effectsToExecute.addAll(randomExtra.selectEffects(effectList));
executionPlan.effects.addAll(randomExtra.selectEffects(effectList));
} else if (hasExtraExecuteInOrder) {
executionPlan.effects.addAll(effectList);
} else {
for (final InteractionWiredEffect effect : effectList) {
effectsToExecute.add(effect); //triggerEffect(effect, roomUnit, room, stuff, millis);
executionPlan.effects.add(effect);
}
}
@@ -234,7 +258,7 @@ public class WiredHandler {
if (effect != null && (effect.canExecute(millis) || (roomUnit != null && effect.requiresTriggeringUser() && Emulator.getConfig().getBoolean("wired.custom.enabled", false) && effect.userCanExecute(roomUnit.getId(), millis)))) {
executed = true;
if (!effect.requiresTriggeringUser() || (roomUnit != null && effect.requiresTriggeringUser())) {
Emulator.getThreading().run(() -> {
Runnable execution = () -> {
if (room.isLoaded() && room.getHabbos().size() > 0) {
try {
if (!effect.execute(roomUnit, room, stuff)) return;
@@ -245,13 +269,108 @@ public class WiredHandler {
effect.activateBox(room, roomUnit, millis);
}
}, effect.getDelay() * 500L);
};
long delayMs = effect.getDelay() * 500L;
long elapsedSinceTrigger = Math.max(0L, System.currentTimeMillis() - millis);
long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger);
if (delayMs <= 0) {
execution.run();
} else {
Emulator.getThreading().run(execution, remainingDelayMs);
}
}
}
return executed;
}
private static void triggerEffects(LinkedHashSet<InteractionWiredEffect> effects, RoomUnit roomUnit, Room room, Object[] stuff, long millis, boolean executeInOrder) {
if (effects == null || effects.isEmpty()) {
return;
}
if (!executeInOrder) {
for (InteractionWiredEffect effect : effects) {
triggerEffect(effect, roomUnit, room, stuff, millis);
}
return;
}
LinkedHashSet<InteractionWiredEffect> queueableEffects = new LinkedHashSet<>();
for (InteractionWiredEffect effect : effects) {
if (canQueueEffect(effect, roomUnit, millis)) {
queueableEffects.add(effect);
}
}
LinkedHashSet<Integer> delays = new LinkedHashSet<>();
for (InteractionWiredEffect effect : queueableEffects) {
delays.add(effect.getDelay());
}
for (Integer delay : delays) {
List<InteractionWiredEffect> delayBatch = new ArrayList<>();
for (InteractionWiredEffect effect : queueableEffects) {
if (effect.getDelay() == delay) {
delayBatch.add(effect);
}
}
if (delayBatch.isEmpty()) {
continue;
}
if (delay > 0) {
long delayMs = delay * 500L;
long elapsedSinceTrigger = Math.max(0L, System.currentTimeMillis() - millis);
long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger);
Emulator.getThreading().run(() -> executeOrderedEffectBatch(delayBatch, roomUnit, room, stuff, millis), remainingDelayMs);
} else {
executeOrderedEffectBatch(delayBatch, roomUnit, room, stuff, millis);
}
}
}
private static boolean canQueueEffect(InteractionWiredEffect effect, RoomUnit roomUnit, long millis) {
if (effect == null) {
return false;
}
boolean canExecute = effect.canExecute(millis)
|| (roomUnit != null && effect.requiresTriggeringUser()
&& Emulator.getConfig().getBoolean("wired.custom.enabled", false)
&& effect.userCanExecute(roomUnit.getId(), millis));
if (!canExecute) {
return false;
}
return !effect.requiresTriggeringUser() || roomUnit != null;
}
private static void executeOrderedEffectBatch(List<InteractionWiredEffect> effects, RoomUnit roomUnit, Room room, Object[] stuff, long millis) {
if (!room.isLoaded() || room.getHabbos().size() <= 0) {
return;
}
for (InteractionWiredEffect effect : effects) {
try {
if (!effect.execute(roomUnit, room, stuff)) {
continue;
}
effect.setCooldown(millis);
effect.activateBox(room, roomUnit, millis);
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
}
}
public static GsonBuilder getGsonBuilder() {
if(gsonBuilder == null) {
gsonBuilder = new GsonBuilder();
@@ -39,6 +39,7 @@ public final class WiredStack {
private final boolean useOrMode; // WiredExtraOrEval present
private final boolean useRandom; // WiredExtraRandom present
private final boolean useUnseen; // WiredExtraUnseen present
private final boolean executeInOrder; // WiredExtraExecuteInOrder present
/**
* Create a new wired stack.
@@ -52,7 +53,7 @@ public final class WiredStack {
IWiredTrigger trigger,
List<IWiredCondition> conditions,
List<IWiredEffect> effects) {
this(triggerItem, trigger, conditions, effects, false, false, false);
this(triggerItem, trigger, conditions, effects, false, false, false, false);
}
/**
@@ -65,6 +66,7 @@ public final class WiredStack {
* @param useOrMode if true, conditions use OR logic (any pass = success)
* @param useRandom if true, select one random effect instead of all
* @param useUnseen if true, execute effects in "unseen" order (round-robin)
* @param executeInOrder if true, execute all regular effects in stable stack order
*/
public WiredStack(HabboItem triggerItem,
IWiredTrigger trigger,
@@ -72,7 +74,8 @@ public final class WiredStack {
List<IWiredEffect> effects,
boolean useOrMode,
boolean useRandom,
boolean useUnseen) {
boolean useUnseen,
boolean executeInOrder) {
this.triggerItem = triggerItem;
this.trigger = trigger;
this.conditions = conditions != null ? Collections.unmodifiableList(conditions) : Collections.emptyList();
@@ -80,6 +83,7 @@ public final class WiredStack {
this.useOrMode = useOrMode;
this.useRandom = useRandom;
this.useUnseen = useUnseen;
this.executeInOrder = executeInOrder;
}
/**
@@ -157,6 +161,15 @@ public final class WiredStack {
return useUnseen;
}
/**
* Check if ordered execution mode is enabled (WiredExtraExecuteInOrder).
* When true, all regular effects execute in stable stack order.
* @return true if ordered execution is enabled
*/
public boolean executeInOrder() {
return executeInOrder;
}
/**
* Get the number of conditions.
* @return condition count
@@ -183,6 +196,7 @@ public final class WiredStack {
", orMode=" + useOrMode +
", random=" + useRandom +
", unseen=" + useUnseen +
", executeInOrder=" + executeInOrder +
'}';
}
}
@@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.wired.core;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecuteInOrder;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraOrEval;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRandom;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUnseen;
@@ -176,6 +177,7 @@ public final class RoomWiredStackIndex implements WiredStackIndex {
boolean useOrMode = specialTypes.hasExtraType(x, y, WiredExtraOrEval.class);
boolean useRandom = specialTypes.hasExtraType(x, y, WiredExtraRandom.class);
boolean useUnseen = specialTypes.hasExtraType(x, y, WiredExtraUnseen.class);
boolean executeInOrder = specialTypes.hasExtraType(x, y, WiredExtraExecuteInOrder.class);
return new WiredStack(
trigger,
@@ -184,7 +186,8 @@ public final class RoomWiredStackIndex implements WiredStackIndex {
effects,
useOrMode,
useRandom,
useUnseen
useUnseen,
executeInOrder
);
}
@@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurni;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUser;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecutionLimit;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRandom;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUnseen;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger;
@@ -179,11 +180,11 @@ public final class WiredEngine {
boolean anyTriggered = false;
boolean suppressSaysOutput = false;
long currentTime = System.currentTimeMillis();
long triggerTime = event.getCreatedAtMs();
for (WiredStack stack : stacks) {
try {
boolean triggered = processStack(stack, event, currentTime);
boolean triggered = processStack(stack, event, triggerTime);
if (triggered) {
anyTriggered = true;
@@ -257,6 +258,15 @@ public final class WiredEngine {
debug(room, "No conditions in stack, proceeding to effects");
}
WiredExtraExecutionLimit executionLimitExtra = getExecutionLimitExtra(room, stack);
if (executionLimitExtra != null && !executionLimitExtra.tryAcquireExecutionSlot(currentTime)) {
debug(room, "Execution limit blocked stack {} (max {} in {} ms)",
stack.triggerItem() != null ? stack.triggerItem().getId() : "null",
executionLimitExtra.getMaxExecutions(),
executionLimitExtra.getTimeWindowMs());
return false;
}
// Fire plugin event (WiredStackTriggeredEvent)
if (!fireTriggeredEvent(stack, event)) {
debug(room, "Stack cancelled by plugin");
@@ -427,6 +437,10 @@ public final class WiredEngine {
debug(ctx.room(), "Unseen mode fallback: selected effect {}/{}", index + 1, regulars.size());
}
}
} else if (stack.executeInOrder()) {
debug(ctx.room(), "Ordered mode: executing effect batches in stack order by delay");
executeOrderedEffects(regulars, ctx, currentTime);
return;
} else {
// Normal mode: regular effects in random order
toExecute = new ArrayList<>(regulars);
@@ -569,9 +583,11 @@ public final class WiredEngine {
/**
* Schedule a delayed effect execution.
*/
private void scheduleDelayedEffect(IWiredEffect effect, WiredContext ctx, int delay, long currentTime) {
private void scheduleDelayedEffect(IWiredEffect effect, WiredContext ctx, int delay, long triggerTime) {
// Delay is in 500ms ticks
long delayMs = delay * 500L;
long elapsedSinceTrigger = Math.max(0L, System.currentTimeMillis() - triggerTime);
long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger);
Room room = ctx.room();
RoomUnit actor = ctx.actor().orElse(null);
@@ -592,7 +608,80 @@ public final class WiredEngine {
} catch (Exception e) {
LOGGER.warn("Error executing delayed effect: {}", e.getMessage());
}
}, delayMs);
}, remainingDelayMs);
}
private void executeOrderedEffects(List<IWiredEffect> effects, WiredContext ctx, long currentTime) {
if (effects == null || effects.isEmpty()) {
return;
}
Map<Integer, List<IWiredEffect>> effectsByDelay = new LinkedHashMap<>();
for (IWiredEffect effect : effects) {
if (effect == null) {
continue;
}
if (effect.requiresActor() && !ctx.hasActor()) {
continue;
}
effectsByDelay.computeIfAbsent(effect.getDelay(), key -> new ArrayList<>()).add(effect);
}
for (Map.Entry<Integer, List<IWiredEffect>> entry : effectsByDelay.entrySet()) {
int delay = entry.getKey();
List<IWiredEffect> batch = entry.getValue();
if (batch.isEmpty()) {
continue;
}
if (delay > 0) {
scheduleOrderedEffectBatch(batch, ctx, delay, currentTime);
} else {
executeOrderedEffectBatch(batch, ctx, currentTime, false);
}
}
}
private void scheduleOrderedEffectBatch(List<IWiredEffect> batch, WiredContext ctx, int delay, long triggerTime) {
long delayMs = delay * 500L;
long elapsedSinceTrigger = Math.max(0L, System.currentTimeMillis() - triggerTime);
long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger);
Room room = ctx.room();
Emulator.getThreading().run(() -> {
if (!room.isLoaded() || room.getHabbos().isEmpty()) {
return;
}
executeOrderedEffectBatch(batch, ctx, System.currentTimeMillis(), true);
}, remainingDelayMs);
}
private void executeOrderedEffectBatch(List<IWiredEffect> batch, WiredContext ctx, long executionTime, boolean useExecutionTimeForCooldown) {
Room room = ctx.room();
RoomUnit actor = ctx.actor().orElse(null);
for (IWiredEffect effect : batch) {
try {
if (!useExecutionTimeForCooldown) {
ctx.state().step();
}
effect.execute(ctx);
if (effect instanceof InteractionWiredEffect) {
InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect;
wiredEffect.setCooldown(executionTime);
wiredEffect.activateBox(room, actor, executionTime);
}
} catch (Exception e) {
LOGGER.warn("Error executing ordered effect batch item: {}", e.getMessage());
}
}
}
/**
@@ -714,6 +803,12 @@ public final class WiredEngine {
return (extra instanceof WiredExtraUnseen) ? (WiredExtraUnseen) extra : null;
}
private WiredExtraExecutionLimit getExecutionLimitExtra(Room room, WiredStack stack) {
InteractionWiredExtra extra = getStackExtra(room, stack, WiredExtraExecutionLimit.class);
return (extra instanceof WiredExtraExecutionLimit) ? (WiredExtraExecutionLimit) extra : null;
}
private <T extends InteractionWiredExtra> InteractionWiredExtra getStackExtra(Room room, WiredStack stack, Class<T> extraClass) {
if (room == null || stack == null || stack.triggerItem() == null || room.getRoomSpecialTypes() == null) {
return null;
@@ -4,8 +4,10 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.catalog.CatalogItem;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra;
import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectGiveReward;
import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectTriggerStacks;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecutionLimit;
import com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerHabboClicksUser;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomTile;
@@ -725,6 +727,34 @@ public final class WiredManager {
return WiredTickService.getInstance();
}
public static boolean isTriggerExecutionAllowed(Room room, HabboItem triggerItem, long timestamp) {
WiredExtraExecutionLimit executionLimit = getExecutionLimitExtra(room, triggerItem);
return executionLimit == null || executionLimit.canExecuteAt(timestamp);
}
public static WiredExtraExecutionLimit getExecutionLimitExtra(Room room, HabboItem triggerItem) {
if (room == null || triggerItem == null || room.getRoomSpecialTypes() == null) {
return null;
}
THashSet<InteractionWiredExtra> extras = room.getRoomSpecialTypes().getExtras(
triggerItem.getX(),
triggerItem.getY());
if (extras == null || extras.isEmpty()) {
return null;
}
for (InteractionWiredExtra extra : extras) {
if (extra instanceof WiredExtraExecutionLimit) {
return (WiredExtraExecutionLimit) extra;
}
}
return null;
}
// ========== Timer Management ==========
/**
@@ -14,6 +14,7 @@ import com.eu.habbo.habbohotel.rooms.RoomUnitStatus;
import com.eu.habbo.habbohotel.rooms.RoomUnitType;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.rooms.WiredMovementsComposer;
import com.eu.habbo.messages.outgoing.rooms.items.FloorItemOnRollerComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer;
@@ -21,16 +22,21 @@ import gnu.trove.set.hash.THashSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class WiredMoveCarryHelper {
private static final double DIRECT_HEIGHT_TOLERANCE = 0.1D;
private static final int STATUS_SUPPRESSION_GRACE_MS = 250;
private static final long USER_FOLLOWER_TTL_MS = 10000L;
private static final ThreadLocal<Set<Integer>> SUPPRESSED_STATUS_ROOM_UNIT_IDS = new ThreadLocal<>();
private static final ThreadLocal<List<WiredMovementsComposer.MovementData>> COLLECTED_MOVEMENTS = new ThreadLocal<>();
private static final ConcurrentHashMap<Integer, Long> SUPPRESSED_STATUS_COMPOSER_UNTIL = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<Integer, ConcurrentHashMap<Integer, UserFollowEntry>> ACTIVE_USER_FOLLOWERS = new ConcurrentHashMap<>();
private WiredMoveCarryHelper() {
}
@@ -60,10 +66,22 @@ public final class WiredMoveCarryHelper {
}
public static FurnitureMovementError moveFurni(Room room, HabboItem stackItem, HabboItem movingItem, RoomTile targetTile, int rotation, Habbo actor, boolean sendUpdates, WiredContext ctx) {
return moveFurni(room, stackItem, movingItem, targetTile, rotation, null, actor, sendUpdates, ctx);
return moveFurni(room, stackItem, movingItem, targetTile, rotation, null, actor, sendUpdates, ctx, null, null, WiredMovementsComposer.FURNI_ANCHOR_NONE, 0);
}
public static FurnitureMovementError moveFurni(Room room, HabboItem stackItem, HabboItem movingItem, RoomTile targetTile, int rotation, Double z, Habbo actor, boolean sendUpdates, WiredContext ctx) {
return moveFurni(room, stackItem, movingItem, targetTile, rotation, z, actor, sendUpdates, ctx, null, null, WiredMovementsComposer.FURNI_ANCHOR_NONE, 0);
}
public static FurnitureMovementError moveFurni(Room room, HabboItem stackItem, HabboItem movingItem, RoomTile targetTile, int rotation, Double z, Habbo actor, boolean sendUpdates, WiredContext ctx, Integer animationDurationOverride) {
return moveFurni(room, stackItem, movingItem, targetTile, rotation, z, actor, sendUpdates, ctx, animationDurationOverride, null, WiredMovementsComposer.FURNI_ANCHOR_NONE, 0);
}
public static FurnitureMovementError moveFurni(Room room, HabboItem stackItem, HabboItem movingItem, RoomTile targetTile, int rotation, Double z, Habbo actor, boolean sendUpdates, WiredContext ctx, Integer animationDurationOverride, Integer animationElapsedOverride) {
return moveFurni(room, stackItem, movingItem, targetTile, rotation, z, actor, sendUpdates, ctx, animationDurationOverride, animationElapsedOverride, WiredMovementsComposer.FURNI_ANCHOR_NONE, 0);
}
public static FurnitureMovementError moveFurni(Room room, HabboItem stackItem, HabboItem movingItem, RoomTile targetTile, int rotation, Double z, Habbo actor, boolean sendUpdates, WiredContext ctx, Integer animationDurationOverride, Integer animationElapsedOverride, int anchorType, int anchorId) {
if (room == null || movingItem == null || targetTile == null) {
return FurnitureMovementError.INVALID_MOVE;
}
@@ -97,7 +115,9 @@ public final class WiredMoveCarryHelper {
}
boolean useWiredMovements = !hasNoAnimationExtra(room, stackItem);
int animationDuration = getAnimationDuration(room, stackItem, WiredMovementsComposer.DEFAULT_DURATION);
int animationDuration = animationDurationOverride != null
? Math.max(50, animationDurationOverride)
: getAnimationDuration(room, stackItem, WiredMovementsComposer.DEFAULT_DURATION);
Set<Integer> previousSuppressedRoomUnitIds = SUPPRESSED_STATUS_ROOM_UNIT_IDS.get();
if (carryContext.active) {
@@ -133,7 +153,7 @@ public final class WiredMoveCarryHelper {
if (!useWiredMovements) {
applyInstantCarryState(room, movingItem, targetTile, rotation, carryContext);
} else if (oldLocation != null) {
sendAnimatedMove(room, movingItem, oldLocation, oldZ, targetTile, rotation, carryContext, animationDuration);
sendAnimatedMove(room, movingItem, oldLocation, oldZ, targetTile, rotation, carryContext, animationDuration, (animationElapsedOverride != null) ? Math.max(0, animationElapsedOverride) : 0, anchorType, anchorId);
}
}
@@ -198,6 +218,165 @@ public final class WiredMoveCarryHelper {
SUPPRESSED_STATUS_COMPOSER_UNTIL.remove(roomUnit.getId());
}
public static void beginMovementCollection() {
COLLECTED_MOVEMENTS.set(new ArrayList<>());
}
public static ServerMessage finishMovementCollection() {
List<WiredMovementsComposer.MovementData> movements = COLLECTED_MOVEMENTS.get();
COLLECTED_MOVEMENTS.remove();
if (movements == null || movements.isEmpty()) {
return null;
}
return new WiredMovementsComposer(movements).compose();
}
public static void registerUserFollower(Room room, HabboItem stackItem, HabboItem movingItem, RoomUnit targetUnit, Double zOverride, WiredContext ctx) {
if (room == null || stackItem == null || movingItem == null || targetUnit == null) {
return;
}
ACTIVE_USER_FOLLOWERS
.computeIfAbsent(targetUnit.getId(), key -> new ConcurrentHashMap<>())
.compute(movingItem.getId(), (key, existing) -> {
if (existing != null
&& existing.roomId == room.getId()
&& existing.stackItemId == stackItem.getId()) {
if (existing.zOverride == null && zOverride != null) {
existing.zOverride = zOverride;
}
existing.ctx = ctx;
existing.touch();
return existing;
}
return new UserFollowEntry(
room.getId(),
stackItem.getId(),
movingItem.getId(),
zOverride,
ctx);
});
}
public static void markUserFollowerProcessed(RoomUnit targetUnit, HabboItem movingItem, long moveStatusTimestamp) {
if (targetUnit == null || movingItem == null || moveStatusTimestamp <= 0L) {
return;
}
ConcurrentHashMap<Integer, UserFollowEntry> followers = ACTIVE_USER_FOLLOWERS.get(targetUnit.getId());
if (followers == null) {
return;
}
UserFollowEntry entry = followers.get(movingItem.getId());
if (entry == null) {
return;
}
entry.markProcessed(moveStatusTimestamp);
}
public static boolean isUserFollowerProcessed(RoomUnit targetUnit, HabboItem movingItem, long moveStatusTimestamp) {
if (targetUnit == null || movingItem == null || moveStatusTimestamp <= 0L) {
return false;
}
ConcurrentHashMap<Integer, UserFollowEntry> followers = ACTIVE_USER_FOLLOWERS.get(targetUnit.getId());
if (followers == null) {
return false;
}
UserFollowEntry entry = followers.get(movingItem.getId());
if (entry == null) {
return false;
}
return entry.lastProcessedMoveTimestamp == moveStatusTimestamp;
}
public static void processUserFollowers(Room room, Collection<RoomUnit> roomUnits) {
if (room == null || roomUnits == null || roomUnits.isEmpty()) {
return;
}
for (RoomUnit roomUnit : roomUnits) {
if (roomUnit == null) {
continue;
}
ConcurrentHashMap<Integer, UserFollowEntry> followers = ACTIVE_USER_FOLLOWERS.get(roomUnit.getId());
if (followers == null || followers.isEmpty()) {
continue;
}
if (!roomUnit.hasStatus(RoomUnitStatus.MOVE) || roomUnit.getCurrentLocation() == null) {
ACTIVE_USER_FOLLOWERS.remove(roomUnit.getId(), followers);
continue;
}
long moveStatusTimestamp = roomUnit.getMoveStatusTimestamp();
List<Integer> toRemove = new ArrayList<>();
if (shouldSettleFollowersForNewStep(followers, moveStatusTimestamp)) {
settleUserFollowers(room, followers);
}
List<Map.Entry<Integer, UserFollowEntry>> orderedFollowers = new ArrayList<>(followers.entrySet());
orderedFollowers.sort(Comparator
.comparingDouble((Map.Entry<Integer, UserFollowEntry> followerEntry) -> {
UserFollowEntry entry = followerEntry.getValue();
return (entry != null && entry.zOverride != null) ? entry.zOverride : Double.MAX_VALUE;
})
.thenComparingInt(Map.Entry::getKey));
for (Map.Entry<Integer, UserFollowEntry> followerEntry : orderedFollowers) {
UserFollowEntry entry = followerEntry.getValue();
if (entry == null || entry.roomId != room.getId() || entry.expiresAt < System.currentTimeMillis()) {
toRemove.add(followerEntry.getKey());
continue;
}
HabboItem stackItem = room.getHabboItem(entry.stackItemId);
HabboItem movingItem = room.getHabboItem(entry.movingItemId);
if (stackItem == null || movingItem == null) {
toRemove.add(followerEntry.getKey());
continue;
}
if (moveStatusTimestamp <= 0L || moveStatusTimestamp == entry.lastProcessedMoveTimestamp) {
continue;
}
int animationElapsed = resolveMoveStepElapsed(roomUnit);
int animationDuration = resolveMoveStepDuration(roomUnit);
Double targetZ = resolveFollowerStackZ(room, movingItem, roomUnit.getCurrentLocation(), movingItem.getRotation());
FurnitureMovementError error = moveFurni(room, stackItem, movingItem, roomUnit.getCurrentLocation(), movingItem.getRotation(), targetZ, null, false, entry.ctx, animationDuration, animationElapsed, WiredMovementsComposer.FURNI_ANCHOR_USER, roomUnit.getId());
if (error != FurnitureMovementError.NONE && entry.zOverride != null) {
error = moveFurni(room, stackItem, movingItem, roomUnit.getCurrentLocation(), movingItem.getRotation(), entry.zOverride, null, false, entry.ctx, animationDuration, animationElapsed, WiredMovementsComposer.FURNI_ANCHOR_USER, roomUnit.getId());
}
if (error == FurnitureMovementError.INVALID_MOVE) {
toRemove.add(followerEntry.getKey());
continue;
}
entry.markProcessed(moveStatusTimestamp);
}
for (Integer movingItemId : toRemove) {
followers.remove(movingItemId);
}
purgeExpiredFollowers(roomUnit.getId(), followers, true);
}
}
public static boolean hasNoAnimationExtra(Room room, HabboItem stackItem) {
return getNoAnimationExtra(room, stackItem) != null;
}
@@ -207,6 +386,141 @@ public final class WiredMoveCarryHelper {
return (extra != null) ? extra.getDurationMs() : fallbackDuration;
}
public static int resolveMoveStepElapsed(RoomUnit roomUnit) {
if (roomUnit == null) {
return 0;
}
long moveStatusTimestamp = roomUnit.getMoveStatusTimestamp();
if (moveStatusTimestamp <= 0L) {
return 0;
}
return (int) Math.max(0L, Math.min(WiredMovementsComposer.DEFAULT_DURATION, System.currentTimeMillis() - moveStatusTimestamp));
}
public static int resolveMoveStepDuration(RoomUnit roomUnit) {
return WiredMovementsComposer.DEFAULT_DURATION;
}
public static Double resolveFollowerStackZ(Room room, HabboItem movingItem, RoomTile targetTile, int rotation) {
if (room == null || movingItem == null || targetTile == null || room.getLayout() == null) {
return null;
}
double targetZ = room.getStackHeight(targetTile.x, targetTile.y, false, movingItem);
THashSet<RoomTile> occupiedTiles = room.getLayout().getTilesAt(
targetTile,
movingItem.getBaseItem().getWidth(),
movingItem.getBaseItem().getLength(),
rotation);
if (occupiedTiles == null || occupiedTiles.isEmpty()) {
return targetZ;
}
for (RoomTile occupiedTile : occupiedTiles) {
if (occupiedTile == null) {
continue;
}
targetZ = Math.max(targetZ, room.getStackHeight(occupiedTile.x, occupiedTile.y, false, movingItem));
}
return targetZ;
}
private static Integer resolveRemainingMoveDuration(RoomUnit roomUnit, HabboItem stackItem, Room room) {
if (roomUnit == null || stackItem == null || room == null) {
return null;
}
long moveStatusTimestamp = roomUnit.getMoveStatusTimestamp();
if (moveStatusTimestamp <= 0L) {
return null;
}
int configuredDuration = getAnimationDuration(room, stackItem, WiredMovementsComposer.DEFAULT_DURATION);
int remainingStepDuration = (int) Math.max(50L, WiredMovementsComposer.DEFAULT_DURATION - Math.max(0L, System.currentTimeMillis() - moveStatusTimestamp));
return Math.min(configuredDuration, remainingStepDuration);
}
private static boolean shouldSettleFollowersForNewStep(ConcurrentHashMap<Integer, UserFollowEntry> followers, long moveStatusTimestamp) {
if (followers == null || followers.isEmpty() || moveStatusTimestamp <= 0L) {
return false;
}
for (UserFollowEntry entry : followers.values()) {
if (entry != null && entry.lastProcessedMoveTimestamp > 0L && entry.lastProcessedMoveTimestamp != moveStatusTimestamp) {
return true;
}
}
return false;
}
private static void settleUserFollowers(Room room, ConcurrentHashMap<Integer, UserFollowEntry> followers) {
if (room == null || followers == null || followers.isEmpty()) {
return;
}
List<Map.Entry<Integer, UserFollowEntry>> entriesToSettle = new ArrayList<>(followers.entrySet());
entriesToSettle.sort(Comparator
.comparingDouble((Map.Entry<Integer, UserFollowEntry> followerEntry) -> {
UserFollowEntry entry = followerEntry.getValue();
return (entry != null && entry.zOverride != null) ? -entry.zOverride : Double.POSITIVE_INFINITY;
})
.thenComparingInt(Map.Entry::getKey));
for (Map.Entry<Integer, UserFollowEntry> followerEntry : entriesToSettle) {
UserFollowEntry entry = followerEntry.getValue();
if (entry == null || entry.roomId != room.getId()) {
continue;
}
HabboItem movingItem = room.getHabboItem(entry.movingItemId);
HabboItem stackItem = room.getHabboItem(entry.stackItemId);
if (movingItem == null || room.getLayout() == null) {
continue;
}
RoomTile currentTile = room.getLayout().getTile(movingItem.getX(), movingItem.getY());
if (currentTile == null) {
continue;
}
Double targetZ = (double) room.getLayout().getHeightAtSquare(currentTile.x, currentTile.y);
if (stackItem != null) {
FurnitureMovementError error = moveFurni(room, stackItem, movingItem, currentTile, movingItem.getRotation(), targetZ, null, false, entry.ctx, WiredMovementsComposer.DEFAULT_DURATION, 0, WiredMovementsComposer.FURNI_ANCHOR_NONE, 0);
if (error == FurnitureMovementError.NONE) {
continue;
}
}
FurnitureMovementError error = room.moveFurniTo(movingItem, currentTile, movingItem.getRotation(), targetZ, null, true, false);
if (error != FurnitureMovementError.NONE) {
room.moveFurniTo(movingItem, currentTile, movingItem.getRotation(), null, true, false);
}
}
}
private static void purgeExpiredFollowers(int roomUnitId, ConcurrentHashMap<Integer, UserFollowEntry> followers, boolean removeEmpty) {
if (followers == null) {
return;
}
long now = System.currentTimeMillis();
followers.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue().expiresAt < now);
if (removeEmpty && followers.isEmpty()) {
ACTIVE_USER_FOLLOWERS.remove(roomUnitId, followers);
}
}
private static boolean hasMovementBehaviorExtra(Room room, HabboItem stackItem) {
THashSet<InteractionWiredExtra> extras = getMovementExtras(room, stackItem);
if (extras == null || extras.isEmpty()) {
@@ -370,7 +684,7 @@ public final class WiredMoveCarryHelper {
return FurnitureMovementError.NONE;
}
private static void sendAnimatedMove(Room room, HabboItem movingItem, RoomTile oldLocation, double oldZ, RoomTile targetTile, int rotation, CarryContext carryContext, int animationDuration) {
private static void sendAnimatedMove(Room room, HabboItem movingItem, RoomTile oldLocation, double oldZ, RoomTile targetTile, int rotation, CarryContext carryContext, int animationDuration, int animationElapsed, int anchorType, int anchorId) {
List<CarriedUnitMove> carriedMoves = getCarriedUnitMoves(room, movingItem, targetTile, rotation, carryContext);
List<WiredMovementsComposer.MovementData> movements = new ArrayList<>();
movements.add(WiredMovementsComposer.furniMovement(
@@ -382,7 +696,10 @@ public final class WiredMoveCarryHelper {
oldZ,
movingItem.getZ(),
movingItem.getRotation(),
animationDuration));
animationDuration,
animationElapsed,
anchorType,
anchorId));
for (CarriedUnitMove carriedMove : carriedMoves) {
suppressStatusComposer(carriedMove.roomUnit, animationDuration);
@@ -399,7 +716,13 @@ public final class WiredMoveCarryHelper {
animationDuration));
}
room.sendComposer(new WiredMovementsComposer(movements).compose());
List<WiredMovementsComposer.MovementData> collectedMovements = COLLECTED_MOVEMENTS.get();
if (collectedMovements != null) {
collectedMovements.addAll(movements);
} else {
room.sendComposer(new WiredMovementsComposer(movements).compose());
}
for (CarriedUnitMove carriedMove : carriedMoves) {
updateCarriedUnitState(carriedMove);
@@ -711,4 +1034,32 @@ public final class WiredMoveCarryHelper {
this.newZ = newZ;
}
}
private static final class UserFollowEntry {
private final int roomId;
private final int stackItemId;
private final int movingItemId;
private Double zOverride;
private WiredContext ctx;
private long expiresAt;
private long lastProcessedMoveTimestamp;
private UserFollowEntry(int roomId, int stackItemId, int movingItemId, Double zOverride, WiredContext ctx) {
this.roomId = roomId;
this.stackItemId = stackItemId;
this.movingItemId = movingItemId;
this.zOverride = zOverride;
this.ctx = ctx;
this.touch();
}
private void markProcessed(long moveStatusTimestamp) {
this.lastProcessedMoveTimestamp = moveStatusTimestamp;
this.touch();
}
private void touch() {
this.expiresAt = System.currentTimeMillis() + USER_FOLLOWER_TTL_MS;
}
}
}
@@ -18,6 +18,7 @@ import java.util.List;
public final class WiredSourceUtil {
public static final int SOURCE_TRIGGER = 0;
public static final int SOURCE_CLICKED_USER = 11;
public static final int SOURCE_SELECTED = 100;
public static final int SOURCE_SELECTOR = 200;
public static final int SOURCE_SIGNAL = 201;
@@ -54,6 +55,11 @@ public final class WiredSourceUtil {
switch (sourceType) {
case SOURCE_TRIGGER:
return ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList());
case SOURCE_CLICKED_USER:
if (ctx.eventType() == WiredEvent.Type.USER_CLICKS_USER) {
return ctx.event().getTargetUnit().map(Collections::singletonList).orElse(Collections.emptyList());
}
return Collections.emptyList();
case SOURCE_SELECTED:
return (selectedUsers != null) ? new ArrayList<>(selectedUsers) : Collections.emptyList();
case SOURCE_SELECTOR:
@@ -71,6 +77,22 @@ public final class WiredSourceUtil {
}
}
public static boolean isDefaultUserSource(int value) {
switch (value) {
case SOURCE_TRIGGER:
case SOURCE_CLICKED_USER:
case SOURCE_SELECTOR:
case SOURCE_SIGNAL:
return true;
default:
return false;
}
}
public static boolean isSelectableUserSource(int value) {
return value == SOURCE_SELECTED || isDefaultUserSource(value);
}
private static WiredTargets getSelectorTargets(WiredContext ctx) {
if (ctx == null) {
return new WiredTargets();
@@ -13,6 +13,10 @@ public class WiredMovementsComposer extends MessageComposer {
public static final int TYPE_WALL_ITEM_MOVE = 2;
public static final int TYPE_USER_DIRECTION = 3;
public static final int FURNI_ANCHOR_NONE = 0;
public static final int FURNI_ANCHOR_USER = 1;
public static final int FURNI_ANCHOR_FURNI = 2;
public static final int USER_MOVEMENT_WALK = 0;
public static final int USER_MOVEMENT_SLIDE = 1;
public static final int DEFAULT_DURATION = 500;
@@ -37,11 +41,19 @@ public class WiredMovementsComposer extends MessageComposer {
}
public static MovementData furniMovement(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ) {
return furniMovement(id, fromX, fromY, toX, toY, fromZ, toZ, 0, DEFAULT_DURATION);
return furniMovement(id, fromX, fromY, toX, toY, fromZ, toZ, 0, DEFAULT_DURATION, 0, FURNI_ANCHOR_NONE, 0);
}
public static MovementData furniMovement(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ, int rotation, int duration) {
return new FurniMovementData(id, fromX, fromY, toX, toY, fromZ, toZ, rotation, duration);
return furniMovement(id, fromX, fromY, toX, toY, fromZ, toZ, rotation, duration, 0, FURNI_ANCHOR_NONE, 0);
}
public static MovementData furniMovement(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ, int rotation, int duration, int elapsed) {
return furniMovement(id, fromX, fromY, toX, toY, fromZ, toZ, rotation, duration, elapsed, FURNI_ANCHOR_NONE, 0);
}
public static MovementData furniMovement(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ, int rotation, int duration, int elapsed, int anchorType, int anchorId) {
return new FurniMovementData(id, fromX, fromY, toX, toY, fromZ, toZ, rotation, duration, elapsed, anchorType, anchorId);
}
public static MovementData userWalkMovement(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ, int bodyDirection, int headDirection, int duration) {
@@ -133,8 +145,11 @@ public class WiredMovementsComposer extends MessageComposer {
private final int id;
private final int rotation;
private final int duration;
private final int elapsed;
private final int anchorType;
private final int anchorId;
private FurniMovementData(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ, int rotation, int duration) {
private FurniMovementData(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ, int rotation, int duration, int elapsed, int anchorType, int anchorId) {
super(TYPE_FURNI_MOVE);
this.id = id;
this.fromX = fromX;
@@ -145,6 +160,9 @@ public class WiredMovementsComposer extends MessageComposer {
this.toZ = toZ;
this.rotation = rotation;
this.duration = duration;
this.elapsed = elapsed;
this.anchorType = anchorType;
this.anchorId = anchorId;
}
@Override
@@ -158,6 +176,9 @@ public class WiredMovementsComposer extends MessageComposer {
response.appendInt(this.id);
response.appendInt(this.rotation);
response.appendInt(this.duration);
response.appendInt(this.elapsed);
response.appendInt(this.anchorType);
response.appendInt(this.anchorId);
}
}