From 1c2ebbe210ef741448fa3ef3f2540e1a11a523ab Mon Sep 17 00:00:00 2001 From: AngrySoundTech Date: Sun, 28 Jun 2026 18:28:59 -0400 Subject: [PATCH 1/2] Allow playing Vanilla Discs --- .../betterrecords/mixin/MixinSoundEngine.java | 54 ++++++------- .../api/client/sound/PlayingSound.kt | 19 +++++ .../block/entity/RecordPlayerBlockEntity.kt | 10 +-- .../block/entity/SingleSlotBlockEntity.kt | 7 +- .../block/renderer/RecordPlayerRenderer.kt | 32 +++++++- .../block/renderer/VanillaDiscProxies.kt | 77 +++++++++++++++++++ .../client/gui/RecordLoadingOverlay.kt | 12 ++- .../client/sound/ProgressInputStream.kt | 3 +- .../sound/RecordPlayingSound.kt} | 20 +++-- .../client/sound/RecordSoundManager.kt | 63 +++++++++++---- .../client/sound/VanillaRecordPlayingSound.kt | 14 ++++ .../network/clientbound/PlayRecordPacket.kt | 62 ++++++++++++--- 12 files changed, 290 insertions(+), 83 deletions(-) create mode 100644 src/main/kotlin/com/hemogoblins/betterrecords/api/client/sound/PlayingSound.kt create mode 100644 src/main/kotlin/com/hemogoblins/betterrecords/block/renderer/VanillaDiscProxies.kt rename src/main/kotlin/com/hemogoblins/betterrecords/{api/client/sound/FileSoundInstance.kt => client/sound/RecordPlayingSound.kt} (80%) create mode 100644 src/main/kotlin/com/hemogoblins/betterrecords/client/sound/VanillaRecordPlayingSound.kt diff --git a/src/main/java/com/hemogoblins/betterrecords/mixin/MixinSoundEngine.java b/src/main/java/com/hemogoblins/betterrecords/mixin/MixinSoundEngine.java index c3148b0..172013d 100644 --- a/src/main/java/com/hemogoblins/betterrecords/mixin/MixinSoundEngine.java +++ b/src/main/java/com/hemogoblins/betterrecords/mixin/MixinSoundEngine.java @@ -3,7 +3,7 @@ import com.hemogoblins.betterrecords.BRConfig; import com.hemogoblins.betterrecords.BetterRecords; import com.hemogoblins.betterrecords.api.client.sound.AudioStreamRegistry; -import com.hemogoblins.betterrecords.api.client.sound.FileSoundInstance; +import com.hemogoblins.betterrecords.client.sound.RecordPlayingSound; import com.hemogoblins.betterrecords.api.client.sound.RecordAudioStream; import com.hemogoblins.betterrecords.client.sound.ProgressInputStream; import com.hemogoblins.betterrecords.client.sound.RecordSoundManager; @@ -39,7 +39,7 @@ public abstract class MixinSoundEngine { @Shadow @Final private ChannelAccess channelAccess; @Unique - private final Map betterRecords$soundHandles = new HashMap<>(); + private final Map betterRecords$soundHandles = new HashMap<>(); /** * Sounds whose audio is still being decoded off-thread. A stop() that @@ -47,7 +47,7 @@ public abstract class MixinSoundEngine { * Only ever touched on the main thread. */ @Unique - private final Set betterRecords$loading = new HashSet<>(); + private final Set betterRecords$loading = new HashSet<>(); // TODO: Ability to boost and reduce the distance of sounds based on their network. // For now, four chunks is pretty good. @@ -82,30 +82,30 @@ public abstract class MixinSoundEngine { @Inject(method="play", at = @At("HEAD"), cancellable = true) public void onPlay(SoundInstance sound, CallbackInfo ci) { - if (!(sound instanceof FileSoundInstance fileSound)) { + if (!(sound instanceof RecordPlayingSound recordSound)) { return; } // We fully handle our own file-backed sounds, so skip vanilla's handling. ci.cancel(); - File soundFile = new File(BRConfig.Client.INSTANCE.getCacheDirectory().get(), fileSound.getFile()); + File soundFile = new File(BRConfig.Client.INSTANCE.getCacheDirectory().get(), recordSound.getFile()); // Mark the sound as loading so a stop() arriving before it is ready can cancel it, - betterRecords$loading.add(fileSound); - fileSound.setLoading(true); + betterRecords$loading.add(recordSound); + recordSound.setLoading(true); CompletableFuture bufferFuture = - CompletableFuture.supplyAsync(() -> betterRecords$decode(fileSound, soundFile), Util.backgroundExecutor()); + CompletableFuture.supplyAsync(() -> betterRecords$decode(recordSound, soundFile), Util.backgroundExecutor()); bufferFuture .thenAcceptBothAsync( this.channelAccess.createHandle(Library.Pool.STATIC), - (soundBuffer, channelHandle) -> betterRecords$startPlaying(fileSound, soundBuffer, channelHandle), + (soundBuffer, channelHandle) -> betterRecords$startPlaying(recordSound, soundBuffer, channelHandle), Minecraft.getInstance()) .exceptionally(throwable -> { - betterRecords$loading.remove(fileSound); - fileSound.setLoading(false); + betterRecords$loading.remove(recordSound); + recordSound.setLoading(false); BetterRecords.INSTANCE.getLogger().error("Failed to play record sound", throwable); return null; }); @@ -115,16 +115,16 @@ public void onPlay(SoundInstance sound, CallbackInfo ci) { * Reads and decodes a cached audio file into a static buffer. */ @Unique - private SoundBuffer betterRecords$decode(FileSoundInstance fileSound, File soundFile) { + private SoundBuffer betterRecords$decode(RecordPlayingSound recordSound, File soundFile) { if (!soundFile.exists()) { throw new RuntimeException("Sound file not found: " + soundFile.getAbsolutePath()); } try (FileInputStream stream = new FileInputStream(soundFile); - ProgressInputStream progressStream = new ProgressInputStream(stream, soundFile.length(), fileSound); + ProgressInputStream progressStream = new ProgressInputStream(stream, soundFile.length(), recordSound); RecordAudioStream audioStream = AudioStreamRegistry.INSTANCE.open(progressStream)) { SoundBuffer soundBuffer = new SoundBuffer(audioStream.readAll(), audioStream.getFormat()); - fileSound.setLoadProgress(1.0F); + recordSound.setLoadProgress(1.0F); return soundBuffer; } catch (Exception e) { throw new RuntimeException(e); @@ -133,10 +133,10 @@ public void onPlay(SoundInstance sound, CallbackInfo ci) { /** Attaches the decoded buffer to a channel and starts playback. Runs on the main thread. */ @Unique - private void betterRecords$startPlaying(FileSoundInstance fileSound, SoundBuffer soundBuffer, ChannelAccess.ChannelHandle channelHandle) { + private void betterRecords$startPlaying(RecordPlayingSound recordSound, SoundBuffer soundBuffer, ChannelAccess.ChannelHandle channelHandle) { // If the sound was stopped while it was still loading, don't start it. - boolean wanted = betterRecords$loading.remove(fileSound); - fileSound.setLoading(false); + boolean wanted = betterRecords$loading.remove(recordSound); + recordSound.setLoading(false); if (channelHandle == null) { return; @@ -148,26 +148,26 @@ public void onPlay(SoundInstance sound, CallbackInfo ci) { } // TODO: Should this just be a blockpos? - Vec3 soundPos = new Vec3(fileSound.getX(), fileSound.getY(), fileSound.getZ()); + Vec3 soundPos = new Vec3(recordSound.getX(), recordSound.getY(), recordSound.getZ()); channelHandle.execute((channel) -> { - channel.setPitch(fileSound.getPitch()); + channel.setPitch(recordSound.getPitch()); channel.setVolume(betterRecords$calculateVolumeByDistance(soundPos)); channel.attachStaticBuffer(soundBuffer); channel.play(); }); - fileSound.setStartedPlayingAt(Util.getMillis()); - betterRecords$soundHandles.put(fileSound, channelHandle); + recordSound.setStartedPlayingAt(Util.getMillis()); + betterRecords$soundHandles.put(recordSound, channelHandle); } @Inject(method="stop", at = @At("HEAD"), cancellable = true) public void onStop(SoundInstance sound, CallbackInfo ci) { - if (sound instanceof FileSoundInstance fileSound) { + if (sound instanceof RecordPlayingSound recordSound) { // Cancel playback if the sound is still being decoded - betterRecords$loading.remove(fileSound); + betterRecords$loading.remove(recordSound); - ChannelAccess.ChannelHandle channelHandle = betterRecords$soundHandles.remove(fileSound); + ChannelAccess.ChannelHandle channelHandle = betterRecords$soundHandles.remove(recordSound); if (channelHandle != null) { channelHandle.execute(Channel::stop); @@ -182,12 +182,12 @@ public void onStop(SoundInstance sound, CallbackInfo ci) { */ @Inject(method="tick", at = @At("TAIL")) public void onTick(CallbackInfo ci) { - Iterator> iterator = + Iterator> iterator = betterRecords$soundHandles.entrySet().iterator(); while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - FileSoundInstance sound = entry.getKey(); + Map.Entry entry = iterator.next(); + RecordPlayingSound sound = entry.getKey(); ChannelAccess.ChannelHandle handle = entry.getValue(); if (handle.isStopped()) { diff --git a/src/main/kotlin/com/hemogoblins/betterrecords/api/client/sound/PlayingSound.kt b/src/main/kotlin/com/hemogoblins/betterrecords/api/client/sound/PlayingSound.kt new file mode 100644 index 0000000..3f9c7f8 --- /dev/null +++ b/src/main/kotlin/com/hemogoblins/betterrecords/api/client/sound/PlayingSound.kt @@ -0,0 +1,19 @@ +package com.hemogoblins.betterrecords.api.client.sound + +/** + * Display state for currently playing sounds + */ +interface PlayingSound { + + /** Human-readable name */ + val name: String + + /** True while the audio is still being downloaded or decoded */ + val loading: Boolean + + /** Download or decode progress in the range 0..1. */ + val loadProgress: Float + + /** Timestamp ([net.minecraft.Util.getMillis]) at which playback started. */ + val startedPlayingAt: Long +} diff --git a/src/main/kotlin/com/hemogoblins/betterrecords/block/entity/RecordPlayerBlockEntity.kt b/src/main/kotlin/com/hemogoblins/betterrecords/block/entity/RecordPlayerBlockEntity.kt index 9d56d58..0d99fd2 100644 --- a/src/main/kotlin/com/hemogoblins/betterrecords/block/entity/RecordPlayerBlockEntity.kt +++ b/src/main/kotlin/com/hemogoblins/betterrecords/block/entity/RecordPlayerBlockEntity.kt @@ -3,7 +3,6 @@ package com.hemogoblins.betterrecords.block.entity import com.hemogoblins.betterrecords.block.ModBlocks import com.hemogoblins.betterrecords.block.RecordPlayerBlock import com.hemogoblins.betterrecords.block.RecordPlayerBlock.Companion.FACING -import com.hemogoblins.betterrecords.capability.ModCapabilities import com.hemogoblins.betterrecords.network.ModNetwork import com.hemogoblins.betterrecords.network.clientbound.PlayRecordPacket import com.hemogoblins.betterrecords.network.clientbound.StopRecordPacket @@ -96,16 +95,11 @@ class RecordPlayerBlockEntity( } private fun sendPlayRecord() { - val cap = getSlottedItem().getCapability(ModCapabilities.MUSIC_HOLDER_CAPABILITY) - val checksum = cap.map { it.checksum }.orElse("") - val url = cap.map { it.url }.orElse("") - val name = cap.map { it.songName }.orElse("") - - if (checksum.isEmpty()) return + val source = PlayRecordPacket.RecordSource.of(getSlottedItem()) ?: return ModNetwork.channel.send( PacketDistributor.TRACKING_CHUNK.with { level!!.getChunkAt(blockPos) }, - PlayRecordPacket(blockPos, checksum, url, name), + PlayRecordPacket(blockPos, source), ) } diff --git a/src/main/kotlin/com/hemogoblins/betterrecords/block/entity/SingleSlotBlockEntity.kt b/src/main/kotlin/com/hemogoblins/betterrecords/block/entity/SingleSlotBlockEntity.kt index 25135ea..3af144c 100644 --- a/src/main/kotlin/com/hemogoblins/betterrecords/block/entity/SingleSlotBlockEntity.kt +++ b/src/main/kotlin/com/hemogoblins/betterrecords/block/entity/SingleSlotBlockEntity.kt @@ -10,6 +10,7 @@ import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket import net.minecraft.world.Containers import net.minecraft.world.SimpleContainer import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.RecordItem import net.minecraft.world.level.block.Block import net.minecraft.world.level.block.entity.BlockEntity import net.minecraft.world.level.block.entity.BlockEntityType @@ -31,10 +32,8 @@ open class SingleSlotBlockEntity( companion object { const val NBT_TAG_INVENTORY = "inventory" - /** might want to change this to check capabilities by generic - * or just move out of static and override in subs */ - fun isItemValid(itemStack: ItemStack): Boolean { - return itemStack.getCapability(ModCapabilities.MUSIC_HOLDER_CAPABILITY).isPresent + fun isItemValid(stack: ItemStack): Boolean { + return stack.getCapability(ModCapabilities.MUSIC_HOLDER_CAPABILITY).isPresent || stack.item is RecordItem } } diff --git a/src/main/kotlin/com/hemogoblins/betterrecords/block/renderer/RecordPlayerRenderer.kt b/src/main/kotlin/com/hemogoblins/betterrecords/block/renderer/RecordPlayerRenderer.kt index 34a0138..6bde4d9 100644 --- a/src/main/kotlin/com/hemogoblins/betterrecords/block/renderer/RecordPlayerRenderer.kt +++ b/src/main/kotlin/com/hemogoblins/betterrecords/block/renderer/RecordPlayerRenderer.kt @@ -22,6 +22,7 @@ import net.minecraft.resources.ResourceLocation import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.ItemStack import net.minecraft.world.level.Level +import net.minecraft.world.item.RecordItem as VanillaRecordItem class RecordPlayerRenderer(context: BlockEntityRendererProvider.Context) : BlockEntityRenderer { @@ -153,11 +154,38 @@ class RecordPlayerRenderer(context: BlockEntityRendererProvider.Context) : Block mulPose(Axis.ZN.rotationDegrees(rotation)) } - itemRenderer.renderStatic(itemStack, ItemDisplayContext.FIXED, packedLight, - packedOverlay, poseStack, bufferSource, level, 1) + if (itemStack.item is VanillaRecordItem) { + renderVanillaDisc(itemStack, poseStack, bufferSource, packedLight, packedOverlay, level) + } else { + renderItem(itemStack, poseStack, bufferSource, packedLight, packedOverlay, level) + } poseStack.popPose() } + + private fun renderVanillaDisc( + itemStack: ItemStack, + poseStack: PoseStack, + bufferSource: MultiBufferSource, + packedLight: Int, + packedOverlay: Int, + level: Level? + ) { + val proxy = VanillaDiscProxies.proxyFor(itemStack, itemRenderer, level) + renderItem(proxy, poseStack, bufferSource, packedLight, packedOverlay, level) + } + + private fun renderItem( + itemStack: ItemStack, + poseStack: PoseStack, + bufferSource: MultiBufferSource, + packedLight: Int, + packedOverlay: Int, + level: Level? + ) { + itemRenderer.renderStatic(itemStack, ItemDisplayContext.FIXED, packedLight, + packedOverlay, poseStack, bufferSource, level, 1) + } } /** For adjusting values in 16ths (1 = 1 pixel). */ diff --git a/src/main/kotlin/com/hemogoblins/betterrecords/block/renderer/VanillaDiscProxies.kt b/src/main/kotlin/com/hemogoblins/betterrecords/block/renderer/VanillaDiscProxies.kt new file mode 100644 index 0000000..ae8bfaf --- /dev/null +++ b/src/main/kotlin/com/hemogoblins/betterrecords/block/renderer/VanillaDiscProxies.kt @@ -0,0 +1,77 @@ +package com.hemogoblins.betterrecords.block.renderer + +import com.hemogoblins.betterrecords.capability.ColorableCapability +import com.hemogoblins.betterrecords.capability.ModCapabilities +import com.hemogoblins.betterrecords.item.ModItems +import net.minecraft.client.renderer.entity.ItemRenderer +import net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import net.minecraft.world.level.Level + +/** + * Creates proxy record itemstacks for rendering purposes out of vanilla discs. + * Samples the color of the disc, and stores it for future plays. + */ +object VanillaDiscProxies { + + private const val DEFAULT_COLOR = 0xFFFFFF + + private val proxies = mutableMapOf() + + fun proxyFor(stack: ItemStack, itemRenderer: ItemRenderer, level: Level?): ItemStack { + return proxies.getOrPut(stack.item) { + val color = sampleDiscColor(stack, itemRenderer, level) + + ItemStack(ModItems.RECORD.get()).apply { + getCapability(ModCapabilities.COLORABLE_CAPABILITY).ifPresent { + (it as ColorableCapability).color = color + } + } + } + } + + private fun sampleDiscColor(stack: ItemStack, itemRenderer: ItemRenderer, level: Level?): Int { + return try { + val sprite = itemRenderer.getModel(stack, level, null, 0).particleIcon + val contents = sprite.contents() + val image = contents.originalImage + + var rAcc = 0.0 + var gAcc = 0.0 + var bAcc = 0.0 + var weight = 0.0 + + for (y in 0 until contents.height()) { + for (x in 0 until contents.width()) { + val argb = image.getPixelRGBA(x, y) // NativeImage is little-endian 0xAABBGGRR + val a = (argb ushr 24) and 0xFF + if (a < 128) continue + + val r = argb and 0xFF + val g = (argb ushr 8) and 0xFF + val b = (argb ushr 16) and 0xFF + + val max = maxOf(r, g, b) + if (max == 0) continue + val min = minOf(r, g, b) + + val saturation = (max - min).toDouble() / max + val w = saturation * saturation * max + rAcc += r * w + gAcc += g * w + bAcc += b * w + weight += w + } + } + + if (weight <= 0.0) return DEFAULT_COLOR + + val r = (rAcc / weight).toInt().coerceIn(0, 255) + val g = (gAcc / weight).toInt().coerceIn(0, 255) + val b = (bAcc / weight).toInt().coerceIn(0, 255) + (r shl 16) or (g shl 8) or b + } catch (e: Exception) { + DEFAULT_COLOR + } + } +} diff --git a/src/main/kotlin/com/hemogoblins/betterrecords/client/gui/RecordLoadingOverlay.kt b/src/main/kotlin/com/hemogoblins/betterrecords/client/gui/RecordLoadingOverlay.kt index 12edddb..a4d74b1 100644 --- a/src/main/kotlin/com/hemogoblins/betterrecords/client/gui/RecordLoadingOverlay.kt +++ b/src/main/kotlin/com/hemogoblins/betterrecords/client/gui/RecordLoadingOverlay.kt @@ -1,6 +1,6 @@ package com.hemogoblins.betterrecords.client.gui -import com.hemogoblins.betterrecords.api.client.sound.FileSoundInstance +import com.hemogoblins.betterrecords.api.client.sound.PlayingSound import com.hemogoblins.betterrecords.client.sound.RecordSoundManager import net.minecraft.Util import net.minecraft.client.Minecraft @@ -54,14 +54,14 @@ object RecordLoadingOverlay : IGuiOverlay { } } - private fun renderLoading(guiGraphics: GuiGraphics, font: Font, sound: FileSoundInstance, top: Int): Int { + private fun renderLoading(guiGraphics: GuiGraphics, font: Font, sound: PlayingSound, top: Int): Int { val panelWidth = BAR_WIDTH + PADDING * 2 val rowHeight = font.lineHeight + GAP + BAR_HEIGHT val progress = sound.loadProgress.coerceIn(0f, 1f) val percent = "${(progress * 100).roundToInt()}%" guiGraphics.fill(PADDING - 2, top - 2, PADDING + panelWidth, top + rowHeight + 2, COLOR_PANEL) - guiGraphics.drawString(font, "♪ " + sound.displayName(), PADDING + 2, top, COLOR_TEXT) + guiGraphics.drawString(font, "♪ " + sound.name, PADDING + 2, top, COLOR_TEXT) guiGraphics.drawString(font, percent, PADDING + panelWidth - font.width(percent) - 2, top, COLOR_SUBTEXT) val barX = PADDING + 2 @@ -73,8 +73,8 @@ object RecordLoadingOverlay : IGuiOverlay { return top + rowHeight + PADDING + GAP } - private fun renderNowPlaying(guiGraphics: GuiGraphics, font: Font, sound: FileSoundInstance, top: Int): Int { - val label = "♪ Now Playing: " + sound.displayName() + private fun renderNowPlaying(guiGraphics: GuiGraphics, font: Font, sound: PlayingSound, top: Int): Int { + val label = "♪ Now Playing: " + sound.name val panelWidth = font.width(label) + PADDING * 2 guiGraphics.fill(PADDING - 2, top - 2, PADDING + panelWidth, top + font.lineHeight + 2, COLOR_PANEL) @@ -82,6 +82,4 @@ object RecordLoadingOverlay : IGuiOverlay { return top + font.lineHeight + PADDING + GAP } - - private fun FileSoundInstance.displayName(): String = name.ifEmpty { "Record" } } diff --git a/src/main/kotlin/com/hemogoblins/betterrecords/client/sound/ProgressInputStream.kt b/src/main/kotlin/com/hemogoblins/betterrecords/client/sound/ProgressInputStream.kt index a09878e..f86fb07 100644 --- a/src/main/kotlin/com/hemogoblins/betterrecords/client/sound/ProgressInputStream.kt +++ b/src/main/kotlin/com/hemogoblins/betterrecords/client/sound/ProgressInputStream.kt @@ -1,6 +1,5 @@ package com.hemogoblins.betterrecords.client.sound -import com.hemogoblins.betterrecords.api.client.sound.FileSoundInstance import java.io.FilterInputStream import java.io.InputStream @@ -10,7 +9,7 @@ import java.io.InputStream class ProgressInputStream( delegate: InputStream, private val total: Long, - private val sound: FileSoundInstance, + private val sound: RecordPlayingSound, ) : FilterInputStream(delegate) { private var read = 0L diff --git a/src/main/kotlin/com/hemogoblins/betterrecords/api/client/sound/FileSoundInstance.kt b/src/main/kotlin/com/hemogoblins/betterrecords/client/sound/RecordPlayingSound.kt similarity index 80% rename from src/main/kotlin/com/hemogoblins/betterrecords/api/client/sound/FileSoundInstance.kt rename to src/main/kotlin/com/hemogoblins/betterrecords/client/sound/RecordPlayingSound.kt index cfce2b4..36f05a2 100644 --- a/src/main/kotlin/com/hemogoblins/betterrecords/api/client/sound/FileSoundInstance.kt +++ b/src/main/kotlin/com/hemogoblins/betterrecords/client/sound/RecordPlayingSound.kt @@ -1,6 +1,7 @@ -package com.hemogoblins.betterrecords.api.client.sound +package com.hemogoblins.betterrecords.client.sound import com.hemogoblins.betterrecords.BetterRecords +import com.hemogoblins.betterrecords.api.client.sound.PlayingSound import net.minecraft.client.resources.sounds.Sound import net.minecraft.client.resources.sounds.SoundInstance import net.minecraft.client.sounds.SoundManager @@ -10,26 +11,29 @@ import net.minecraft.resources.ResourceLocation import net.minecraft.sounds.SoundSource import net.minecraft.util.valueproviders.ConstantFloat -class FileSoundInstance( +/** + * [com.hemogoblins.betterrecords.api.client.sound.PlayingSound] for a record + */ +class RecordPlayingSound( val file: String, val pos: BlockPos, /** Human-readable name */ - val name: String = "", -) : SoundInstance { + override val name: String, +) : SoundInstance, PlayingSound { /** True while still being decoded by [com.hemogoblins.betterrecords.mixin.MixinSoundEngine]. */ @Volatile - var loading: Boolean = false + override var loading: Boolean = false /** Decode progress in the range 0..1. */ @Volatile - var loadProgress: Float = 0f + override var loadProgress: Float = 0f /** * Timestamp at which playback actually started */ @Volatile - var startedPlayingAt: Long = 0L + override var startedPlayingAt: Long = 0L override fun getLocation() = ResourceLocation(BetterRecords.ID, file) @@ -67,4 +71,4 @@ class FileSoundInstance( override fun getZ() = pos.z.toDouble() override fun getAttenuation() = SoundInstance.Attenuation.LINEAR -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/hemogoblins/betterrecords/client/sound/RecordSoundManager.kt b/src/main/kotlin/com/hemogoblins/betterrecords/client/sound/RecordSoundManager.kt index a908843..d0e2f6d 100644 --- a/src/main/kotlin/com/hemogoblins/betterrecords/client/sound/RecordSoundManager.kt +++ b/src/main/kotlin/com/hemogoblins/betterrecords/client/sound/RecordSoundManager.kt @@ -1,46 +1,64 @@ package com.hemogoblins.betterrecords.client.sound import com.hemogoblins.betterrecords.BetterRecords -import com.hemogoblins.betterrecords.api.client.sound.FileSoundInstance +import com.hemogoblins.betterrecords.api.client.sound.PlayingSound +import com.hemogoblins.betterrecords.network.clientbound.PlayRecordPacket import net.minecraft.Util import net.minecraft.client.Minecraft +import net.minecraft.client.resources.sounds.SimpleSoundInstance +import net.minecraft.client.resources.sounds.SoundInstance import net.minecraft.core.BlockPos +import net.minecraft.world.phys.Vec3 +import net.minecraftforge.registries.ForgeRegistries import java.util.concurrent.CompletableFuture +import net.minecraft.world.item.RecordItem as VanillaRecordItem /** * Client-side registry of currently playing records * - * A lot of the heavy happens transparently by [com.hemogoblins.betterrecords.mixin.MixinSoundEngine], - * but this class maps an instance of a playing sound to a position so that we can actually start and stop it. + * A lot of the heavy lifting happens transparently in [com.hemogoblins.betterrecords.mixin.MixinSoundEngine] + * for file-backed sounds, but this class maps a playing sound to a position so that we can actually + * start and stop it. */ object RecordSoundManager { - private val activeSounds = mutableMapOf() + /** A sound playing at a position, paired with the [PlayingSound] the overlay displays for it. */ + private class ActiveSound(val sound: SoundInstance, val display: PlayingSound) - /** Starts playing the record at [pos], replacing anything already playing there. */ - fun play(pos: BlockPos, checksum: String, url: String, name: String) { + private val activeSounds = mutableMapOf() + + /** Starts playing [source] at [pos], replacing anything already playing there. */ + fun play(pos: BlockPos, source: PlayRecordPacket.RecordSource) { stop(pos) - val instance = FileSoundInstance(checksum, pos, name).apply { + when (source) { + is PlayRecordPacket.RecordSource.File -> playFile(pos, source) + is PlayRecordPacket.RecordSource.Vanilla -> playVanilla(pos, source) + } + } + + /** Plays a BetterRecords record: downloads/decodes the cached file, then plays it via the mixin. */ + private fun playFile(pos: BlockPos, source: PlayRecordPacket.RecordSource.File) { + val instance = RecordPlayingSound(source.checksum, pos, source.name).apply { loading = true loadProgress = 0f } - activeSounds[pos] = instance + activeSounds[pos] = ActiveSound(instance, instance) CompletableFuture .runAsync({ - BetterRecords.cache.get(url, checksum) { percent -> + BetterRecords.cache.get(source.url, source.checksum) { percent -> instance.loadProgress = (percent / 100f).coerceIn(0f, 1f) } }, Util.ioPool()) .thenRunAsync({ // The record may have been removed or replaced while downloading. - if (activeSounds[pos] === instance) { + if (activeSounds[pos]?.sound === instance) { Minecraft.getInstance().soundManager.play(instance) } }, Minecraft.getInstance()) .exceptionallyAsync({ throwable -> - if (activeSounds[pos] === instance) { + if (activeSounds[pos]?.sound === instance) { activeSounds.remove(pos) instance.loading = false } @@ -49,18 +67,33 @@ object RecordSoundManager { }, Minecraft.getInstance()) } + /** Plays a vanilla music disc through vanilla's sound engine. */ + private fun playVanilla(pos: BlockPos, source: PlayRecordPacket.RecordSource.Vanilla) { + val event = ForgeRegistries.SOUND_EVENTS.getValue(source.sound) + if (event == null) { + BetterRecords.logger.warn("Unknown sound event for vanilla disc at $pos: ${source.sound}") + return + } + + val instance = SimpleSoundInstance.forRecord(event, Vec3.atCenterOf(pos)) + val name = VanillaRecordItem.getBySound(event)?.displayName?.string ?: "Record" + activeSounds[pos] = ActiveSound(instance, VanillaRecordPlayingSound(name, Util.getMillis())) + Minecraft.getInstance().soundManager.play(instance) + } + /** Stops the record playing at [pos], if any. */ fun stop(pos: BlockPos) { activeSounds.remove(pos)?.let { - Minecraft.getInstance().soundManager.stop(it) + Minecraft.getInstance().soundManager.stop(it.sound) } } - fun onFinished(instance: FileSoundInstance) { - if (activeSounds[instance.pos] === instance) { + fun onFinished(instance: RecordPlayingSound) { + if (activeSounds[instance.pos]?.sound === instance) { activeSounds.remove(instance.pos) } } - fun activeRecords(): List = activeSounds.values.toList() + /** Sounds currently playing, used by the loading / "Now Playing" overlay. */ + fun activeRecords(): List = activeSounds.values.map { it.display } } diff --git a/src/main/kotlin/com/hemogoblins/betterrecords/client/sound/VanillaRecordPlayingSound.kt b/src/main/kotlin/com/hemogoblins/betterrecords/client/sound/VanillaRecordPlayingSound.kt new file mode 100644 index 0000000..ddb7227 --- /dev/null +++ b/src/main/kotlin/com/hemogoblins/betterrecords/client/sound/VanillaRecordPlayingSound.kt @@ -0,0 +1,14 @@ +package com.hemogoblins.betterrecords.client.sound + +import com.hemogoblins.betterrecords.api.client.sound.PlayingSound + +/** + * [PlayingSound] for a vanilla music disc. + */ +class VanillaRecordPlayingSound( + override val name: String, + override val startedPlayingAt: Long, +) : PlayingSound { + override val loading = false + override val loadProgress = 1f +} diff --git a/src/main/kotlin/com/hemogoblins/betterrecords/network/clientbound/PlayRecordPacket.kt b/src/main/kotlin/com/hemogoblins/betterrecords/network/clientbound/PlayRecordPacket.kt index 7627615..03e35c3 100644 --- a/src/main/kotlin/com/hemogoblins/betterrecords/network/clientbound/PlayRecordPacket.kt +++ b/src/main/kotlin/com/hemogoblins/betterrecords/network/clientbound/PlayRecordPacket.kt @@ -1,8 +1,12 @@ package com.hemogoblins.betterrecords.network.clientbound +import com.hemogoblins.betterrecords.capability.ModCapabilities import com.hemogoblins.betterrecords.client.sound.RecordSoundManager import net.minecraft.core.BlockPos import net.minecraft.network.FriendlyByteBuf +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.RecordItem import net.minecraftforge.api.distmarker.Dist import net.minecraftforge.fml.DistExecutor import net.minecraftforge.network.NetworkEvent.Context @@ -13,23 +17,19 @@ import java.util.function.Supplier */ class PlayRecordPacket( val pos: BlockPos, - val checksum: String, - val url: String, - val name: String, + val source: RecordSource, ) { fun encode(buf: FriendlyByteBuf) { buf.writeBlockPos(pos) - buf.writeUtf(checksum) - buf.writeUtf(url) - buf.writeUtf(name) + source.encode(buf) } companion object { fun handle(msg: PlayRecordPacket, ctx: Supplier) { ctx.get().enqueueWork { DistExecutor.unsafeRunWhenOn(Dist.CLIENT) { - Runnable { RecordSoundManager.play(msg.pos, msg.checksum, msg.url, msg.name) } + Runnable { RecordSoundManager.play(msg.pos, msg.source) } } } ctx.get().packetHandled = true @@ -38,10 +38,52 @@ class PlayRecordPacket( fun decode(buf: FriendlyByteBuf): PlayRecordPacket { return PlayRecordPacket( buf.readBlockPos(), - buf.readUtf(), - buf.readUtf(), - buf.readUtf(), + RecordSource.decode(buf), ) } } + + sealed class RecordSource { + + abstract fun encode(buf: FriendlyByteBuf) + + /** A BetterRecords record */ + data class File(val name: String, val url: String, val checksum: String) : RecordSource() { + override fun encode(buf: FriendlyByteBuf) { + buf.writeByte(Type.FILE.ordinal) + buf.writeUtf(name) + buf.writeUtf(url) + buf.writeUtf(checksum) + } + } + + /** A vanilla music disc */ + data class Vanilla(val sound: ResourceLocation) : RecordSource() { + override fun encode(buf: FriendlyByteBuf) { + buf.writeByte(Type.VANILLA.ordinal) + buf.writeResourceLocation(sound) + } + } + + private enum class Type { FILE, VANILLA } + + companion object { + fun decode(buf: FriendlyByteBuf): RecordSource = + when (Type.values()[buf.readByte().toInt()]) { + Type.FILE -> File(buf.readUtf(), buf.readUtf(), buf.readUtf()) + Type.VANILLA -> Vanilla(buf.readResourceLocation()) + } + + fun of(stack: ItemStack): RecordSource? { + val holder = stack.getCapability(ModCapabilities.MUSIC_HOLDER_CAPABILITY).resolve() + if (holder.isPresent) { + val music = holder.get() + return if (music.checksum.isNotEmpty()) File(music.songName, music.url, music.checksum) else null + } + + (stack.item as? RecordItem)?.let { return Vanilla(it.sound.location) } + return null + } + } + } } From 3d24e6dc5d777c276d89301ee34dbacc635badf3 Mon Sep 17 00:00:00 2001 From: AngrySoundTech Date: Thu, 2 Jul 2026 21:57:03 -0400 Subject: [PATCH 2/2] Stop record from playing when player is broken --- .../hemogoblins/betterrecords/block/RecordPlayerBlock.kt | 2 +- .../betterrecords/block/entity/RecordPlayerBlockEntity.kt | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/hemogoblins/betterrecords/block/RecordPlayerBlock.kt b/src/main/kotlin/com/hemogoblins/betterrecords/block/RecordPlayerBlock.kt index cfe6542..aa1ed54 100644 --- a/src/main/kotlin/com/hemogoblins/betterrecords/block/RecordPlayerBlock.kt +++ b/src/main/kotlin/com/hemogoblins/betterrecords/block/RecordPlayerBlock.kt @@ -96,7 +96,7 @@ class RecordPlayerBlock(properties: Properties) : BaseEntityBlock(properties) { override fun onRemove(state: BlockState, level: Level, pos: BlockPos, newState: BlockState, isMoving: Boolean) { if (state.block != newState.block) { level.getBlockEntity(pos).takeIf { it is RecordPlayerBlockEntity }.let { - (it as RecordPlayerBlockEntity).dropContents() + (it as RecordPlayerBlockEntity).stopPlaying() } } diff --git a/src/main/kotlin/com/hemogoblins/betterrecords/block/entity/RecordPlayerBlockEntity.kt b/src/main/kotlin/com/hemogoblins/betterrecords/block/entity/RecordPlayerBlockEntity.kt index 0d99fd2..464e3b3 100644 --- a/src/main/kotlin/com/hemogoblins/betterrecords/block/entity/RecordPlayerBlockEntity.kt +++ b/src/main/kotlin/com/hemogoblins/betterrecords/block/entity/RecordPlayerBlockEntity.kt @@ -103,6 +103,14 @@ class RecordPlayerBlockEntity( ) } + fun stopPlaying() { + if (level?.isClientSide == false && isPlaying) { + togglePlaying(false) + sendStopRecord() + dropContents() + } + } + private fun sendStopRecord() { ModNetwork.channel.send( PacketDistributor.TRACKING_CHUNK.with { level!!.getChunkAt(blockPos) },