From a219ab46b9e25fe381b80510ab69d7b6d9ec67c6 Mon Sep 17 00:00:00 2001 From: EnderKill98 Date: Tue, 16 Apr 2024 01:20:57 +0200 Subject: [PATCH 1/6] Fix a null pointer Seems to crash the playback thread when leaving while playing. In sync it could crash the entire client. The playback threads gets restarted when rejoining and playing again through. So not the cause for #16 or similar issues. --- src/main/java/semmiedev/disc_jockey/SongPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/semmiedev/disc_jockey/SongPlayer.java b/src/main/java/semmiedev/disc_jockey/SongPlayer.java index 8eca181..7c86e5c 100644 --- a/src/main/java/semmiedev/disc_jockey/SongPlayer.java +++ b/src/main/java/semmiedev/disc_jockey/SongPlayer.java @@ -150,7 +150,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; } From 3529607761fb47359f4abaa6809d1ffed2babc48 Mon Sep 17 00:00:00 2001 From: EnderKill98 Date: Tue, 16 Apr 2024 01:36:54 +0200 Subject: [PATCH 2/6] Add option to disable async playback. --- src/main/java/semmiedev/disc_jockey/Config.java | 1 + src/main/java/semmiedev/disc_jockey/SongPlayer.java | 13 +++++++++++++ .../resources/assets/disc_jockey/lang/en_us.json | 3 +++ 3 files changed, 17 insertions(+) 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/SongPlayer.java b/src/main/java/semmiedev/disc_jockey/SongPlayer.java index 7c86e5c..01320da 100644 --- a/src/main/java/semmiedev/disc_jockey/SongPlayer.java +++ b/src/main/java/semmiedev/disc_jockey/SongPlayer.java @@ -67,6 +67,11 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { 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) { @@ -417,6 +422,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..72564b9 100644 --- a/src/main/resources/assets/disc_jockey/lang/en_us.json +++ b/src/main/resources/assets/disc_jockey/lang/en_us.json @@ -28,6 +28,9 @@ "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" From 3e359b643570b054a19644140f3f98645386c699 Mon Sep 17 00:00:00 2001 From: EnderKill98 Date: Tue, 16 Apr 2024 01:40:45 +0200 Subject: [PATCH 3/6] Correct wrong assumption in comment --- src/main/java/semmiedev/disc_jockey/SongPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/semmiedev/disc_jockey/SongPlayer.java b/src/main/java/semmiedev/disc_jockey/SongPlayer.java index 01320da..1407e37 100644 --- a/src/main/java/semmiedev/disc_jockey/SongPlayer.java +++ b/src/main/java/semmiedev/disc_jockey/SongPlayer.java @@ -327,7 +327,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 { From 8de17aacc8b7d1fd244347383126444b78928bad Mon Sep 17 00:00:00 2001 From: EnderKill98 Date: Tue, 16 Apr 2024 02:45:47 +0200 Subject: [PATCH 4/6] Expose ability to map instruments via command --- .../disc_jockey/DiscjockeyCommand.java | 116 ++++++++++++++++++ .../assets/disc_jockey/lang/en_us.json | 8 ++ 2 files changed, 124 insertions(+) diff --git a/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java b/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java index 862d6a1..1c527c6 100644 --- a/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java +++ b/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java @@ -5,19 +5,32 @@ 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 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"); + commandDispatcher.register( literal("discjockey") .executes(context -> { @@ -98,6 +111,109 @@ 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(instrumentNames, builder)) + .executes(context -> { + String originalInstrumentStr = StringArgumentType.getString(context, "originalInstrument"); + String newInstrumentStr = StringArgumentType.getString(context, "newInstrument"); + 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) { + context.getSource().sendFeedback(Text.translatable(Main.MOD_ID + ".invalid_instrument", newInstrumentStr)); + return 0; + } + + 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().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; + }) + ) + ) + ); } 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 72564b9..0c4ab11 100644 --- a/src/main/resources/assets/disc_jockey/lang/en_us.json +++ b/src/main/resources/assets/disc_jockey/lang/en_us.json @@ -23,6 +23,14 @@ "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.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", From 281048f33a82c3c389bb6059269e75e7bf35d4ce Mon Sep 17 00:00:00 2001 From: EnderKill98 Date: Tue, 16 Apr 2024 02:56:42 +0200 Subject: [PATCH 5/6] Add ability to loop the same song via command --- .../disc_jockey/DiscjockeyCommand.java | 19 ++++++++++++++++++- .../semmiedev/disc_jockey/SongPlayer.java | 4 ++++ .../assets/disc_jockey/lang/en_us.json | 3 +++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java b/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java index 1c527c6..e0e863b 100644 --- a/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java +++ b/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java @@ -214,7 +214,24 @@ public class DiscjockeyCommand { ) ) - + .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 1407e37..78eb6dd 100644 --- a/src/main/java/semmiedev/disc_jockey/SongPlayer.java +++ b/src/main/java/semmiedev/disc_jockey/SongPlayer.java @@ -60,6 +60,7 @@ 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); @@ -205,6 +206,9 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { if (index >= song.notes.length) { stop(); didSongReachEnd = true; + if(loopSong) { + start(song); + } break; } } else { 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 0c4ab11..553df6c 100644 --- a/src/main/resources/assets/disc_jockey/lang/en_us.json +++ b/src/main/resources/assets/disc_jockey/lang/en_us.json @@ -31,6 +31,9 @@ "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", From 26d925f4fab9387ad55d2f6f451b63853954d6a3 Mon Sep 17 00:00:00 2001 From: EnderKill98 Date: Wed, 17 Apr 2024 17:26:15 +0200 Subject: [PATCH 6/6] Add option and support to specify "nothing" as a mapped instrument Allows to discard instruments entirely --- .../disc_jockey/DiscjockeyCommand.java | 16 ++++++--- .../semmiedev/disc_jockey/SongPlayer.java | 36 ++++++++++++++++--- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java b/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java index e0e863b..33898dc 100644 --- a/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java +++ b/src/main/java/semmiedev/disc_jockey/DiscjockeyCommand.java @@ -10,6 +10,7 @@ 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; @@ -30,6 +31,8 @@ public class DiscjockeyCommand { } final ArrayList instrumentNamesAndAll = new ArrayList<>(instrumentNames); instrumentNamesAndAll.add("all"); + final ArrayList instrumentNamesAndNothing = new ArrayList<>(instrumentNames); + instrumentNamesAndNothing.add("nothing"); commandDispatcher.register( literal("discjockey") @@ -120,11 +123,11 @@ public class DiscjockeyCommand { .then(argument("originalInstrument", StringArgumentType.word()) .suggests((context, builder) -> CommandSource.suggestMatching(instrumentNamesAndAll, builder)) .then(argument("newInstrument", StringArgumentType.word()) - .suggests((context, builder) -> CommandSource.suggestMatching(instrumentNames, builder)) + .suggests((context, builder) -> CommandSource.suggestMatching(instrumentNamesAndNothing, builder)) .executes(context -> { String originalInstrumentStr = StringArgumentType.getString(context, "originalInstrument"); String newInstrumentStr = StringArgumentType.getString(context, "newInstrument"); - Instrument originalInstrument = null, newInstrument = null; + @Nullable Instrument originalInstrument = null, newInstrument = null; for(Instrument maybeInstrument : Instrument.values()) { if(maybeInstrument.toString().equalsIgnoreCase(originalInstrumentStr)) { originalInstrument = maybeInstrument; @@ -139,11 +142,14 @@ public class DiscjockeyCommand { return 0; } - if(newInstrument == null) { + 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()) { @@ -198,8 +204,8 @@ public class DiscjockeyCommand { } maps .append(entry.getKey().toString().toLowerCase()) - .append(" -> ") - .append(entry.getValue().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; diff --git a/src/main/java/semmiedev/disc_jockey/SongPlayer.java b/src/main/java/semmiedev/disc_jockey/SongPlayer.java index 78eb6dd..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; @@ -66,7 +67,7 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick { 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; @@ -164,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)); @@ -273,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; @@ -282,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) { @@ -310,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); @@ -369,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;