diff --git a/common/src/main/java/com/viaversion/viabackwards/api/entities/EntityScaleData.java b/common/src/main/java/com/viaversion/viabackwards/api/entities/EntityScaleData.java new file mode 100644 index 00000000..a23b0f9e --- /dev/null +++ b/common/src/main/java/com/viaversion/viabackwards/api/entities/EntityScaleData.java @@ -0,0 +1,46 @@ +/* + * This file is part of ViaBackwards - https://github.com/ViaVersion/ViaBackwards + * Copyright (C) 2016-2025 ViaVersion and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.viaversion.viabackwards.api.entities; + +import com.viaversion.viaversion.api.data.entity.StoredEntityData; + +/** + * Storage object used within {@link StoredEntityData} to track the scaling state of an entity. + * This ensures that scale updates are only sent when the calculated scale actually changes. + */ +public final class EntityScaleData { + + private boolean isBaby; + private float scale = 1.0f; + + public boolean isBaby() { + return isBaby; + } + + public void setBaby(boolean baby) { + isBaby = baby; + } + + public float getScale() { + return scale; + } + + public void setScale(float scale) { + this.scale = scale; + } +} diff --git a/common/src/main/java/com/viaversion/viabackwards/api/entities/EntityScaleHelper.java b/common/src/main/java/com/viaversion/viabackwards/api/entities/EntityScaleHelper.java new file mode 100644 index 00000000..0065bda6 --- /dev/null +++ b/common/src/main/java/com/viaversion/viabackwards/api/entities/EntityScaleHelper.java @@ -0,0 +1,126 @@ +/* + * This file is part of ViaBackwards - https://github.com/ViaVersion/ViaBackwards + * Copyright (C) 2016-2025 ViaVersion and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.viaversion.viabackwards.api.entities; + +import com.viaversion.viabackwards.api.rewriters.EntityRewriter; +import com.viaversion.viaversion.api.data.entity.StoredEntityData; +import com.viaversion.viaversion.api.minecraft.entities.EntityType; +import com.viaversion.viaversion.api.minecraft.entitydata.EntityData; +import com.viaversion.viaversion.api.protocol.Protocol; +import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; +import com.viaversion.viaversion.api.protocol.remapper.PacketHandlers; +import com.viaversion.viaversion.api.type.Types; +import com.viaversion.viaversion.api.protocol.packet.ClientboundPacketType; +import com.viaversion.viaversion.api.protocol.remapper.PacketHandler; +import com.viaversion.viaversion.rewriter.entitydata.EntityDataHandlerEvent; + +import java.util.HashMap; +import java.util.Map; +import com.viaversion.viaversion.api.data.FullMappings; + +/** + * A modular helper for tracking and injecting entity scaling on older clients. + * When older clients lack a specific entity, ViaBackwards maps it to a chosen existing entity. + * Sometimes this mapping requires the entity to be scaled (e.g. mapping a tiny baby mob to a large adult mob). + * + * Example usage for future scenarios: + * If Mojang adds a Vulture and a Baby Vulture, and let's say map the adult Vulture to a Bat: + * - We'd need to scale UP the entire Bat so it looks like an adult Vulture. + * - If that happens, you'd apply a baseline scale increase (like 2.0x) to both adult and baby via the main rewriter. + * - But since there's no baby Bat, you'd also need to track `isBaby` here. + * - If `isBaby` is true, you'd apply a reduced scale relative to the adult Vulture's new scale (e.g., 0.65x). + * - Because `EntityScaleAttributeRewriter` dynamically multiplies the attribute value mid-flight, you simply + * register `0.65f` for the baby here. The final scale sent to the client will correctly be (2.0 * 0.65) = 1.3x. + */ +public class EntityScaleHelper { + + private static final int BABY_INDEX = 16; // Standard Ageable "is_baby" index + private final Map babyScales = new HashMap<>(); + private final String scaleAttributeId; + private final ClientboundPacketType updateAttributesPacket; + + public EntityScaleHelper(String scaleAttributeId, ClientboundPacketType updateAttributesPacket) { + this.scaleAttributeId = scaleAttributeId; + this.updateAttributesPacket = updateAttributesPacket; + } + + /** + * Registers a scaling factor for a specific baby entity type. + * @param type The entity type that requires scaling when it is a baby. + * @param babyScale The multiplier to apply to the generic.scale attribute when it is a baby. + */ + public void addBabyScale(EntityType type, float babyScale) { + babyScales.put(type, babyScale); + } + + /** + * Checks the metadata, tracks the scale state natively, and injects an UPDATE_ATTRIBUTES + * packet down the pipeline right as the metadata is processed by the client. + * Use this inside an Entity Rewriter's filter().handler(). + * + * @param event The meta packet processing event. + * @param data The current metadata piece being processed. + * @param protocol The protocol handling the translation, used for mapping lookups. + */ + public void trackAndInject(EntityDataHandlerEvent event, EntityData data, Protocol protocol) { + if (data.id() == BABY_INDEX && data.value() instanceof Boolean) { + StoredEntityData storedEntityData = event.user().getEntityTracker(protocol.getClass()).entityData(event.entityId()); + if (storedEntityData == null) return; + + // Check if this protocol layer actually cares about scaling this entity + Float babyScaleFactor = babyScales.get(storedEntityData.type()); + if (babyScaleFactor == null) { + return; // Not registered for this protocol, do nothing + } + + EntityScaleData scaleData = storedEntityData.get(EntityScaleData.class); + if (scaleData == null) { + scaleData = new EntityScaleData(); + storedEntityData.put(scaleData); + } + + boolean isBaby = (Boolean) data.value(); + float scale = isBaby ? babyScaleFactor : 1.0f; + + if (scaleData.isBaby() != isBaby || scaleData.getScale() != scale) { + scaleData.setBaby(isBaby); + scaleData.setScale(scale); + + // Actively inject the packet so the client receives the scale update immediately + try { + PacketWrapper updatePacket = PacketWrapper.create(updateAttributesPacket, event.user()); + updatePacket.write(Types.VAR_INT, event.entityId()); + updatePacket.write(Types.VAR_INT, 1); // 1 attribute + + FullMappings attributeMappings = protocol.getMappingData().getAttributeMappings(); + int serverId = attributeMappings != null ? attributeMappings.id(scaleAttributeId) : -1; + int mappedId = serverId != -1 ? protocol.getMappingData().getNewAttributeId(serverId) : -1; + + if (mappedId != -1) { + updatePacket.write(Types.VAR_INT, mappedId); + updatePacket.write(Types.DOUBLE, (double) scaleData.getScale()); + updatePacket.write(Types.VAR_INT, 0); // 0 modifiers + updatePacket.scheduleSend(protocol.getClass()); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } +} \ No newline at end of file diff --git a/common/src/main/java/com/viaversion/viabackwards/api/rewriters/EntityScaleAttributeRewriter.java b/common/src/main/java/com/viaversion/viabackwards/api/rewriters/EntityScaleAttributeRewriter.java new file mode 100644 index 00000000..ea3d777b --- /dev/null +++ b/common/src/main/java/com/viaversion/viabackwards/api/rewriters/EntityScaleAttributeRewriter.java @@ -0,0 +1,112 @@ +/* + * This file is part of ViaBackwards - https://github.com/ViaVersion/ViaBackwards + * Copyright (C) 2016-2025 ViaVersion and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.viaversion.viabackwards.api.rewriters; + +import com.viaversion.viabackwards.api.entities.EntityScaleData; +import com.viaversion.viaversion.api.data.entity.StoredEntityData; +import com.viaversion.viaversion.api.protocol.Protocol; +import com.viaversion.viaversion.api.protocol.packet.ClientboundPacketType; +import com.viaversion.viaversion.api.type.Types; +import com.viaversion.viaversion.rewriter.AttributeRewriter; + +/** + * An extension of AttributeRewriter that intercepts UPDATE_ATTRIBUTES packets + * and dynamically multiplies the scale value if the entity's tracked state + * (EntityScaleData) currently dictates it should be shrunk (e.g. a baby mob mapped to an adult). + * + * This protects scaled entities from instantly growing to adult size when the server + * issues random attribute updates for other reasons. + */ +public class EntityScaleAttributeRewriter extends AttributeRewriter { + + private final String scaleAttributeId; + private final Protocol scaleProtocol; + + public EntityScaleAttributeRewriter(Protocol protocol, String scaleAttributeId) { + super(protocol); + this.scaleProtocol = protocol; + this.scaleAttributeId = scaleAttributeId; + } + + public EntityScaleAttributeRewriter(Protocol protocol) { + this(protocol, "minecraft:scale"); + } + + @Override + public void register1_21(C packetType) { + scaleProtocol.registerClientbound(packetType, wrapper -> { + final int entityId = wrapper.passthrough(Types.VAR_INT); + + // Fast lookup for scaling factor, negligible overhead if not present + float scale = 1.0f; + try { + StoredEntityData data = wrapper.user().getEntityTracker(scaleProtocol.getClass()).entityData(entityId); + if (data != null && data.has(EntityScaleData.class)) { + scale = data.get(EntityScaleData.class).getScale(); + } + } catch (Exception ignored) { + // If entity tracker is missing or data is malformed, just don't scale it + } + + final int size = wrapper.passthrough(Types.VAR_INT); + int newSize = size; + + int scaleId = -1; + if (scaleProtocol.getMappingData().getAttributeMappings() != null) { + scaleId = scaleProtocol.getMappingData().getAttributeMappings().id(scaleAttributeId); + } + + for (int i = 0; i < size; i++) { + final int attributeId = wrapper.read(Types.VAR_INT); + final int mappedId = scaleProtocol.getMappingData().getNewAttributeId(attributeId); + if (mappedId == -1) { + newSize--; + + wrapper.read(Types.DOUBLE); // Base + final int modifierSize = wrapper.read(Types.VAR_INT); + for (int j = 0; j < modifierSize; j++) { + wrapper.read(Types.STRING); // ID + wrapper.read(Types.DOUBLE); // Amount + wrapper.read(Types.BYTE); // Operation + } + continue; + } + + wrapper.write(Types.VAR_INT, mappedId); + double value = wrapper.read(Types.DOUBLE); // Base + + // Multiply the server's requested scale by our tracked scale modifier + if (scale != 1.0f && scaleId != -1 && attributeId == scaleId) { + value *= scale; + } + wrapper.write(Types.DOUBLE, value); + + final int modifierSize = wrapper.passthrough(Types.VAR_INT); + for (int j = 0; j < modifierSize; j++) { + wrapper.passthrough(Types.STRING); // ID + wrapper.passthrough(Types.DOUBLE); // Amount + wrapper.passthrough(Types.BYTE); // Operation + } + } + + if (size != newSize) { + wrapper.set(Types.VAR_INT, 1, newSize); + } + }); + } +} diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_2to1_21/Protocol1_21_2To1_21.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_2to1_21/Protocol1_21_2To1_21.java index f69ff0ab..c47c7d7d 100644 --- a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_2to1_21/Protocol1_21_2To1_21.java +++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_2to1_21/Protocol1_21_2To1_21.java @@ -88,7 +88,7 @@ protected void registerPackets() { particleRewriter.registerLevelParticles1_20_5(ClientboundPackets1_21_2.LEVEL_PARTICLES); new StatisticsRewriter<>(this).register(ClientboundPackets1_21_2.AWARD_STATS); - new AttributeRewriter<>(this).register1_21(ClientboundPackets1_21_2.UPDATE_ATTRIBUTES); + new com.viaversion.viabackwards.api.rewriters.EntityScaleAttributeRewriter<>(this).register1_21(ClientboundPackets1_21_2.UPDATE_ATTRIBUTES); translatableRewriter.registerComponentPacket(ClientboundPackets1_21_2.SET_ACTION_BAR_TEXT); translatableRewriter.registerComponentPacket(ClientboundPackets1_21_2.SET_TITLE_TEXT); diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_2to1_21/rewriter/EntityPacketRewriter1_21_2.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_2to1_21/rewriter/EntityPacketRewriter1_21_2.java index 82be0892..d816e626 100644 --- a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_2to1_21/rewriter/EntityPacketRewriter1_21_2.java +++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_2to1_21/rewriter/EntityPacketRewriter1_21_2.java @@ -23,6 +23,7 @@ import com.viaversion.nbt.tag.ListTag; import com.viaversion.nbt.tag.Tag; import com.viaversion.viabackwards.ViaBackwards; +import com.viaversion.viabackwards.api.entities.EntityScaleHelper; import com.viaversion.viabackwards.api.rewriters.BackwardsRegistryRewriter; import com.viaversion.viabackwards.api.rewriters.EntityRewriter; import com.viaversion.viabackwards.protocol.v1_21_2to1_21.Protocol1_21_2To1_21; @@ -30,6 +31,7 @@ import com.viaversion.viabackwards.protocol.v1_21_2to1_21.storage.SignStorage; import com.viaversion.viabackwards.utils.VelocityUtil; import com.viaversion.viaversion.api.data.entity.EntityTracker; +import com.viaversion.viaversion.api.data.entity.StoredEntityData; import com.viaversion.viaversion.api.minecraft.Holder; import com.viaversion.viaversion.api.minecraft.Particle; import com.viaversion.viaversion.api.minecraft.RegistryEntry; @@ -643,6 +645,15 @@ protected void registerRewrites() { ); registerBlockStateHandler(EntityTypes1_21_2.ABSTRACT_MINECART, 11); + final EntityScaleHelper scaleHelper = new EntityScaleHelper("minecraft:scale", ClientboundPackets1_21.UPDATE_ATTRIBUTES); + scaleHelper.addBabyScale(EntityTypes1_21_2.SQUID, 0.5f); + scaleHelper.addBabyScale(EntityTypes1_21_2.GLOW_SQUID, 0.5f); + scaleHelper.addBabyScale(EntityTypes1_21_2.DOLPHIN, 0.65f); + + filter().handler((event, meta) -> { + scaleHelper.trackAndInject(event, meta, protocol); + }); + filter().type(EntityTypes1_21_2.CREAKING).cancel(17); // Active filter().type(EntityTypes1_21_2.CREAKING).cancel(16); // Can move diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_6to1_21_5/Protocol1_21_6To1_21_5.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_6to1_21_5/Protocol1_21_6To1_21_5.java index 2d1e08eb..ae172e07 100644 --- a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_6to1_21_5/Protocol1_21_6To1_21_5.java +++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_6to1_21_5/Protocol1_21_6To1_21_5.java @@ -24,6 +24,10 @@ import com.viaversion.viabackwards.api.data.BackwardsMappingData; import com.viaversion.viabackwards.api.rewriters.SoundRewriter; import com.viaversion.viabackwards.api.rewriters.text.NBTComponentRewriter; +import com.viaversion.viabackwards.api.entities.EntityScaleHelper; +import com.viaversion.viabackwards.api.entities.EntityScaleData; +import com.viaversion.viaversion.api.data.entity.StoredEntityData; +import com.viaversion.viaversion.api.minecraft.entities.EntityType; import com.viaversion.viabackwards.protocol.v1_21_6to1_21_5.data.Dialog; import com.viaversion.viabackwards.protocol.v1_21_6to1_21_5.provider.ChestDialogViewProvider; import com.viaversion.viabackwards.protocol.v1_21_6to1_21_5.provider.DialogViewProvider; @@ -113,7 +117,9 @@ protected void registerPackets() { }); new StatisticsRewriter<>(this).register(ClientboundPackets1_21_6.AWARD_STATS); - new AttributeRewriter<>(this).register1_21(ClientboundPackets1_21_6.UPDATE_ATTRIBUTES); + + new com.viaversion.viabackwards.api.rewriters.EntityScaleAttributeRewriter<>(this).register1_21(ClientboundPackets1_21_6.UPDATE_ATTRIBUTES); + new CommandRewriter1_19_4<>(this) { @Override public void handleArgument(final PacketWrapper wrapper, final String argumentType) { diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_6to1_21_5/rewriter/EntityPacketRewriter1_21_6.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_6to1_21_5/rewriter/EntityPacketRewriter1_21_6.java index b02da50c..5baae524 100644 --- a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_6to1_21_5/rewriter/EntityPacketRewriter1_21_6.java +++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_6to1_21_5/rewriter/EntityPacketRewriter1_21_6.java @@ -17,8 +17,11 @@ */ package com.viaversion.viabackwards.protocol.v1_21_6to1_21_5.rewriter; +import com.viaversion.viabackwards.api.entities.EntityScaleData; +import com.viaversion.viabackwards.api.entities.EntityScaleHelper; import com.viaversion.viabackwards.api.rewriters.EntityRewriter; import com.viaversion.viabackwards.protocol.v1_21_6to1_21_5.Protocol1_21_6To1_21_5; +import com.viaversion.viaversion.api.data.entity.StoredEntityData; import com.viaversion.viaversion.api.minecraft.entities.EntityType; import com.viaversion.viaversion.api.minecraft.entities.EntityTypes1_21_6; import com.viaversion.viaversion.api.minecraft.entitydata.types.EntityDataTypes1_21_5; @@ -105,6 +108,13 @@ protected void registerRewrites() { filter().type(EntityTypes1_21_6.HANGING_ENTITY).removeIndex(8); // Direction filter().type(EntityTypes1_21_6.HAPPY_GHAST).cancel(17); // Leash holder filter().type(EntityTypes1_21_6.HAPPY_GHAST).cancel(18); // Stays still + + final EntityScaleHelper scaleHelper = new EntityScaleHelper("minecraft:scale", ClientboundPackets1_21_5.UPDATE_ATTRIBUTES); + scaleHelper.addBabyScale(EntityTypes1_21_6.HAPPY_GHAST, 0.2375f); + + filter().handler((event, meta) -> { + scaleHelper.trackAndInject(event, meta, protocol); + }); } @Override