diff --git a/build.gradle b/build.gradle index e8f1c1a..7c7acec 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ dependencies { // modCompileOnly("com.terraformersmc:modmenu:13.0.3")、 modCompileOnly files("libs/modmenu-13.0.3.jar") + modImplementation files("libs/LibGui-12.0.1+1.21.2.jar") } processResources { diff --git a/gradle.properties b/gradle.properties index dc9a287..23c4965 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.022 +mod_version=1.14.514.028 maven_group=semmiedev archives_base_name=disc_jockey_revive # Dependencies diff --git a/libs/LibGui-12.0.1+1.21.2.pom b/libs/LibGui-12.0.1+1.21.2.pom new file mode 100644 index 0000000..0bd6e7c --- /dev/null +++ b/libs/LibGui-12.0.1+1.21.2.pom @@ -0,0 +1,78 @@ + + + + + + + + 4.0.0 + io.github.cottonmc + LibGui + 12.0.1+1.21.2 + LibGui + Minecraft GUI Library + https://github.com/CottonMC/LibGui + + + MIT + https://github.com/CottonMC/LibGui/blob/HEAD/LICENSE + + + + + CottonMC + https://github.com/CottonMC + + + + + io.github.juuxel + libninepatch + 1.2.0 + compile + + + net.fabricmc.fabric-api + fabric-api-base + 0.4.48+c47b9d4373 + compile + + + net.fabricmc.fabric-api + fabric-networking-api-v1 + 4.3.3+56ec7ac673 + compile + + + net.fabricmc + fabric-loader + 0.16.7 + runtime + + + net.fabricmc.fabric-api + fabric-lifecycle-events-v1 + 2.3.22+c47b9d4373 + runtime + + + net.fabricmc.fabric-api + fabric-rendering-v1 + 8.0.5+c47b9d4373 + runtime + + + net.fabricmc.fabric-api + fabric-resource-loader-v0 + 3.0.5+c47b9d4373 + runtime + + + io.github.cottonmc + Jankson-Fabric + 9.0.0+j1.2.3 + runtime + + + diff --git a/src/main/java/semmiedev/disc_jockey_revive/KeyMappingManager.java b/src/main/java/semmiedev/disc_jockey_revive/KeyMappingManager.java new file mode 100644 index 0000000..60e9c64 --- /dev/null +++ b/src/main/java/semmiedev/disc_jockey_revive/KeyMappingManager.java @@ -0,0 +1,172 @@ +package semmiedev.disc_jockey_revive; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.util.InputUtil; +import net.minecraft.block.enums.NoteBlockInstrument; +import net.minecraft.text.Text; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +public class KeyMappingManager { + private static final File MAPPINGS_FILE = new File(FabricLoader.getInstance().getConfigDir().toFile(), "disc_jockey" + "/key_mappings.json"); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Type MAPPING_TYPE = new TypeToken>() {}.getType(); + + private Map mappings = new HashMap<>(); + + private static class NoteData { + String instrument; + byte note; + + public NoteData(Note note) { + this.instrument = note.instrument().name(); + this.note = note.note(); + } + + public Note toNote() { + try { + NoteBlockInstrument instrumentEnum = NoteBlockInstrument.valueOf(instrument); + return new Note(instrumentEnum, note); + } catch (IllegalArgumentException e) { + Main.LOGGER.error("键位映射中出现未知乐器 '{}' ", instrument); + return null; + } + } + } + + public KeyMappingManager() { + loadMappings(); + } + + public void loadMappings() { + if (!MAPPINGS_FILE.exists()) { + Main.LOGGER.info("未找到键映射文件,正在创建默认的类似于 FL Studio 的映射。"); + mappings.clear(); + + NoteBlockInstrument dirt = NoteBlockInstrument.HARP; + NoteBlockInstrument wood = NoteBlockInstrument.BASS; + NoteBlockInstrument gold = NoteBlockInstrument.BELL; + + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_Z, 0), new Note(wood, (byte) 6)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_S, 0), new Note(wood, (byte) 7)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_X, 0), new Note(wood, (byte) 8)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_D, 0), new Note(wood, (byte) 9)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_C, 0), new Note(wood, (byte) 10)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_V, 0), new Note(wood, (byte) 11)); + + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_G, 0), new Note(dirt, (byte) 0)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_B, 0), new Note(dirt, (byte) 1)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_H, 0), new Note(dirt, (byte) 2)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_N, 0), new Note(dirt, (byte) 3)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_J, 0), new Note(dirt, (byte) 4)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_M, 0), new Note(dirt, (byte) 5)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_COMMA, 0), new Note(dirt, (byte) 6)); + + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_Q, 0), new Note(dirt, (byte) 6)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_2, 0), new Note(dirt, (byte) 7)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_W, 0), new Note(dirt, (byte) 8)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_3, 0), new Note(dirt, (byte) 9)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_E, 0), new Note(dirt, (byte) 10)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_R, 0), new Note(dirt, (byte) 11)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_5, 0), new Note(dirt, (byte) 12)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_T, 0), new Note(dirt, (byte) 13)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_6, 0), new Note(dirt, (byte) 14)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_Y, 0), new Note(dirt, (byte) 15)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_7, 0), new Note(dirt, (byte) 16)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_U, 0), new Note(dirt, (byte) 17)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_I, 0), new Note(dirt, (byte) 18)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_9, 0), new Note(dirt, (byte) 19)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_O, 0), new Note(dirt, (byte) 20)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_0, 0), new Note(dirt, (byte) 21)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_P, 0), new Note(dirt, (byte) 22)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_LEFT_BRACKET, 0), new Note(dirt, (byte) 23)); + mappings.put(InputUtil.fromKeyCode(InputUtil.GLFW_KEY_EQUAL, 0), new Note(dirt, (byte) 24)); + + + saveMappings(); + return; + } + + try (FileReader reader = new FileReader(MAPPINGS_FILE)) { + HashMap loadedData = GSON.fromJson(reader, MAPPING_TYPE); + mappings.clear(); + if (loadedData != null) { + for (Map.Entry entry : loadedData.entrySet()) { + InputUtil.Key key = InputUtil.fromTranslationKey(entry.getKey()); + Note note = entry.getValue().toNote(); + if (key != InputUtil.UNKNOWN_KEY && note != null) { + mappings.put(key, note); + } + } + } + Main.LOGGER.info("已加载按键映射。"); + } catch (IOException e) { + Main.LOGGER.error("加载按键映射失败。", e); + } + } + + + + public void saveMappings() { + MAPPINGS_FILE.getParentFile().mkdirs(); + try (FileWriter writer = new FileWriter(MAPPINGS_FILE)) { + HashMap dataToSave = new HashMap<>(); + for (Map.Entry entry : mappings.entrySet()) { + dataToSave.put(entry.getKey().getTranslationKey(), new NoteData(entry.getValue())); + } + GSON.toJson(dataToSave, writer); + Main.LOGGER.info("已保存按键映射。"); + } catch (IOException e) { + Main.LOGGER.error("保存按键映射失败。", e); + } + } + + public Note getNoteForKey(InputUtil.Key key) { + return mappings.get(key); + } + + public void setMapping(InputUtil.Key key, Note note) { + mappings.put(key, note); + } + + public void removeMapping(InputUtil.Key key) { + mappings.remove(key); + } + + public Map getMappings() { + return new HashMap<>(mappings); + } + + public static String getNoteDisplayName(Note note) { + if (note == null) return "未设置"; + + String instrumentTranslationKey = "block.minecraft.note_block.instrument." + note.instrument().asString(); + String instrumentName = Text.translatable(instrumentTranslationKey).getString(); + + int pitch = note.note(); + String[] noteNames = {"F#", "G", "G#", "A", "A#", "B", "C", "C#", "D", "D#", "E", "F"}; + + int octave; + if (pitch >= 19) { + octave = 1; + } else if (pitch >= 7) { + octave = 0; + } else { + octave = -1; + } + + int noteIndexInArray = (pitch - 7 + 12 * 2) % 12; + String noteName = noteNames[noteIndexInArray]; + + return instrumentName + " " + noteName + "(" + octave + ")"; + } +} diff --git a/src/main/java/semmiedev/disc_jockey_revive/Main.java b/src/main/java/semmiedev/disc_jockey_revive/Main.java index 9fcbd6e..27956ee 100644 --- a/src/main/java/semmiedev/disc_jockey_revive/Main.java +++ b/src/main/java/semmiedev/disc_jockey_revive/Main.java @@ -5,6 +5,7 @@ import me.shedaniel.autoconfig.ConfigHolder; import me.shedaniel.autoconfig.serializer.JanksonConfigSerializer; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.fabricmc.fabric.api.client.networking.v1.ClientLoginConnectionEvents; @@ -22,6 +23,7 @@ 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.screen.DiscJockeyScreen; +import semmiedev.disc_jockey_revive.gui.screen.LiveDjScreen; import java.io.File; import java.util.ArrayList; @@ -33,11 +35,15 @@ public class Main implements ClientModInitializer { public static final ArrayList TICK_LISTENERS = new ArrayList<>(); public static final Previewer PREVIEWER = new Previewer(); public static final SongPlayer SONG_PLAYER = new SongPlayer(); + public static KeyMappingManager keyMappingManager; public static File songsFolder; public static ModConfig config; public static ConfigHolder configHolder; + private static KeyBinding openLiveDjScreenKeyBind; + + @Override public void onInitializeClient() { configHolder = AutoConfig.register(ModConfig.class, JanksonConfigSerializer::new); @@ -46,10 +52,14 @@ public class Main implements ClientModInitializer { songsFolder = new File(FabricLoader.getInstance().getConfigDir()+File.separator+"disc_jockey"+File.separator+"songs"); if (!songsFolder.isDirectory()) songsFolder.mkdirs(); + keyMappingManager = new KeyMappingManager(); + SongLoader.loadSongs(); KeyBinding openScreenKeyBind = KeyBindingHelper.registerKeyBinding(new KeyBinding(MOD_ID+".key_bind.open_screen", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_J, "key.category."+MOD_ID)); + openLiveDjScreenKeyBind = KeyBindingHelper.registerKeyBinding(new KeyBinding(MOD_ID+".key_bind.open_live_dj_screen", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_UNKNOWN, "key.category."+MOD_ID)); + ClientTickEvents.START_CLIENT_TICK.register(new ClientTickEvents.StartTick() { private ClientWorld prevWorld; @@ -69,11 +79,22 @@ public class Main implements ClientModInitializer { client.setScreen(new DiscJockeyScreen()); } } + + if (openLiveDjScreenKeyBind.wasPressed()) { + if (client.currentScreen == null || client.currentScreen instanceof DiscJockeyScreen) { + client.setScreen(new LiveDjScreen()); + } + } } }); ClientTickEvents.START_WORLD_TICK.register(world -> { - for (ClientTickEvents.StartWorldTick listener : TICK_LISTENERS) listener.onStartTick(world); + ArrayList listenersCopy = new ArrayList<>(TICK_LISTENERS); + for (ClientTickEvents.StartWorldTick listener : listenersCopy) { + if (TICK_LISTENERS.contains(listener)) { + listener.onStartTick(world); + } + } }); ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { @@ -86,5 +107,9 @@ public class Main implements ClientModInitializer { }); HudRenderCallback.EVENT.register(BlocksOverlay::render); + + ClientLifecycleEvents.CLIENT_STOPPING.register(client -> { + keyMappingManager.saveMappings(); + }); } } 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..2f80b89 100644 --- a/src/main/java/semmiedev/disc_jockey_revive/SongPlayer.java +++ b/src/main/java/semmiedev/disc_jockey_revive/SongPlayer.java @@ -33,9 +33,9 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { public boolean running; public Song song; - private int index; - private double tick; // Aka song position - private HashMap> noteBlocks = null; + public int index; + public double tick; + public HashMap> noteBlocks = null; public boolean tuned; private long lastPlaybackTickAt = -1L; @@ -63,7 +63,10 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { private HashMap> notePredictions = new HashMap<>(); public boolean didSongReachEnd = false; public boolean loopSong = false; - private long pausePlaybackUntil = -1L; // Set after tuning, if configured + private long pausePlaybackUntil = -1L; + + private boolean manualTuningRequested = false; + public SongPlayer() { Main.TICK_LISTENERS.add(this); @@ -85,7 +88,7 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { }catch (Exception ex) { ex.printStackTrace(); } - tickPlayback(); + MinecraftClient.getInstance().executeSync(this::tickPlayback); } }); this.playbackThread.start(); @@ -98,7 +101,7 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { STOP_AFTER // 播完就停 } - private PlayMode playMode = PlayMode.STOP_AFTER; + public PlayMode playMode = PlayMode.STOP_AFTER; private boolean isRandomPlaying = false; private int randomIndex = -1; @@ -114,17 +117,16 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { } if (running) stop(); this.song = song; + //Main.LOGGER.info("Song length: " + song.length + " and tempo " + song.tempo); + //Main.TICK_LISTENERS.add(this); this.loopSong = playMode == PlayMode.SINGLE_LOOP; if(this.playbackThread == null) startPlaybackThread(); running = true; + index = 0; + tick = 0; + startTuning(); + lastPlaybackTickAt = System.currentTimeMillis(); - last100MsSpanAt = System.currentTimeMillis(); - last100MsSpanEstimatedPackets = 0; - reducePacketsUntil = -1L; - stopPacketsUntil = -1L; - lastLookSentAt = -1L; - lastSwingSentAt = -1L; - missingInstrumentBlocks.clear(); didSongReachEnd = false; isRandomPlaying = playMode == PlayMode.RANDOM; if (isRandomPlaying) { @@ -150,108 +152,184 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { notePredictions.clear(); tuned = false; tuneInitialUntunedBlocks = -1; + manualTuningRequested = false; + lastPlaybackTickAt = -1L; - last100MsSpanAt = -1L; - last100MsSpanEstimatedPackets = 0; - reducePacketsUntil = -1L; - stopPacketsUntil = -1L; - lastLookSentAt = -1L; - lastSwingSentAt = -1L; + // last100MsSpanAt = -1L; + // last100MsSpanEstimatedPackets = 0; + // reducePacketsUntil = -1L; + // stopPacketsUntil = -1L; + // lastLookSentAt = -1L; + // lastSwingSentAt = -1L; didSongReachEnd = false; // Change after running stop() if actually ended cleanly } + public synchronized void startTuning() { + if (tuned && noteBlocks != null) { + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.retuning").formatted(Formatting.YELLOW)); + } else { + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.tuning_started").formatted(Formatting.YELLOW)); + } + noteBlocks = null; + notePredictions.clear(); + tuned = false; + tuneInitialUntunedBlocks = -1; + manualTuningRequested = true; + } + + public boolean isTuningEnabled() { + return running || manualTuningRequested; + } + + + public boolean playNoteBlock(Note note) { + MinecraftClient client = MinecraftClient.getInstance(); + ClientWorld world = client.world; + ClientPlayerEntity player = client.player; + GameMode gameMode = client.interactionManager == null ? null : client.interactionManager.getCurrentGameMode(); + + if (world == null || player == null || gameMode == null || !gameMode.isSurvivalLike()) { + return false; + } + + if (noteBlocks == null || !tuned) { + return false; + } + + @Nullable BlockPos blockPos = noteBlocks.get(note.instrument()).get(note.note()); + if(blockPos == null) { + return false; + } + + if (!canInteractWith(player, blockPos)) { + return false; + } + + return sendNotePacket(blockPos, note); + } + + + private boolean sendNotePacket(BlockPos blockPos, Note note) { + MinecraftClient client = MinecraftClient.getInstance(); + ClientPlayerEntity player = client.player; + long now = System.currentTimeMillis(); + + // Update packet rate tracking + if(last100MsSpanAt != -1L && now - last100MsSpanAt >= 100) { + last100MsSpanEstimatedPackets = 0; + last100MsSpanAt = now; + }else if (last100MsSpanAt == -1L) { + last100MsSpanAt = now; + last100MsSpanEstimatedPackets = 0; + } + + if (stopPacketsUntil != -1L && stopPacketsUntil >= now) { + return false; + } + if (reducePacketsUntil != -1L && reducePacketsUntil >= now && last100MsSpanEstimatedPackets >= last100MsReducePacketsAfter) { + return false; + } + + if(lastInteractAt != -1L) { + availableInteracts += ((System.currentTimeMillis() - lastInteractAt) / (310.0f / 8.0f)); + availableInteracts = Math.min(8f, Math.max(0f, availableInteracts)); + }else { + availableInteracts = 8f; + lastInteractAt = System.currentTimeMillis(); + } + + if (availableInteracts < 1f) { + return false; + } + + + Vec3d unit = Vec3d.ofCenter(blockPos, 0.5).subtract(player.getEyePos()).normalize(); + boolean packetsSent = false; + + if((lastLookSentAt == -1L || now - lastLookSentAt >= 50) && last100MsSpanEstimatedPackets < last100MsReducePacketsAfter) { + client.getNetworkHandler().sendPacket(new PlayerMoveC2SPacket.LookAndOnGround(MathHelper.wrapDegrees((float) (MathHelper.atan2(unit.z, unit.x) * 57.2957763671875) - 90.0f), MathHelper.wrapDegrees((float) (-(MathHelper.atan2(unit.y, Math.sqrt(unit.x * unit.x + unit.z * unit.z)) * 57.2957763671875))), true, false)); + last100MsSpanEstimatedPackets++; + lastLookSentAt = now; + packetsSent = true; + } else if (last100MsSpanEstimatedPackets >= last100MsReducePacketsAfter) { + reducePacketsUntil = Math.max(reducePacketsUntil, now + 500); + } + + + if(last100MsSpanEstimatedPackets < last100MsStopPacketsAfter) { + client.player.networkHandler.sendPacket(new PlayerActionC2SPacket(PlayerActionC2SPacket.Action.START_DESTROY_BLOCK, blockPos, Direction.UP, 0)); + last100MsSpanEstimatedPackets++; + client.player.networkHandler.sendPacket(new PlayerActionC2SPacket(PlayerActionC2SPacket.Action.ABORT_DESTROY_BLOCK, blockPos, Direction.UP, 0)); + last100MsSpanEstimatedPackets++; + lastInteractAt = now; + availableInteracts -= 1f; + packetsSent = true; + } else { + Main.LOGGER.info("短时间内暂停所有数据包!"); + stopPacketsUntil = Math.max(stopPacketsUntil, now + 250); + reducePacketsUntil = Math.max(reducePacketsUntil, now + 10000); + } + + + if((lastSwingSentAt == -1L || now - lastSwingSentAt >= 50) && last100MsSpanEstimatedPackets < last100MsReducePacketsAfter) { + client.executeSync(() -> client.player.swingHand(Hand.MAIN_HAND)); + lastSwingSentAt = now; + last100MsSpanEstimatedPackets++; + packetsSent = true; + } else if (last100MsSpanEstimatedPackets >= last100MsReducePacketsAfter) { + reducePacketsUntil = Math.max(reducePacketsUntil, now + 500); + } + + return packetsSent; + } + + public synchronized void tickPlayback() { - if (!running) { + if (!running || song == null) { lastPlaybackTickAt = -1L; - last100MsSpanAt = -1L; + // last100MsSpanAt = -1L; return; } - long previousPlaybackTickAt = lastPlaybackTickAt; - lastPlaybackTickAt = System.currentTimeMillis(); - if(last100MsSpanAt != -1L && System.currentTimeMillis() - last100MsSpanAt >= 100) { - last100MsSpanEstimatedPackets = 0; - last100MsSpanAt = System.currentTimeMillis(); - }else if (last100MsSpanAt == -1L) { - last100MsSpanAt = System.currentTimeMillis(); - last100MsSpanEstimatedPackets = 0; - } - if(noteBlocks != null && tuned) { - if(pausePlaybackUntil != -1L && System.currentTimeMillis() <= pausePlaybackUntil) return; - while (running) { - MinecraftClient client = MinecraftClient.getInstance(); - GameMode gameMode = client.interactionManager == null ? null : client.interactionManager.getCurrentGameMode(); - // In the best case, gameMode would only be queried in sync Ticks, no here - if (gameMode == null || !gameMode.isSurvivalLike()) { - client.inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.invalid_game_mode", gameMode == null ? "unknown" : gameMode.getTranslatableName()).formatted(Formatting.RED)); - stop(); - return; - } + if (pausePlaybackUntil != -1L) { + if (System.currentTimeMillis() <= pausePlaybackUntil) { + return; + } else { + tick = 0; + index = 0; + pausePlaybackUntil = -1L; + lastPlaybackTickAt = System.currentTimeMillis(); + } + } + + long currentNow = System.currentTimeMillis(); + long elapsedMs = lastPlaybackTickAt != -1L ? currentNow - lastPlaybackTickAt : (16); + tick += song.millisecondsToTicks(elapsedMs) * speed; + lastPlaybackTickAt = currentNow; + + + if(noteBlocks != null && tuned) { + while (index < song.notes.length) { long note = song.notes[index]; - final long now = System.currentTimeMillis(); - if ((short)note <= Math.round(tick)) { - @Nullable BlockPos blockPos = noteBlocks.get(Note.INSTRUMENTS[(byte)(note >> Note.INSTRUMENT_SHIFT)]).get((byte)(note >> Note.NOTE_SHIFT)); - if(blockPos == null) { - // Instrument got likely mapped to "nothing". Skip it - index++; - continue; - } - if (!canInteractWith(client.player, blockPos)) { - stop(); - client.inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.to_far").formatted(Formatting.RED)); - return; - } - Vec3d unit = Vec3d.ofCenter(blockPos, 0.5).subtract(client.player.getEyePos()).normalize(); - if((lastLookSentAt == -1L || now - lastLookSentAt >= 50) && last100MsSpanEstimatedPackets < last100MsReducePacketsAfter && (reducePacketsUntil == -1L || reducePacketsUntil < now)) { - client.getNetworkHandler().sendPacket(new PlayerMoveC2SPacket.LookAndOnGround(MathHelper.wrapDegrees((float) (MathHelper.atan2(unit.z, unit.x) * 57.2957763671875) - 90.0f), MathHelper.wrapDegrees((float) (-(MathHelper.atan2(unit.y, Math.sqrt(unit.x * unit.x + unit.z * unit.z)) * 57.2957763671875))), true, false)); - last100MsSpanEstimatedPackets++; - lastLookSentAt = now; - }else if(last100MsSpanEstimatedPackets >= last100MsReducePacketsAfter){ - reducePacketsUntil = Math.max(reducePacketsUntil, now + 500); - } - if(last100MsSpanEstimatedPackets < last100MsStopPacketsAfter && (stopPacketsUntil == -1L || stopPacketsUntil < now)) { - // TODO: 5/30/2022 Check if the block needs tuning - //client.interactionManager.attackBlock(blockPos, Direction.UP); - 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!"); - stopPacketsUntil = Math.max(stopPacketsUntil, now + 250); - reducePacketsUntil = Math.max(reducePacketsUntil, now + 10000); - } - if(last100MsSpanEstimatedPackets < last100MsReducePacketsAfter && (reducePacketsUntil == -1L || reducePacketsUntil < now)) { - client.player.networkHandler.sendPacket(new PlayerActionC2SPacket(PlayerActionC2SPacket.Action.ABORT_DESTROY_BLOCK, blockPos, Direction.UP, 0)); - last100MsSpanEstimatedPackets++; - }else if(last100MsSpanEstimatedPackets >= last100MsReducePacketsAfter){ - reducePacketsUntil = Math.max(reducePacketsUntil, now + 500); - } - if((lastSwingSentAt == -1L || now - lastSwingSentAt >= 50) &&last100MsSpanEstimatedPackets < last100MsReducePacketsAfter && (reducePacketsUntil == -1L || reducePacketsUntil < now)) { - client.executeSync(() -> client.player.swingHand(Hand.MAIN_HAND)); - lastSwingSentAt = now; - last100MsSpanEstimatedPackets++; - }else if(last100MsSpanEstimatedPackets >= last100MsReducePacketsAfter){ - reducePacketsUntil = Math.max(reducePacketsUntil, now + 500); - } + + if ((double)(short)note <= tick) { + Note currentNote = new Note(Note.INSTRUMENTS[(byte)(note >> Note.INSTRUMENT_SHIFT)], (byte)(note >> Note.NOTE_SHIFT)); + + playNoteBlock(currentNote); index++; - if (index >= song.notes.length) { - stop(); - didSongReachEnd = true; - if (playMode == PlayMode.SINGLE_LOOP) { - start(song); - } else if (playMode == PlayMode.LIST_LOOP || playMode == PlayMode.RANDOM) { - playNextSong(); - } - break; - } } else { break; } } - if(running) { // Might not be running anymore (prevent small offset on song, even if that is not played anymore) - long elapsedMs = previousPlaybackTickAt != -1L && lastPlaybackTickAt != -1L ? lastPlaybackTickAt - previousPlaybackTickAt : (16); // Assume 16ms if unknown - tick += song.millisecondsToTicks(elapsedMs) * speed; + if (index >= song.notes.length) { + stop(); + didSongReachEnd = true; + if (playMode == PlayMode.SINGLE_LOOP) { + } else if (playMode == PlayMode.LIST_LOOP || playMode == PlayMode.RANDOM) { + playNextSong(); + } } } } @@ -260,7 +338,12 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { if (SongLoader.currentFolder == null || SongLoader.currentFolder.songs.isEmpty()) return; int currentIndex = SongLoader.currentFolder.songs.indexOf(song); - if (currentIndex == -1) return; + if (currentIndex == -1) { + if (!SongLoader.currentFolder.songs.isEmpty()) { + start(SongLoader.currentFolder.songs.get(0)); + } + return; + } if (playMode == PlayMode.RANDOM) { int newIndex; @@ -279,14 +362,30 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { 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. @Override public void onStartTick(ClientWorld world) { MinecraftClient client = MinecraftClient.getInstance(); if(world == null || client.world == null || client.player == null) return; - if(song == null || !running) return; + long now = System.currentTimeMillis(); + if(last100MsSpanAt != -1L && now - last100MsSpanAt >= 100) { + last100MsSpanEstimatedPackets = 0; + last100MsSpanAt = now; + }else if (last100MsSpanAt == -1L) { + last100MsSpanAt = now; + last100MsSpanEstimatedPackets = 0; + } + + if(lastInteractAt != -1L) { + availableInteracts += ((System.currentTimeMillis() - lastInteractAt) / (310.0f / 8.0f)); + availableInteracts = Math.min(8f, Math.max(0f, availableInteracts)); + }else { + availableInteracts = 8f; + lastInteractAt = System.currentTimeMillis(); + } + // Clear outdated note predictions ArrayList outdatedPredictions = new ArrayList<>(); @@ -296,120 +395,157 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { } for(BlockPos outdatedPrediction : outdatedPredictions) notePredictions.remove(outdatedPrediction); - if (noteBlocks == null) { - noteBlocks = new HashMap<>(); - - ClientPlayerEntity player = client.player; - - // Create list of available noteblock positions per used instrument - HashMap> noteblocksForInstrument = new HashMap<>(); - for(NoteBlockInstrument instrument : NoteBlockInstrument.values()) - noteblocksForInstrument.put(instrument, new ArrayList<>()); - final Vec3d playerEyePos = player.getEyePos(); - - final int maxOffset; // Rough estimates, of which blocks could be in reach - if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.v1_20_4_Or_Earlier) { - maxOffset = 7; - }else if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.v1_20_5_Or_Later) { - maxOffset = (int) Math.ceil(player.getBlockInteractionRange() + 1.0 + 1.0); - }else if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.All) { - maxOffset = Math.min(7, (int) Math.ceil(player.getBlockInteractionRange() + 1.0 + 1.0)); - }else { - throw new NotImplementedException("ExpectedServerVersion Value not implemented: " + Main.config.expectedServerVersion.name()); - } - final ArrayList orderedOffsets = new ArrayList<>(); - for(int offset = 0; offset <= maxOffset; offset++) { - orderedOffsets.add(offset); - if(offset != 0) orderedOffsets.add(offset * -1); + if ((noteBlocks == null || !tuned) && (running || manualTuningRequested)) { + ClientPlayerEntity player = client.getInstance().player; + GameMode gameMode = client.interactionManager == null ? null : client.interactionManager.getCurrentGameMode(); + if (player == null || gameMode == null || !gameMode.isSurvivalLike()) { + noteBlocks = null; + notePredictions.clear(); + tuned = false; + tuneInitialUntunedBlocks = -1; + manualTuningRequested = false; + client.inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.invalid_state_tuning").formatted(Formatting.RED)); + return; } - for(NoteBlockInstrument instrument : noteblocksForInstrument.keySet().toArray(new NoteBlockInstrument[0])) { - for (int y : orderedOffsets) { - for (int x : orderedOffsets) { - for (int z : orderedOffsets) { - Vec3d vec3d = playerEyePos.add(x, y, z); - BlockPos blockPos = new BlockPos(MathHelper.floor(vec3d.x), MathHelper.floor(vec3d.y), MathHelper.floor(vec3d.z)); - if (!canInteractWith(player, blockPos)) - continue; - BlockState blockState = world.getBlockState(blockPos); - if (!blockState.isOf(Blocks.NOTE_BLOCK) || !world.isAir(blockPos.up())) - continue; + if (noteBlocks == null) { + noteBlocks = new HashMap<>(); - if (blockState.get(Properties.INSTRUMENT) == instrument) - noteblocksForInstrument.get(instrument).add(blockPos); + HashMap> noteblocksForInstrument = new HashMap<>(); + for(NoteBlockInstrument instrument : NoteBlockInstrument.values()) + noteblocksForInstrument.put(instrument, new ArrayList<>()); + final Vec3d playerEyePos = player.getEyePos(); + + final int maxOffset; + if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.v1_20_4_Or_Earlier) { + maxOffset = 7; + }else if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.v1_20_5_Or_Later) { + maxOffset = (int) Math.ceil(player.getBlockInteractionRange() + 1.0 + 1.0); + }else if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.All) { + maxOffset = Math.min(7, (int) Math.ceil(player.getBlockInteractionRange() + 1.0 + 1.0)); + }else { + throw new NotImplementedException("ExpectedServerVersion Value not implemented: " + Main.config.expectedServerVersion.name()); + } + final ArrayList orderedOffsets = new ArrayList<>(); + for(int offset = 0; offset <= maxOffset; offset++) { + orderedOffsets.add(offset); + if(offset != 0) orderedOffsets.add(offset * -1); + } + + for(NoteBlockInstrument instrument : noteblocksForInstrument.keySet().toArray(new NoteBlockInstrument[0])) { + for (int y : orderedOffsets) { + for (int x : orderedOffsets) { + for (int z : orderedOffsets) { + Vec3d vec3d = playerEyePos.add(x, y, z); + BlockPos blockPos = new BlockPos(MathHelper.floor(vec3d.x), MathHelper.floor(vec3d.y), MathHelper.floor(vec3d.z)); + if (!canInteractWith(player, blockPos)) + continue; + BlockState blockState = world.getBlockState(blockPos); + if (!blockState.isOf(Blocks.NOTE_BLOCK) || !world.isAir(blockPos.up())) + continue; + + if (blockState.get(Properties.INSTRUMENT) == instrument) + noteblocksForInstrument.get(instrument).add(blockPos); + } } } } - } - // Remap instruments for funzies - if(!instrumentMap.isEmpty()) { - HashMap> newNoteblocksForInstrument = new HashMap<>(); - for(NoteBlockInstrument orig : noteblocksForInstrument.keySet()) { - NoteBlockInstrument mappedInstrument = instrumentMap.getOrDefault(orig, orig); - if(mappedInstrument == null) { - // Instrument got likely mapped to "nothing" - newNoteblocksForInstrument.put(orig, null); + if(!instrumentMap.isEmpty()) { + HashMap> newNoteblocksForInstrument = new HashMap<>(); + for(NoteBlockInstrument orig : noteblocksForInstrument.keySet()) { + NoteBlockInstrument mappedInstrument = instrumentMap.getOrDefault(orig, orig); + if(mappedInstrument == null) { + newNoteblocksForInstrument.put(orig, null); + continue; + } + + newNoteblocksForInstrument.put(orig, noteblocksForInstrument.getOrDefault(instrumentMap.getOrDefault(orig, orig), new ArrayList<>())); + } + noteblocksForInstrument = newNoteblocksForInstrument; + } + + ArrayList capturedNotes = new ArrayList<>(); + + ArrayList neededNotes = new ArrayList<>(); + if (song != null) { + neededNotes.addAll(song.uniqueNotes); + } + if (Main.keyMappingManager != null) { + for (Note mappedNote : Main.keyMappingManager.getMappings().values()) { + if (mappedNote != null && !neededNotes.contains(mappedNote)) { + neededNotes.add(mappedNote); + } + } + } + + + for(Note note : neededNotes) { + ArrayList availableBlocks = noteblocksForInstrument.get(note.instrument()); + if(availableBlocks == null) { + getNotes(note.instrument()).put(note.note(), null); continue; } + BlockPos bestBlockPos = null; + int bestBlockTuningSteps = Integer.MAX_VALUE; + for(BlockPos blockPos : availableBlocks) { + boolean alreadyAssigned = false; + if (noteBlocks != null) { + for (HashMap instrumentNotes : noteBlocks.values()) { + if (instrumentNotes != null && instrumentNotes.containsValue(blockPos)) { + alreadyAssigned = true; + break; + } + } + } + if (alreadyAssigned) continue; - newNoteblocksForInstrument.put(orig, noteblocksForInstrument.getOrDefault(instrumentMap.getOrDefault(orig, orig), new ArrayList<>())); - } - noteblocksForInstrument = newNoteblocksForInstrument; - } - // Find fitting noteblocks with the least amount of adjustments required (to reduce tuning time) - ArrayList capturedNotes = new ArrayList<>(); - for(Note note : song.uniqueNotes) { - ArrayList availableBlocks = noteblocksForInstrument.get(note.instrument()); - if(availableBlocks == null) { - // Note was mapped to "nothing". Pretend it got captured, but just ignore it - capturedNotes.add(note); - getNotes(note.instrument()).put(note.note(), null); - continue; - } - BlockPos bestBlockPos = null; - int bestBlockTuningSteps = Integer.MAX_VALUE; - for(BlockPos blockPos : availableBlocks) { - int wantedNote = note.note(); - int currentNote = client.world.getBlockState(blockPos).get(Properties.NOTE); - int tuningSteps = wantedNote >= currentNote ? wantedNote - currentNote : (25 - currentNote) + wantedNote; + int wantedNote = note.note(); + BlockState blockState = world.getBlockState(blockPos); + if (!blockState.contains(Properties.NOTE)) continue; + int currentNote = blockState.get(Properties.NOTE); - if(tuningSteps < bestBlockTuningSteps) { - bestBlockPos = blockPos; - bestBlockTuningSteps = tuningSteps; + int tuningSteps = wantedNote >= currentNote ? wantedNote - currentNote : (25 - currentNote) + wantedNote; + + if(tuningSteps < bestBlockTuningSteps) { + bestBlockPos = blockPos; + bestBlockTuningSteps = tuningSteps; + } + } + + if(bestBlockPos != null) { + capturedNotes.add(note); + availableBlocks.remove(bestBlockPos); + getNotes(note.instrument()).put(note.note(), bestBlockPos); } } - if(bestBlockPos != null) { - capturedNotes.add(note); - availableBlocks.remove(bestBlockPos); - getNotes(note.instrument()).put(note.note(), bestBlockPos); - } // else will be a missing note - } + ArrayList missingNotes = new ArrayList<>(neededNotes); + missingNotes.removeAll(capturedNotes); + if (!missingNotes.isEmpty()) { + ChatHud chatHud = MinecraftClient.getInstance().inGameHud.getChatHud(); + chatHud.addMessage(Text.translatable(Main.MOD_ID+".player.invalid_note_blocks").formatted(Formatting.RED)); - ArrayList missingNotes = new ArrayList<>(song.uniqueNotes); - missingNotes.removeAll(capturedNotes); - if (!missingNotes.isEmpty()) { - ChatHud chatHud = MinecraftClient.getInstance().inGameHud.getChatHud(); - chatHud.addMessage(Text.translatable(Main.MOD_ID+".player.invalid_note_blocks").formatted(Formatting.RED)); + HashMap missing = new HashMap<>(); + for (Note note : missingNotes) { + NoteBlockInstrument mappedInstrument = instrumentMap.getOrDefault(note.instrument(), note.instrument()); + if(mappedInstrument == null) continue; + Block block = Note.INSTRUMENT_BLOCKS.get(mappedInstrument); + Integer got = missing.get(block); + if (got == null) got = 0; + missing.put(block, got + 1); + } - HashMap missing = new HashMap<>(); - for (Note note : missingNotes) { - NoteBlockInstrument mappedInstrument = instrumentMap.getOrDefault(note.instrument(), note.instrument()); - if(mappedInstrument == null) continue; // Ignore if mapped to nothing - Block block = Note.INSTRUMENT_BLOCKS.get(mappedInstrument); - Integer got = missing.get(block); - if (got == null) got = 0; - missing.put(block, got + 1); + missingInstrumentBlocks = missing; + missing.forEach((block, integer) -> chatHud.addMessage(Text.literal(block.getName().getString()+" × "+integer).formatted(Formatting.RED))); + + } else { + missingInstrumentBlocks.clear(); } - missingInstrumentBlocks = missing; - missing.forEach((block, integer) -> chatHud.addMessage(Text.literal(block.getName().getString()+" × "+integer).formatted(Formatting.RED))); - stop(); } - } else if (!tuned) { - //tuned = true; + int ping = 0; { @@ -418,112 +554,119 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { ping = playerListEntry.getLatency(); } - if(lastInteractAt != -1L) { - // Paper allows 8 interacts per 300 ms (actually 9 it turns out, but lets keep it a bit lower anyway) - availableInteracts += ((System.currentTimeMillis() - lastInteractAt) / (310.0f / 8.0f)); - availableInteracts = Math.min(8f, Math.max(0f, availableInteracts)); - }else { - availableInteracts = 8f; - lastInteractAt = System.currentTimeMillis(); - } - int fullyTunedBlocks = 0; HashMap untunedNotes = new HashMap<>(); - for (Note note : song.uniqueNotes) { - if(noteBlocks == null || noteBlocks.get(note.instrument()) == null) - continue; - BlockPos blockPos = noteBlocks.get(note.instrument()).get(note.note()); - if(blockPos == null) continue; - BlockState blockState = world.getBlockState(blockPos); - int assumedNote = notePredictions.containsKey(blockPos) ? notePredictions.get(blockPos).getLeft() : blockState.get(Properties.NOTE); - if (blockState.contains(Properties.NOTE)) { - if(assumedNote == note.note() && blockState.get(Properties.NOTE) == note.note()) - fullyTunedBlocks++; - if (assumedNote != note.note()) { - if (!canInteractWith(client.player, blockPos)) { - stop(); - client.inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.to_far").formatted(Formatting.RED)); + if (noteBlocks != null) { + for (HashMap instrumentNotes : noteBlocks.values()) { + if (instrumentNotes == null) continue; + for (Map.Entry entry : instrumentNotes.entrySet()) { + BlockPos blockPos = entry.getValue(); + Byte wantedNote = entry.getKey(); + + if (blockPos == null) continue; + + BlockState blockState = world.getBlockState(blockPos); + if (!blockState.contains(Properties.NOTE)) { + Main.LOGGER.warn("注意 {} 处音符盒在调整过程中改变了状态", blockPos); + noteBlocks = null; + tuned = false; + manualTuningRequested = false; return; } - untunedNotes.put(blockPos, blockState.get(Properties.NOTE)); - } - } else { - noteBlocks = null; - break; - } - } - if(tuneInitialUntunedBlocks == -1 || tuneInitialUntunedBlocks < untunedNotes.size()) - tuneInitialUntunedBlocks = untunedNotes.size(); + int assumedNote = notePredictions.containsKey(blockPos) ? notePredictions.get(blockPos).getLeft() : blockState.get(Properties.NOTE); // blockState.get(Properties.NOTE) returns Integer - int existingUniqueNotesCount = 0; - for(Note n : song.uniqueNotes) { - if(noteBlocks.get(n.instrument()).get(n.note()) != null) - existingUniqueNotesCount++; - } + byte wantedNotePrimitive = wantedNote.byteValue(); - if(untunedNotes.isEmpty() && fullyTunedBlocks == existingUniqueNotesCount) { - // Wait roundrip + 100ms before considering tuned after changing notes (in case the server rejects an interact) - if(lastInteractAt == -1 || System.currentTimeMillis() - lastInteractAt >= ping * 2 + 100) { - tuned = true; - pausePlaybackUntil = System.currentTimeMillis() + (long) (Math.abs(Main.config.delayPlaybackStartBySecs) * 1000); - tuneInitialUntunedBlocks = -1; - // Tuning finished - } - } - - BlockPos lastBlockPos = null; - int lastTunedNote = Integer.MIN_VALUE; - float roughTuneProgress = 1 - (untunedNotes.size() / Math.max(tuneInitialUntunedBlocks + 0f, 1f)); - while(availableInteracts >= 1f && untunedNotes.size() > 0) { - BlockPos blockPos = null; - int searches = 0; - while(blockPos == null) { - searches++; - // Find higher note - for (Map.Entry entry : untunedNotes.entrySet()) { - if (entry.getValue() > lastTunedNote) { - blockPos = entry.getKey(); - break; + if(assumedNote == wantedNotePrimitive && blockState.get(Properties.NOTE).intValue() == wantedNotePrimitive) { + fullyTunedBlocks++; + } else if (assumedNote != wantedNotePrimitive) { + untunedNotes.put(blockPos, blockState.get(Properties.NOTE).intValue()); } } - // Find higher note or equal - if (blockPos == null) { - for (Map.Entry entry : untunedNotes.entrySet()) { - if (entry.getValue() >= lastTunedNote) { - blockPos = entry.getKey(); - break; + } + } + + + int existingUniqueNotesCount = 0; + if (noteBlocks != null) { + for(HashMap instrumentNotes : noteBlocks.values()) { + if (instrumentNotes != null) { + for (BlockPos pos : instrumentNotes.values()) { + if (pos != null) { + existingUniqueNotesCount++; } } } - // Not found. Reset last note - if(blockPos == null) - lastTunedNote = Integer.MIN_VALUE; - if(blockPos == null && searches > 1) { - // Something went wrong. Take any note (one should at least exist here) - blockPos = untunedNotes.keySet().toArray(new BlockPos[0])[0]; - break; - } } - if(blockPos == null) return; // Something went very, very wrong! + } - lastTunedNote = untunedNotes.get(blockPos); - untunedNotes.remove(blockPos); - int assumedNote = notePredictions.containsKey(blockPos) ? notePredictions.get(blockPos).getLeft() : client.world.getBlockState(blockPos).get(Properties.NOTE); - notePredictions.put(blockPos, new Pair<>((assumedNote + 1) % 25, System.currentTimeMillis() + ping * 2 + 100)); - client.interactionManager.interactBlock(client.player, Hand.MAIN_HAND, new BlockHitResult(Vec3d.of(blockPos), Direction.UP, blockPos, false)); - lastInteractAt = System.currentTimeMillis(); - availableInteracts -= 1f; - lastBlockPos = blockPos; + + if(untunedNotes.isEmpty() && fullyTunedBlocks == existingUniqueNotesCount) { + if(lastInteractAt == -1 || System.currentTimeMillis() - lastInteractAt >= ping * 2 + 100) { + tuned = true; + if (running && tick == 0 && index == 0) { + pausePlaybackUntil = System.currentTimeMillis() + (long) (Math.abs(Main.config.delayPlaybackStartBySecs) * 1000); + } else { + pausePlaybackUntil = -1L; + } + tuneInitialUntunedBlocks = -1; + manualTuningRequested = false; + client.inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.tuned").formatted(Formatting.GREEN)); + } + } else { + tuned = false; + if(tuneInitialUntunedBlocks == -1 || tuneInitialUntunedBlocks < untunedNotes.size()) + tuneInitialUntunedBlocks = untunedNotes.size(); + if (availableInteracts >= 1f && !untunedNotes.isEmpty()) { + BlockPos blockPosToTune = untunedNotes.keySet().iterator().next(); + int currentNote = untunedNotes.get(blockPosToTune); + Byte targetNote = null; + if (noteBlocks != null) { + for (HashMap instrumentNotes : noteBlocks.values()) { + if (instrumentNotes != null) { + for (Map.Entry entry : instrumentNotes.entrySet()) { + if (blockPosToTune.equals(entry.getValue())) { + targetNote = entry.getKey(); + break; + } + } + } + if (targetNote != null) break; + } + } + + if (targetNote != null) { + byte targetNotePrimitive = targetNote.byteValue(); + int tuningSteps = targetNotePrimitive >= currentNote ? targetNotePrimitive - currentNote : (25 - currentNote) + targetNotePrimitive; + + if (tuningSteps > 0) { + int predictedNote = (currentNote + 1) % 25; + notePredictions.put(blockPosToTune, new Pair<>(predictedNote, System.currentTimeMillis() + ping * 2 + 100)); + client.interactionManager.interactBlock(client.player, Hand.MAIN_HAND, new BlockHitResult(Vec3d.of(blockPosToTune), Direction.UP, blockPosToTune, false)); + lastInteractAt = System.currentTimeMillis(); + availableInteracts -= 1f; + + client.player.swingHand(Hand.MAIN_HAND); + + int totalUntuned = untunedNotes.size(); + int tunedCount = existingUniqueNotesCount - totalUntuned; + + } else { + + untunedNotes.remove(blockPosToTune); + } + } else { + + untunedNotes.remove(blockPosToTune); // Remove to avoid infinite loop + } + } else if (!untunedNotes.isEmpty()) { + + } } - if(lastBlockPos != null) { - // Turn head into spinning with time and lookup up further the further tuning is progressed - //client.getNetworkHandler().sendPacket(new PlayerMoveC2SPacket.LookAndOnGround(((float) (System.currentTimeMillis() % 2000)) * (360f/2000f), (1 - roughTuneProgress) * 180 - 90, true)); - client.player.swingHand(Hand.MAIN_HAND); - } - }else if((playbackThread == null || !playbackThread.isAlive()) && running && Main.config.disableAsyncPlayback) { - // Sync playback (off by default). Replacement for playback thread + } + if((playbackThread == null || !playbackThread.isAlive()) && running && Main.config.disableAsyncPlayback) { try { tickPlayback(); }catch (Exception ex) { @@ -534,13 +677,16 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { } private HashMap getNotes(NoteBlockInstrument instrument) { + if (noteBlocks == null) { + noteBlocks = new HashMap<>(); + } return noteBlocks.computeIfAbsent(instrument, k -> new HashMap<>()); } // Before 1.20.5, the server limits interacts to 6 Blocks from Player Eye to Block Center // With 1.20.5 and later, the server does a more complex check, to the closest point of a full block hitbox // (max distance is BlockInteractRange + 1.0). - private boolean canInteractWith(ClientPlayerEntity player, BlockPos blockPos) { + public boolean canInteractWith(ClientPlayerEntity player, BlockPos blockPos) { final Vec3d eyePos = player.getEyePos(); if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.v1_20_4_Or_Earlier) { return eyePos.squaredDistanceTo(blockPos.toCenterPos()) <= 6.0 * 6.0; diff --git a/src/main/java/semmiedev/disc_jockey_revive/gui/KeyMappingListWidget.java b/src/main/java/semmiedev/disc_jockey_revive/gui/KeyMappingListWidget.java new file mode 100644 index 0000000..1323062 --- /dev/null +++ b/src/main/java/semmiedev/disc_jockey_revive/gui/KeyMappingListWidget.java @@ -0,0 +1,136 @@ +package semmiedev.disc_jockey_revive.gui; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.EntryListWidget; +import net.minecraft.client.util.InputUtil; +import net.minecraft.text.Text; +import semmiedev.disc_jockey_revive.KeyMappingManager; +import semmiedev.disc_jockey_revive.Main; +import semmiedev.disc_jockey_revive.Note; +import semmiedev.disc_jockey_revive.gui.screen.EditKeyMappingsScreen; + +import java.util.Comparator; +import java.util.Map; +import java.util.Objects; + +public class KeyMappingListWidget extends EntryListWidget { + + private final EditKeyMappingsScreen parentScreen; + private boolean buttonsActive = true; + + public KeyMappingListWidget(MinecraftClient client, int width, int height, int top, int itemHeight, EditKeyMappingsScreen parentScreen) { + super(client, width, height, top, itemHeight); + this.parentScreen = parentScreen; + this.centerListVertically = false; + } + public void setMappings(Map mappings) { + this.clearEntries(); + for (Map.Entry entry : mappings.entrySet()) { + + this.addEntry(new KeyMappingEntry(entry.getKey(), entry.getValue(), this.parentScreen)); + } + + this.children().sort(Comparator.comparing(entry -> entry.getKey().getTranslationKey())); + + setButtonsActive(this.buttonsActive); + } + public void setButtonsActive(boolean active) { + this.buttonsActive = active; + + for (KeyMappingEntry entry : children()) { + entry.setButtonsActive(active); + } + } + @Override + public int getRowWidth() { + return this.width - 40; + } + + @Override + protected int getScrollbarX() { + return this.width - 10; + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + } + + public class KeyMappingEntry extends EntryListWidget.Entry { + private final InputUtil.Key key; + private final Note note; + private final EditKeyMappingsScreen screen; + + private ButtonWidget changeButton; + private ButtonWidget removeButton; + + public KeyMappingEntry(InputUtil.Key key, Note note, EditKeyMappingsScreen screen) { + this.key = key; + this.note = note; + this.screen = screen; + } + + public InputUtil.Key getKey() { + return key; + } + + public Note getNote() { + return note; + } + + public void setButtonsActive(boolean active) { + if (changeButton != null) changeButton.active = active; + if (removeButton != null) removeButton.active = active; + } + @Override + public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + MinecraftClient client = MinecraftClient.getInstance(); + int textY = y + (entryHeight - client.textRenderer.fontHeight) / 2; + Text keyText = Text.translatable(key.getTranslationKey()); + context.drawTextWithShadow(client.textRenderer, keyText, x + 5, textY, 0xFFFFFF); + String noteDisplayName = KeyMappingManager.getNoteDisplayName(note); + context.drawTextWithShadow(client.textRenderer, noteDisplayName, x + 100, textY, 0xAAAAAA); + int buttonWidth = 50; + int buttonHeight = 18; + int buttonY = y + (entryHeight - buttonHeight) / 2; + int buttonX = x + entryWidth - buttonWidth - 5; + changeButton = ButtonWidget.builder(Text.translatable(Main.MOD_ID + ".screen.edit_mappings.change"), button -> { + screen.startWaitingForKeyPress(this); + }).dimensions(buttonX - buttonWidth - 5, buttonY, buttonWidth, buttonHeight).build(); + changeButton.render(context, mouseX, mouseY, tickDelta); + changeButton.active = buttonsActive; + removeButton = ButtonWidget.builder(Text.translatable(Main.MOD_ID + ".screen.edit_mappings.remove"), button -> { + screen.removeMapping(key); + }).dimensions(buttonX, buttonY, buttonWidth, buttonHeight).build(); + removeButton.render(context, mouseX, mouseY, tickDelta); + removeButton.active = buttonsActive; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + + if (changeButton.mouseClicked(mouseX, mouseY, button)) { + return true; + } + if (removeButton.mouseClicked(mouseX, mouseY, button)) { + return true; + } + return false; + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return super.isMouseOver(mouseX, mouseY) || + (changeButton != null && changeButton.isMouseOver(mouseX, mouseY)) || + (removeButton != null && removeButton.isMouseOver(mouseX, mouseY)); + } + public void appendClickableNarrations(NarrationMessageBuilder builder) { + builder.put(NarrationPart.TITLE, Text.translatable(key.getTranslationKey()).append(" -> ").append(KeyMappingManager.getNoteDisplayName(note))); + if (changeButton != null) changeButton.appendNarrations(builder); + if (removeButton != null) removeButton.appendNarrations(builder); + } + } +} 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..eca3e73 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 @@ -9,12 +9,14 @@ import net.minecraft.item.ItemStack; import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.util.Formatting; +import org.lwjgl.glfw.GLFW; 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.SongListWidget; import semmiedev.disc_jockey_revive.gui.hud.BlocksOverlay; +import semmiedev.disc_jockey_revive.gui.widget.ProgressBarWidget; import java.io.File; import java.io.IOException; @@ -26,6 +28,7 @@ import java.util.stream.Collectors; import semmiedev.disc_jockey_revive.SongLoader.SongFolder; import semmiedev.disc_jockey_revive.SongPlayer.PlayMode; +import org.jetbrains.annotations.Nullable; public class DiscJockeyScreen extends Screen { private static final MutableText @@ -35,12 +38,14 @@ public class DiscJockeyScreen extends Screen { PREVIEW = Text.translatable(Main.MOD_ID+".screen.preview"), PREVIEW_STOP = Text.translatable(Main.MOD_ID+".screen.preview.stop"), DROP_HINT = Text.translatable(Main.MOD_ID+".screen.drop_hint").formatted(Formatting.GRAY) - ; + ; private SongListWidget songListWidget; - private ButtonWidget playButton, previewButton; + private ButtonWidget playButton, previewButton, prevButton, nextButton; public boolean shouldFilter; private String query = ""; + private ProgressBarWidget progressBar; + private TextFieldWidget searchBar; private static final MutableText FOLDER_UP = Text.literal("↑"), @@ -66,39 +71,137 @@ public class DiscJockeyScreen extends Screen { @Override protected void init() { shouldFilter = true; - songListWidget = new SongListWidget(client, width, height - 64 - 32, 32, 20, this); - // 恢复播放模式 currentPlayMode = Main.config.playMode; - // 恢复文件夹状态 if (!Main.config.currentFolderPath.isEmpty()) { currentFolder = findFolderByPath(Main.config.currentFolderPath); SongLoader.currentFolder = currentFolder; } - addDrawableChild(songListWidget); - for (int i = 0; i < SongLoader.SONGS.size(); i++) { - Song song = SongLoader.SONGS.get(i); - song.entry.songListWidget = songListWidget; - if (song.entry.selected) songListWidget.setSelected(song.entry); - // 添加文件夹条目 - if (song.folder != null && !songListWidget.children().contains(song.folder.entry)) { - song.folder.entry = new SongListWidget.FolderEntry(song.folder, songListWidget); - songListWidget.children().add(song.folder.entry); + int listTop = 40; + int listBottom = height - 100; + int listHeight = listBottom - listTop; + songListWidget = new SongListWidget(client, width, height - 100, 40, 20, this); + addDrawableChild(songListWidget); + + + int playbackButtonHeight = 20; + int playbackButtonY = listBottom +50; + int centerX = width / 2; + + // 上一首 + prevButton = ButtonWidget.builder(Text.literal("⏮"), button -> playPreviousSong()) + .dimensions(centerX - 80, playbackButtonY, 40, playbackButtonHeight).build(); + addDrawableChild(prevButton); + + // 播放 + playButton = ButtonWidget.builder(PLAY, button -> { + if (Main.SONG_PLAYER.running) { + Main.SONG_PLAYER.stop(); + } else { + SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull(); + if (entry != null) { + Main.SONG_PLAYER.start(entry.song); + } } - } + }).dimensions(centerX - 30, playbackButtonY, 60, playbackButtonHeight).build(); + addDrawableChild(playButton); + + // 下一首 + nextButton = ButtonWidget.builder(Text.literal("⏭"), button -> playNextSong()) + .dimensions(centerX + 40, playbackButtonY, 40, playbackButtonHeight).build(); + addDrawableChild(nextButton); + + // 预览 + previewButton = ButtonWidget.builder(PREVIEW, button -> { + if (Main.PREVIEWER.running) { + Main.PREVIEWER.stop(); + } else { + SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull(); + if (entry != null) Main.PREVIEWER.start(entry.song); + } + }).dimensions(centerX + 90, playbackButtonY, 60, playbackButtonHeight).build(); + addDrawableChild(previewButton); + + // 进度条 + int progressBarHeight = 10; + int progressBarY = playbackButtonY + playbackButtonHeight + 5; + progressBar = new ProgressBarWidget(centerX - 100, progressBarY, 200, progressBarHeight, + Text.translatable(Main.MOD_ID + ".progress"), 0, 100) { + @Override + protected void onProgressChanged(double progress) { + if (Main.SONG_PLAYER.running && Main.SONG_PLAYER.song != null) { + double targetTick = progress * Main.SONG_PLAYER.song.length / 100.0; + Main.SONG_PLAYER.tick = targetTick; + updateNoteIndexFromTick(); + } + } + }; + addDrawableChild(progressBar); + + // 搜索框 + int searchBarWidth = 150; + int searchBarHeight = 20; + int searchBarX = 10; + int searchBarY = height - searchBarHeight - 10; + searchBar = new TextFieldWidget(textRenderer, searchBarX, searchBarY, searchBarWidth, searchBarHeight, + Text.translatable(Main.MOD_ID+".screen.search")); + searchBar.setChangedListener(query -> { + query = query.toLowerCase().replaceAll("\\s", ""); + if (this.query.equals(query)) return; + this.query = query; + shouldFilter = true; + }); + addDrawableChild(searchBar); + int otherButtonWidth = 100; + int otherButtonHeight = 20; + int otherButtonMargin = 10; + + // Blocks + int blocksButtonX = width - otherButtonWidth - otherButtonMargin; + int blocksButtonY = height - otherButtonHeight - otherButtonMargin - otherButtonHeight - otherButtonMargin; + addDrawableChild(ButtonWidget.builder(Text.translatable(Main.MOD_ID+".screen.blocks"), button -> { + if (BlocksOverlay.itemStacks == null) { + SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull(); + if (entry != null) { + client.setScreen(null); + updateBlocksOverlay(entry.song); + } + } else { + BlocksOverlay.itemStacks = null; + client.setScreen(null); + } + }).dimensions(blocksButtonX, blocksButtonY, otherButtonWidth, otherButtonHeight).build()); + + // 打开文件夹 + int openFolderButtonX = width - otherButtonWidth - otherButtonMargin; + int openFolderButtonY = height - otherButtonHeight - otherButtonMargin - otherButtonHeight - otherButtonMargin + otherButtonHeight + otherButtonMargin; // 在 Blocks 按钮下方一行 + addDrawableChild(ButtonWidget.builder(OPEN_FOLDER, button -> openSongsFolder()) + .dimensions(openFolderButtonX, openFolderButtonY, otherButtonWidth, otherButtonHeight).build()); + + // 重新加载 + int reloadButtonX = width - otherButtonWidth - otherButtonMargin - otherButtonWidth - otherButtonMargin; + int reloadButtonY = openFolderButtonY; + addDrawableChild(ButtonWidget.builder(RELOAD, button -> { + SongLoader.loadSongs(); + client.setScreen(null); + }).dimensions(reloadButtonX, reloadButtonY, otherButtonWidth, otherButtonHeight).build()); + + folderUpButton = ButtonWidget.builder(FOLDER_UP, button -> { if (currentFolder != null) { - currentFolder = null; - SongLoader.currentFolder = null; + SongFolder parent = findParentFolder(currentFolder); + currentFolder = parent; + SongLoader.currentFolder = parent; shouldFilter = true; } }).dimensions(10, 10, 20, 20).build(); addDrawableChild(folderUpButton); + playModeButton = ButtonWidget.builder(getPlayModeText(), button -> { switch (currentPlayMode) { case SINGLE_LOOP -> currentPlayMode = PlayMode.LIST_LOOP; @@ -111,131 +214,200 @@ public class DiscJockeyScreen extends Screen { }).dimensions(width - 120, 10, 100, 20).build(); addDrawableChild(playModeButton); - playButton = ButtonWidget.builder(PLAY, button -> { - if (Main.SONG_PLAYER.running) { - Main.SONG_PLAYER.stop(); - } else { - SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull(); - if (entry != null) { - Main.SONG_PLAYER.start(entry.song); - client.setScreen(null); - } - } - }).dimensions(width / 2 - 160, height - 61, 100, 20).build(); - addDrawableChild(playButton); + for (int i = 0; i < SongLoader.SONGS.size(); i++) { + Song song = SongLoader.SONGS.get(i); + song.entry.songListWidget = songListWidget; + if (song.entry.selected) songListWidget.setSelected(song.entry); - previewButton = ButtonWidget.builder(PREVIEW, button -> { - if (Main.PREVIEWER.running) { - Main.PREVIEWER.stop(); - } else { - SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull(); - if (entry != null) Main.PREVIEWER.start(entry.song); - } - }).dimensions(width / 2 - 50, height - 61, 100, 20).build(); - addDrawableChild(previewButton); - - addDrawableChild(ButtonWidget.builder(Text.translatable(Main.MOD_ID+".screen.blocks"), button -> { - // TODO: 6/2/2022 Add an auto build mode - if (BlocksOverlay.itemStacks == null) { - SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull(); - if (entry != null) { - client.setScreen(null); - - BlocksOverlay.itemStacks = new ItemStack[0]; - BlocksOverlay.amounts = new int[0]; - BlocksOverlay.amountOfNoteBlocks = entry.song.uniqueNotes.size(); - - for (Note note : entry.song.uniqueNotes) { - ItemStack itemStack = Note.INSTRUMENT_BLOCKS.get(note.instrument()).asItem().getDefaultStack(); - int index = -1; - - for (int i = 0; i < BlocksOverlay.itemStacks.length; i++) { - if (BlocksOverlay.itemStacks[i].getItem() == itemStack.getItem()) { - index = i; - break; - } - } - - if (index == -1) { - BlocksOverlay.itemStacks = Arrays.copyOf(BlocksOverlay.itemStacks, BlocksOverlay.itemStacks.length + 1); - BlocksOverlay.amounts = Arrays.copyOf(BlocksOverlay.amounts, BlocksOverlay.amounts.length + 1); - - BlocksOverlay.itemStacks[BlocksOverlay.itemStacks.length - 1] = itemStack; - BlocksOverlay.amounts[BlocksOverlay.amounts.length - 1] = 1; - } else { - BlocksOverlay.amounts[index] = BlocksOverlay.amounts[index] + 1; - } + if (song.folder != null) { + boolean folderEntryExists = SongLoader.FOLDERS.stream() + .anyMatch(f -> f == song.folder || findFolderByPathInSubfolders(f, song.folder.path) != null); + if (!folderEntryExists) { + } else { + if (song.folder.entry == null) { + song.folder.entry = new SongListWidget.FolderEntry(song.folder, songListWidget); } } - } else { - BlocksOverlay.itemStacks = null; - client.setScreen(null); } - }).dimensions(width / 2 + 60, height - 61, 100, 20).build()); - - // 打开文件夹 - addDrawableChild(ButtonWidget.builder(OPEN_FOLDER, button -> { - try { - String folderPath = currentFolder != null ? - currentFolder.path : - Main.songsFolder.getAbsolutePath(); // 使用绝对路径 - - File target = new File(folderPath); - if (!target.exists()) { - client.inGameHud.getChatHud().addMessage( - Text.translatable(Main.MOD_ID+".screen.folder_not_exist", folderPath) - .formatted(Formatting.RED)); - return; - } - - // Windows 专用命令,其他的不会 - if (System.getProperty("os.name").toLowerCase().contains("win")) { - new ProcessBuilder("explorer.exe", "/select,", target.getAbsolutePath()).start(); - } else { - java.awt.Desktop.getDesktop().open(target); - } - } catch (Exception e) { - Main.LOGGER.error("打开文件夹失败", e); - client.inGameHud.getChatHud().addMessage( - Text.translatable(Main.MOD_ID+".screen.open_folder_failed") - .formatted(Formatting.RED)); - } - }).dimensions(width / 2 - 160, height - 31, 100, 20).build()); - - - // 重新加载 - addDrawableChild(ButtonWidget.builder(RELOAD, button -> { - SongLoader.loadSongs(); - client.setScreen(null); - }).dimensions(width / 2 + 60, height - 31, 100, 20).build()); - - TextFieldWidget searchBar = new TextFieldWidget(textRenderer, width / 2 - 50, 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; - this.query = query; - shouldFilter = true; - }); - addDrawableChild(searchBar); - - // TODO: 6/2/2022 Add a reload button + } } + // 播放进度 + private void updateNoteIndexFromTick() { + if (Main.SONG_PLAYER.song == null) return; + + double targetTick = Main.SONG_PLAYER.tick; + int newIndex = 0; + for (int i = 0; i < Main.SONG_PLAYER.song.notes.length; i++) { + long note = Main.SONG_PLAYER.song.notes[i]; + if ((short)note > targetTick) { + newIndex = i; + break; + } + newIndex = i; + } + Main.SONG_PLAYER.index = newIndex; + } + + private void updateBlocksOverlay(Song song) { + BlocksOverlay.itemStacks = new ItemStack[0]; + BlocksOverlay.amounts = new int[0]; + BlocksOverlay.amountOfNoteBlocks = song.uniqueNotes.size(); + + for (Note note : song.uniqueNotes) { + ItemStack itemStack = Note.INSTRUMENT_BLOCKS.get(note.instrument()).asItem().getDefaultStack(); + int index = -1; + + for (int i = 0; i < BlocksOverlay.itemStacks.length; i++) { + if (BlocksOverlay.itemStacks[i].getItem() == itemStack.getItem()) { + index = i; + break; + } + } + + if (index == -1) { + BlocksOverlay.itemStacks = Arrays.copyOf(BlocksOverlay.itemStacks, BlocksOverlay.itemStacks.length + 1); + BlocksOverlay.amounts = Arrays.copyOf(BlocksOverlay.amounts, BlocksOverlay.amounts.length + 1); + + BlocksOverlay.itemStacks[BlocksOverlay.itemStacks.length - 1] = itemStack; + BlocksOverlay.amounts[BlocksOverlay.amounts.length - 1] = 1; + } else { + BlocksOverlay.amounts[index] = BlocksOverlay.amounts[index] + 1; + } + } + } + + // 打开文件夹 + private void openSongsFolder() { + try { + String folderPath = currentFolder != null ? + currentFolder.path : Main.songsFolder.getAbsolutePath(); + + File target = new File(folderPath); + if (!target.exists()) { + client.inGameHud.getChatHud().addMessage( + Text.translatable(Main.MOD_ID+".screen.folder_not_exist", folderPath) + .formatted(Formatting.RED)); + return; + } + + if (System.getProperty("os.name").toLowerCase().contains("win")) { + new ProcessBuilder("explorer.exe", "/select,", target.getAbsolutePath()).start(); + } else { + java.awt.Desktop.getDesktop().open(target); + } + } catch (Exception e) { + Main.LOGGER.error("打开文件夹失败", e); + client.inGameHud.getChatHud().addMessage( + Text.translatable(Main.MOD_ID+".screen.open_folder_failed") + .formatted(Formatting.RED)); + } + } + + // 预览 + private void playPreviousSong() { + if (SongLoader.currentFolder == null || SongLoader.currentFolder.songs.isEmpty()) return; + + int currentIndex = SongLoader.currentFolder.songs.indexOf(Main.SONG_PLAYER.song); + if (currentIndex == -1) { + if (!SongLoader.currentFolder.songs.isEmpty()) { + startSong(SongLoader.currentFolder.songs.get(0)); + } + return; + } + + int prevIndex = (currentIndex - 1 + SongLoader.currentFolder.songs.size()) % SongLoader.currentFolder.songs.size(); + startSong(SongLoader.currentFolder.songs.get(prevIndex)); + } + + // 下一首 + private void playNextSong() { + if (SongLoader.currentFolder == null || SongLoader.currentFolder.songs.isEmpty()) return; + + if (Main.SONG_PLAYER.playMode == PlayMode.RANDOM) { + int newIndex; + int currentIndex = SongLoader.currentFolder.songs.indexOf(Main.SONG_PLAYER.song); + do { + newIndex = (int)(Math.random() * SongLoader.currentFolder.songs.size()); + } while (newIndex == currentIndex && + SongLoader.currentFolder.songs.size() > 1); + startSong(SongLoader.currentFolder.songs.get(newIndex)); + } else { + int currentIndex = SongLoader.currentFolder.songs.indexOf(Main.SONG_PLAYER.song); + if (currentIndex == -1) { + if (!SongLoader.currentFolder.songs.isEmpty()) { + startSong(SongLoader.currentFolder.songs.get(0)); + } + return; + } + + int nextIndex = (currentIndex + 1) % SongLoader.currentFolder.songs.size(); + startSong(SongLoader.currentFolder.songs.get(nextIndex)); + } + } + + private void startSong(Song song) { + SongListWidget.SongEntry entry = findEntryForSong(song); + if (entry != null) { + songListWidget.setSelected(entry); + Main.SONG_PLAYER.start(song); + } + } + + private SongListWidget.SongEntry findEntryForSong(Song song) { + for (SongListWidget.Entry entry : songListWidget.children()) { + if (entry instanceof SongListWidget.SongEntry songEntry && songEntry.song == song) { + return songEntry; + } + } + return null; + } + + @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { + renderBackground(context, mouseX, mouseY, delta); super.render(context, mouseX, mouseY, delta); + // 标题 context.drawCenteredTextWithShadow(textRenderer, DROP_HINT, width / 2, 5, 0xFFFFFF); context.drawCenteredTextWithShadow(textRenderer, SELECT_SONG, width / 2, 20, 0xFFFFFF); - // 显示当前文件夹和播放模式 + // 文件夹 播放模式 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); + + // 进度条 + if (Main.SONG_PLAYER.running && Main.SONG_PLAYER.song != null) { + double progress = (Main.SONG_PLAYER.tick / Main.SONG_PLAYER.song.length) * 100; + if (!progressBar.isDragging()) { + progressBar.setProgress(progress); + } + String timeText = formatTime(Main.SONG_PLAYER.getSongElapsedSeconds()) + " / " + + formatTime(Main.SONG_PLAYER.song.getLengthInSeconds()); + int timeTextY = progressBar.getY() + progressBar.getHeight() + 2; + context.drawTextWithShadow(textRenderer, timeText, width / 2 - textRenderer.getWidth(timeText) / 2, timeTextY, 0xFFFFFF); + } else { + progressBar.setProgress(0); + String timeText = formatTime(0) + " / " + formatTime(0); + int timeTextY = progressBar.getY() + progressBar.getHeight() + 2; + context.drawTextWithShadow(textRenderer, timeText, width / 2 - textRenderer.getWidth(timeText) / 2, timeTextY, 0xFFFFFF); + } + } + + // 时间格式hua + private String formatTime(double seconds) { + int totalSeconds = (int) seconds; + int minutes = totalSeconds / 60; + int secs = totalSeconds % 60; + return String.format("%02d:%02d", minutes, secs); } @Override public void tick() { + super.tick(); + previewButton.setMessage(Main.PREVIEWER.running ? PREVIEW_STOP : PREVIEW); playButton.setMessage(Main.SONG_PLAYER.running ? PLAY_STOP : PLAY); @@ -245,7 +417,7 @@ public class DiscJockeyScreen extends Screen { boolean empty = query.isEmpty(); boolean isInSongsOrSubfolder = currentFolder == null || - currentFolder.path.startsWith(Main.songsFolder.getPath()); + (Main.songsFolder != null && currentFolder.path.startsWith(Main.songsFolder.getPath())); if (currentFolder == null) { for (SongFolder folder : SongLoader.FOLDERS) { @@ -257,12 +429,10 @@ public class DiscJockeyScreen extends Screen { } } } else { - // 返回上级 SongListWidget.FolderEntry parentEntry = new SongListWidget.FolderEntry(null, songListWidget); parentEntry.displayName = ".."; songListWidget.children().add(parentEntry); - // 子文件夹 for (SongFolder subFolder : currentFolder.subFolders) { if (empty || subFolder.name.toLowerCase().contains(query)) { if (subFolder.entry == null) { @@ -273,9 +443,7 @@ public class DiscJockeyScreen extends Screen { } } - // 只有在songs目录或其子目录中才显示歌曲(原作者的💩跑我这了) if (isInSongsOrSubfolder) { - // 歌曲条目 List songsToShow = currentFolder == null ? SongLoader.SONGS.stream() .filter(song -> song.folder == null) @@ -284,14 +452,14 @@ public class DiscJockeyScreen extends Screen { .filter(song -> song.folder == currentFolder) .collect(Collectors.toList()); - // 已收藏歌曲 + // 已收藏 for (Song song : songsToShow) { if (song.entry.favorite && (empty || song.searchableFileName.contains(query) || song.searchableName.contains(query))) { songListWidget.children().add(song.entry); } } - // 未收藏歌曲 + // 未收藏 for (Song song : songsToShow) { if (!song.entry.favorite && (empty || song.searchableFileName.contains(query) || song.searchableName.contains(query))) { songListWidget.children().add(song.entry); @@ -301,40 +469,7 @@ public class DiscJockeyScreen extends Screen { } } - - - public SongFolder findParentFolder(SongFolder current) { - if (current == null) return null; - - if (SongLoader.FOLDERS.contains(current)) { - return null; - } - - for (SongFolder folder : SongLoader.FOLDERS) { - if (folder.subFolders.contains(current)) { - return folder; - } - SongFolder found = findParentInSubfolders(folder, current); - if (found != null) { - return found; - } - } - return null; - } - - private SongFolder findParentInSubfolders(SongFolder parent, SongFolder target) { - for (SongFolder subFolder : parent.subFolders) { - if (subFolder == target) { - return parent; - } - SongFolder found = findParentInSubfolders(subFolder, target); - if (found != null) { - return found; - } - } - return null; - } - + // 拖文件 @Override public void onFilesDropped(List paths) { String string = paths.stream().map(Path::getFileName).map(Path::toString).collect(Collectors.joining(", ")); @@ -350,15 +485,14 @@ public class DiscJockeyScreen extends Screen { Song song = SongLoader.loadSong(file); if (song != null) { - Files.copy(path, Main.songsFolder.toPath().resolve(file.getName())); - SongLoader.SONGS.add(song); + File targetFolder = currentFolder != null ? new File(currentFolder.path) : Main.songsFolder; + Files.copy(path, targetFolder.toPath().resolve(file.getName())); + SongLoader.loadSongs(); } } 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); } }); - - SongLoader.sort(); } client.setScreen(this); }, Text.translatable(Main.MOD_ID+".screen.drop_confirm"), Text.literal(string))); @@ -372,27 +506,11 @@ public class DiscJockeyScreen extends Screen { @Override public void close() { super.close(); - // 保存当前文件夹 Main.config.currentFolderPath = currentFolder != null ? currentFolder.path : ""; - // 保存播放模式 Main.config.playMode = currentPlayMode; - // 异步保存配置 new Thread(() -> Main.configHolder.save()).start(); } - private SongFolder findInSubFolders(SongFolder parent, SongFolder target) { - for (SongFolder subFolder : parent.subFolders) { - if (subFolder == target) { - return parent; - } - SongFolder found = findInSubFolders(subFolder, target); - if (found != null) { - return found; - } - } - return null; - } - private SongFolder findFolderByPath(String path) { for (SongFolder folder : SongLoader.FOLDERS) { if (folder.path.equals(path)) { @@ -409,6 +527,7 @@ public class DiscJockeyScreen extends Screen { return null; } + @Nullable private SongFolder findFolderByPathInSubfolders(SongFolder parent, String targetPath) { for (SongFolder subFolder : parent.subFolders) { if (subFolder.path.equals(targetPath)) { @@ -422,6 +541,38 @@ public class DiscJockeyScreen extends Screen { return null; } + @Nullable + public SongFolder findParentFolder(@Nullable SongFolder targetFolder) { + if (targetFolder == null) { + return null; + } + for (SongFolder folder : SongLoader.FOLDERS) { + if (folder.subFolders.contains(targetFolder)) { + return folder; + } + SongFolder parentInSub = findParentFolderInSubfoldersForParent(folder, targetFolder); + if (parentInSub != null) { + return parentInSub; + } + } + return null; + } + + @Nullable + private SongFolder findParentFolderInSubfoldersForParent(SongFolder current, SongFolder targetFolder) { + for (SongFolder subFolder : current.subFolders) { + if (subFolder.subFolders.contains(targetFolder)) { + return subFolder; + } + SongFolder parentInSub = findParentFolderInSubfoldersForParent(subFolder, targetFolder); + if (parentInSub != null) { + return parentInSub; + } + } + return null; + } + + private Text getPlayModeText() { return switch (currentPlayMode) { case SINGLE_LOOP -> MODE_SINGLE; diff --git a/src/main/java/semmiedev/disc_jockey_revive/gui/screen/EditKeyMappingsScreen.java b/src/main/java/semmiedev/disc_jockey_revive/gui/screen/EditKeyMappingsScreen.java new file mode 100644 index 0000000..ae6e656 --- /dev/null +++ b/src/main/java/semmiedev/disc_jockey_revive/gui/screen/EditKeyMappingsScreen.java @@ -0,0 +1,150 @@ +package semmiedev.disc_jockey_revive.gui.screen; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.util.InputUtil; +import net.minecraft.text.Text; +import org.lwjgl.glfw.GLFW; +import semmiedev.disc_jockey_revive.Main; +import semmiedev.disc_jockey_revive.Note; +import semmiedev.disc_jockey_revive.gui.KeyMappingListWidget; + +import java.util.Map; + +public class EditKeyMappingsScreen extends Screen { + + private static final Text TITLE = Text.translatable(Main.MOD_ID + ".screen.edit_mappings.title"); + private static final Text ADD_MAPPING_BUTTON_TEXT = Text.translatable(Main.MOD_ID + ".screen.edit_mappings.add_mapping"); + private static final Text DONE_BUTTON_TEXT = Text.translatable("gui.done"); + private static final Text PRESS_KEY_INSTRUCTION = Text.translatable(Main.MOD_ID + ".screen.edit_mappings.press_key"); + + private final Screen parent; + private KeyMappingListWidget mappingListWidget; + private ButtonWidget addMappingButton; + private ButtonWidget doneButton; + + private boolean waitingForKeyPress = false; + private KeyMappingListWidget.KeyMappingEntry entryToEdit = null; + + public EditKeyMappingsScreen(Screen parent) { + super(TITLE); + this.parent = parent; + } + + @Override + protected void init() { + super.init(); + + int listTop = 40; + int listBottom = this.height - 50; + int listHeight = listBottom - listTop; + + mappingListWidget = new KeyMappingListWidget(this.client, this.width, listHeight, listTop, 20, this); + addDrawableChild(mappingListWidget); + refreshMappingList(); + int buttonWidth = 100; + int buttonHeight = 20; + int buttonY = this.height - 30; + int buttonX = this.width / 2 - buttonWidth - 5; + + addMappingButton = ButtonWidget.builder(ADD_MAPPING_BUTTON_TEXT, button -> { + startWaitingForKeyPress(null); + }).dimensions(buttonX, buttonY, buttonWidth, buttonHeight).build(); + addDrawableChild(addMappingButton); + buttonX = this.width / 2 + 5; + doneButton = ButtonWidget.builder(DONE_BUTTON_TEXT, button -> { + Main.keyMappingManager.saveMappings(); + this.client.setScreen(this.parent); + }).dimensions(buttonX, buttonY, buttonWidth, buttonHeight).build(); + addDrawableChild(doneButton); + } + + private void refreshMappingList() { + + mappingListWidget.setMappings(Main.keyMappingManager.getMappings()); + } + public void startWaitingForKeyPress(KeyMappingListWidget.KeyMappingEntry entry) { + this.waitingForKeyPress = true; + this.entryToEdit = entry; + + addMappingButton.active = false; + doneButton.active = false; + mappingListWidget.setButtonsActive(false); + } + public void addNewMapping(InputUtil.Key key, Note note) { + Main.keyMappingManager.setMapping(key, note); + refreshMappingList(); + + } + public void removeMapping(InputUtil.Key key) { + Main.keyMappingManager.removeMapping(key); + refreshMappingList(); + } + public void stopWaitingForKeyPress() { + this.waitingForKeyPress = false; + this.entryToEdit = null; + + addMappingButton.active = true; + doneButton.active = true; + mappingListWidget.setButtonsActive(true); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (waitingForKeyPress) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + + stopWaitingForKeyPress(); + return true; + } + + InputUtil.Key pressedKey = InputUtil.fromKeyCode(keyCode, scanCode); + + if (entryToEdit != null) { + + Note note = entryToEdit.getNote(); + + Main.keyMappingManager.removeMapping(entryToEdit.getKey()); + + Main.keyMappingManager.setMapping(pressedKey, note); + refreshMappingList(); + stopWaitingForKeyPress(); + } else { + this.client.setScreen(new SelectNoteScreen(this, pressedKey)); + + } + return true; + } + if (mappingListWidget.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + Main.keyMappingManager.saveMappings(); + this.client.setScreen(this.parent); + return true; + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + super.render(context, mouseX, mouseY, delta); + context.drawCenteredTextWithShadow(textRenderer, TITLE, this.width / 2, 10, 0xFFFFFF); + if (waitingForKeyPress) { + + context.fill(0, 0, this.width, this.height, 0x80000000); + context.drawCenteredTextWithShadow(textRenderer, PRESS_KEY_INSTRUCTION, this.width / 2, this.height / 2 - 10, 0xFFFFFF); + } + mappingListWidget.render(context, mouseX, mouseY, delta); + } + + @Override + public boolean shouldPause() { + + return true; + } +} diff --git a/src/main/java/semmiedev/disc_jockey_revive/gui/screen/LiveDjScreen.java b/src/main/java/semmiedev/disc_jockey_revive/gui/screen/LiveDjScreen.java new file mode 100644 index 0000000..a8a9ef7 --- /dev/null +++ b/src/main/java/semmiedev/disc_jockey_revive/gui/screen/LiveDjScreen.java @@ -0,0 +1,175 @@ +package semmiedev.disc_jockey_revive.gui.screen; + +import me.shedaniel.cloth.clothconfig.shadowed.blue.endless.jankson.annotation.Nullable; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.util.InputUtil; +import net.minecraft.sound.SoundCategory; +import net.minecraft.text.Text; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; +import semmiedev.disc_jockey_revive.Main; +import semmiedev.disc_jockey_revive.Note; +import semmiedev.disc_jockey_revive.KeyMappingManager; +import org.lwjgl.glfw.GLFW; + +import java.util.HashMap; +import java.util.Map; + +public class LiveDjScreen extends Screen { + + private static final Text TITLE = Text.translatable(Main.MOD_ID + ".screen.live_dj.title"); + private static final Text INSTRUCTIONS = Text.translatable(Main.MOD_ID + ".screen.live_dj.instructions"); + private static final Text EDIT_MAPPINGS_BUTTON_TEXT = Text.translatable(Main.MOD_ID + ".screen.live_dj.edit_mappings"); + private static final Text START_TUNING_BUTTON_TEXT = Text.translatable(Main.MOD_ID + ".screen.live_dj.start_tuning"); + private static final Text NOT_TUNED_MESSAGE = Text.translatable(Main.MOD_ID + ".player.not_tuned").formatted(net.minecraft.util.Formatting.RED); + private static final Text NOTE_BLOCK_MISSING_MESSAGE = Text.translatable(Main.MOD_ID + ".player.note_block_missing_live").formatted(net.minecraft.util.Formatting.RED); + private static final Text TOO_FAR_MESSAGE = Text.translatable(Main.MOD_ID + ".player.to_far_live").formatted(net.minecraft.util.Formatting.RED); + private static final Text RATE_LIMITED_MESSAGE = Text.translatable(Main.MOD_ID + ".player.rate_limited_live").formatted(net.minecraft.util.Formatting.YELLOW); + private ButtonWidget startTuningButton; + + public LiveDjScreen() { + super(TITLE); + } + + @Override + protected void init() { + super.init(); + + int centerX = this.width / 2; + int buttonWidth = 150; + int buttonHeight = 20; + int buttonY = this.height - 30; + int margin = 5; + addDrawableChild(ButtonWidget.builder(EDIT_MAPPINGS_BUTTON_TEXT, button -> { + MinecraftClient.getInstance().setScreen(new EditKeyMappingsScreen(this)); + }).dimensions(centerX - buttonWidth - margin, buttonY, buttonWidth, buttonHeight).build()); + startTuningButton = ButtonWidget.builder(START_TUNING_BUTTON_TEXT, button -> { + Main.SONG_PLAYER.startTuning(); + }).dimensions(centerX + margin, buttonY, buttonWidth, buttonHeight).build(); + addDrawableChild(startTuningButton); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + super.render(context, mouseX, mouseY, delta); + context.drawCenteredTextWithShadow(textRenderer, TITLE, this.width / 2, 10, 0xFFFFFF); + context.drawCenteredTextWithShadow(textRenderer, INSTRUCTIONS, this.width / 2, 30, 0xFFFFFF); + Text tuningStatusText; + if (Main.SONG_PLAYER.noteBlocks == null) { + tuningStatusText = Text.translatable(Main.MOD_ID + ".player.discovering").formatted(net.minecraft.util.Formatting.YELLOW); + startTuningButton.active = true; + startTuningButton.visible = true; + } else if (!Main.SONG_PLAYER.tuned) { + + int totalNeeded = 0; + if (Main.SONG_PLAYER.noteBlocks != null) { + for (HashMap instrumentNotes : Main.SONG_PLAYER.noteBlocks.values()) { + if (instrumentNotes != null) { + for (net.minecraft.util.math.BlockPos pos : instrumentNotes.values()) { + if (pos != null) { + totalNeeded++; + } + } + } + } + } + int tunedCount = 0; + + MinecraftClient client = MinecraftClient.getInstance(); + if (client.world != null && Main.SONG_PLAYER.noteBlocks != null) { + for (HashMap instrumentNotes : Main.SONG_PLAYER.noteBlocks.values()) { + if (instrumentNotes != null) { + for (Map.Entry entry : instrumentNotes.entrySet()) { + net.minecraft.util.math.BlockPos pos = entry.getValue(); + Byte wantedNote = entry.getKey(); + if (pos != null) { + + if (client.world.getBlockState(pos).contains(net.minecraft.state.property.Properties.NOTE)) { + int currentNote = client.world.getBlockState(pos).get(net.minecraft.state.property.Properties.NOTE); + if (currentNote == wantedNote.byteValue()) { + tunedCount++; + } + } + } + } + } + } + } + if (totalNeeded > 0) { + tuningStatusText = Text.translatable(Main.MOD_ID + ".player.tuning_progress", tunedCount, totalNeeded).formatted(net.minecraft.util.Formatting.YELLOW); + } else { + + tuningStatusText = Text.translatable(Main.MOD_ID + ".player.finding_blocks").formatted(net.minecraft.util.Formatting.YELLOW); + } + startTuningButton.active = true; + startTuningButton.visible = true; + + } else { + tuningStatusText = Text.translatable(Main.MOD_ID + ".player.tuned").formatted(net.minecraft.util.Formatting.GREEN); + startTuningButton.active = false; + startTuningButton.visible = false; + } + context.drawCenteredTextWithShadow(textRenderer, tuningStatusText, this.width / 2, 50, 0xFFFFFF); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + + InputUtil.Key key = InputUtil.fromKeyCode(keyCode, scanCode); + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + this.close(); + return true; + } + Note note = Main.keyMappingManager.getNoteForKey(key); + + if (note != null) { + + if (!Main.SONG_PLAYER.tuned) { + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(NOT_TUNED_MESSAGE); + return true; + } + boolean played = Main.SONG_PLAYER.playNoteBlock(note); + if (!played) { + @Nullable net.minecraft.util.math.BlockPos blockPos = null; + if (Main.SONG_PLAYER.noteBlocks != null && Main.SONG_PLAYER.noteBlocks.containsKey(note.instrument())) { + blockPos = Main.SONG_PLAYER.noteBlocks.get(note.instrument()).get(note.note()); + } + + if (blockPos == null) { + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(NOTE_BLOCK_MISSING_MESSAGE.copy().append(" (" + KeyMappingManager.getNoteDisplayName(note) + ")")); + } else if (!Main.SONG_PLAYER.canInteractWith(MinecraftClient.getInstance().player, blockPos)) { + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(TOO_FAR_MESSAGE.copy().append(" (" + KeyMappingManager.getNoteDisplayName(note) + ")")); + } else { + + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(RATE_LIMITED_MESSAGE); + } + } + + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public void tick() { + super.tick(); + + if (startTuningButton != null) { + startTuningButton.visible = !Main.SONG_PLAYER.tuned; + startTuningButton.active = !Main.SONG_PLAYER.isTuningEnabled(); + } + } + @Override + public boolean shouldPause() { + return false; + } + + @Override + public void close() { + + super.close(); + } +} diff --git a/src/main/java/semmiedev/disc_jockey_revive/gui/screen/SelectNoteScreen.java b/src/main/java/semmiedev/disc_jockey_revive/gui/screen/SelectNoteScreen.java new file mode 100644 index 0000000..eedfc81 --- /dev/null +++ b/src/main/java/semmiedev/disc_jockey_revive/gui/screen/SelectNoteScreen.java @@ -0,0 +1,173 @@ +package semmiedev.disc_jockey_revive.gui.screen; + +import net.minecraft.block.enums.NoteBlockInstrument; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.CyclingButtonWidget; +import net.minecraft.client.gui.widget.SliderWidget; +import net.minecraft.client.util.InputUtil; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; +import org.lwjgl.glfw.GLFW; +import semmiedev.disc_jockey_revive.Main; +import semmiedev.disc_jockey_revive.Note; +import semmiedev.disc_jockey_revive.KeyMappingManager; + +public class SelectNoteScreen extends Screen { + + private static final Text TITLE = Text.translatable(Main.MOD_ID + ".screen.select_note.title"); + private static final Text INSTRUMENT_TEXT = Text.translatable(Main.MOD_ID + ".screen.select_note.instrument"); + private static final Text PITCH_TEXT = Text.translatable(Main.MOD_ID + ".screen.select_note.pitch"); + private static final Text DONE_BUTTON_TEXT = Text.translatable("gui.done"); + private static final Text CANCEL_BUTTON_TEXT = Text.translatable("gui.cancel"); + private static final Text PREVIEW_BUTTON_TEXT = Text.translatable(Main.MOD_ID + ".screen.select_note.preview"); + private final EditKeyMappingsScreen parent; + private final InputUtil.Key keyToMap; + + private NoteBlockInstrument selectedInstrument = NoteBlockInstrument.HARP; + private int selectedPitch = 12; + private CustomPitchSlider pitchSlider; + + private ButtonWidget previewButton; + + public SelectNoteScreen(EditKeyMappingsScreen parent, InputUtil.Key keyToMap) { + super(TITLE); + this.parent = parent; + this.keyToMap = keyToMap; + } + + @Override + protected void init() { + super.init(); + + int centerX = this.width / 2; + int startY = this.height / 2 - 50; + int widgetWidth = 200; + int widgetHeight = 20; + int margin = 5; + CyclingButtonWidget instrumentButton = CyclingButtonWidget.builder((NoteBlockInstrument instrument) -> Text.translatable("block.minecraft.note_block.instrument." + instrument.asString())) + .values(NoteBlockInstrument.values()) + .initially(selectedInstrument) + + .build(centerX - widgetWidth / 2, startY, widgetWidth, widgetHeight, INSTRUMENT_TEXT, (button, instrument) -> { + this.selectedInstrument = instrument; + updatePitchSliderDisplay(); + updatePreviewButton(); + }); + addDrawableChild(instrumentButton); + pitchSlider = new CustomPitchSlider(centerX - widgetWidth / 2, startY + widgetHeight + margin, widgetWidth, widgetHeight, PITCH_TEXT, (selectedPitch / 24.0)); + updatePitchSliderDisplay(); + addDrawableChild(pitchSlider); + previewButton = ButtonWidget.builder(PREVIEW_BUTTON_TEXT, button -> { + playPreviewNote(); + }).dimensions(centerX - widgetWidth / 2, startY + (widgetHeight + margin) * 2, widgetWidth, widgetHeight).build(); + addDrawableChild(previewButton); + int buttonWidth = 100; + int buttonY = this.height - 30; + int doneButtonX = centerX - buttonWidth - margin; + addDrawableChild(ButtonWidget.builder(DONE_BUTTON_TEXT, button -> { + Note selectedNote = new Note(selectedInstrument, (byte) selectedPitch); + parent.addNewMapping(keyToMap, selectedNote); + this.client.setScreen(this.parent); + parent.stopWaitingForKeyPress(); + }).dimensions(doneButtonX, buttonY, buttonWidth, widgetHeight).build()); + int cancelButtonX = centerX + margin; + addDrawableChild(ButtonWidget.builder(CANCEL_BUTTON_TEXT, button -> { + this.client.setScreen(this.parent); + parent.stopWaitingForKeyPress(); + }).dimensions(cancelButtonX, buttonY, buttonWidth, widgetHeight).build()); + } + private class CustomPitchSlider extends SliderWidget { + public CustomPitchSlider(int x, int y, int width, int height, Text text, double value) { + super(x, y, width, height, text, value); + } + + @Override + protected void updateMessage() { + + this.setMessage(PITCH_TEXT.copy().append(": " + KeyMappingManager.getNoteDisplayName(new Note(selectedInstrument, (byte) selectedPitch)))); + } + + @Override + protected void applyValue() { + selectedPitch = (int) Math.round(MathHelper.lerp(this.value, 0.0, 24.0)); + updateMessage(); + updatePreviewButton(); + } + public void forceUpdateDisplay() { + this.updateMessage(); + } + } + private void updatePitchSliderDisplay() { + if (pitchSlider != null) { + + pitchSlider.forceUpdateDisplay(); + } + } + + private void updatePreviewButton() { + + } + + private void playPreviewNote() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.world != null && client.gameRenderer != null) { + Vec3d pos = client.gameRenderer.getCamera().getPos(); + try { + Note note = new Note(selectedInstrument, (byte) selectedPitch); + + if (note.instrument().canBePitched()) { + Identifier soundId = note.instrument().getSound().value().id(); + float pitchMultiplier = (float) Math.pow(2.0, (note.note() - 12) / 12.0); + client.world.playSound(pos.x, pos.y, pos.z, SoundEvent.of(soundId), SoundCategory.RECORDS, 3.0f, pitchMultiplier, false); + } else { + + Identifier soundId = note.instrument().getSound().value().id(); + client.world.playSound(pos.x, pos.y, pos.z, SoundEvent.of(soundId), SoundCategory.RECORDS, 3.0f, 1.0f, false); + } + + } catch (Exception e) { + Main.LOGGER.error("无法播放预览声音。", e); + } + } + } + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + + super.render(context, mouseX, mouseY, delta); + + context.drawCenteredTextWithShadow(textRenderer, TITLE, this.width / 2, 10, 0xFFFFFF); + + context.drawCenteredTextWithShadow(textRenderer, Text.translatable(Main.MOD_ID + ".screen.select_note.mapping_key", Text.translatable(keyToMap.getTranslationKey())), this.width / 2, 30, 0xFFFFFF); + } + + @Override + public boolean shouldPause() { + return true; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + this.client.setScreen(this.parent); + parent.stopWaitingForKeyPress(); + return true; + } + + if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { + Note selectedNote = new Note(selectedInstrument, (byte) selectedPitch); + parent.addNewMapping(keyToMap, selectedNote); + this.client.setScreen(this.parent); + parent.stopWaitingForKeyPress(); + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } +} diff --git a/src/main/java/semmiedev/disc_jockey_revive/gui/widget/ProgressBarWidget.java b/src/main/java/semmiedev/disc_jockey_revive/gui/widget/ProgressBarWidget.java new file mode 100644 index 0000000..ac5275b --- /dev/null +++ b/src/main/java/semmiedev/disc_jockey_revive/gui/widget/ProgressBarWidget.java @@ -0,0 +1,91 @@ +package semmiedev.disc_jockey_revive.gui.widget; + +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.text.Text; +import net.minecraft.util.math.MathHelper; + +public class ProgressBarWidget extends ClickableWidget { + private double progress; + private final int minValue; + private final int maxValue; + private boolean dragging; + + public ProgressBarWidget(int x, int y, int width, int height, Text message, int minValue, int maxValue) { + super(x, y, width, height, message); + this.minValue = minValue; + this.maxValue = maxValue; + this.progress = minValue; + } + + @Override + public void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + // 背景 + context.fill(getX(), getY(), getX() + width, getY() + height, 0xFF555555); + + // 进度条 + int progressWidth = (int)(width * (progress - minValue) / (maxValue - minValue)); + context.fill(getX(), getY(), getX() + progressWidth, getY() + height, 0xFF00FF00); + + // 滑块 + int sliderX = getX() + progressWidth - 3; + context.fill(sliderX, getY() - 2, sliderX + 6, getY() + height + 2, 0xFFFFFFFF); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && isMouseOver(mouseX, mouseY)) { + setProgressFromMouse(mouseX); + dragging = true; + return true; + } + return false; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + if (dragging) { + setProgressFromMouse(mouseX); + return true; + } + return false; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (button == 0) { + dragging = false; + return true; + } + return false; + } + + private void setProgressFromMouse(double mouseX) { + double relativeX = MathHelper.clamp(mouseX - getX(), 0, width); + double newProgress = (relativeX / width) * (maxValue - minValue) + minValue; + setProgress(newProgress); + } + + public void setProgress(double progress) { + this.progress = MathHelper.clamp(progress, minValue, maxValue); + onProgressChanged(this.progress); + } + + public double getProgress() { + return progress; + } + + public boolean isDragging() { + return this.dragging; + } + + protected void onProgressChanged(double progress) { + + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + appendDefaultNarrations(builder); + } +} 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..2539473 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,54 @@ "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...", + + "key.disc_jockey_revive.open_screen": "Open Disc Jockey Screen", + "key.disc_jockey_revive.open_live_dj_screen": "Open Live DJ Screen", + + "disc_jockey_revive.screen.live_dj.title": "Live DJ Mode", + "disc_jockey_revive.screen.live_dj.instructions": "Press mapped keys to play notes. Press ESC to exit. Default key mapping reference FL Studio", + "disc_jockey_revive.screen.live_dj.edit_mappings": "Edit Mappings", + + "disc_jockey_revive.screen.edit_mappings.title": "Edit Key Mappings", + "disc_jockey_revive.screen.edit_mappings.add_mapping": "Add Mapping", + "disc_jockey_revive.screen.edit_mappings.change": "Change Key", + "disc_jockey_revive.screen.edit_mappings.remove": "Remove", + "disc_jockey_revive.screen.edit_mappings.press_key": "Press any key to map...", + + "disc_jockey_revive.screen.select_note.title": "Select Note", + "disc_jockey_revive.screen.select_note.mapping_key": "Mapping Key: %s", + "disc_jockey_revive.screen.select_note.instrument": "Instrument", + "disc_jockey_revive.screen.select_note.pitch": "Pitch", + "disc_jockey_revive.screen.select_note.preview": "Preview Note", + + "block.minecraft.note_block.instrument.harp": "Air", + "block.minecraft.note_block.instrument.basedrum": "Stone", + "block.minecraft.note_block.instrument.snare": "Sand", + "block.minecraft.note_block.instrument.hat": "Glass", + "block.minecraft.note_block.instrument.bass": "Wood", + "block.minecraft.note_block.instrument.flute": "Clay", + "block.minecraft.note_block.instrument.bell": "Gold Block", + "block.minecraft.note_block.instrument.guitar": "Wool", + "block.minecraft.note_block.instrument.chime": "Packed Ice", + "block.minecraft.note_block.instrument.xylophone": "Bone Block", + "block.minecraft.note_block.instrument.iron_xylophone": "Iron Block", + "block.minecraft.note_block.instrument.cow_bell": "Soul Sand", + "block.minecraft.note_block.instrument.didgeridoo": "Pumpkin", + "block.minecraft.note_block.instrument.bit": "Emerald Block", + "block.minecraft.note_block.instrument.banjo": "Hay Bale", + "block.minecraft.note_block.instrument.pling": "Glowstone", + + "disc_jockey_revive.player.not_tuned": "Note blocks are not tuned yet!", + "disc_jockey_revive.player.discovering": "Discovering note blocks...", + "disc_jockey_revive.player.finding_blocks": "Finding note blocks...", + "disc_jockey_revive.player.tuning_progress": "Tuning note blocks: %s/%s", + "disc_jockey_revive.player.tuned": "Note blocks are tuned!", + "disc_jockey_revive.player.note_block_missing_live": "Note block missing for this note!", + "disc_jockey_revive.player.to_far_live": "Too far from note block for this note!", + "disc_jockey_revive.player.rate_limited_live": "Rate limited. Cannot play note right now.", + + "disc_jockey_revive.screen.live_dj.start_tuning": "Start Tuning", + "disc_jockey_revive.player.tuning_started": "Tuning started...", + "disc_jockey_revive.player.retuning": "Retuning note blocks..." } \ 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..d1cec9f 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,54 @@ "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": "正在重新加载...", + + "key.disc_jockey_revive.open_screen": "打开Disc Jockey界面", + "key.disc_jockey_revive.open_live_dj_screen": "打开现场演奏界面", + + "disc_jockey_revive.screen.live_dj.title": "现场演奏模式", + "disc_jockey_revive.screen.live_dj.instructions": "按下已映射的按键来弹奏音符。按 ESC 退出。默认按键映射参考FL Studio", + "disc_jockey_revive.screen.live_dj.edit_mappings": "编辑按键映射", + + "disc_jockey_revive.screen.edit_mappings.title": "编辑按键映射", + "disc_jockey_revive.screen.edit_mappings.add_mapping": "添加映射", + "disc_jockey_revive.screen.edit_mappings.change": "更改按键", + "disc_jockey_revive.screen.edit_mappings.remove": "移除", + "disc_jockey_revive.screen.edit_mappings.press_key": "按下任意按键进行映射...", + + "disc_jockey_revive.screen.select_note.title": "选择音符", + "disc_jockey_revive.screen.select_note.mapping_key": "映射按键:%s", + "disc_jockey_revive.screen.select_note.instrument": "乐器", + "disc_jockey_revive.screen.select_note.pitch": "音高", + "disc_jockey_revive.screen.select_note.preview": "试听音符", + + "block.minecraft.note_block.instrument.harp": "空气", + "block.minecraft.note_block.instrument.basedrum": "石头", + "block.minecraft.note_block.instrument.snare": "沙子", + "block.minecraft.note_block.instrument.hat": "玻璃", + "block.minecraft.note_block.instrument.bass": "木头", + "block.minecraft.note_block.instrument.flute": "粘土", + "block.minecraft.note_block.instrument.bell": "金块", + "block.minecraft.note_block.instrument.guitar": "羊毛", + "block.minecraft.note_block.instrument.chime": "浮冰", + "block.minecraft.note_block.instrument.xylophone": "骨块", + "block.minecraft.note_block.instrument.iron_xylophone": "铁块", + "block.minecraft.note_block.instrument.cow_bell": "灵魂沙", + "block.minecraft.note_block.instrument.didgeridoo": "南瓜", + "block.minecraft.note_block.instrument.bit": "绿宝石", + "block.minecraft.note_block.instrument.banjo": "甘草快", + "block.minecraft.note_block.instrument.pling": "荧石", + + "disc_jockey_revive.player.not_tuned": "音符盒尚未调音!", + "disc_jockey_revive.player.discovering": "正在发现音符盒...", + "disc_jockey_revive.player.finding_blocks": "正在查找音符盒...", + "disc_jockey_revive.player.tuning_progress": "正在调音音符盒:%s/%s", + "disc_jockey_revive.player.tuned": "音符盒已调音!", + "disc_jockey_revive.player.note_block_missing_live": "此音符的音符盒缺失!", + "disc_jockey_revive.player.to_far_live": "离此音符的音符盒太远!", + "disc_jockey_revive.player.rate_limited_live": "速率受限。暂时无法弹奏音符。", + + "disc_jockey_revive.screen.live_dj.start_tuning": "开始调音", + "disc_jockey_revive.player.tuning_started": "调音已开始...", + "disc_jockey_revive.player.retuning": "正在重新调音音符盒..." } \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 2ed4cf6..40191c8 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "disc_jockey_revive", "version": "${version}", - "name": "Disc Jockey", + "name": "Disc Jockey Revive", "description": "在游戏中播放音符盒(打碟机)", "authors": [ "SemmieDev", @@ -10,11 +10,11 @@ "BRanulf(仅限该版本,请支持上面两个原作者)" ], "contact": { - "homepage": "https://git.branulf.top/Branulf", + "homepage": "https://git.branulf.top/Branulf/DIsc_Jockey_revive", "sources": "https://git.branulf.top/Branulf/DIsc_Jockey_revive" }, "license": "MIT", - "icon": "assets/disc_jockey/icon.png", + "icon": "assets/disc_jockey/icon1.png", "environment": "client", "entrypoints": { "client": [