From 276953f6cbbd3055fd8957d0f8557e8a6a27546d Mon Sep 17 00:00:00 2001 From: EnZaXD Date: Fri, 22 Nov 2024 10:36:42 +0100 Subject: [PATCH] Backup inconvertible item component data for creative clients in 1.21+ (#907) Co-authored-by: Nassim Jahnke --- .../BackwardsStructuredItemRewriter.java | 54 ++++ .../BlockItemPacketRewriter1_21_2.java | 280 ++++++++++++++++++ .../rewriter/BlockItemPacketRewriter1_21.java | 67 +++++ 3 files changed, 401 insertions(+) diff --git a/common/src/main/java/com/viaversion/viabackwards/api/rewriters/BackwardsStructuredItemRewriter.java b/common/src/main/java/com/viaversion/viabackwards/api/rewriters/BackwardsStructuredItemRewriter.java index e146c111..bbb3479e 100644 --- a/common/src/main/java/com/viaversion/viabackwards/api/rewriters/BackwardsStructuredItemRewriter.java +++ b/common/src/main/java/com/viaversion/viabackwards/api/rewriters/BackwardsStructuredItemRewriter.java @@ -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(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 Tag holderToTag(final Holder holder, final BiConsumer valueSaveFunction) { + if (holder.hasId()) { + return new IntTag(holder.id()); + } else { + final CompoundTag savedTag = new CompoundTag(); + valueSaveFunction.accept(holder.value(), savedTag); + return savedTag; + } + } + + protected Holder restoreHolder(final CompoundTag tag, final String key, final Function 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(); diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_2to1_21/rewriter/BlockItemPacketRewriter1_21_2.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_2to1_21/rewriter/BlockItemPacketRewriter1_21_2.java index 3b58cf1f..bc2d25c1 100644 --- a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_2to1_21/rewriter/BlockItemPacketRewriter1_21_2.java +++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_2to1_21/rewriter/BlockItemPacketRewriter1_21_2.java @@ -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 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 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 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 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 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 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 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 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 = 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 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); + } } diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21to1_20_5/rewriter/BlockItemPacketRewriter1_21.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21to1_20_5/rewriter/BlockItemPacketRewriter1_21.java index 96bc8554..af894a82 100644 --- a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21to1_20_5/rewriter/BlockItemPacketRewriter1_21.java +++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21to1_20_5/rewriter/BlockItemPacketRewriter1_21.java @@ -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 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, 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 = 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) { } }