This commit is contained in:
BRanulf 2025-06-12 21:22:10 +08:00
parent 9aae601fc8
commit 727da9f523
16 changed files with 1874 additions and 478 deletions

View File

@ -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 {

View File

@ -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

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.cottonmc</groupId>
<artifactId>LibGui</artifactId>
<version>12.0.1+1.21.2</version>
<name>LibGui</name>
<description>Minecraft GUI Library</description>
<url>https://github.com/CottonMC/LibGui</url>
<licenses>
<license>
<name>MIT</name>
<url>https://github.com/CottonMC/LibGui/blob/HEAD/LICENSE</url>
</license>
</licenses>
<developers>
<developer>
<name>CottonMC</name>
<url>https://github.com/CottonMC</url>
</developer>
</developers>
<dependencies>
<dependency>
<groupId>io.github.juuxel</groupId>
<artifactId>libninepatch</artifactId>
<version>1.2.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.fabricmc.fabric-api</groupId>
<artifactId>fabric-api-base</artifactId>
<version>0.4.48+c47b9d4373</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.fabricmc.fabric-api</groupId>
<artifactId>fabric-networking-api-v1</artifactId>
<version>4.3.3+56ec7ac673</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.fabricmc</groupId>
<artifactId>fabric-loader</artifactId>
<version>0.16.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>net.fabricmc.fabric-api</groupId>
<artifactId>fabric-lifecycle-events-v1</artifactId>
<version>2.3.22+c47b9d4373</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>net.fabricmc.fabric-api</groupId>
<artifactId>fabric-rendering-v1</artifactId>
<version>8.0.5+c47b9d4373</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>net.fabricmc.fabric-api</groupId>
<artifactId>fabric-resource-loader-v0</artifactId>
<version>3.0.5+c47b9d4373</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.github.cottonmc</groupId>
<artifactId>Jankson-Fabric</artifactId>
<version>9.0.0+j1.2.3</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>

View File

@ -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<HashMap<String, NoteData>>() {}.getType();
private Map<InputUtil.Key, Note> 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<String, NoteData> loadedData = GSON.fromJson(reader, MAPPING_TYPE);
mappings.clear();
if (loadedData != null) {
for (Map.Entry<String, NoteData> 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<String, NoteData> dataToSave = new HashMap<>();
for (Map.Entry<InputUtil.Key, Note> 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<InputUtil.Key, Note> 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 + ")";
}
}

View File

@ -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<ClientTickEvents.StartWorldTick> 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<ModConfig> 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<ClientTickEvents.StartWorldTick> 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();
});
}
}

View File

@ -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);
}
}
}

View File

