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": [