Backup inconvertible item component data for creative clients in 1.21+ (#907)

Co-authored-by: Nassim Jahnke <nassim@njahnke.dev>
This commit is contained in:
EnZaXD 2024-11-22 10:36:42 +01:00 committed by GitHub
parent 1dfef5b97e
commit 276953f6cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 401 additions and 0 deletions

View File

@ -18,13 +18,17 @@
package com.viaversion.viabackwards.api.rewriters;
import com.viaversion.nbt.tag.CompoundTag;
import com.viaversion.nbt.tag.IntArrayTag;
import com.viaversion.nbt.tag.IntTag;
import com.viaversion.nbt.tag.ListTag;
import com.viaversion.nbt.tag.StringTag;
import com.viaversion.nbt.tag.Tag;
import com.viaversion.viabackwards.api.BackwardsProtocol;
import com.viaversion.viabackwards.api.data.BackwardsMappingData;
import com.viaversion.viabackwards.api.data.MappedItem;
import com.viaversion.viaversion.api.connection.UserConnection;
import com.viaversion.viaversion.api.minecraft.Holder;
import com.viaversion.viaversion.api.minecraft.HolderSet;
import com.viaversion.viaversion.api.minecraft.data.StructuredDataContainer;
import com.viaversion.viaversion.api.minecraft.data.StructuredDataKey;
import com.viaversion.viaversion.api.minecraft.item.Item;
@ -34,6 +38,8 @@ import com.viaversion.viaversion.api.type.Type;
import com.viaversion.viaversion.rewriter.StructuredItemRewriter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
import org.checkerframework.checker.nullness.qual.Nullable;
public class BackwardsStructuredItemRewriter<C extends ClientboundPacketType, S extends ServerboundPacketType,
@ -165,6 +171,54 @@ public class BackwardsStructuredItemRewriter<C extends ClientboundPacketType, S
return new ArrayList<>(data.values());
}
protected Tag holderSetToTag(final HolderSet set) {
if (set.hasIds()) {
return new IntArrayTag(set.ids());
} else {
return new StringTag(set.tagKey());
}
}
protected HolderSet restoreHolderSet(final CompoundTag tag, final String key) {
final Tag savedTag = tag.get(key);
if (savedTag == null) {
return HolderSet.of(new int[0]);
}
if (savedTag instanceof StringTag tagKey) {
return HolderSet.of(tagKey.getValue());
} else if (savedTag instanceof IntArrayTag idsTag) {
return HolderSet.of(idsTag.getValue());
} else {
return HolderSet.of(new int[0]);
}
}
protected <V> Tag holderToTag(final Holder<V> holder, final BiConsumer<V, CompoundTag> valueSaveFunction) {
if (holder.hasId()) {
return new IntTag(holder.id());
} else {
final CompoundTag savedTag = new CompoundTag();
valueSaveFunction.accept(holder.value(), savedTag);
return savedTag;
}
}
protected <V> Holder<V> restoreHolder(final CompoundTag tag, final String key, final Function<CompoundTag, V> valueRestoreFunction) {
final Tag savedTag = tag.get(key);
if (savedTag == null) {
return Holder.of(0);
}
if (savedTag instanceof IntTag idTag) {
return Holder.of(idTag.asInt());
} else if (savedTag instanceof CompoundTag compoundTag) {
return Holder.of(valueRestoreFunction.apply(compoundTag));
} else {
return Holder.of(0);
}
}
@Override
public String nbtTagName() {
return "VB|" + protocol.getClass().getSimpleName();

View File

@ -17,14 +17,32 @@
*/
package com.viaversion.viabackwards.protocol.v1_21_2to1_21.rewriter;
import com.viaversion.nbt.tag.ByteTag;
import com.viaversion.nbt.tag.CompoundTag;
import com.viaversion.nbt.tag.FloatTag;
import com.viaversion.nbt.tag.IntTag;
import com.viaversion.nbt.tag.ListTag;
import com.viaversion.nbt.tag.Tag;
import com.viaversion.viabackwards.api.rewriters.BackwardsStructuredItemRewriter;
import com.viaversion.viabackwards.protocol.v1_21_2to1_21.Protocol1_21_2To1_21;
import com.viaversion.viabackwards.protocol.v1_21_2to1_21.storage.InventoryStateIdStorage;
import com.viaversion.viabackwards.protocol.v1_21_2to1_21.storage.RecipeStorage;
import com.viaversion.viaversion.api.connection.UserConnection;
import com.viaversion.viaversion.api.data.MappingData;
import com.viaversion.viaversion.api.minecraft.Holder;
import com.viaversion.viaversion.api.minecraft.HolderSet;
import com.viaversion.viaversion.api.minecraft.Particle;
import com.viaversion.viaversion.api.minecraft.SoundEvent;
import com.viaversion.viaversion.api.minecraft.data.StructuredDataContainer;
import com.viaversion.viaversion.api.minecraft.data.StructuredDataKey;
import com.viaversion.viaversion.api.minecraft.item.Item;
import com.viaversion.viaversion.api.minecraft.item.data.Consumable1_21_2;
import com.viaversion.viaversion.api.minecraft.item.data.DeathProtection;
import com.viaversion.viaversion.api.minecraft.item.data.Equippable;
import com.viaversion.viaversion.api.minecraft.item.data.Instrument1_21_2;
import com.viaversion.viaversion.api.minecraft.item.data.PotionEffect;
import com.viaversion.viaversion.api.minecraft.item.data.PotionEffectData;
import com.viaversion.viaversion.api.minecraft.item.data.UseCooldown;
import com.viaversion.viaversion.api.protocol.packet.PacketWrapper;
import com.viaversion.viaversion.api.type.Types;
import com.viaversion.viaversion.api.type.types.chunk.ChunkType1_20_2;
@ -39,6 +57,7 @@ import com.viaversion.viaversion.rewriter.BlockRewriter;
import com.viaversion.viaversion.rewriter.SoundRewriter;
import com.viaversion.viaversion.util.Key;
import com.viaversion.viaversion.util.Limit;
import com.viaversion.viaversion.util.Unit;
import static com.viaversion.viaversion.protocols.v1_21to1_21_2.rewriter.BlockItemPacketRewriter1_21_2.downgradeItemData;
import static com.viaversion.viaversion.protocols.v1_21to1_21_2.rewriter.BlockItemPacketRewriter1_21_2.updateItemData;
@ -276,6 +295,7 @@ public final class BlockItemPacketRewriter1_21_2 extends BackwardsStructuredItem
@Override
public Item handleItemToClient(final UserConnection connection, final Item item) {
backupInconvertibleData(item);
super.handleItemToClient(connection, item);
downgradeItemData(item);
return item;
@ -285,6 +305,266 @@ public final class BlockItemPacketRewriter1_21_2 extends BackwardsStructuredItem
public Item handleItemToServer(final UserConnection connection, final Item item) {
super.handleItemToServer(connection, item);
updateItemData(item);
restoreInconvertibleData(item);
return item;
}
// Backup inconvertible data and later restore to prevent data loss for creative mode clients
private void backupInconvertibleData(final Item item) {
final StructuredDataContainer data = item.dataContainer();
data.setIdLookup(protocol, true);
final CompoundTag backupTag = new CompoundTag();
final Holder<Instrument1_21_2> instrument = data.get(StructuredDataKey.INSTRUMENT1_21_2);
if (instrument != null && instrument.isDirect()) {
backupTag.put("instrument_description", instrument.value().description());
}
final HolderSet repairable = data.get(StructuredDataKey.REPAIRABLE);
if (repairable != null) {
backupTag.put("repairable", holderSetToTag(repairable));
}
final Integer enchantable = data.get(StructuredDataKey.ENCHANTABLE);
if (enchantable != null) {
backupTag.putInt("enchantable", enchantable);
}
final UseCooldown useCooldown = data.get(StructuredDataKey.USE_COOLDOWN);
if (useCooldown != null) {
final CompoundTag tag = new CompoundTag();
tag.putFloat("seconds", useCooldown.seconds());
if (useCooldown.cooldownGroup() != null) {
tag.putString("cooldown_group", useCooldown.cooldownGroup());
}
backupTag.put("use_cooldown", tag);
}
final String itemModel = data.get(StructuredDataKey.ITEM_MODEL);
if (itemModel != null) {
backupTag.putString("item_model", itemModel);
}
final Equippable equippable = data.get(StructuredDataKey.EQUIPPABLE);
if (equippable != null) {
final CompoundTag tag = new CompoundTag();
tag.putInt("equipment_slot", equippable.equipmentSlot());
convertSoundEventHolder(tag, equippable.soundEvent());
final String model = equippable.model();
if (model != null) {
tag.putString("model", model);
}
final String cameraOverlay = equippable.cameraOverlay();
if (cameraOverlay != null) {
tag.putString("camera_overlay", cameraOverlay);
}
if (equippable.allowedEntities() != null) {
tag.put("allowed_entities", holderSetToTag(equippable.allowedEntities()));
}
tag.putBoolean("dispensable", equippable.dispensable());
tag.putBoolean("swappable", equippable.swappable());
tag.putBoolean("damage_on_hurt", equippable.damageOnHurt());
backupTag.put("equippable", tag);
}
final Unit glider = data.get(StructuredDataKey.GLIDER);
if (glider != null) {
backupTag.putBoolean("glider", true);
}
final String tooltipStyle = data.get(StructuredDataKey.TOOLTIP_STYLE);
if (tooltipStyle != null) {
backupTag.putString("tooltip_style", tooltipStyle);
}
final DeathProtection deathProtection = data.get(StructuredDataKey.DEATH_PROTECTION);
if (deathProtection != null) {
final ListTag<CompoundTag> tag = new ListTag<>(CompoundTag.class);
for (final Consumable1_21_2.ConsumeEffect<?> effect : deathProtection.deathEffects()) {
final CompoundTag effectTag = new CompoundTag();
convertConsumableEffect(effectTag, effect);
tag.add(effectTag);
}
backupTag.put("death_protection", tag);
}
if (!backupTag.isEmpty()) {
saveTag(createCustomTag(item), backupTag, "inconvertible_data");
}
}
private void convertConsumableEffect(final CompoundTag tag, Consumable1_21_2.ConsumeEffect<?> effect) {
tag.putInt("id", effect.id());
if (effect.type() == Consumable1_21_2.ApplyStatusEffects.TYPE && effect.value() instanceof Consumable1_21_2.ApplyStatusEffects value) {
tag.putString("type", "apply_effects");
final ListTag<CompoundTag> effects = new ListTag<>(CompoundTag.class);
for (final PotionEffect potionEffect : value.effects()) {
final CompoundTag effectTag = new CompoundTag();
effectTag.putInt("effect", potionEffect.effect());
convertPotionEffectData(effectTag, potionEffect.effectData());
effects.add(effectTag);
}
tag.put("effects", effects);
tag.putFloat("probability", value.probability());
} else if (effect.type() == Types.HOLDER_SET && effect.value() instanceof HolderSet set) {
tag.putString("type", "remove_effects");
tag.put("remove_effects", holderSetToTag(set));
} else if (effect.type() == Types.EMPTY) {
tag.putString("type", "clear_all_effects");
} else if (effect.type() == Types.FLOAT) {
tag.putString("type", "teleport_randomly");
tag.putFloat("probability", (Float) effect.value());
} else if (effect.type() == Types.SOUND_EVENT && effect.value() instanceof Holder sound) {
tag.putString("type", "play_sound");
convertSoundEventHolder(tag, sound);
}
}
private void convertPotionEffectData(final CompoundTag tag, final PotionEffectData data) {
tag.putInt("amplifier", data.amplifier());
tag.putInt("duration", data.duration());
tag.putBoolean("ambient", data.ambient());
tag.putBoolean("show_particles", data.showParticles());
tag.putBoolean("show_icon", data.showIcon());
if (data.hiddenEffect() != null) {
final CompoundTag hiddenEffect = new CompoundTag();
convertPotionEffectData(hiddenEffect, data.hiddenEffect());
tag.put("hidden_effect", hiddenEffect);
}
}
private void convertSoundEventHolder(final CompoundTag tag, final Holder<SoundEvent> holder) {
tag.put("sound_event", holderToTag(holder, (event, soundEventTag) -> {
soundEventTag.putString("identifier", event.identifier());
if (event.fixedRange() != null) {
soundEventTag.putFloat("fixed_range", event.fixedRange());
}
}));
}
private Consumable1_21_2.ConsumeEffect<?> convertConsumableEffect(final CompoundTag tag) {
final int id = tag.getInt("id");
final String type = tag.getString("type");
if ("apply_effects".equals(type)) {
final ListTag<CompoundTag> effects = tag.getListTag("effects", CompoundTag.class);
final PotionEffect[] potionEffects = new PotionEffect[effects.size()];
for (int i = 0; i < effects.size(); i++) {
final CompoundTag effectTag = effects.get(i);
final int effect = effectTag.getInt("effect");
final PotionEffectData data = convertPotionEffectData(effectTag.getCompoundTag("data"));
potionEffects[i] = new PotionEffect(effect, data);
}
final float probability = tag.getFloat("probability");
return new Consumable1_21_2.ConsumeEffect<>(id, Consumable1_21_2.ApplyStatusEffects.TYPE, new Consumable1_21_2.ApplyStatusEffects(potionEffects, probability));
} else if ("remove_effects".equals(type)) {
final HolderSet set = restoreHolderSet(tag, "remove_effects");
return new Consumable1_21_2.ConsumeEffect<>(id, Types.HOLDER_SET, set);
} else if ("clear_all_effects".equals(type)) {
return new Consumable1_21_2.ConsumeEffect<>(id, Types.EMPTY, Unit.INSTANCE);
} else if ("teleport_randomly".equals(type)) {
final float probability = tag.getFloat("probability");
return new Consumable1_21_2.ConsumeEffect<>(id, Types.FLOAT, probability);
} else if ("play_sound".equals(type)) {
final Holder<SoundEvent> sound = convertSoundEventHolder(tag);
return new Consumable1_21_2.ConsumeEffect<>(id, Types.SOUND_EVENT, sound);
}
return null;
}
private PotionEffectData convertPotionEffectData(final CompoundTag tag) {
final int amplifier = tag.getInt("amplifier");
final int duration = tag.getInt("duration");
final boolean ambient = tag.getBoolean("ambient");
final boolean showParticles = tag.getBoolean("show_particles");
final boolean showIcon = tag.getBoolean("show_icon");
final CompoundTag hiddenEffect = tag.getCompoundTag("hidden_effect");
return new PotionEffectData(amplifier, duration, ambient, showParticles, showIcon, hiddenEffect != null ? convertPotionEffectData(hiddenEffect) : null);
}
private Holder<SoundEvent> convertSoundEventHolder(final CompoundTag tag) {
return restoreHolder(tag, "sound_event", soundEventTag -> {
final String identifier = soundEventTag.getString("identifier");
final FloatTag fixedRange = soundEventTag.getFloatTag("fixed_range");
return new SoundEvent(identifier, fixedRange != null ? fixedRange.asFloat() : null);
});
}
private void restoreInconvertibleData(final Item item) {
final StructuredDataContainer data = item.dataContainer();
final CompoundTag customData = data.get(StructuredDataKey.CUSTOM_DATA);
if (customData == null || !(customData.remove(nbtTagName("inconvertible_data")) instanceof CompoundTag backupTag)) {
return;
}
final Holder<Instrument1_21_2> instrument = data.get(StructuredDataKey.INSTRUMENT1_21_2);
if (instrument != null && instrument.isDirect()) {
final Tag description = backupTag.get("instrument_description");
if (description != null) {
final Instrument1_21_2 delegate = instrument.value();
data.set(StructuredDataKey.INSTRUMENT1_21_2, Holder.of(new Instrument1_21_2(delegate.soundEvent(), delegate.useDuration(), delegate.range(), description)));
}
}
if (backupTag.contains("repairable")) {
data.set(StructuredDataKey.REPAIRABLE, restoreHolderSet(backupTag, "repairable"));
}
final IntTag enchantable = backupTag.getIntTag("enchantable");
if (enchantable != null) {
data.set(StructuredDataKey.ENCHANTABLE, enchantable.asInt());
}
final CompoundTag useCooldown = backupTag.getCompoundTag("use_cooldown");
if (useCooldown != null) {
final float seconds = useCooldown.getFloat("seconds");
final String cooldownGroup = useCooldown.getString("cooldown_group");
data.set(StructuredDataKey.USE_COOLDOWN, new UseCooldown(seconds, cooldownGroup));
}
final String itemModel = backupTag.getString("item_model");
if (itemModel != null) {
data.set(StructuredDataKey.ITEM_MODEL, itemModel);
}
final CompoundTag equippable = backupTag.getCompoundTag("equippable");
if (equippable != null) {
final int equipmentSlot = equippable.getInt("equipment_slot");
final Holder<SoundEvent> soundEvent = convertSoundEventHolder(equippable);
final String model = equippable.getString("model");
final String cameraOverlay = equippable.getString("camera_overlay");
final HolderSet allowedEntities = equippable.contains("allowed_entities") ? restoreHolderSet(equippable, "allowed_entities") : null;
final boolean dispensable = equippable.getBoolean("dispensable");
final boolean swappable = equippable.getBoolean("swappable");
final boolean damageOnHurt = equippable.getBoolean("damage_on_hurt");
data.set(StructuredDataKey.EQUIPPABLE, new Equippable(equipmentSlot, soundEvent, model, cameraOverlay, allowedEntities, dispensable, swappable, damageOnHurt));
}
final ByteTag glider = backupTag.getByteTag("glider");
if (glider != null) {
data.set(StructuredDataKey.GLIDER, Unit.INSTANCE);
}
final String tooltipStyle = backupTag.getString("tooltip_style");
if (tooltipStyle != null) {
data.set(StructuredDataKey.TOOLTIP_STYLE, tooltipStyle);
}
final ListTag<CompoundTag> deathProtection = backupTag.getListTag("death_protection", CompoundTag.class);
if (deathProtection != null) {
final Consumable1_21_2.ConsumeEffect<?>[] effects = new Consumable1_21_2.ConsumeEffect[deathProtection.size()];
for (int i = 0; i < deathProtection.size(); i++) {
effects[i] = convertConsumableEffect(deathProtection.get(i));
}
data.set(StructuredDataKey.DEATH_PROTECTION, new DeathProtection(effects));
}
removeCustomTag(data, customData);
}
}

View File

@ -29,10 +29,13 @@ import com.viaversion.viabackwards.protocol.v1_21to1_20_5.storage.EnchantmentsPa
import com.viaversion.viabackwards.protocol.v1_21to1_20_5.storage.OpenScreenStorage;
import com.viaversion.viabackwards.protocol.v1_21to1_20_5.storage.PlayerRotationStorage;
import com.viaversion.viaversion.api.connection.UserConnection;
import com.viaversion.viaversion.api.minecraft.Holder;
import com.viaversion.viaversion.api.minecraft.SoundEvent;
import com.viaversion.viaversion.api.minecraft.data.StructuredDataContainer;
import com.viaversion.viaversion.api.minecraft.data.StructuredDataKey;
import com.viaversion.viaversion.api.minecraft.item.Item;
import com.viaversion.viaversion.api.minecraft.item.data.Enchantments;
import com.viaversion.viaversion.api.minecraft.item.data.JukeboxPlayable;
import com.viaversion.viaversion.api.type.Types;
import com.viaversion.viaversion.api.type.types.chunk.ChunkType1_20_2;
import com.viaversion.viaversion.api.type.types.version.Types1_20_5;
@ -49,6 +52,7 @@ import com.viaversion.viaversion.protocols.v1_20_5to1_21.packet.ClientboundPacke
import com.viaversion.viaversion.protocols.v1_20_5to1_21.packet.ClientboundPackets1_21;
import com.viaversion.viaversion.rewriter.BlockRewriter;
import com.viaversion.viaversion.rewriter.IdRewriteFunction;
import com.viaversion.viaversion.util.Either;
import com.viaversion.viaversion.util.SerializerVersion;
import java.util.ArrayList;
import java.util.List;
@ -187,6 +191,7 @@ public final class BlockItemPacketRewriter1_21 extends BackwardsStructuredItemRe
final int identifier = item.identifier();
// Order is important
backupInconvertibleData(item);
super.handleItemToClient(connection, item);
downgradeItemData(item);
@ -222,6 +227,7 @@ public final class BlockItemPacketRewriter1_21 extends BackwardsStructuredItemRe
// Order is important
super.handleItemToServer(connection, item);
updateItemData(item);
restoreInconvertibleData(item);
final CompoundTag customData = data.get(StructuredDataKey.CUSTOM_DATA);
if (customData == null) {
@ -264,6 +270,67 @@ public final class BlockItemPacketRewriter1_21 extends BackwardsStructuredItemRe
}
}
private void backupInconvertibleData(final Item item) {
final StructuredDataContainer data = item.dataContainer();
data.setIdLookup(protocol, true);
final JukeboxPlayable jukeboxPlayable = data.get(StructuredDataKey.JUKEBOX_PLAYABLE);
if (jukeboxPlayable == null) {
return;
}
final CompoundTag tag = new CompoundTag();
if (jukeboxPlayable.song().isLeft()) {
final Holder<JukeboxPlayable.JukeboxSong> songHolder = jukeboxPlayable.song().left();
tag.put("song", holderToTag(songHolder, (song, songTag) -> {
songTag.put("sound_event", holderToTag(song.soundEvent(), (soundEvent, soundEventTag) -> {
soundEventTag.putString("identifier", soundEvent.identifier());
if (soundEvent.fixedRange() != null) {
soundEventTag.putFloat("fixed_range", soundEvent.fixedRange());
}
}));
songTag.put("description", song.description());
songTag.putFloat("length_in_seconds", song.lengthInSeconds());
songTag.putInt("comparator_output", song.comparatorOutput());
}));
} else {
tag.putString("song_identifier", jukeboxPlayable.song().right());
}
tag.putBoolean("show_in_tooltip", jukeboxPlayable.showInTooltip());
saveTag(createCustomTag(item), tag, "jukebox_playable");
}
private void restoreInconvertibleData(final Item item) {
final StructuredDataContainer data = item.dataContainer();
final CompoundTag customData = data.get(StructuredDataKey.CUSTOM_DATA);
if (customData == null || !(customData.remove(nbtTagName("jukebox_playable")) instanceof CompoundTag tag)) {
return;
}
final Either<Holder<JukeboxPlayable.JukeboxSong>, String> song;
final String songIdentifier = tag.getString("song_identifier");
if (songIdentifier != null) {
song = Either.right(tag.getString(songIdentifier));
} else {
song = Either.left(restoreHolder(tag, "song", songTag -> {
final Holder<SoundEvent> soundEvent = restoreHolder(songTag, "sound_event", soundTag -> {
final String identifier = soundTag.getString("identifier");
final Float fixedRange = soundTag.contains("fixed_range") ? soundTag.getFloat("fixed_range") : null;
return new SoundEvent(identifier, fixedRange);
});
final Tag description = songTag.get("description");
final float lengthInSeconds = songTag.getFloat("length_in_seconds");
final int comparatorOutput = songTag.getInt("comparator_output");
return new JukeboxPlayable.JukeboxSong(soundEvent, description, lengthInSeconds, comparatorOutput);
}));
}
final JukeboxPlayable jukeboxPlayable = new JukeboxPlayable(song, tag.getBoolean("show_in_tooltip"));
data.set(StructuredDataKey.JUKEBOX_PLAYABLE, jukeboxPlayable);
removeCustomTag(data, customData);
}
private record PendingIdChange(int id, int mappedId, int level) {
}
}