@ -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<NoteBlockInstrument, HashMap<Byte, BlockPos>> noteBlocks = null;
public int index;
public double tick;
public HashMap<NoteBlockInstrument, HashMap<Byte, BlockPos>> noteBlocks = null;
public boolean tuned;
private long lastPlaybackTickAt = -1L;
@ -63,7 +63,10 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
private HashMap<BlockPos, Pair<Integer, Long>> 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 tickPlayback() {
if (!running) {
lastPlaybackTickAt = -1L;
last100MsSpanAt = -1L;
return;
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));
}
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;
noteBlocks = null;
notePredictions.clear();
tuned = false;
tuneInitialUntunedBlocks = -1;
manualTuningRequested = true;
}
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));
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) {
// Instrument got likely mapped to "nothing". Skip it
index++;
continue;
return false;
}
if (!canInteractWith(client.player, blockPos)) {
stop();
client.inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.to_far").formatted(Formatting.RED));
return;
if (!canInteractWith(player, blockPos)) {
return false;
}
Vec3d unit = Vec3d.ofCenter(blockPos, 0.5).subtract(client.player.getEyePos()).normalize();
if((lastLookSentAt == -1L || now - lastLookSentAt >= 50) && last100MsSpanEstimatedPackets < last100MsReducePacketsAfter && (reducePacketsUntil == -1L || reducePacketsUntil < now)) {
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;
}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++;
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 || song == null) {
lastPlaybackTickAt = -1L;
// last100MsSpanAt = -1L;
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];
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 authors 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<BlockPos> outdatedPredictions = new ArrayList<>();
@ -296,18 +395,28 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
}
for(BlockPos outdatedPrediction : outdatedPredictions) notePredictions.remove(outdatedPrediction);
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;
}
if (noteBlocks == null) {
noteBlocks = new HashMap<>();
ClientPlayerEntity player = client.player;
// Create list of available noteblock positions per used instrument
HashMap<NoteBlockInstrument, ArrayList<BlockPos>> 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
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) {
@ -342,13 +451,11 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
}
}
// Remap instruments for funzies
if(!instrumentMap.isEmpty()) {
HashMap<NoteBlockInstrument, ArrayList<BlockPos>> 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);
continue;
}
@ -358,21 +465,47 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
noteblocksForInstrument = newNoteblocksForInstrument;
}
// Find fitting noteblocks with the least amount of adjustments required (to reduce tuning time)
ArrayList<Note> capturedNotes = new ArrayList<>();
for(Note note : song.uniqueNotes) {
ArrayList<Note> 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<BlockPos> 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) {
boolean alreadyAssigned = false;
if (noteBlocks != null) {
for (HashMap<Byte, BlockPos> instrumentNotes : noteBlocks.values()) {
if (instrumentNotes != null && instrumentNotes.containsValue(blockPos)) {
alreadyAssigned = true;
break;
}
}
}
if (alreadyAssigned) continue;
int wantedNote = note.note();
int currentNote = client.world.getBlockState(blockPos).get(Properties.NOTE);
BlockState blockState = world.getBlockState(blockPos);
if (!blockState.contains(Properties.NOTE)) continue;
int currentNote = blockState.get(Properties.NOTE);
int tuningSteps = wantedNote >= currentNote ? wantedNote - currentNote : (25 - currentNote) + wantedNote;
if(tuningSteps < bestBlockTuningSteps) {
@ -385,10 +518,10 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
capturedNotes.add(note);
availableBlocks.remove(bestBlockPos);
getNotes(note.instrument()).put(note.note(), bestBlockPos);
} // else will be a missing note
}
}
ArrayList<Note> missingNotes = new ArrayList<>(song.uniqueNotes);
ArrayList<Note> missingNotes = new ArrayList<>(neededNotes);
missingNotes.removeAll(capturedNotes);
if (!missingNotes.isEmpty()) {
ChatHud chatHud = MinecraftClient.getInstance().inGameHud.getChatHud();
@ -397,7 +530,7 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
HashMap<Block, Integer> missing = new HashMap<>();
for (Note note : missingNotes) {
NoteBlockInstrument mappedInstrument = instrumentMap.getOrDefault(note.instrument(), note.instrument());
if(mappedInstrument == null) continue; // Ignore if mapped to nothing
if(mappedInstrument == null) continue;
Block block = Note.INSTRUMENT_BLOCKS.get(mappedInstrument);
Integer got = missing.get(block);
if (got == null) got = 0;
@ -406,10 +539,13 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
missingInstrumentBlocks = missing;
missing.forEach((block, integer) -> chatHud.addMessage(Text.literal(block.getName().getString()+" × "+integer).formatted(Formatting.RED)));
stop();
} else {
missingInstrumentBlocks.clear();
}
} 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<BlockPos, Integer> 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<Byte, BlockPos> instrumentNotes : noteBlocks.values()) {
if (instrumentNotes == null) continue;
for (Map.Entry<Byte, BlockPos> 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));
int assumedNote = notePredictions.containsKey(blockPos) ? notePredictions.get(blockPos).getLeft() : blockState.get(Properties.NOTE); // blockState.get(Properties.NOTE) returns Integer
byte wantedNotePrimitive = wantedNote.byteValue();
if(assumedNote == wantedNotePrimitive && blockState.get(Properties.NOTE).intValue() == wantedNotePrimitive) {
fullyTunedBlocks++;
} else if (assumedNote != wantedNotePrimitive) {
untunedNotes.put(blockPos, blockState.get(Properties.NOTE).intValue());
}
}
} else {
noteBlocks = null;
break;
}
}
if(tuneInitialUntunedBlocks == -1 || tuneInitialUntunedBlocks < untunedNotes.size())
tuneInitialUntunedBlocks = untunedNotes.size();
int existingUniqueNotesCount = 0;
for(Note n : song.uniqueNotes) {
if(noteBlocks.get(n.instrument()).get(n.note()) != null)
if (noteBlocks != null) {
for(HashMap<Byte, BlockPos> instrumentNotes : noteBlocks.values()) {
if (instrumentNotes != null) {
for (BlockPos pos : instrumentNotes.values()) {
if (pos != null) {
existingUniqueNotesCount++;
}
}
}
}
}
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;
if (running && tick == 0 && index == 0) {
pausePlaybackUntil = System.currentTimeMillis() + (long) (Math.abs(Main.config.delayPlaybackStartBySecs) * 1000);
} else {
pausePlaybackUntil = -1L;
}
tuneInitialUntunedBlocks = -1;
// Tuning finished
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<Byte, BlockPos> instrumentNotes : noteBlocks.values()) {
if (instrumentNotes != null) {
for (Map.Entry<Byte, BlockPos> entry : instrumentNotes.entrySet()) {
if (blockPosToTune.equals(entry.getValue())) {
targetNote = entry.getKey();
break;
}
}
}
if (targetNote != null) break;
}
}
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<BlockPos, Integer> entry : untunedNotes.entrySet()) {
if (entry.getValue() > lastTunedNote) {
blockPos = entry.getKey();
break;
}
}
// Find higher note or equal
if (blockPos == null) {
for (Map.Entry<BlockPos, Integer> entry : untunedNotes.entrySet()) {
if (entry.getValue() >= lastTunedNote) {
blockPos = entry.getKey();
break;
}
}
}
// 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!
if (targetNote != null) {
byte targetNotePrimitive = targetNote.byteValue();
int tuningSteps = targetNotePrimitive >= currentNote ? targetNotePrimitive - currentNote : (25 - currentNote) + targetNotePrimitive;
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));
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;
lastBlockPos = blockPos;
}
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);
int totalUntuned = untunedNotes.size();
int tunedCount = existingUniqueNotesCount - totalUntuned;
} else {
untunedNotes.remove(blockPosToTune);
}
}else if((playbackThread == null || !playbackThread.isAlive()) && running && Main.config.disableAsyncPlayback) {
// Sync playback (off by default). Replacement for playback thread
} else {
untunedNotes.remove(blockPosToTune); // Remove to avoid infinite loop
}
} else if (!untunedNotes.isEmpty()) {
}
}
}
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<Byte, BlockPos> 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;

