diff --git a/gradle.properties b/gradle.properties index ac197c7..28ece6b 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.034 +mod_version=1.14.514.037 maven_group=semmiedev archives_base_name=disc_jockey_revive # Dependencies diff --git a/src/main/java/semmiedev/disc_jockey_revive/DebugLogger.java b/src/main/java/semmiedev/disc_jockey_revive/DebugLogger.java new file mode 100644 index 0000000..b088550 --- /dev/null +++ b/src/main/java/semmiedev/disc_jockey_revive/DebugLogger.java @@ -0,0 +1,25 @@ +package semmiedev.disc_jockey_revive; + +import org.apache.logging.log4j.Logger; + +// 这玩意一般不用,我自己用的,但是不想删 +public class DebugLogger { + + public static void log(String message) { + if (Main.config != null && Main.config.debugModeEnabled) { + Main.LOGGER.info("[DiscJockeyRevive-调试] " + message); + } + } + + public static void log(String format, Object... arguments) { + if (Main.config != null && Main.config.debugModeEnabled) { + Main.LOGGER.info("[DiscJockeyRevive-调试] " + format, arguments); + } + } + + public static void log(String message, Throwable t) { + if (Main.config != null && Main.config.debugModeEnabled) { + Main.LOGGER.error("[DiscJockeyRevive-调试] " + message, t); + } + } +} 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..46d02d0 --- /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/LiveDjPlayer.java b/src/main/java/semmiedev/disc_jockey_revive/LiveDjPlayer.java new file mode 100644 index 0000000..bd6a6de --- /dev/null +++ b/src/main/java/semmiedev/disc_jockey_revive/LiveDjPlayer.java @@ -0,0 +1,475 @@ +package semmiedev.disc_jockey_revive; + +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.block.enums.NoteBlockInstrument; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.hud.ChatHud; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket; +import net.minecraft.state.property.Properties; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Hand; +import net.minecraft.util.Pair; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.*; +import net.minecraft.world.GameMode; +import org.apache.commons.lang3.NotImplementedException; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class LiveDjPlayer implements ClientTickEvents.StartWorldTick { + + private HashMap> noteBlocks = null; + private boolean tuned = false; + private boolean tuningRequested = false; + + private long last100MsSpanAt = -1L; + private int last100MsSpanEstimatedPackets = 0; + final private int last100MsReducePacketsAfter = 300 / 10, last100MsStopPacketsAfter = 450 / 10; + private long reducePacketsUntil = -1L, stopPacketsUntil = -1L; + private long lastLookSentAt = -1L, lastSwingSentAt = -1L; + private long lastInteractAt = -1; + private float availableInteracts = 8; + private int tuneInitialUntunedBlocks = -1; + private HashMap> notePredictions = new HashMap<>(); + + public HashMap missingInstrumentBlocks = new HashMap<>(); + + public LiveDjPlayer() { + Main.TICK_LISTENERS.add(this); + } + + // 调音 + 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; + tuningRequested = true; + + last100MsSpanAt = System.currentTimeMillis(); + last100MsSpanEstimatedPackets = 0; + reducePacketsUntil = -1L; + stopPacketsUntil = -1L; + lastLookSentAt = -1L; + lastSwingSentAt = -1L; + lastInteractAt = -1; + availableInteracts = 8; + } + + // 停止调音 + public synchronized void stopTuning() { + tuningRequested = false; + } + + // 是否正在调音 + public boolean isTuningActive() { + return tuningRequested; + } + + // 是否已调音 + public boolean isTuned() { + return tuned; + } + + public HashMap> getNoteBlocks() { + return noteBlocks; + } + + // 播放 + 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); + } + + // 获取可交互的 + private boolean sendNotePacket(BlockPos blockPos) { + MinecraftClient client = MinecraftClient.getInstance(); + ClientPlayerEntity player = client.player; + long now = System.currentTimeMillis(); + + 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("LiveDjPlayer: 短暂暂停所有数据包,因为速率受限!"); + 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; + } + + + @Override + public void onStartTick(ClientWorld world) { + MinecraftClient client = MinecraftClient.getInstance(); + if(world == null || client.world == null || client.player == null) 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(); + } + + ArrayList outdatedPredictions = new ArrayList<>(); + for(Map.Entry> entry : notePredictions.entrySet()) { + if(entry.getValue().getRight() < System.currentTimeMillis()) + outdatedPredictions.add(entry.getKey()); + } + for(BlockPos outdatedPrediction : outdatedPredictions) notePredictions.remove(outdatedPrediction); + + if (tuningRequested && !tuned) { + 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; + tuningRequested = false; + client.inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.invalid_state_tuning").formatted(Formatting.RED)); + return; + } + + if (noteBlocks == null) { + noteBlocks = new HashMap<>(); + + 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); + } + } + } + } + + ArrayList neededNotes = new ArrayList<>(); + + if (Main.keyMappingManager != null) { + for (Note mappedNote : Main.keyMappingManager.getMappings().values()) { + if (mappedNote != null && !neededNotes.contains(mappedNote)) { + neededNotes.add(mappedNote); + } + } + } + + ArrayList capturedNotes = new ArrayList<>(); + for(Note note : neededNotes) { + ArrayList availableBlocks = noteblocksForInstrument.get(note.instrument()); + if(availableBlocks == null) { + getNotesMapForInstrument(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; + + int wantedNote = note.note(); + BlockState blockState = world.getBlockState(blockPos); + if (!blockState.contains(Properties.NOTE)) continue; + int currentNote = blockState.get(Properties.NOTE); + 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); + getNotesMapForInstrument(note.instrument()).put(note.note(), bestBlockPos); + } + } + + 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)); + + HashMap missing = new HashMap<>(); + for (Note note : missingNotes) { + Block block = Note.INSTRUMENT_BLOCKS.get(note.instrument()); + 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(); + } + } + + + int ping = 0; + { + PlayerListEntry playerListEntry; + if (client.getNetworkHandler() != null && (playerListEntry = client.getNetworkHandler().getPlayerListEntry(client.player.getGameProfile().getId())) != null) + ping = playerListEntry.getLatency(); + } + + int fullyTunedBlocks = 0; + HashMap untunedNotes = new HashMap<>(); + + 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("LiveDjPlayer: 音符盒在 {} 处在调音过程中改变了状态", blockPos); + noteBlocks = null; + tuned = false; + tuningRequested = false; + return; + } + + int assumedNote = notePredictions.containsKey(blockPos) ? notePredictions.get(blockPos).getLeft() : blockState.get(Properties.NOTE); + + byte wantedNotePrimitive = wantedNote.byteValue(); + + if(assumedNote == wantedNotePrimitive && blockState.get(Properties.NOTE).intValue() == wantedNotePrimitive) { + fullyTunedBlocks++; + } else if (assumedNote != wantedNotePrimitive) { + untunedNotes.put(blockPos, blockState.get(Properties.NOTE).intValue()); + } + } + } + } + + int existingUniqueNotesCount = 0; + if (noteBlocks != null) { + for(HashMap instrumentNotes : noteBlocks.values()) { + if (instrumentNotes != null) { + for (BlockPos pos : instrumentNotes.values()) { + if (pos != null) { + existingUniqueNotesCount++; + } + } + } + } + } + + if(untunedNotes.isEmpty() && fullyTunedBlocks == existingUniqueNotesCount) { + if(lastInteractAt == -1 || System.currentTimeMillis() - lastInteractAt >= ping * 2 + 100) { + tuned = true; + tuningRequested = false; + tuneInitialUntunedBlocks = -1; + 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); + } else { + untunedNotes.remove(blockPosToTune); + } + } else { + untunedNotes.remove(blockPosToTune); + } + } + } + } + } + + private HashMap getNotesMapForInstrument(NoteBlockInstrument instrument) { + if (noteBlocks == null) { + noteBlocks = new HashMap<>(); + } + return noteBlocks.computeIfAbsent(instrument, k -> new HashMap<>()); + } + + // 检测玩家是否可以与指定方块进行交互 + 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; + }else if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.v1_20_5_Or_Later) { + double blockInteractRange = player.getBlockInteractionRange() + 1.0; + return new Box(blockPos).squaredMagnitude(eyePos) < blockInteractRange * blockInteractRange; + }else if(Main.config.expectedServerVersion == ModConfig.ExpectedServerVersion.All) { + double blockInteractRange = player.getBlockInteractionRange() + 1.0; + return eyePos.squaredDistanceTo(blockPos.toCenterPos()) <= 6.0 * 6.0 && new Box(blockPos).squaredMagnitude(eyePos) < blockInteractRange * blockInteractRange; + }else { + throw new NotImplementedException("ExpectedServerVersion Value not implemented: " + Main.config.expectedServerVersion.name()); + } + } +} diff --git a/src/main/java/semmiedev/disc_jockey_revive/Main.java b/src/main/java/semmiedev/disc_jockey_revive/Main.java index 72a902b..37b0108 100644 --- a/src/main/java/semmiedev/disc_jockey_revive/Main.java +++ b/src/main/java/semmiedev/disc_jockey_revive/Main.java @@ -23,6 +23,7 @@ import org.lwjgl.glfw.GLFW; import semmiedev.disc_jockey_revive.gui.hud.BlocksOverlay; import semmiedev.disc_jockey_revive.gui.hud.PlaybackProgressOverlay; import semmiedev.disc_jockey_revive.gui.screen.DiscJockeyScreen; +import semmiedev.disc_jockey_revive.gui.screen.LiveDjScreen; import java.io.File; import java.util.ArrayList; @@ -34,10 +35,13 @@ 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 final LiveDjPlayer LIVE_DJ_PLAYER = new LiveDjPlayer(); public static File songsFolder; public static ModConfig config; public static ConfigHolder configHolder; + public static KeyMappingManager keyMappingManager; + @Override public void onInitializeClient() { @@ -49,8 +53,12 @@ public class Main implements ClientModInitializer { SongLoader.loadSongs(); + keyMappingManager = new KeyMappingManager(); + KeyBinding openScreenKeyBind = KeyBindingHelper.registerKeyBinding(new KeyBinding(MOD_ID+".key_bind.open_screen", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_J, "key.category."+MOD_ID)); + KeyBinding 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; @@ -59,6 +67,7 @@ public class Main implements ClientModInitializer { if (prevWorld != client.world) { PREVIEWER.stop(); SONG_PLAYER.stop(); + LIVE_DJ_PLAYER.stopTuning(); } prevWorld = client.world; @@ -70,6 +79,14 @@ public class Main implements ClientModInitializer { client.setScreen(new DiscJockeyScreen()); } } + if (openLiveDjScreenKeyBind.wasPressed()) { + if (SongLoader.loadingSongs) { + client.inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".still_loading").formatted(Formatting.RED)); + SongLoader.showToast = true; + } else { + client.setScreen(new LiveDjScreen()); + } + } } }); @@ -84,6 +101,7 @@ public class Main implements ClientModInitializer { ClientLoginConnectionEvents.DISCONNECT.register((handler, client) -> { PREVIEWER.stop(); SONG_PLAYER.stop(); + LIVE_DJ_PLAYER.stopTuning(); }); HudRenderCallback.EVENT.register(new PlaybackProgressOverlay()); diff --git a/src/main/java/semmiedev/disc_jockey_revive/ModConfig.java b/src/main/java/semmiedev/disc_jockey_revive/ModConfig.java index 2ad0fba..3e81cc9 100644 --- a/src/main/java/semmiedev/disc_jockey_revive/ModConfig.java +++ b/src/main/java/semmiedev/disc_jockey_revive/ModConfig.java @@ -49,4 +49,7 @@ public class ModConfig implements ConfigData { @ConfigEntry.Gui.Tooltip(count = 1) public boolean showHudProgressBar = true; + + @ConfigEntry.Gui.Tooltip(count = 1) + public boolean debugModeEnabled = false; } 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..52230fe --- /dev/null +++ b/src/main/java/semmiedev/disc_jockey_revive/gui/KeyMappingListWidget.java @@ -0,0 +1,135 @@ +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; + +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/hud/PlaybackProgressOverlay.java b/src/main/java/semmiedev/disc_jockey_revive/gui/hud/PlaybackProgressOverlay.java index e095c23..8142767 100644 --- a/src/main/java/semmiedev/disc_jockey_revive/gui/hud/PlaybackProgressOverlay.java +++ b/src/main/java/semmiedev/disc_jockey_revive/gui/hud/PlaybackProgressOverlay.java @@ -29,7 +29,7 @@ public class PlaybackProgressOverlay implements HudRenderCallback { int screenHeight = context.getScaledWindowHeight(); int barX = screenWidth / 2 - PROGRESS_BAR_WIDTH / 2; - int barY = screenHeight - 50; + int barY = screenHeight - 55; progressBarRenderer.renderProgressBar( context, 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 58269d3..aae8c14 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 @@ -56,14 +56,12 @@ public class DiscJockeyScreen extends Screen { private static final MutableText OPEN_FOLDER = Text.translatable(Main.MOD_ID+".screen.open_folder"), - RELOAD = Text.translatable(Main.MOD_ID+".screen.reload"); + RELOAD = Text.translatable(Main.MOD_ID+".screen.reload"), + LIVE_DJ = Text.translatable(Main.MOD_ID+".screen.live_dj").formatted(Formatting.GOLD); private ButtonWidget folderUpButton, playModeButton; public SongFolder currentFolder; private PlayMode currentPlayMode = PlayMode.STOP_AFTER; - private int progressBarWidth = 200; // 进度条宽度 - private int progressBarHeight = 5; // 进度条高度 - private int progressBarYOffset = 5; // 进度条Y偏移 private ProgressBarRenderer progressBarRenderer; @@ -126,14 +124,14 @@ public class DiscJockeyScreen extends Screen { if (isLargeScreen){ buttonY = height - 30; } else { - buttonY = height - 60; + buttonY = height - 30; // awa } int centerX = width / 2; // 上一首 addDrawableChild(ButtonWidget.builder(Text.literal("◀◀◀"), button -> { Main.SONG_PLAYER.playPreviousSong(); - }).dimensions(centerX - 110, buttonY, 40, 20).build()); + }).dimensions(centerX - 100, buttonY, 40, 20).build()); // 播放暂停 playButton = ButtonWidget.builder(PLAY, button -> { @@ -168,44 +166,47 @@ public class DiscJockeyScreen extends Screen { int bottomY = height - 30; - addDrawableChild(ButtonWidget.builder(Text.translatable(Main.MOD_ID+".screen.blocks"), button -> { - // TODO: 6/2/2022 Add an auto build mode - if (BlocksOverlay.itemStacks == null) { - SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull(); - if (entry != null) { - client.setScreen(null); + // 音符盒 + if (isLargeScreen) { + 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(); + 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 (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; + 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 (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; - } } + } else { + BlocksOverlay.itemStacks = null; + client.setScreen(null); } - } else { - BlocksOverlay.itemStacks = null; - client.setScreen(null); - } - }).dimensions(width - 110, height - 31, 100, 20).build()); + }).dimensions(width - 110, height - 31, 100, 20).build()); + } // 打开文件夹 addDrawableChild(ButtonWidget.builder(OPEN_FOLDER, button -> { @@ -238,19 +239,31 @@ public class DiscJockeyScreen extends Screen { // 重新加载 - addDrawableChild(ButtonWidget.builder(RELOAD, button -> { - SongLoader.loadSongs(); - client.setScreen(null); - }).dimensions(120, bottomY, 100, 20).build()); + if (isLargeScreen) { + addDrawableChild(ButtonWidget.builder(RELOAD, button -> { + SongLoader.loadSongs(); + client.setScreen(null); + }).dimensions(120, bottomY, 100, 20).build()); + } - TextFieldWidget searchBar = new TextFieldWidget(textRenderer, 230, height - 31, 100, 20, Text.translatable(Main.MOD_ID+".screen.search")); - searchBar.setChangedListener(query -> { - query = query.toLowerCase().replaceAll("\\s", ""); - if (this.query.equals(query)) return; - this.query = query; - shouldFilter = true; - }); - addDrawableChild(searchBar); + // 即兴演奏 + if (isLargeScreen) { + addDrawableChild(ButtonWidget.builder(LIVE_DJ, button -> { + client.setScreen(new LiveDjScreen()); + }).dimensions(width - 220, bottomY, 100, 20).build()); + } + + // 搜索框 + if (isLargeScreen) { + TextFieldWidget searchBar = new TextFieldWidget(textRenderer, 230, height - 31, 100, 20, Text.translatable(Main.MOD_ID + ".screen.search")); + searchBar.setChangedListener(query -> { + query = query.toLowerCase().replaceAll("\\s", ""); + if (this.query.equals(query)) return; + this.query = query; + shouldFilter = true; + }); + addDrawableChild(searchBar); + } // Main.LOGGER.info("播放界面初始化完成!"); @@ -328,7 +341,7 @@ public class DiscJockeyScreen extends Screen { } } - // 只有在songs目录或其子目录中才显示歌曲(原作者的💩跑我这了) + // 只有在songs目录或其子目录中才显示歌曲(在搞了在搞了) if (isInSongsOrSubfolder) { // 歌曲条目 List songsToShow = currentFolder == null ? 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..569b9bb --- /dev/null +++ b/src/main/java/semmiedev/disc_jockey_revive/gui/screen/EditKeyMappingsScreen.java @@ -0,0 +1,146 @@ +package semmiedev.disc_jockey_revive.gui.screen; + +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.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; + +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..f5b89ab --- /dev/null +++ b/src/main/java/semmiedev/disc_jockey_revive/gui/screen/LiveDjScreen.java @@ -0,0 +1,179 @@ +package semmiedev.disc_jockey_revive.gui.screen; + +import me.shedaniel.cloth.clothconfig.shadowed.blue.endless.jankson.annotation.Nullable; +import net.minecraft.block.Block; +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.util.InputUtil; +import net.minecraft.text.Text; +import semmiedev.disc_jockey_revive.DebugLogger; +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.LIVE_DJ_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.LIVE_DJ_PLAYER.getNoteBlocks() == null) { + tuningStatusText = Text.translatable(Main.MOD_ID + ".player.discovering").formatted(net.minecraft.util.Formatting.YELLOW); + startTuningButton.active = true; + startTuningButton.visible = true; + } else if (!Main.LIVE_DJ_PLAYER.isTuned()) { + int totalNeeded = 0; + if (Main.LIVE_DJ_PLAYER.getNoteBlocks() != null) { + for (HashMap instrumentNotes : Main.LIVE_DJ_PLAYER.getNoteBlocks().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.LIVE_DJ_PLAYER.getNoteBlocks() != null) { + for (HashMap instrumentNotes : Main.LIVE_DJ_PLAYER.getNoteBlocks().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); + + + if (!Main.LIVE_DJ_PLAYER.missingInstrumentBlocks.isEmpty()) { + int yOffset = 70; + context.drawCenteredTextWithShadow(textRenderer, Text.translatable(Main.MOD_ID+".player.invalid_note_blocks").formatted(net.minecraft.util.Formatting.RED), this.width / 2, yOffset, 0xFFFFFF); + yOffset += 12; + for (Map.Entry entry : Main.LIVE_DJ_PLAYER.missingInstrumentBlocks.entrySet()) { + context.drawCenteredTextWithShadow(textRenderer, Text.literal(entry.getKey().getName().getString()+" × "+entry.getValue()).formatted(net.minecraft.util.Formatting.RED), this.width / 2, yOffset, 0xFFFFFF); + yOffset += 10; + } + } + } + + @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.LIVE_DJ_PLAYER.isTuned()) { + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(NOT_TUNED_MESSAGE); + return true; + } + boolean played = Main.LIVE_DJ_PLAYER.playNoteBlock(note); + if (!played) { + @Nullable net.minecraft.util.math.BlockPos blockPos = null; + if (Main.LIVE_DJ_PLAYER.getNoteBlocks() != null && Main.LIVE_DJ_PLAYER.getNoteBlocks().containsKey(note.instrument())) { + blockPos = Main.LIVE_DJ_PLAYER.getNoteBlocks().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.LIVE_DJ_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.LIVE_DJ_PLAYER.isTuned(); + startTuningButton.active = !Main.LIVE_DJ_PLAYER.isTuningActive(); + } + } + + @Override + public boolean shouldPause() { + return false; + } + + @Override + public void close() { + super.close(); + Main.LIVE_DJ_PLAYER.stopTuning(); + } +} 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/resources/assets/disc_jockey/lang/en_us.json b/src/main/resources/assets/disc_jockey/lang/en_us.json index db78995..540064c 100644 --- a/src/main/resources/assets/disc_jockey/lang/en_us.json +++ b/src/main/resources/assets/disc_jockey/lang/en_us.json @@ -63,5 +63,35 @@ "disc_jockey_revive.screen.reload": "Reload Songs", "disc_jockey_revive.screen.reloading": "Reloading songs...", "text.autoconfig.disc_jockey_revive.option.showHudProgressBar": "Show HUD progress bar", - "text.autoconfig.disc_jockey_revive.option.showHudProgressBar.@Tooltip": "Show song playback progress bar in game。" -} \ No newline at end of file + "text.autoconfig.disc_jockey_revive.option.showHudProgressBar.@Tooltip": "Show song playback progress bar in game。", + + "disc_jockey_revive.key_bind.open_live_dj_screen": "Open Live DJ Screen", + "disc_jockey_revive.screen.live_dj": "Live DJ", + "disc_jockey_revive.screen.live_dj.title": "Live DJ Performance", + "disc_jockey_revive.screen.live_dj.instructions": "Press mapped keys to play notes. Tune note blocks first!", + "disc_jockey_revive.screen.live_dj.edit_mappings": "Edit Key Mappings", + "disc_jockey_revive.screen.live_dj.start_tuning": "Start Tuning", + "disc_jockey_revive.screen.edit_mappings.title": "Edit Key Mappings", + "disc_jockey_revive.screen.edit_mappings.add_mapping": "Add New 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 a key to map...", + "disc_jockey_revive.screen.select_note.title": "Select Note for Key", + "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", + "disc_jockey_revive.screen.select_note.mapping_key": "Mapping Key: %s", + "disc_jockey_revive.player.not_tuned": "Note blocks are not tuned yet!", + "disc_jockey_revive.player.note_block_missing_live": "Missing note block for this note!", + "disc_jockey_revive.player.to_far_live": "You are too far from the note block!", + "disc_jockey_revive.player.rate_limited_live": "Rate limited by server, try again soon.", + "disc_jockey_revive.player.discovering": "Discovering note blocks...", + "disc_jockey_revive.player.tuning_progress": "Tuning: %s/%s tuned", + "disc_jockey_revive.player.finding_blocks": "Finding note blocks...", + "disc_jockey_revive.player.retuning": "Retuning note blocks...", + "disc_jockey_revive.player.tuning_started": "Tuning started. Please wait...", + "disc_jockey_revive.player.invalid_state_tuning": "Cannot tune: Invalid game state or mode.", + "disc_jockey_revive.player.tuned": "Tuning completed。", + "text.autoconfig.disc_jockey_revive.option.debugModeEnabled": "Enable Debug Mode", + "text.autoconfig.disc_jockey_revive.option.debugModeEnabled.@Tooltip": "Enables verbose logging for debugging purposes. \nKeep off unless troubleshooting. \nOf course, many debug-related parts have already been removed." +} 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 42e2812..32edfa3 100644 --- a/src/main/resources/assets/disc_jockey/lang/zh_cn.json +++ b/src/main/resources/assets/disc_jockey/lang/zh_cn.json @@ -10,7 +10,7 @@ "disc_jockey_revive.screen.drop_hint": "将歌曲文件拖入此窗口以添加", "disc_jockey_revive.screen.drop_confirm": "是否将以下歌曲添加到 Disc Jockey?", "disc_jockey_revive.player.invalid_note_blocks": "附近的音符盒配置不正确。缺失:", - "disc_jockey_revive.player.invalid_game_mode": "无法在 %s 模式下播放", + "disc_jockey_revive.player.invalid_game_mode": "无法在 %s 下播放", "disc_jockey_revive.player.to_far": "你距离太远了", "disc_jockey_revive.still_loading": "歌曲仍在加载中", "disc_jockey_revive.reloading": "正在重新加载所有歌曲", @@ -57,10 +57,41 @@ "disc_jockey_revive.screen.mode_single": "单曲循环", "disc_jockey_revive.screen.mode_list": "列表循环", "disc_jockey_revive.screen.mode_random": "随机播放", - "disc_jockey_revive.screen.mode_stop": "播完停止","disc_jockey_revive.screen.open_folder": "打开文件夹", + "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": "正在重新加载...", "text.autoconfig.disc_jockey_revive.option.showHudProgressBar": "显示HUD进度条", - "text.autoconfig.disc_jockey_revive.option.showHudProgressBar.@Tooltip": "在游戏界面显示歌曲播放进度条" -} \ No newline at end of file + "text.autoconfig.disc_jockey_revive.option.showHudProgressBar.@Tooltip": "在游戏界面显示歌曲播放进度条", + + "disc_jockey_revive.key_bind.open_live_dj_screen": "打开即兴演奏界面", + "disc_jockey_revive.screen.live_dj": "即兴演奏", + "disc_jockey_revive.screen.live_dj.title": "即兴演奏", + "disc_jockey_revive.screen.live_dj.instructions": "按下映射的按键来演奏音符。请先调音音符盒!", + "disc_jockey_revive.screen.live_dj.edit_mappings": "编辑按键映射", + "disc_jockey_revive.screen.live_dj.start_tuning": "开始调音", + "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.instrument": "乐器", + "disc_jockey_revive.screen.select_note.pitch": "音高", + "disc_jockey_revive.screen.select_note.preview": "试听音符", + "disc_jockey_revive.screen.select_note.mapping_key": "映射按键:%s", + "disc_jockey_revive.player.not_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.player.discovering": "正在发现音符盒...", + "disc_jockey_revive.player.tuning_progress": "调音中:%s/%s 已调音", + "disc_jockey_revive.player.finding_blocks": "正在寻找音符盒...", + "disc_jockey_revive.player.retuning": "正在重新调音音符盒...", + "disc_jockey_revive.player.tuning_started": "调音已开始。请稍候...", + "disc_jockey_revive.player.invalid_state_tuning": "无法调音:游戏状态或模式无效。", + "disc_jockey_revive.player.tuned": "调音完成。", + "text.autoconfig.disc_jockey_revive.option.debugModeEnabled": "启用调试模式", + "text.autoconfig.disc_jockey_revive.option.debugModeEnabled.@Tooltip": "启用详细日志输出以进行调试。\n除非排查问题,否则请保持关闭。\n当然,很多调试用的部分已经移除。" +}