diff --git a/Emulator/.idea/misc.xml b/Emulator/.idea/misc.xml index 5ddb3b31..6568344f 100644 --- a/Emulator/.idea/misc.xml +++ b/Emulator/.idea/misc.xml @@ -8,5 +8,5 @@ - + \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java index d4ac19c7..5ee05fe3 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java @@ -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)); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionActorDir.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionActorDir.java index 8c0e79af..a9670b2e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionActorDir.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionActorDir.java @@ -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) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionGroupMember.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionGroupMember.java index d442add5..441a5989 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionGroupMember.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionGroupMember.java @@ -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) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotInGroup.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotInGroup.java index 04bbe22c..06055f4d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotInGroup.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotInGroup.java @@ -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) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java index 87e09812..590ea9d8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java @@ -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: diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBase.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBase.java index 8e05c562..1b5dd1d9 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBase.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBase.java @@ -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) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggererMatch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggererMatch.java index f9997436..e7f01dcb 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggererMatch.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggererMatch.java @@ -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; } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPerformsAction.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPerformsAction.java index a24ce86e..5e49a6a1 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPerformsAction.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPerformsAction.java @@ -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) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotGiveHandItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotGiveHandItem.java index 62fef4e0..53a8be66 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotGiveHandItem.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotGiveHandItem.java @@ -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) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToUser.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToUser.java index 9d4fad7d..63274e00 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToUser.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToUser.java @@ -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 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 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); } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserFurniBase.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserFurniBase.java index bc86f16d..9f8df66b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserFurniBase.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserFurniBase.java @@ -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 resolveItems(WiredContext ctx) { Room room = ctx.room(); List 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 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( diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraExecuteInOrder.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraExecuteInOrder.java new file mode 100644 index 00000000..d6023d4e --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraExecuteInOrder.java @@ -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 { + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraExecutionLimit.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraExecutionLimit.java new file mode 100644 index 00000000..f616b855 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraExecutionLimit.java @@ -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 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; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java index c1bfecef..9f460464 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java @@ -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); - } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java index 24475dcc..8864f4c3 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java @@ -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); - } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java index e91a43f3..2ac70e88 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java @@ -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); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java index dadac6be..3986d5b8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java @@ -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); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterShort.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterShort.java index 00800e67..e20f5bae 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterShort.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterShort.java @@ -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); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java index 989f7f57..f8f61b86 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java @@ -2032,6 +2032,10 @@ public class Room implements Comparable, ISerialize, Runnable { this.messagingManager.sendComposer(message); } + public void sendComposers(Collection messages) { + this.messagingManager.sendComposers(messages); + } + public void sendComposerToHabbosWithRights(ServerMessage message) { this.messagingManager.sendComposerToHabbosWithRights(message); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java index 33e1cde8..8f5bbc51 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java @@ -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 batchedMessages = new ArrayList<>(2); + batchedMessages.add(statusComposer); + batchedMessages.add(wiredMovementsComposer); + this.room.sendComposers(batchedMessages); + } else { + this.room.sendComposer(statusComposer); + } } // Cycle trax manager diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomMessagingManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomMessagingManager.java index 10c8173c..5807cdec 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomMessagingManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomMessagingManager.java @@ -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 messages) { + if (messages == null || messages.isEmpty()) { + return; + } + + ArrayList 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. */ diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java index 66d2e5e5..bda55501 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java @@ -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. diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java index 47ee4f1c..759f5b86 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java @@ -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 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 effectsToExecute = new THashSet(); + List executionPlans = new ArrayList<>(); List triggeredTiles = new ArrayList<>(); for (InteractionWiredTrigger trigger : triggers) { @@ -81,10 +89,10 @@ public class WiredHandler { if (triggeredTiles.contains(tile)) continue; - THashSet tEffectsToExecute = new THashSet(); + 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 effectsToExecute = new THashSet(); + List executionPlans = new ArrayList<>(); List triggeredTiles = new ArrayList<>(); for (InteractionWiredTrigger trigger : triggers) { @@ -130,44 +138,51 @@ public class WiredHandler { if (triggeredTiles.contains(tile)) continue; - THashSet tEffectsToExecute = new THashSet(); + 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 effectsToExecute = new THashSet(); + 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 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 conditions = room.getRoomSpecialTypes().getConditions(trigger.getX(), trigger.getY()); THashSet effects = room.getRoomSpecialTypes().getEffects(trigger.getX(), trigger.getY()); - if (Emulator.getPluginManager().fireEvent(new WiredStackTriggeredEvent(room, roomUnit, trigger, effects, conditions)).isCancelled()) - return false; + THashSet 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 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 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 effectList = new ArrayList<>(effects); + List 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 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 queueableEffects = new LinkedHashSet<>(); + + for (InteractionWiredEffect effect : effects) { + if (canQueueEffect(effect, roomUnit, millis)) { + queueableEffects.add(effect); + } + } + + LinkedHashSet delays = new LinkedHashSet<>(); + for (InteractionWiredEffect effect : queueableEffects) { + delays.add(effect.getDelay()); + } + + for (Integer delay : delays) { + List 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 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(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/WiredStack.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/WiredStack.java index 0713715a..9833f53f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/WiredStack.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/WiredStack.java @@ -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 conditions, List 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 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 + '}'; } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java index 8aa7b883..d67c7b82 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java @@ -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 ); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java index 0ceb4035..5d039ba6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java @@ -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 effects, WiredContext ctx, long currentTime) { + if (effects == null || effects.isEmpty()) { + return; + } + + Map> 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> entry : effectsByDelay.entrySet()) { + int delay = entry.getKey(); + List 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 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 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 InteractionWiredExtra getStackExtra(Room room, WiredStack stack, Class extraClass) { if (room == null || stack == null || stack.triggerItem() == null || room.getRoomSpecialTypes() == null) { return null; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java index 239266f0..0e15a5e0 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java @@ -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 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 ========== /** diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java index 40b86acd..74ab5e11 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java @@ -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> SUPPRESSED_STATUS_ROOM_UNIT_IDS = new ThreadLocal<>(); + private static final ThreadLocal> COLLECTED_MOVEMENTS = new ThreadLocal<>(); private static final ConcurrentHashMap SUPPRESSED_STATUS_COMPOSER_UNTIL = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap> 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 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 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 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 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 roomUnits) { + if (room == null || roomUnits == null || roomUnits.isEmpty()) { + return; + } + + for (RoomUnit roomUnit : roomUnits) { + if (roomUnit == null) { + continue; + } + + ConcurrentHashMap 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 toRemove = new ArrayList<>(); + + if (shouldSettleFollowersForNewStep(followers, moveStatusTimestamp)) { + settleUserFollowers(room, followers); + } + + List> orderedFollowers = new ArrayList<>(followers.entrySet()); + orderedFollowers.sort(Comparator + .comparingDouble((Map.Entry followerEntry) -> { + UserFollowEntry entry = followerEntry.getValue(); + return (entry != null && entry.zOverride != null) ? entry.zOverride : Double.MAX_VALUE; + }) + .thenComparingInt(Map.Entry::getKey)); + + for (Map.Entry 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 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 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 followers) { + if (room == null || followers == null || followers.isEmpty()) { + return; + } + + List> entriesToSettle = new ArrayList<>(followers.entrySet()); + entriesToSettle.sort(Comparator + .comparingDouble((Map.Entry followerEntry) -> { + UserFollowEntry entry = followerEntry.getValue(); + return (entry != null && entry.zOverride != null) ? -entry.zOverride : Double.POSITIVE_INFINITY; + }) + .thenComparingInt(Map.Entry::getKey)); + + for (Map.Entry 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 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 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 carriedMoves = getCarriedUnitMoves(room, movingItem, targetTile, rotation, carryContext); List 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 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; + } + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java index b8ec7046..c2094c6b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java @@ -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(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/WiredMovementsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/WiredMovementsComposer.java index 07c61622..29414eb0 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/WiredMovementsComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/WiredMovementsComposer.java @@ -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); } }