View File

@ -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<KeyMappingListWidget.KeyMappingEntry> {
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<InputUtil.Key, Note> mappings) {
this.clearEntries();
for (Map.Entry<InputUtil.Key, Note> 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<KeyMappingEntry> {
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);
}
}
}

View File

@ -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
@ -38,9 +41,11 @@ public class DiscJockeyScreen extends Screen {
;
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,41 +214,47 @@ 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();
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) {
boolean folderEntryExists = SongLoader.FOLDERS.stream()
.anyMatch(f -> f == song.folder || findFolderByPathInSubfolders(f, song.folder.path) != null);
if (!folderEntryExists) {
} else {
SongListWidget.SongEntry entry = songListWidget.getSelectedSongOrNull();
if (entry != null) {
Main.SONG_PLAYER.start(entry.song);
client.setScreen(null);
if (song.folder.entry == null) {
song.folder.entry = new SongListWidget.FolderEntry(song.folder, songListWidget);
}
}
}
}
}).dimensions(width / 2 - 160, height - 61, 100, 20).build();
addDrawableChild(playButton);
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);
// 播放进度
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 = entry.song.uniqueNotes.size();
BlocksOverlay.amountOfNoteBlocks = song.uniqueNotes.size();
for (Note note : entry.song.uniqueNotes) {
for (Note note : song.uniqueNotes) {
ItemStack itemStack = Note.INSTRUMENT_BLOCKS.get(note.instrument()).asItem().getDefaultStack();
int index = -1;
@ -167,18 +276,12 @@ public class DiscJockeyScreen extends Screen {
}
}
}
} else {
BlocksOverlay.itemStacks = null;
client.setScreen(null);
}
}).dimensions(width / 2 + 60, height - 61, 100, 20).build());
// 打开文件夹
addDrawableChild(ButtonWidget.builder(OPEN_FOLDER, button -> {
private void openSongsFolder() {
try {
String folderPath = currentFolder != null ?
currentFolder.path :
Main.songsFolder.getAbsolutePath(); // 使用绝对路径
currentFolder.path : Main.songsFolder.getAbsolutePath();
File target = new File(folderPath);
if (!target.exists()) {
@ -188,7 +291,6 @@ public class DiscJockeyScreen extends Screen {
return;
}
// Windows 专用命令其他的不会
if (System.getProperty("os.name").toLowerCase().contains("win")) {
new ProcessBuilder("explorer.exe", "/select,", target.getAbsolutePath()).start();
} else {
@ -200,42 +302,112 @@ public class DiscJockeyScreen extends Screen {
Text.translatable(Main.MOD_ID+".screen.open_folder_failed")
.formatted(Formatting.RED));
}
}).dimensions(width / 2 - 160, height - 31, 100, 20).build());
// 重新加载
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 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<Song> 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<Path> 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;

View File

@ -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;
}
}

View File

@ -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<Byte, net.minecraft.util.math.BlockPos> 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<Byte, net.minecraft.util.math.BlockPos> instrumentNotes : Main.SONG_PLAYER.noteBlocks.values()) {
if (instrumentNotes != null) {
for (Map.Entry<Byte, net.minecraft.util.math.BlockPos> 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();
}
}

View File

@ -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<NoteBlockInstrument> 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);
}
}

View File

@ -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);
}
}

View File

@ -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..."
}

View File

@ -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": "正在重新调音音符盒..."
}

View File

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