Re-implement jukebox songs

This commit is contained in:
Camotoy 2024-06-06 20:28:38 -04:00
parent 8f5d1560a2
commit 29c9515d55
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
7 changed files with 141 additions and 44 deletions

View file

@ -27,13 +27,15 @@ package org.geysermc.geyser.item.enchantment;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.inventory.item.BedrockEnchantment;
import org.geysermc.geyser.session.cache.tags.EnchantmentTag;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry;
import java.util.*;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @param description only populated if {@link #bedrockEnchantment()} is not null.
@ -59,7 +61,7 @@ public record Enchantment(String identifier,
String exclusiveSet = data.getString("exclusive_set", null);
EnchantmentTag exclusiveSetTag = exclusiveSet == null ? null : EnchantmentTag.ALL_ENCHANTMENT_TAGS.get(exclusiveSet.substring(1));
BedrockEnchantment bedrockEnchantment = BedrockEnchantment.getByJavaIdentifier(entry.getId());
String description = bedrockEnchantment == null ? readDescription(data) : null;
String description = bedrockEnchantment == null ? MessageTranslator.deserializeDescription(data) : null;
return new Enchantment(entry.getId(), effects, ItemTag.ALL_ITEM_TAGS.get(supportedItems), maxLevel,
description, anvilCost, exclusiveSetTag, bedrockEnchantment);
@ -74,14 +76,4 @@ public record Enchantment(String identifier,
}
return Set.copyOf(components); // Also ensures any empty sets are consolidated
}
private static String readDescription(NbtMap tag) {
NbtMap description = tag.getCompound("description");
String translate = description.getString("translate", null);
if (translate == null) {
GeyserImpl.getInstance().getLogger().debug("Don't know how to read description! " + tag);
return "";
}
return translate;
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.level;
import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry;
public record JukeboxSong(String soundEvent, String description) {
public static JukeboxSong read(RegistryEntry entry) {
NbtMap data = entry.getData();
String soundEvent = data.getString("sound_event");
String description = MessageTranslator.deserializeDescription(data);
return new JukeboxSong(soundEvent, description);
}
}

View file

@ -40,6 +40,7 @@ import org.geysermc.geyser.inventory.item.BannerPattern;
import org.geysermc.geyser.inventory.recipe.TrimRecipe;
import org.geysermc.geyser.item.enchantment.Enchantment;
import org.geysermc.geyser.level.JavaDimension;
import org.geysermc.geyser.level.JukeboxSong;
import org.geysermc.geyser.level.PaintingType;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.registry.JavaRegistry;
@ -76,6 +77,7 @@ public final class RegistryCache {
register("chat_type", cache -> cache.chatTypes, ($, entry) -> TextDecoration.readChatType(entry));
register("dimension_type", cache -> cache.dimensions, ($, entry) -> JavaDimension.read(entry));
register("enchantment", cache -> cache.enchantments, ($, entry) -> Enchantment.read(entry));
register("jukebox_song", cache -> cache.jukeboxSongs, ($, entry) -> JukeboxSong.read(entry));
register("painting_variant", cache -> cache.paintings, ($, entry) -> PaintingType.getByName(entry.getId()));
register("trim_material", cache -> cache.trimMaterials, TrimRecipe::readTrimMaterial);
register("trim_pattern", cache -> cache.trimPatterns, TrimRecipe::readTrimPattern);
@ -115,6 +117,7 @@ public final class RegistryCache {
*/
private final JavaRegistry<JavaDimension> dimensions = new SimpleJavaRegistry<>();
private final JavaRegistry<Enchantment> enchantments = new SimpleJavaRegistry<>();
private final JavaRegistry<JukeboxSong> jukeboxSongs = new SimpleJavaRegistry<>();
private final JavaRegistry<PaintingType> paintings = new SimpleJavaRegistry<>();
private final JavaRegistry<TrimMaterial> trimMaterials = new SimpleJavaRegistry<>();
private final JavaRegistry<TrimPattern> trimPatterns = new SimpleJavaRegistry<>();

View file

@ -28,8 +28,10 @@ package org.geysermc.geyser.session.cache;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntMaps;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Getter;
import lombok.Setter;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket;
import org.geysermc.geyser.scoreboard.Scoreboard;
@ -39,6 +41,7 @@ import org.geysermc.geyser.util.ChunkUtils;
import org.geysermc.mcprotocollib.protocol.data.game.setting.Difficulty;
import java.util.Iterator;
import java.util.Map;
public final class WorldCache {
private final GeyserSession session;
@ -61,6 +64,8 @@ public final class WorldCache {
private int currentSequence;
private final Object2IntMap<Vector3i> unverifiedPredictions = new Object2IntOpenHashMap<>(1);
private final Map<Vector3i, String> activeRecords = new Object2ObjectOpenHashMap<>(1); // Assume the average player won't be listening to many records
@Getter
@Setter
private boolean editingSignOnFront;
@ -185,4 +190,15 @@ public final class WorldCache {
}
}
}
public void addActiveRecord(Vector3i pos, String bedrockPlaySound) {
this.activeRecords.put(pos, bedrockPlaySound);
}
// Implementation note: positions aren't removed unless the server calls, but this seems to match 1.21 Java
// client behavior.
@Nullable
public String removeActiveRecord(Vector3i pos) {
return this.activeRecords.remove(pos);
}
}

View file

@ -25,28 +25,27 @@
package org.geysermc.geyser.translator.protocol.java.level;
import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction;
import org.geysermc.mcprotocollib.protocol.data.game.level.event.*;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundLevelEventPacket;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.protocol.bedrock.data.ParticleType;
import org.cloudburstmc.protocol.bedrock.data.SoundEvent;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventGenericPacket;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.TextPacket;
import org.cloudburstmc.protocol.bedrock.packet.*;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.level.JukeboxSong;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.SoundMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.level.event.LevelEventTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.SoundUtils;
import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction;
import org.geysermc.mcprotocollib.protocol.data.game.level.event.*;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundLevelEventPacket;
import java.util.Collections;
import java.util.Locale;
import java.util.Set;
@Translator(packet = ClientboundLevelEventPacket.class)
@ -60,13 +59,25 @@ public class JavaLevelEventTranslator extends PacketTranslator<ClientboundLevelE
// Separate case since each RecordEventData in Java is an individual track in Bedrock
if (levelEvent == LevelEventType.RECORD) {
RecordEventData recordEventData = (RecordEventData) packet.getData();
SoundEvent soundEvent = Registries.RECORDS.get(recordEventData.getRecordId());
if (soundEvent == null) {
JukeboxSong jukeboxSong = session.getRegistryCache().jukeboxSongs().byId(recordEventData.getRecordId());
if (jukeboxSong == null) {
return;
}
Vector3i origin = packet.getPosition();
Vector3f pos = Vector3f.from(origin.getX() + 0.5f, origin.getY() + 0.5f, origin.getZ() + 0.5f);
// Prioritize level events because it makes parrots dance.
SoundMapping mapping = Registries.SOUNDS.get(jukeboxSong.soundEvent().replace("minecraft:", ""));
System.out.println(jukeboxSong.soundEvent() + " " + mapping);
SoundEvent soundEvent = null;
if (mapping != null) {
String bedrock = mapping.getBedrock();
if (bedrock != null && !bedrock.isEmpty()) {
soundEvent = SoundUtils.toSoundEvent(bedrock);
}
}
if (soundEvent != null) {
LevelSoundEventPacket levelSoundEvent = new LevelSoundEventPacket();
levelSoundEvent.setIdentifier("");
levelSoundEvent.setSound(soundEvent);
@ -75,8 +86,21 @@ public class JavaLevelEventTranslator extends PacketTranslator<ClientboundLevelE
levelSoundEvent.setExtraData(-1);
levelSoundEvent.setBabySound(false);
session.sendUpstreamPacket(levelSoundEvent);
} else {
String bedrockSound = SoundUtils.translatePlaySound(jukeboxSong.soundEvent());
// Pitch and volume from Java 1.21
PlaySoundPacket playSoundPacket = new PlaySoundPacket();
playSoundPacket.setPosition(pos);
playSoundPacket.setSound(bedrockSound);
playSoundPacket.setPitch(1.0f);
playSoundPacket.setVolume(4.0f);
session.sendUpstreamPacket(playSoundPacket);
// Send text packet as it seems to be handled in Java Edition client-side.
// Special behavior so we can cancel the record on our end
session.getWorldCache().addActiveRecord(origin, bedrockSound);
}
// The level event for Java also indicates to show the text packet with the jukebox's description
TextPacket textPacket = new TextPacket();
textPacket.setType(TextPacket.Type.JUKEBOX_POPUP);
textPacket.setNeedsTranslation(true);
@ -84,8 +108,7 @@ public class JavaLevelEventTranslator extends PacketTranslator<ClientboundLevelE
textPacket.setPlatformChatId("");
textPacket.setSourceName(null);
textPacket.setMessage("record.nowPlaying");
String recordString = "%item." + soundEvent.name().toLowerCase(Locale.ROOT) + ".desc";
textPacket.setParameters(Collections.singletonList(MinecraftLocale.getLocaleString(recordString, session.locale())));
textPacket.setParameters(Collections.singletonList(MinecraftLocale.getLocaleString(jukeboxSong.description(), session.locale())));
session.sendUpstreamPacket(textPacket);
return;
}
@ -325,6 +348,9 @@ public class JavaLevelEventTranslator extends PacketTranslator<ClientboundLevelE
return;
}
case STOP_RECORD -> {
String bedrockSound = session.getWorldCache().removeActiveRecord(origin);
if (bedrockSound == null) {
// Vanilla record
LevelSoundEventPacket levelSoundEvent = new LevelSoundEventPacket();
levelSoundEvent.setIdentifier("");
levelSoundEvent.setSound(SoundEvent.STOP_RECORD);
@ -333,6 +359,12 @@ public class JavaLevelEventTranslator extends PacketTranslator<ClientboundLevelE
levelSoundEvent.setExtraData(-1);
levelSoundEvent.setBabySound(false);
session.sendUpstreamPacket(levelSoundEvent);
} else {
// Custom record
StopSoundPacket stopSound = new StopSoundPacket();
stopSound.setSoundName(bedrockSound);
session.sendUpstreamPacket(stopSound);
}
return;
}
default -> {

View file

@ -35,6 +35,7 @@ import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.CharacterAndFormat;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.protocol.bedrock.packet.TextPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.session.GeyserSession;
@ -424,6 +425,20 @@ public class MessageTranslator {
return new String(newChars, 0, count - (whitespacesCount > 0 ? 1 : 0)).trim();
}
/**
* Deserialize an NbtMap provided from a registry into a string.
*/
// This may be a Component in the future.
public static String deserializeDescription(NbtMap tag) {
NbtMap description = tag.getCompound("description");
String translate = description.getString("translate", null);
if (translate == null) {
GeyserImpl.getInstance().getLogger().debug("Don't know how to read description! " + tag);
return "";
}
return translate;
}
public static void init() {
// no-op
}

View file

@ -33,7 +33,6 @@ import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlaySoundPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.level.block.type.Block;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.Registries;
@ -52,7 +51,7 @@ public final class SoundUtils {
* @param sound the sound name
* @return a sound event from the given sound
*/
private static @Nullable SoundEvent toSoundEvent(String sound) {
public static @Nullable SoundEvent toSoundEvent(String sound) {
try {
return SoundEvent.valueOf(sound.toUpperCase(Locale.ROOT).replace(".", "_"));
} catch (Exception ex) {