diff --git a/src/main/java/semmiedev/disc_jockey/Config.java b/src/main/java/semmiedev/disc_jockey/Config.java index 31b9bc6..c5b094c 100644 --- a/src/main/java/semmiedev/disc_jockey/Config.java +++ b/src/main/java/semmiedev/disc_jockey/Config.java @@ -9,6 +9,7 @@ import java.util.ArrayList; @me.shedaniel.autoconfig.annotation.Config.Gui.Background("textures/block/note_block.png") public class Config implements ConfigData { public boolean hideWarning; + @ConfigEntry.Gui.Tooltip(count = 2) public boolean disableAsyncPlayback; @ConfigEntry.Gui.Excluded @ConfigEntry.Gui.Tooltip(count = 2) public boolean monoNoteBlocks; @ConfigEntry.Gui.Excluded diff --git a/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java b/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java index 862d6a1..33898dc 100644 --- a/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java +++ b/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java @@ -5,19 +5,35 @@ import com.mojang.brigadier.arguments.FloatArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.block.enums.Instrument; import net.minecraft.client.MinecraftClient; import net.minecraft.command.CommandSource; import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; import semmiedev.disc_jockey.gui.screen.DiscJockeyScreen; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Map; import java.util.Optional; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; public class DiscjockeyCommand { + + public static void register(CommandDispatcher commandDispatcher) { + final ArrayList instrumentNames = new ArrayList<>(); + for (Instrument instrument : Instrument.values()) { + instrumentNames.add(instrument.toString().toLowerCase()); + } + final ArrayList instrumentNamesAndAll = new ArrayList<>(instrumentNames); + instrumentNamesAndAll.add("all"); + final ArrayList instrumentNamesAndNothing = new ArrayList<>(instrumentNames); + instrumentNamesAndNothing.add("nothing"); + commandDispatcher.register( literal("discjockey") .executes(context -> { @@ -98,7 +114,130 @@ public class DiscjockeyCommand { } }) ) + .then(literal("remapInstruments") + .executes(context -> { + context.getSource().sendFeedback(Text.translatable(Main.MOD_ID + ".instrument_info")); + return 0; + }) + .then(literal("map") + .then(argument("originalInstrument", StringArgumentType.word()) + .suggests((context, builder) -> CommandSource.suggestMatching(instrumentNamesAndAll, builder)) + .then(argument("newInstrument", StringArgumentType.word()) + .suggests((context, builder) -> CommandSource.suggestMatching(instrumentNamesAndNothing, builder)) + .executes(context -> { + String originalInstrumentStr = StringArgumentType.getString(context, "originalInstrument"); + String newInstrumentStr = StringArgumentType.getString(context, "newInstrument"); + @Nullable Instrument originalInstrument = null, newInstrument = null; + for(Instrument maybeInstrument : Instrument.values()) { + if(maybeInstrument.toString().equalsIgnoreCase(originalInstrumentStr)) { + originalInstrument = maybeInstrument; + } + if(maybeInstrument.toString().equalsIgnoreCase(newInstrumentStr)) { + newInstrument = maybeInstrument; + } + } + if(originalInstrument == null && !originalInstrumentStr.equalsIgnoreCase("all")) { + context.getSource().sendFeedback(Text.translatable(Main.MOD_ID + ".invalid_instrument", originalInstrumentStr)); + return 0; + } + + if(newInstrument == null && !newInstrumentStr.equalsIgnoreCase("nothing")) { + context.getSource().sendFeedback(Text.translatable(Main.MOD_ID + ".invalid_instrument", newInstrumentStr)); + return 0; + } + + // (originalInstrument == null) means: all instruments + // (newInstrument == null) means: nothing (represented by null in hashmap, so no special handling below) + + if(originalInstrument == null) { + // All instruments + for(Instrument instrument : Instrument.values()) { + Main.SONG_PLAYER.instrumentMap.put(instrument, newInstrument); + } + context.getSource().sendFeedback(Text.translatable(Main.MOD_ID + ".instrument_mapped_all", newInstrumentStr.toLowerCase())); + }else { + Main.SONG_PLAYER.instrumentMap.put(originalInstrument, newInstrument); + context.getSource().sendFeedback(Text.translatable(Main.MOD_ID + ".instrument_mapped", originalInstrumentStr.toLowerCase(), newInstrumentStr.toLowerCase())); + } + return 1; + }) + ) + ) + ) + .then(literal("unmap") + .then(argument("instrument", StringArgumentType.word()) + .suggests((context, builder) -> CommandSource.suggestMatching(instrumentNames, builder)) + .executes(context -> { + String instrumentStr = StringArgumentType.getString(context, "instrument"); + + Instrument instrument = null; + for(Instrument maybeInstrument : Instrument.values()) { + if(maybeInstrument.toString().equalsIgnoreCase(instrumentStr)) { + instrument = maybeInstrument; + break; + } + } + + if(instrument == null) { + context.getSource().sendFeedback(Text.translatable(Main.MOD_ID + ".invalid_instrument", instrumentStr)); + return 0; + } + + Main.SONG_PLAYER.instrumentMap.remove(instrument); + context.getSource().sendFeedback(Text.translatable(Main.MOD_ID + ".instrument_unmapped", instrumentStr.toLowerCase())); + return 1; + }) + ) + ) + .then(literal("show") + .executes(context -> { + if(Main.SONG_PLAYER.instrumentMap.isEmpty()) { + context.getSource().sendFeedback(Text.translatable(Main.MOD_ID + ".no_mapped_instruments")); + return 1; + } + + StringBuilder maps = new StringBuilder(); + for(Map.Entry entry : Main.SONG_PLAYER.instrumentMap.entrySet()) { + if(maps.length() > 0) { + maps.append(", "); + } + maps + .append(entry.getKey().toString().toLowerCase()) + .append("->") + .append(entry.getValue() == null ? "nothing" : entry.getValue().toString().toLowerCase()); + } + context.getSource().sendFeedback(Text.translatable(Main.MOD_ID + ".mapped_instruments", maps.toString())); + return 1; + }) + ) + .then(literal("clear") + .executes(context -> { + Main.SONG_PLAYER.instrumentMap.clear(); + context.getSource().sendFeedback(Text.translatable(Main.MOD_ID + ".instrument_maps_cleared")); + return 1; + }) + ) + ) + + .then(literal("loop") + .executes(context -> { + context.getSource().sendFeedback(Text.translatable(Main.MOD_ID + ".loop_status", Main.SONG_PLAYER.loopSong ? "yes" : "no")); + return 1; + }) + .then(literal("yes") + .executes(context -> { + Main.SONG_PLAYER.loopSong = true; + context.getSource().sendFeedback(Text.translatable(Main.MOD_ID + ".loop_enabled")); + return 1; + })) + .then(literal("no") + .executes(context -> { + Main.SONG_PLAYER.loopSong = false; + context.getSource().sendFeedback(Text.translatable(Main.MOD_ID + ".loop_disabled")); + return 1; + })) + ) ); } diff --git a/src/main/java/semmiedev/disc_jockey/SongPlayer.java b/src/main/java/semmiedev/disc_jockey/SongPlayer.java index 8eca181..a7e73c0 100644 --- a/src/main/java/semmiedev/disc_jockey/SongPlayer.java +++ b/src/main/java/semmiedev/disc_jockey/SongPlayer.java @@ -21,6 +21,7 @@ import net.minecraft.util.hit.BlockHitResult; import net.minecraft.util.math.*; import net.minecraft.world.GameMode; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.HashMap; @@ -60,13 +61,19 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { private int tuneInitialUntunedBlocks = -1; private HashMap> notePredictions = new HashMap<>(); public boolean didSongReachEnd = false; + public boolean loopSong = false; public SongPlayer() { Main.TICK_LISTENERS.add(this); } - public @NotNull HashMap instrumentMap = new HashMap<>(); // Toy + public @NotNull HashMap instrumentMap = new HashMap<>(); // Toy public synchronized void startPlaybackThread() { + if(Main.config.disableAsyncPlayback) { + playbackThread = null; + return; + } + this.playbackThread = new Thread(() -> { Thread ownThread = this.playbackThread; while(ownThread == this.playbackThread) { @@ -150,7 +157,7 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { 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.getTranslatableName()).formatted(Formatting.RED)); + client.inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.invalid_game_mode", gameMode == null ? "unknown" : gameMode.getTranslatableName()).formatted(Formatting.RED)); stop(); return; } @@ -158,7 +165,12 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { long note = song.notes[index]; final long now = System.currentTimeMillis(); if ((short)note <= Math.round(tick)) { - BlockPos blockPos = noteBlocks.get(Note.INSTRUMENTS[(byte)(note >> Note.INSTRUMENT_SHIFT)]).get((byte)(note >> Note.NOTE_SHIFT)); + @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)); @@ -200,6 +212,9 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { if (index >= song.notes.length) { stop(); didSongReachEnd = true; + if(loopSong) { + start(song); + } break; } } else { @@ -264,6 +279,13 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { if(!instrumentMap.isEmpty()) { HashMap> newNoteblocksForInstrument = new HashMap<>(); for(Instrument orig : noteblocksForInstrument.keySet()) { + Instrument mappedInstrument = instrumentMap.getOrDefault(orig, orig); + if(mappedInstrument == null) { + // Instrument got likely mapped to "nothing" + newNoteblocksForInstrument.put(orig, null); + continue; + } + newNoteblocksForInstrument.put(orig, noteblocksForInstrument.getOrDefault(instrumentMap.getOrDefault(orig, orig), new ArrayList<>())); } noteblocksForInstrument = newNoteblocksForInstrument; @@ -273,6 +295,12 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { 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) { @@ -301,7 +329,9 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { HashMap missing = new HashMap<>(); for (Note note : missingNotes) { - Block block = Note.INSTRUMENT_BLOCKS.get(instrumentMap.getOrDefault(note.instrument, note.instrument)); + Instrument 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); @@ -322,7 +352,7 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { } if(lastInteractAt != -1L) { - // Paper allows 8 interacts per 300 ms + // 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.0 / 8.0)); availableInteracts = Math.min(8f, Math.max(0f, availableInteracts)); }else { @@ -360,7 +390,14 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { if(tuneInitialUntunedBlocks == -1 || tuneInitialUntunedBlocks < untunedNotes.size()) tuneInitialUntunedBlocks = untunedNotes.size(); - if(untunedNotes.isEmpty() && fullyTunedBlocks == song.uniqueNotes.size()) { + int existingUniqueNotesCount = 0; + for(Note n : song.uniqueNotes) { + if(noteBlocks.get(n.instrument).get(n.note) != null) + existingUniqueNotesCount++; + } + System.out.println("existingUniqueNotesCount = " + 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; @@ -417,6 +454,14 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { //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 + try { + tickPlayback(); + }catch (Exception ex) { + ex.printStackTrace(); + stop(); + } } } 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 adf62fd..553df6c 100644 --- a/src/main/resources/assets/disc_jockey/lang/en_us.json +++ b/src/main/resources/assets/disc_jockey/lang/en_us.json @@ -23,11 +23,25 @@ "disc_jockey.info_tuning": "Tuning: (Speed: %s)", "disc_jockey.info_playing": "Playing: [%s/%s] %s (Speed: %s)", "disc_jockey.info_finished": "Finished: %s (Speed: %s)", + "disc_jockey.instrument_info": "This maps instruments to be played by noteblocks for a different instrument instead.", + "disc_jockey.invalid_instrument": "Invalid instrument: %s", + "disc_jockey.instrument_mapped": "Mapped %s to %s", + "disc_jockey.instrument_mapped_all": "Mapped all instruments to %s", + "disc_jockey.instrument_unmapped": "Unmapped %s", + "disc_jockey.mapped_instruments": "Mapped instruments: %s", + "disc_jockey.no_mapped_instruments": "No instruments mapped, yet.", + "disc_jockey.instrument_maps_cleared": "Instrument mappings cleared.", + "disc_jockey.loop_status": "Loop song: %s", + "disc_jockey.loop_enabled": "Enabled looping of current song.", + "disc_jockey.loop_disabled": "Disabled looping of current song.", "disc_jockey.warning": "WARNING!!! This mod is very likely to get false flagged as hacks, please contact a server administrator before using this mod! (You can disable this warning in the mod settings)", "key.category.disc_jockey": "Disc Jockey", "disc_jockey.key_bind.open_screen": "Open song selection screen", "text.autoconfig.disc_jockey.title": "Disc Jockey", "text.autoconfig.disc_jockey.option.hideWarning": "Hide Warning", + "text.autoconfig.disc_jockey.option.disableAsyncPlayback": "Disable Async Playback", + "text.autoconfig.disc_jockey.option.disableAsyncPlayback.@Tooltip[0]": "Will force notes to play synchronously with client ticks instead of in a separate thread.", + "text.autoconfig.disc_jockey.option.disableAsyncPlayback.@Tooltip[1]": "This can lead to performance loss, especially when you client has low or inconsistent fps but can fix issues when playback does not happen at all.", "text.autoconfig.disc_jockey.option.monoNoteBlocks": "Non-Directional Note Block Sounds", "text.autoconfig.disc_jockey.option.monoNoteBlocks.@Tooltip[0]": "Makes all note block sounds when playing a song non-directional, creating a more pleasurable listening experience (clientside)", "text.autoconfig.disc_jockey.option.monoNoteBlocks.@Tooltip[1]": "If you don't know what that means, I recommend you just try it and hear the difference"