diff --git a/gradle.properties b/gradle.properties index f0c19d0..ac197c7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ minecraft_version=1.21.4 yarn_mappings=1.21.4+build.8 loader_version=0.16.10 # Mod Properties -mod_version=1.14.514.032 +mod_version=1.14.514.034 maven_group=semmiedev archives_base_name=disc_jockey_revive # Dependencies diff --git a/src/main/java/semmiedev/disc_jockey_revive/Main.java b/src/main/java/semmiedev/disc_jockey_revive/Main.java index 9fcbd6e..72a902b 100644 --- a/src/main/java/semmiedev/disc_jockey_revive/Main.java +++ b/src/main/java/semmiedev/disc_jockey_revive/Main.java @@ -21,6 +21,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.lwjgl.glfw.GLFW; import semmiedev.disc_jockey_revive.gui.hud.BlocksOverlay; +import semmiedev.disc_jockey_revive.gui.hud.PlaybackProgressOverlay; import semmiedev.disc_jockey_revive.gui.screen.DiscJockeyScreen; import java.io.File; @@ -85,6 +86,6 @@ public class Main implements ClientModInitializer { SONG_PLAYER.stop(); }); - HudRenderCallback.EVENT.register(BlocksOverlay::render); + HudRenderCallback.EVENT.register(new PlaybackProgressOverlay()); } } diff --git a/src/main/java/semmiedev/disc_jockey_revive/ModConfig.java b/src/main/java/semmiedev/disc_jockey_revive/ModConfig.java index 2cf4404..2ad0fba 100644 --- a/src/main/java/semmiedev/disc_jockey_revive/ModConfig.java +++ b/src/main/java/semmiedev/disc_jockey_revive/ModConfig.java @@ -46,4 +46,7 @@ public class ModConfig implements ConfigData { @ConfigEntry.Gui.Excluded public ArrayList favorites = new ArrayList<>(); + + @ConfigEntry.Gui.Tooltip(count = 1) + public boolean showHudProgressBar = true; } diff --git a/src/main/java/semmiedev/disc_jockey_revive/SongLoader.java b/src/main/java/semmiedev/disc_jockey_revive/SongLoader.java index f7aff09..087073f 100644 --- a/src/main/java/semmiedev/disc_jockey_revive/SongLoader.java +++ b/src/main/java/semmiedev/disc_jockey_revive/SongLoader.java @@ -79,7 +79,7 @@ public class SongLoader { song.folder = songFolder; } } catch (Exception exception) { - Main.LOGGER.error("Unable to read or parse song {}", file.getName(), exception); + Main.LOGGER.error("无法读取或解析歌曲 {}", file.getName(), exception); } } } diff --git a/src/main/java/semmiedev/disc_jockey_revive/SongPlayer.java b/src/main/java/semmiedev/disc_jockey_revive/SongPlayer.java index abb4ac0..0eed3cf 100644 --- a/src/main/java/semmiedev/disc_jockey_revive/SongPlayer.java +++ b/src/main/java/semmiedev/disc_jockey_revive/SongPlayer.java @@ -215,7 +215,7 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { client.player.networkHandler.sendPacket(new PlayerActionC2SPacket(PlayerActionC2SPacket.Action.START_DESTROY_BLOCK, blockPos, Direction.UP, 0)); last100MsSpanEstimatedPackets++; }else if(last100MsSpanEstimatedPackets >= last100MsStopPacketsAfter) { - Main.LOGGER.info("Stopping all packets for a bit!"); + Main.LOGGER.info("短暂暂停所有数据包!"); stopPacketsUntil = Math.max(stopPacketsUntil, now + 250); reducePacketsUntil = Math.max(reducePacketsUntil, now + 10000); } @@ -256,29 +256,62 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { } } - private void playNextSong() { + + public synchronized void setPlayMode(PlayMode mode) { + this.playMode = mode; + this.loopSong = mode == PlayMode.SINGLE_LOOP; + } + + // 获取当前播放进度 (0.0 - 1.0) + public synchronized float getProgress() { + if (song == null || !running) return 0; + return Math.min(1.0f, (float) (tick / song.length)); + } + + // 获取格式化时间字符串 + public synchronized String getFormattedTime() { + if (song == null) return "00:00 / 00:00"; + + double totalSeconds = song.getLengthInSeconds(); + double currentSeconds = song.ticksToMilliseconds(tick) / 1000.0; + + return formatTime((int) currentSeconds) + " / " + formatTime((int) totalSeconds); + } + + // 格式化时间为 mm:ss + private String formatTime(int seconds) { + int min = seconds / 60; + int sec = seconds % 60; + return String.format("%02d:%02d", min, sec); + } + + // 播放上一首 + public synchronized void playPreviousSong() { if (SongLoader.currentFolder == null || SongLoader.currentFolder.songs.isEmpty()) return; int currentIndex = SongLoader.currentFolder.songs.indexOf(song); if (currentIndex == -1) return; + int prevIndex = (currentIndex - 1 + SongLoader.currentFolder.songs.size()) % SongLoader.currentFolder.songs.size(); + start(SongLoader.currentFolder.songs.get(prevIndex)); + } + + // 播放下一首 + public synchronized void playNextSong() { if (playMode == PlayMode.RANDOM) { - int newIndex; - do { - newIndex = (int) (Math.random() * SongLoader.currentFolder.songs.size()); - } while (newIndex == currentIndex && SongLoader.currentFolder.songs.size() > 1); - start(SongLoader.currentFolder.songs.get(newIndex)); - } else if (playMode == PlayMode.LIST_LOOP) { + randomIndex = getRandomSongIndex(); + start(SongLoader.currentFolder.songs.get(randomIndex)); + } else { + if (SongLoader.currentFolder == null || SongLoader.currentFolder.songs.isEmpty()) return; + + int currentIndex = SongLoader.currentFolder.songs.indexOf(song); + if (currentIndex == -1) return; + int nextIndex = (currentIndex + 1) % SongLoader.currentFolder.songs.size(); start(SongLoader.currentFolder.songs.get(nextIndex)); } } - public synchronized void setPlayMode(PlayMode mode) { - this.playMode = mode; - this.loopSong = mode == PlayMode.SINGLE_LOOP; - } - // this is the original author‘s comment, i dont wanna delete it // TODO: 6/2/2022 Play note blocks every song tick, instead of every tick. That way the song will sound better // 11/1/2023 Playback now done in separate thread. Not ideal but better especially when FPS are low. diff --git a/src/main/java/semmiedev/disc_jockey_revive/gui/ProgressBarRenderer.java b/src/main/java/semmiedev/disc_jockey_revive/gui/ProgressBarRenderer.java new file mode 100644 index 0000000..30ad087 --- /dev/null +++ b/src/main/java/semmiedev/disc_jockey_revive/gui/ProgressBarRenderer.java @@ -0,0 +1,23 @@ +package semmiedev.disc_jockey_revive.gui; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; + +public class ProgressBarRenderer { + private static final int BACKGROUND_COLOR = 0x80808080; + private static final int PROGRESS_COLOR = 0x8000FF00; // 绿色 + private static final int TEXT_COLOR = 0xFFFFFF; + + public void renderProgressBar(DrawContext context, int x, int y, int width, int height, float progress, String timeText) { + context.fill(x, y, x + width, y + height, BACKGROUND_COLOR); + + int progressWidth = (int) (width * progress); + + context.fill(x, y, x + progressWidth, y + height, PROGRESS_COLOR); + + MinecraftClient client = MinecraftClient.getInstance(); + int textX = x + (width - client.textRenderer.getWidth(timeText)) / 2; + int textY = y - client.textRenderer.fontHeight - 2; + context.drawTextWithShadow(client.textRenderer, timeText, textX, textY, TEXT_COLOR); + } +} diff --git a/src/main/java/semmiedev/disc_jockey_revive/gui/hud/PlaybackProgressOverlay.java b/src/main/java/semmiedev/disc_jockey_revive/gui/hud/PlaybackProgressOverlay.java new file mode 100644 index 0000000..e095c23 --- /dev/null +++ b/src/main/java/semmiedev/disc_jockey_revive/gui/hud/PlaybackProgressOverlay.java @@ -0,0 +1,46 @@ +package semmiedev.disc_jockey_revive.gui.hud; + +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.render.RenderTickCounter; +import semmiedev.disc_jockey_revive.Main; +import semmiedev.disc_jockey_revive.gui.ProgressBarRenderer; +import net.minecraft.client.gui.screen.ConfirmScreen; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; + +import static net.minecraft.client.toast.TutorialToast.PROGRESS_BAR_WIDTH; + +public class PlaybackProgressOverlay implements HudRenderCallback { + private final ProgressBarRenderer progressBarRenderer = new ProgressBarRenderer(); + + @Override + public void onHudRender(DrawContext context, RenderTickCounter tickCounter) { + if (!Main.config.showHudProgressBar) return; + + MinecraftClient client = MinecraftClient.getInstance(); + + boolean isInGame = client.currentScreen == null; + + if (Main.SONG_PLAYER.running && Main.SONG_PLAYER.song != null && isInGame) { + int screenWidth = context.getScaledWindowWidth(); + int screenHeight = context.getScaledWindowHeight(); + + int barX = screenWidth / 2 - PROGRESS_BAR_WIDTH / 2; + int barY = screenHeight - 50; + + progressBarRenderer.renderProgressBar( + context, + barX, + barY, + PROGRESS_BAR_WIDTH, + 5, + Main.SONG_PLAYER.getProgress(), + Main.SONG_PLAYER.getFormattedTime() + ); + } + + } +} diff --git a/src/main/java/semmiedev/disc_jockey_revive/gui/screen/DiscJockeyScreen.java b/src/main/java/semmiedev/disc_jockey_revive/gui/screen/DiscJockeyScreen.java index 33a4127..58269d3 100644 --- a/src/main/java/semmiedev/disc_jockey_revive/gui/screen/DiscJockeyScreen.java +++ b/src/main/java/semmiedev/disc_jockey_revive/gui/screen/DiscJockeyScreen.java @@ -13,6 +13,7 @@ import semmiedev.disc_jockey_revive.Main; import semmiedev.disc_jockey_revive.Note; import semmiedev.disc_jockey_revive.Song; import semmiedev.disc_jockey_revive.SongLoader; +import semmiedev.disc_jockey_revive.gui.ProgressBarRenderer; import semmiedev.disc_jockey_revive.gui.SongListWidget; import semmiedev.disc_jockey_revive.gui.hud.BlocksOverlay; @@ -27,6 +28,8 @@ import java.util.stream.Collectors; import semmiedev.disc_jockey_revive.SongLoader.SongFolder; import semmiedev.disc_jockey_revive.SongPlayer.PlayMode; +import static net.minecraft.client.toast.TutorialToast.PROGRESS_BAR_WIDTH; + public class DiscJockeyScreen extends Screen { private static final MutableText SELECT_SONG = Text.translatable(Main.MOD_ID+".screen.select_song"), @@ -58,15 +61,23 @@ public class DiscJockeyScreen extends Screen { private ButtonWidget folderUpButton, playModeButton; public SongFolder currentFolder; private PlayMode currentPlayMode = PlayMode.STOP_AFTER; + private int progressBarWidth = 200; // 进度条宽度 + private int progressBarHeight = 5; // 进度条高度 + private int progressBarYOffset = 5; // 进度条Y偏移 + + private ProgressBarRenderer progressBarRenderer; public DiscJockeyScreen() { super(Main.NAME); + this.progressBarRenderer = new ProgressBarRenderer(); } @Override protected void init() { shouldFilter = true; - songListWidget = new SongListWidget(client, width, height - 64 - 32, 32, 20, this); + songListWidget = new SongListWidget(client, width, height - 100, 32, 20, this); + boolean isLargeScreen = width > 900; + int progressBarY = height - 90; // 恢复播放模式 currentPlayMode = Main.config.playMode; @@ -111,6 +122,20 @@ public class DiscJockeyScreen extends Screen { }).dimensions(width - 120, 10, 100, 20).build(); addDrawableChild(playModeButton); + int buttonY; + if (isLargeScreen){ + buttonY = height - 30; + } else { + buttonY = height - 60; + } + int centerX = width / 2; + + // 上一首 + addDrawableChild(ButtonWidget.builder(Text.literal("◀◀◀"), button -> { + Main.SONG_PLAYER.playPreviousSong(); + }).dimensions(centerX - 110, buttonY, 40, 20).build()); + + // 播放暂停 playButton = ButtonWidget.builder(PLAY, button -> { if (Main.SONG_PLAYER.running) { Main.SONG_PLAYER.stop(); @@ -118,12 +143,18 @@ public class DiscJockeyScreen extends Screen { SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull(); if (entry != null) { Main.SONG_PLAYER.start(entry.song); - client.setScreen(null); + // client.setScreen(null); } } - }).dimensions(width / 2 - 160, height - 61, 100, 20).build(); + }).dimensions(centerX - 50, buttonY, 100, 20).build(); addDrawableChild(playButton); + // 下一首 + addDrawableChild(ButtonWidget.builder(Text.literal("▶▶▶"), button -> { + Main.SONG_PLAYER.playNextSong(); + }).dimensions(centerX + 60, buttonY, 40, 20).build()); + + // 预览 previewButton = ButtonWidget.builder(PREVIEW, button -> { if (Main.PREVIEWER.running) { Main.PREVIEWER.stop(); @@ -131,9 +162,12 @@ public class DiscJockeyScreen extends Screen { SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull(); if (entry != null) Main.PREVIEWER.start(entry.song); } - }).dimensions(width / 2 - 50, height - 61, 100, 20).build(); + }).dimensions(centerX + 110, buttonY, 100, 20).build(); addDrawableChild(previewButton); + int bottomY = height - 30; + + addDrawableChild(ButtonWidget.builder(Text.translatable(Main.MOD_ID+".screen.blocks"), button -> { // TODO: 6/2/2022 Add an auto build mode if (BlocksOverlay.itemStacks == null) { @@ -171,7 +205,7 @@ public class DiscJockeyScreen extends Screen { BlocksOverlay.itemStacks = null; client.setScreen(null); } - }).dimensions(width / 2 + 60, height - 61, 100, 20).build()); + }).dimensions(width - 110, height - 31, 100, 20).build()); // 打开文件夹 addDrawableChild(ButtonWidget.builder(OPEN_FOLDER, button -> { @@ -200,16 +234,16 @@ public class DiscJockeyScreen extends Screen { Text.translatable(Main.MOD_ID+".screen.open_folder_failed") .formatted(Formatting.RED)); } - }).dimensions(width / 2 - 160, height - 31, 100, 20).build()); + }).dimensions(10, bottomY, 100, 20).build()); // 重新加载 addDrawableChild(ButtonWidget.builder(RELOAD, button -> { SongLoader.loadSongs(); client.setScreen(null); - }).dimensions(width / 2 + 60, height - 31, 100, 20).build()); + }).dimensions(120, bottomY, 100, 20).build()); - TextFieldWidget searchBar = new TextFieldWidget(textRenderer, width / 2 - 50, height - 31, 100, 20, Text.translatable(Main.MOD_ID+".screen.search")); + TextFieldWidget searchBar = new TextFieldWidget(textRenderer, 230, height - 31, 100, 20, Text.translatable(Main.MOD_ID+".screen.search")); searchBar.setChangedListener(query -> { query = query.toLowerCase().replaceAll("\\s", ""); if (this.query.equals(query)) return; @@ -218,6 +252,8 @@ public class DiscJockeyScreen extends Screen { }); addDrawableChild(searchBar); + // Main.LOGGER.info("播放界面初始化完成!"); + // TODO: 6/2/2022 Add a reload button } @@ -232,6 +268,25 @@ public class DiscJockeyScreen extends Screen { String folderName = currentFolder == null ? "/" : currentFolder.name; context.drawTextWithShadow(textRenderer, CURRENT_FOLDER.getString() + ": " + folderName, 35, 15, 0xFFFFFF); context.drawTextWithShadow(textRenderer, PLAY_MODE.getString() + ":", width - 220, 15, 0xFFFFFF); + + int screenWidth = context.getScaledWindowWidth(); + + // 进度条 + if (Main.SONG_PLAYER.running && Main.SONG_PLAYER.song != null) { + int progressBarX = 10; + int progressBarY = height - 50; // 按钮上方 + int barWidth = screenWidth - 20; + + progressBarRenderer.renderProgressBar( + context, + progressBarX, + progressBarY, + barWidth, + 5, + Main.SONG_PLAYER.getProgress(), + Main.SONG_PLAYER.getFormattedTime() + ); + } } @Override @@ -354,7 +409,7 @@ public class DiscJockeyScreen extends Screen { SongLoader.SONGS.add(song); } } catch (IOException exception) { - Main.LOGGER.warn("Failed to copy song file from {} to {}", path, Main.songsFolder.toPath(), exception); + Main.LOGGER.warn("无法将歌曲文件从 {} 复制到 {} ", path, Main.songsFolder.toPath(), exception); } }); diff --git a/src/main/resources/assets/disc_jockey/lang/en_us.json b/src/main/resources/assets/disc_jockey/lang/en_us.json index c5bb33f..db78995 100644 --- a/src/main/resources/assets/disc_jockey/lang/en_us.json +++ b/src/main/resources/assets/disc_jockey/lang/en_us.json @@ -61,5 +61,7 @@ "disc_jockey_revive.screen.open_folder": "Open Folder", "disc_jockey_revive.screen.open_folder_failed": "Failed to open folder", "disc_jockey_revive.screen.reload": "Reload Songs", - "disc_jockey_revive.screen.reloading": "Reloading songs..." + "disc_jockey_revive.screen.reloading": "Reloading songs...", + "text.autoconfig.disc_jockey_revive.option.showHudProgressBar": "Show HUD progress bar", + "text.autoconfig.disc_jockey_revive.option.showHudProgressBar.@Tooltip": "Show song playback progress bar in game。" } \ No newline at end of file diff --git a/src/main/resources/assets/disc_jockey/lang/zh_cn.json b/src/main/resources/assets/disc_jockey/lang/zh_cn.json index 0a5cfb8..42e2812 100644 --- a/src/main/resources/assets/disc_jockey/lang/zh_cn.json +++ b/src/main/resources/assets/disc_jockey/lang/zh_cn.json @@ -60,5 +60,7 @@ "disc_jockey_revive.screen.mode_stop": "播完停止","disc_jockey_revive.screen.open_folder": "打开文件夹", "disc_jockey_revive.screen.open_folder_failed": "无法打开文件夹", "disc_jockey_revive.screen.reload": "重新加载", - "disc_jockey_revive.screen.reloading": "正在重新加载..." + "disc_jockey_revive.screen.reloading": "正在重新加载...", + "text.autoconfig.disc_jockey_revive.option.showHudProgressBar": "显示HUD进度条", + "text.autoconfig.disc_jockey_revive.option.showHudProgressBar.@Tooltip": "在游戏界面显示歌曲播放进度条" } \ No newline at end of file