Merge pull request #23 from Lorenzune/pr/wired-altitude-relative-move-clean-20260318

feat(wired): add altitude and relative move effects
This commit is contained in:
DuckieTM
2026-03-19 10:05:39 +01:00
committed by GitHub
4 changed files with 575 additions and 10 deletions
@@ -264,11 +264,8 @@ public class ItemManager {
this.interactionsList.add(new ItemInteraction("wf_act_alert", WiredEffectAlert.class));
this.interactionsList.add(new ItemInteraction("wf_act_give_handitem", WiredEffectGiveHandItem.class));
this.interactionsList.add(new ItemInteraction("wf_act_give_effect", WiredEffectGiveEffect.class));
this.interactionsList.add(new ItemInteraction("wf_act_freeze", WiredEffectFreeze.class));
this.interactionsList.add(new ItemInteraction("wf_act_unfreeze", WiredEffectUnfreeze.class));
this.interactionsList.add(new ItemInteraction("wf_act_furni_to_user", WiredEffectFurniToUser.class));
this.interactionsList.add(new ItemInteraction("wf_act_user_to_furni", WiredEffectUserToFurni.class));
this.interactionsList.add(new ItemInteraction("wf_act_furni_to_furni", WiredEffectFurniToFurni.class));
this.interactionsList.add(new ItemInteraction("wf_act_set_altitude", WiredEffectSetAltitude.class));
this.interactionsList.add(new ItemInteraction("wf_act_rel_mov", WiredEffectRelativeMove.class));
this.interactionsList.add(new ItemInteraction("wf_slc_furni_area", WiredEffectFurniArea.class));
this.interactionsList.add(new ItemInteraction("wf_slc_furni_neighborhood", WiredEffectFurniNeighborhood.class));
this.interactionsList.add(new ItemInteraction("wf_slc_furni_bytype", WiredEffectFurniByType.class));
@@ -0,0 +1,283 @@
package com.eu.habbo.habbohotel.items.interactions.wired.effects;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect;
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.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.WiredSourceUtil;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.incoming.wired.WiredSaveException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class WiredEffectRelativeMove extends InteractionWiredEffect {
private static final int HORIZONTAL_NEGATIVE = 0;
private static final int HORIZONTAL_POSITIVE = 1;
private static final int VERTICAL_NEGATIVE = 0;
private static final int VERTICAL_POSITIVE = 1;
private static final int MAX_DISTANCE = 20;
public static final WiredEffectType type = WiredEffectType.RELATIVE_MOVE;
private final List<HabboItem> items = new ArrayList<>();
private int horizontalDirection = HORIZONTAL_POSITIVE;
private int horizontalDistance = 0;
private int verticalDirection = VERTICAL_POSITIVE;
private int verticalDistance = 0;
private int furniSource = WiredSourceUtil.SOURCE_TRIGGER;
public WiredEffectRelativeMove(ResultSet set, Item baseItem) throws SQLException {
super(set, baseItem);
}
public WiredEffectRelativeMove(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
super(id, userId, item, extradata, limitedStack, limitedSells);
}
@Override
public void execute(WiredContext ctx) {
Room room = ctx.room();
if (room == null || room.getLayout() == null) {
return;
}
List<HabboItem> effectiveItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items);
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
this.items.removeIf(item -> item == null
|| item.getRoomId() != this.getRoomId()
|| room.getHabboItem(item.getId()) == null);
}
int deltaX = this.getHorizontalOffset();
int deltaY = this.getVerticalOffset();
if (deltaX == 0 && deltaY == 0) {
return;
}
for (HabboItem item : effectiveItems) {
if (item == null || item.getRoomId() != this.getRoomId()) {
continue;
}
short targetX = (short) (item.getX() + deltaX);
short targetY = (short) (item.getY() + deltaY);
RoomTile targetTile = room.getLayout().getTile(targetX, targetY);
if (targetTile == null) {
continue;
}
room.moveFurniTo(item, targetTile, item.getRotation(), null, true, false);
}
}
@Deprecated
@Override
public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) {
return false;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(
this.getDelay(),
this.items.stream().map(HabboItem::getId).collect(Collectors.toList()),
this.horizontalDirection,
this.horizontalDistance,
this.verticalDirection,
this.verticalDistance,
this.furniSource
));
}
@Override
public void loadWiredData(ResultSet set, Room room) throws SQLException {
this.items.clear();
String wiredData = set.getString("wired_data");
if (wiredData != null && wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
this.setDelay(data.delay);
this.horizontalDirection = this.normalizeBinary(data.horizontalDirection, HORIZONTAL_POSITIVE);
this.horizontalDistance = this.normalizeDistance(data.horizontalDistance);
this.verticalDirection = this.normalizeBinary(data.verticalDirection, VERTICAL_POSITIVE);
this.verticalDistance = this.normalizeDistance(data.verticalDistance);
this.furniSource = data.furniSource;
if (data.itemIds != null) {
for (Integer id : data.itemIds) {
HabboItem item = room.getHabboItem(id);
if (item != null) {
this.items.add(item);
}
}
}
return;
}
this.horizontalDirection = HORIZONTAL_POSITIVE;
this.horizontalDistance = 0;
this.verticalDirection = VERTICAL_POSITIVE;
this.verticalDistance = 0;
this.furniSource = WiredSourceUtil.SOURCE_TRIGGER;
this.setDelay(0);
}
@Override
public void onPickUp() {
this.items.clear();
this.horizontalDirection = HORIZONTAL_POSITIVE;
this.horizontalDistance = 0;
this.verticalDirection = VERTICAL_POSITIVE;
this.verticalDistance = 0;
this.furniSource = WiredSourceUtil.SOURCE_TRIGGER;
this.setDelay(0);
}
@Override
public WiredEffectType getType() {
return type;
}
@Override
public void serializeWiredData(ServerMessage message, Room room) {
List<HabboItem> itemsSnapshot = new ArrayList<>(this.items);
itemsSnapshot.removeIf(item -> item == null
|| item.getRoomId() != this.getRoomId()
|| room.getHabboItem(item.getId()) == null);
this.items.clear();
this.items.addAll(itemsSnapshot);
message.appendBoolean(false);
message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION);
message.appendInt(itemsSnapshot.size());
for (HabboItem item : itemsSnapshot) {
message.appendInt(item.getId());
}
message.appendInt(this.getBaseItem().getSpriteId());
message.appendInt(this.getId());
message.appendString("");
message.appendInt(5);
message.appendInt(this.horizontalDirection);
message.appendInt(this.horizontalDistance);
message.appendInt(this.verticalDirection);
message.appendInt(this.verticalDistance);
message.appendInt(this.furniSource);
message.appendInt(0);
message.appendInt(this.getType().code);
message.appendInt(this.getDelay());
message.appendInt(0);
}
@Override
public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException {
int[] params = settings.getIntParams();
if (params.length < 5) {
throw new WiredSaveException("Invalid data");
}
this.horizontalDirection = this.normalizeBinary(params[0], HORIZONTAL_POSITIVE);
this.horizontalDistance = this.normalizeDistance(params[1]);
this.verticalDirection = this.normalizeBinary(params[2], VERTICAL_POSITIVE);
this.verticalDistance = this.normalizeDistance(params[3]);
this.furniSource = params[4];
int delay = settings.getDelay();
if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) {
throw new WiredSaveException("Delay too long");
}
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId());
if (room == null) {
throw new WiredSaveException("Room not found");
}
if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) {
throw new WiredSaveException("Too many furni selected");
}
List<HabboItem> newItems = new ArrayList<>();
for (int itemId : settings.getFurniIds()) {
HabboItem item = room.getHabboItem(itemId);
if (item == null) {
throw new WiredSaveException(String.format("Item %s not found", itemId));
}
newItems.add(item);
}
this.items.clear();
this.items.addAll(newItems);
this.setDelay(delay);
return true;
}
private int getHorizontalOffset() {
if (this.horizontalDistance <= 0) {
return 0;
}
return (this.horizontalDirection == HORIZONTAL_NEGATIVE) ? -this.horizontalDistance : this.horizontalDistance;
}
private int getVerticalOffset() {
if (this.verticalDistance <= 0) {
return 0;
}
return (this.verticalDirection == VERTICAL_NEGATIVE) ? -this.verticalDistance : this.verticalDistance;
}
private int normalizeBinary(int value, int fallback) {
if (value == 0 || value == 1) {
return value;
}
return fallback;
}
private int normalizeDistance(int value) {
return Math.max(0, Math.min(MAX_DISTANCE, value));
}
static class JsonData {
int delay;
List<Integer> itemIds;
int horizontalDirection;
int horizontalDistance;
int verticalDirection;
int verticalDistance;
int furniSource;
public JsonData(int delay, List<Integer> itemIds, int horizontalDirection, int horizontalDistance, int verticalDirection, int verticalDistance, int furniSource) {
this.delay = delay;
this.itemIds = itemIds;
this.horizontalDirection = horizontalDirection;
this.horizontalDistance = horizontalDistance;
this.verticalDirection = verticalDirection;
this.verticalDistance = verticalDistance;
this.furniSource = furniSource;
}
}
}
@@ -0,0 +1,288 @@
package com.eu.habbo.habbohotel.items.interactions.wired.effects;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect;
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.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.WiredSourceUtil;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.incoming.wired.WiredSaveException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class WiredEffectSetAltitude extends InteractionWiredEffect {
private static final Pattern ALTITUDE_PATTERN = Pattern.compile("^\\d+(\\.\\d{1,2})?$");
private static final int OPERATOR_INCREASE = 0;
private static final int OPERATOR_DECREASE = 1;
private static final int OPERATOR_SET = 2;
public static final WiredEffectType type = WiredEffectType.SET_ALTITUDE;
private final List<HabboItem> items = new ArrayList<>();
private int operator = OPERATOR_SET;
private double altitude = 0.0D;
private int furniSource = WiredSourceUtil.SOURCE_TRIGGER;
public WiredEffectSetAltitude(ResultSet set, Item baseItem) throws SQLException {
super(set, baseItem);
}
public WiredEffectSetAltitude(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
super(id, userId, item, extradata, limitedStack, limitedSells);
}
@Override
public void execute(WiredContext ctx) {
Room room = ctx.room();
if (room == null) {
return;
}
List<HabboItem> effectiveItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items);
if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) {
this.items.removeIf(item -> item == null
|| item.getRoomId() != this.getRoomId()
|| room.getHabboItem(item.getId()) == null);
}
for (HabboItem item : effectiveItems) {
if (item == null || item.getRoomId() != this.getRoomId()) {
continue;
}
RoomTile tile = room.getLayout().getTile(item.getX(), item.getY());
if (tile == null) {
continue;
}
double nextAltitude = this.computeAltitude(item.getZ());
room.moveFurniTo(item, tile, item.getRotation(), nextAltitude, null, true, false);
}
}
@Deprecated
@Override
public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) {
return false;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(
this.getDelay(),
this.items.stream().map(HabboItem::getId).collect(Collectors.toList()),
this.operator,
this.formatAltitude(this.altitude),
this.furniSource
));
}
@Override
public void loadWiredData(ResultSet set, Room room) throws SQLException {
this.items.clear();
String wiredData = set.getString("wired_data");
if (wiredData != null && wiredData.startsWith("{")) {
JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class);
this.setDelay(data.delay);
this.operator = this.normalizeOperator(data.operator);
this.altitude = this.parseAltitudeOrDefault(data.altitude);
this.furniSource = data.furniSource;
if (data.itemIds != null) {
for (Integer id : data.itemIds) {
HabboItem item = room.getHabboItem(id);
if (item != null) {
this.items.add(item);
}
}
}
return;
}
this.operator = OPERATOR_SET;
this.altitude = 0.0D;
this.furniSource = WiredSourceUtil.SOURCE_TRIGGER;
this.setDelay(0);
}
@Override
public void onPickUp() {
this.items.clear();
this.operator = OPERATOR_SET;
this.altitude = 0.0D;
this.furniSource = WiredSourceUtil.SOURCE_TRIGGER;
this.setDelay(0);
}
@Override
public WiredEffectType getType() {
return type;
}
@Override
public void serializeWiredData(ServerMessage message, Room room) {
List<HabboItem> itemsSnapshot = new ArrayList<>(this.items);
itemsSnapshot.removeIf(item -> item == null
|| item.getRoomId() != this.getRoomId()
|| room.getHabboItem(item.getId()) == null);
this.items.clear();
this.items.addAll(itemsSnapshot);
message.appendBoolean(false);
message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION);
message.appendInt(itemsSnapshot.size());
for (HabboItem item : itemsSnapshot) {
message.appendInt(item.getId());
}
message.appendInt(this.getBaseItem().getSpriteId());
message.appendInt(this.getId());
message.appendString(this.formatAltitude(this.altitude));
message.appendInt(2);
message.appendInt(this.operator);
message.appendInt(this.furniSource);
message.appendInt(0);
message.appendInt(this.getType().code);
message.appendInt(this.getDelay());
message.appendInt(0);
}
@Override
public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException {
int[] params = settings.getIntParams();
this.operator = (params.length > 0) ? this.normalizeOperator(params[0]) : OPERATOR_SET;
this.furniSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER;
int delay = settings.getDelay();
if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) {
throw new WiredSaveException("Delay too long");
}
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId());
if (room == null) {
throw new WiredSaveException("Room not found");
}
if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) {
throw new WiredSaveException("Too many furni selected");
}
List<HabboItem> newItems = new ArrayList<>();
for (int itemId : settings.getFurniIds()) {
HabboItem item = room.getHabboItem(itemId);
if (item == null) {
throw new WiredSaveException(String.format("Item %s not found", itemId));
}
newItems.add(item);
}
this.altitude = this.parseAltitude(settings.getStringParam());
this.items.clear();
this.items.addAll(newItems);
this.setDelay(delay);
return true;
}
private int normalizeOperator(int value) {
if (value < OPERATOR_INCREASE || value > OPERATOR_SET) {
return OPERATOR_SET;
}
return value;
}
private double computeAltitude(double currentAltitude) {
double nextAltitude;
switch (this.operator) {
case OPERATOR_INCREASE:
nextAltitude = currentAltitude + this.altitude;
break;
case OPERATOR_DECREASE:
nextAltitude = currentAltitude - this.altitude;
break;
case OPERATOR_SET:
default:
nextAltitude = this.altitude;
break;
}
return this.normalizeAltitude(nextAltitude);
}
private double parseAltitude(String value) throws WiredSaveException {
String normalized = (value != null) ? value.trim() : "";
if (normalized.isEmpty()) {
return 0.0D;
}
if (!ALTITUDE_PATTERN.matcher(normalized).matches()) {
throw new WiredSaveException("Invalid altitude value");
}
try {
return this.normalizeAltitude(new BigDecimal(normalized).doubleValue());
} catch (NumberFormatException exception) {
throw new WiredSaveException("Invalid altitude value");
}
}
private double parseAltitudeOrDefault(String value) {
try {
return this.parseAltitude(value);
} catch (WiredSaveException exception) {
return 0.0D;
}
}
private double normalizeAltitude(double value) {
double clampedValue = Math.max(0.0D, Math.min(Room.MAXIMUM_FURNI_HEIGHT, value));
return BigDecimal.valueOf(clampedValue).setScale(2, RoundingMode.HALF_UP).doubleValue();
}
private String formatAltitude(double value) {
BigDecimal decimal = BigDecimal.valueOf(this.normalizeAltitude(value)).stripTrailingZeros();
return (decimal.scale() < 0 ? decimal.setScale(0, RoundingMode.DOWN) : decimal).toPlainString();
}
static class JsonData {
int delay;
List<Integer> itemIds;
int operator;
String altitude;
int furniSource;
public JsonData(int delay, List<Integer> itemIds, int operator, String altitude, int furniSource) {
this.delay = delay;
this.itemIds = itemIds;
this.operator = operator;
this.altitude = altitude;
this.furniSource = furniSource;
}
}
}
@@ -33,11 +33,8 @@ public enum WiredEffectType {
USERS_AREA_SELECTOR(31),
USERS_NEIGHBORHOOD_SELECTOR(32),
SEND_SIGNAL(33),
FREEZE(34),
UNFREEZE(35),
FURNI_TO_USER(36),
USER_TO_FURNI(37),
FURNI_TO_FURNI(38);
SET_ALTITUDE(39),
RELATIVE_MOVE(40);
public final int code;