diff --git a/src/main/java/io/github/homchom/recode/mod/commands/impl/other/NBSCommand.java b/src/main/java/io/github/homchom/recode/mod/commands/impl/other/NBSCommand.java index 41983d6ba..839ec0aa0 100644 --- a/src/main/java/io/github/homchom/recode/mod/commands/impl/other/NBSCommand.java +++ b/src/main/java/io/github/homchom/recode/mod/commands/impl/other/NBSCommand.java @@ -8,6 +8,7 @@ import io.github.homchom.recode.mod.features.commands.nbs.NBSDecoder; import io.github.homchom.recode.mod.features.commands.nbs.NBSToTemplate; import io.github.homchom.recode.mod.features.commands.nbs.SongData; +import io.github.homchom.recode.mod.features.commands.nbs.alt.*; import io.github.homchom.recode.mod.features.commands.nbs.exceptions.OutdatedNBSException; import io.github.homchom.recode.sys.file.ExternalFile; import io.github.homchom.recode.sys.hypercube.templates.TemplateUtil; @@ -59,6 +60,36 @@ public static void loadNbs(File file, String fileName) { }); } + public static void loadNbsU(File file, String fileName) { + LegacyRecode.executor.submit(() -> { + try { + SongData d = NBSDecoderU.parse(file); + String code = new NBSToTemplate(d).convert(); + ItemStack stack = new ItemStack(Items.NOTE_BLOCK); + TemplateUtil.compressTemplateNBT(stack, d.getName(), d.getAuthor(), code); + + if (d.getName().length() == 0) { + String name; + if (d.getFileName().indexOf(".") > 0) { + name = d.getFileName().substring(0, d.getFileName().lastIndexOf(".")); + } else { + name = d.getFileName(); + } + stack.setHoverName(Component.literal("§5SONG§7 -§f " + name)); + } else { + stack.setHoverName(Component.literal("§5SONG§7 -§f " + d.getName())); + } + + ToasterUtil.sendToaster("NBS Loaded!", fileName, SystemToast.SystemToastId.NARRATOR_TOGGLE); + ItemUtil.giveCreativeItem(stack, true); + } catch (OutdatedNBSException e) { + ToasterUtil.sendToaster("§cLoading Error!", "Unsupported file version", SystemToast.SystemToastId.NARRATOR_TOGGLE); + } catch (IOException e) { + ToasterUtil.sendToaster("§cLoading Error!", "Invalid file", SystemToast.SystemToastId.NARRATOR_TOGGLE); + } + }); + } + @Override public void register(Minecraft mc, CommandDispatcher cd, CommandBuildContext context) { cd.register(ArgBuilder.literal("nbs") @@ -73,6 +104,17 @@ public void register(Minecraft mc, CommandDispatcher return 1; }) ) + ).then(ArgBuilder.literal("load2") + .then(ArgBuilder.argument("filename", PathArgumentType.folder(ExternalFile.NBS_FILES.getPath(), true)) + .executes(ctx -> { + if (this.isCreative(mc)) { + File file = PathArgumentType.getPath(ctx, "filename").toFile(); + + loadNbsU(file, file.getName()); + } + return 1; + }) + ) ) .then(ArgBuilder.literal("player") .executes(ctx -> { diff --git a/src/main/java/io/github/homchom/recode/mod/features/commands/NbsSearchMenu.java b/src/main/java/io/github/homchom/recode/mod/features/commands/NbsSearchMenu.java index eadcdcd57..7bd18eebd 100644 --- a/src/main/java/io/github/homchom/recode/mod/features/commands/NbsSearchMenu.java +++ b/src/main/java/io/github/homchom/recode/mod/features/commands/NbsSearchMenu.java @@ -126,7 +126,7 @@ public void open(String... args) throws CommandSyntaxException { int length = Integer .parseInt(notearr[notearr.length - 1].split(":")[0]); SongData d = new SongData("Song " + id, "Recode", 20f, - length, notes, "", "", 1, 0, 0); + length, notes, "", "", 1, 0, 0, new String[1]); String code = new NBSToTemplate(d).convert(); diff --git a/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/NBSDecoder.java b/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/NBSDecoder.java index b03331b8a..957d4c4f4 100644 --- a/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/NBSDecoder.java +++ b/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/NBSDecoder.java @@ -165,13 +165,14 @@ private static SongData parse(InputStream inputStream, File songFile) throws IOE customInstruments = dataInputStream.readByte(); int[] customPitchList = new int[customInstruments]; + String[] customNameList = new String[customInstruments]; if (customInstruments >= 1) { for (int i = 0; i < customInstruments; i++) { int instrumentOffset = vanillaInstruments + customInstruments; int instrumentPitch = 0; - readString(dataInputStream); //Instrument name + customNameList[i] = readString(dataInputStream); //Instrument name readString(dataInputStream); //Sound file instrumentPitch = dataInputStream.readByte(); //Sound pitch @@ -223,7 +224,7 @@ private static SongData parse(InputStream inputStream, File songFile) throws IOE } - return new SongData(title, author, speed, (int) Math.ceil((length + 1.0) / (4 * timeSignature)) * (4 * timeSignature), stringBuilder.toString(), file, layerStringBuilder.toString(), (loopTick + 1), loopCount, customInstruments); + return new SongData(title, author, speed, (int) Math.ceil((length + 1.0) / (4 * timeSignature)) * (4 * timeSignature), stringBuilder.toString(), file, layerStringBuilder.toString(), (loopTick + 1), loopCount, customInstruments, customNameList); } private static short readShort(DataInputStream dataInputStream) throws IOException { diff --git a/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/NBSToTemplate.java b/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/NBSToTemplate.java index 37f9f3fa6..3363fb009 100644 --- a/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/NBSToTemplate.java +++ b/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/NBSToTemplate.java @@ -16,6 +16,7 @@ public class NBSToTemplate { final int loopTick; final int loopCount; final int customInstrumentCount; + final String[] customInstrumentNames; String name; String author; @@ -33,6 +34,7 @@ public NBSToTemplate(SongData song) { this.loopTick = song.getLoopTick(); this.loopCount = song.getLoopCount(); this.customInstrumentCount = song.getCustomInstrumentCount(); + this.customInstrumentNames = song.getCustomInstrumentNames(); } public String convert() { @@ -43,7 +45,6 @@ public String convert() { StringBuilder instList = new StringBuilder(); String songTempo = new BigDecimal(this.speed).stripTrailingZeros().toPlainString(); - if (name.length() == 0) { if (filename.indexOf(".") > 0) { name = filename.substring(0, filename.lastIndexOf(".")); @@ -60,7 +61,6 @@ public String convert() { boolean chestInited = false; int noteCount = 0; boolean finalNote = false; - for (int i = 0; i < songData.length; i++) { boolean closeChest = false; if (slot == 1) { @@ -73,7 +73,6 @@ public String convert() { if (slot >= 27) { closeChest = true; } - if (!closeChest) { String currentNote = songData[i]; String revertString = currentNotes.toString(); @@ -84,7 +83,6 @@ public String convert() { currentNotes.append("=").append(currentNote); } noteCount++; - if (currentNotes.length() > 1930) { currentNotes = new StringBuilder(revertString); currentBlock.append(String.format(",{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"%s\"}},\"slot\":%d}", currentNotes, slot)); @@ -111,7 +109,6 @@ public String convert() { } else { varActionType = "AppendValue"; } - currentBlock.append(String.format("]},\"action\":\"%s\"},", varActionType)); code.append(currentBlock); currentBlock.setLength(0); @@ -124,7 +121,6 @@ public String convert() { slot = 1; } } - //CreateList: instrumentNames if (customInstrumentCount == 0) { code.append("{\"id\":\"block\",\"block\":\"set_var\",\"args\":{\"items\":[{\"item\":{\"id\":\"var\",\"data\":{\"name\":\"instrumentNames\",\"scope\":\"local\"}},\"slot\":0},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Harp\"}},\"slot\":1},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Bass\"}},\"slot\":2},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Bass Drum\"}},\"slot\":3},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Snare Drum\"}},\"slot\":4},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Click\"}},\"slot\":5},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Guitar\"}},\"slot\":6},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Flute\"}},\"slot\":7},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Bell\"}},\"slot\":8},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Chime\"}},\"slot\":9},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Xylophone\"}},\"slot\":10},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Iron Xylophone\"}},\"slot\":11},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Cow Bell\"}},\"slot\":12},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Didgeridoo\"}},\"slot\":13},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Bit\"}},\"slot\":14},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Banjo\"}},\"slot\":15},{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"Pling\"}},\"slot\":16}]},\"action\":\"CreateList\"},"); @@ -134,12 +130,22 @@ public String convert() { int currentSlot; currentSlot = 17; + int currentFails = 0; + for (int currentInstID = 1; currentInstID <= customInstrumentCount; currentInstID++) { + String currentName = "Custom Instrument"; + currentName = customInstrumentNames[currentInstID - 1]; + if(currentName == null || currentName.matches("^(\\s+)$") || currentName == ""){ + currentFails++; + currentName = ""; + } + String formatted; if (currentInstID == customInstrumentCount) { - instList.append(String.format("{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"\"}},\"slot\":%d}", currentInstID, currentSlot)); + formatted = "{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"" + currentName + "\"}},\"slot\":" + currentSlot + "}"; } else { - instList.append(String.format("{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"\"}},\"slot\":%d},", currentInstID, currentSlot)); + formatted = "{\"item\":{\"id\":\"txt\",\"data\":{\"name\":\"" + currentName + "\"}},\"slot\":" + currentSlot + "},"; } + instList.append(formatted); currentSlot++; } instList.append("]},\"action\":\"CreateList\"},"); diff --git a/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/SongData.java b/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/SongData.java index 802634d13..913de8a43 100644 --- a/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/SongData.java +++ b/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/SongData.java @@ -7,13 +7,14 @@ public class SongData { final int loopTick; final int loopCount; final int customInstrumentCount; + final String[] customInstrumentNames; String name; String author; float speed; String fileName; String notes; - public SongData(String name, String author, float speed, int length, String notes, String fileName, String layers, int loopTick, int loopCount, int customInstrumentCount) { + public SongData(String name, String author, float speed, int length, String notes, String fileName, String layers, int loopTick, int loopCount, int customInstrumentCount, String[] customInstrumentNames) { this.name = name; this.author = author; this.speed = speed; @@ -24,6 +25,7 @@ public SongData(String name, String author, float speed, int length, String note this.loopTick = loopTick; this.loopCount = loopCount; this.customInstrumentCount = customInstrumentCount; + this.customInstrumentNames = customInstrumentNames; } public String getName() { @@ -85,4 +87,8 @@ public int getLoopCount() { public int getCustomInstrumentCount() { return customInstrumentCount; } + + public String[] getCustomInstrumentNames() { + return customInstrumentNames; + } } diff --git a/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/alt/NBSDecoderU.java b/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/alt/NBSDecoderU.java new file mode 100644 index 000000000..74780d2a6 --- /dev/null +++ b/src/main/java/io/github/homchom/recode/mod/features/commands/nbs/alt/NBSDecoderU.java @@ -0,0 +1,274 @@ +package io.github.homchom.recode.mod.features.commands.nbs.alt; + +import io.github.homchom.recode.mod.features.commands.nbs.*; +import io.github.homchom.recode.mod.features.commands.nbs.exceptions.*; +import io.github.homchom.recode.sys.player.chat.*; + +import java.io.*; +import java.math.*; + +// Credit to https://github.com/koca2000/NoteBlockAPI/blob/master/src/main/java/com/xxmicloxx/NoteBlockAPI/NBSDecoder.java +public class NBSDecoderU { + + public static SongData parse(File songFile) throws IOException, OutdatedNBSException { + try { + return parse(new FileInputStream(songFile), songFile); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return null; + } + + private static SongData parse(InputStream inputStream, File songFile) throws IOException, OutdatedNBSException { + String title = ""; + String author = ""; + String file = songFile.getName(); + float speed = 0f; + float actualSpeed = 0f; + short timeSignature = 4; + int loopTick = 0; + int loopCount = 0; + int vanillaInstruments = 9; + + StringBuilder stringBuilder = new StringBuilder(); + StringBuilder layerStringBuilder = new StringBuilder(); + + DataInputStream dataInputStream = new DataInputStream(inputStream); + short length = readShort(dataInputStream); + int nbsversion = 0; + if (length == 0) { + nbsversion = dataInputStream.readByte(); + vanillaInstruments = dataInputStream.readByte(); + if (nbsversion >= 3) { + length = readShort(dataInputStream); + } else if (nbsversion == 1 || nbsversion == 2) { + throw new OutdatedNBSException(); + } + } else { + ChatUtil.sendMessage("§a§lHEY!§r Looks like you are using original Note Block Studio. I recommend you to use §bOpen Note Block Studio§r, which is an unofficial continuation of Note Block Studio!"); + } + short layers = readShort(dataInputStream); //song height + title = readString(dataInputStream); //title + author = readString(dataInputStream); //author + readString(dataInputStream); //original author + String description = readString(dataInputStream); //description + actualSpeed = readShort(dataInputStream); //speed + speed = actualSpeed / 100f; //speed + dataInputStream.readBoolean(); //autosave + dataInputStream.readByte(); //autosave duration + timeSignature = dataInputStream.readByte(); //time signature + readInt(dataInputStream); //minutes spent + readInt(dataInputStream); //left clicks + readInt(dataInputStream); //right clicks + readInt(dataInputStream); //blocks added + readInt(dataInputStream); //nlocks removed + readString(dataInputStream); // mid or schematic filename + if (nbsversion >= 4) { + dataInputStream.readByte(); //loop on/off + loopCount = dataInputStream.readByte(); //loop count + loopTick = readShort(dataInputStream); //loop start tick + } + + short tick = -1; + String[][] addStringList = new String[layers][length + 1]; + int[][] instrumentList = new int[layers][length + 1]; + int[][] pitchList = new int[layers][length + 1]; + int[][] finepitchList = new int[layers][length + 1]; + int[][] velocityList = new int[layers][length + 1]; + int[][] panningList = new int[layers][length + 1]; + boolean[] columnExistence = new boolean[length + 1]; + boolean[][] noteExistence = new boolean[layers][length + 1]; + boolean firstNoted = false; + + while (true) { //Read notes + short t = readShort(dataInputStream); + if (t == 0) { + break; + } + tick += t; + + columnExistence[tick] = true; + + short layer = -1; + while (true) { + short jumpLayers = readShort(dataInputStream); + if (jumpLayers == 0) { + break; + } + layer += jumpLayers; + byte instrument = dataInputStream.readByte(); + byte note = dataInputStream.readByte(); + byte velocity = 100; + int panning = 100; + short finepitch = 0; + if (nbsversion >= 4) { + velocity = dataInputStream.readByte(); + panning = Byte.toUnsignedInt(dataInputStream.readByte()); + finepitch = readShort(dataInputStream); + } + + instrumentList[layer][tick] = instrument; + pitchList[layer][tick] = note; + finepitchList[layer][tick] = finepitch; + velocityList[layer][tick] = velocity; + panningList[layer][tick] = panning; + noteExistence[layer][tick] = true; + } + } + + for (int i = 0; i < layers; i++) { //Read layer data + + String name = readString(dataInputStream); + + if (nbsversion >= 4) { + dataInputStream.readByte(); + } + + byte volume = dataInputStream.readByte(); + int panning = 100; + + if (nbsversion >= 2) { + panning = Byte.toUnsignedInt(dataInputStream.readByte()); + } + + for (int currentTick = 0; currentTick < length + 1; currentTick++) { + boolean noteExists = noteExistence[i][currentTick]; + if (noteExists) { + + int noteVelocity = velocityList[i][currentTick]; + int notePanning = panningList[i][currentTick]; + + double averageVelocity = noteVelocity * (volume / 100d); + double averagePanning = (notePanning + panning) / 2d; + + double preFinalPanning = (averagePanning - 100) / 50; + + String finalVelocity = new BigDecimal(averageVelocity).setScale(3, BigDecimal.ROUND_FLOOR).stripTrailingZeros().toPlainString(); + String finalPanning = new BigDecimal(preFinalPanning).setScale(3, BigDecimal.ROUND_FLOOR).stripTrailingZeros().toPlainString(); + + String finalString; + if (preFinalPanning == 0) { + finalString = "," + finalVelocity; + } else { + finalString = "," + finalVelocity + "," + finalPanning; + } + addStringList[i][currentTick] = finalString; + } + } + + String finalLayerVolume = new BigDecimal(volume).setScale(3, BigDecimal.ROUND_FLOOR).stripTrailingZeros().toPlainString(); + String finalLayerPanning = new BigDecimal(panning).setScale(3, BigDecimal.ROUND_FLOOR).stripTrailingZeros().toPlainString(); + + layerStringBuilder.append("=").append(finalLayerVolume).append(",").append(finalLayerPanning); + } + + int customInstruments = 0; + customInstruments = dataInputStream.readByte(); + + int[] customPitchList = new int[customInstruments]; + String[] customNameList = new String[customInstruments]; + + if (customInstruments >= 1) { + for (int i = 0; i < customInstruments; i++) { + int instrumentOffset = vanillaInstruments + customInstruments; + int instrumentPitch = 0; + + customNameList[i] = readString(dataInputStream); //Instrument name + readString(dataInputStream); //Sound file + + instrumentPitch = dataInputStream.readByte(); //Sound pitch + + customPitchList[i] = instrumentPitch; + + dataInputStream.readByte(); //Press key + } + } + + dataInputStream.close(); + + for (int currentTick = 0; currentTick < length + 1; currentTick++) { + boolean columnExists = columnExistence[currentTick]; + if (columnExists) { + StringBuilder columnStringBuilder = new StringBuilder(); + if (!firstNoted) { + columnStringBuilder.append(currentTick + 1); + firstNoted = true; + } else { + columnStringBuilder.append("=").append(currentTick + 1); + } + boolean firstAppend = true; + for (int i = 0; i < layers; i++) { + boolean noteExists = noteExistence[i][currentTick]; + if (noteExists) { + String laterNoteString = addStringList[i][currentTick]; + + int noteInstrument = instrumentList[i][currentTick]; + int noteKey = pitchList[i][currentTick]; + int noteFinePitch = finepitchList[i][currentTick]; + int noteKeyOffset = 0; + + + if (noteInstrument >= vanillaInstruments) { + int instrumentId = noteInstrument - vanillaInstruments; + noteKeyOffset = customPitchList[instrumentId] - 45; + } + if (firstAppend) { + columnStringBuilder.append(":").append(noteInstrument + 1).append(",").append(getMinecraftPitch(noteKey + (double) noteFinePitch / 100d, noteKeyOffset)).append(laterNoteString); + firstAppend = false; + } else { + columnStringBuilder.append(";").append(noteInstrument + 1).append(",").append(getMinecraftPitch(noteKey + (double) noteFinePitch / 100d, noteKeyOffset)).append(laterNoteString); + } + } + } + stringBuilder.append(columnStringBuilder); + } + } + + + return new SongData(title, author, speed, (int) Math.ceil((length + 1.0) / (4 * timeSignature)) * (4 * timeSignature), stringBuilder.toString(), file, layerStringBuilder.toString(), (loopTick + 1), loopCount, customInstruments, customNameList); + } + + private static short readShort(DataInputStream dataInputStream) throws IOException { + int byte1 = dataInputStream.readUnsignedByte(); + int byte2 = dataInputStream.readUnsignedByte(); + return (short) (byte1 + (byte2 << 8)); + } + + private static int readInt(DataInputStream dataInputStream) throws IOException { + int byte1 = dataInputStream.readUnsignedByte(); + int byte2 = dataInputStream.readUnsignedByte(); + int byte3 = dataInputStream.readUnsignedByte(); + int byte4 = dataInputStream.readUnsignedByte(); + return (byte1 + (byte2 << 8) + (byte3 << 16) + (byte4 << 24)); + } + + private static String readString(DataInputStream dataInputStream) throws IOException { + int length = readInt(dataInputStream); + StringBuilder builder = new StringBuilder(length); + for (; length > 0; --length) { + char c = (char) dataInputStream.readByte(); + if (c == (char) 0x0D) { + c = ' '; + } + builder.append(c); + } + return builder.toString(); + } + + private static int getMinecraftPitch(double key, double offset) { + + //if (key < 33) key -= 9; + //else if (key > 57) key -= 57; + //else key -= 33; + + key += offset; + + double finalValue = (0.5 * (Math.pow(2, (key / 12)))) * 1000; + + return (int) key * 1000; + } + + public SongData parse(InputStream inputStream) throws IOException, OutdatedNBSException { + return parse(inputStream, null); + } +} diff --git a/src/main/java/io/github/homchom/recode/util/math/IntegerMath.kt b/src/main/java/io/github/homchom/recode/util/math/IntegerMath.kt new file mode 100644 index 000000000..9a12560f9 --- /dev/null +++ b/src/main/java/io/github/homchom/recode/util/math/IntegerMath.kt @@ -0,0 +1,20 @@ +package io.github.homchom.recode.util.math + +/** + * @return the [floor modulo](https://en.wikipedia.org/wiki/Modulo#Variants_of_the_definition) + * of this Int and [other]. + */ +infix fun Int.mod(other: Int) = Math.floorMod(this, other) + +/** + * @return the [floor modulo](https://en.wikipedia.org/wiki/Modulo#Variants_of_the_definition) + * of this Long and [other]. + */ +infix fun Long.mod(other: Long) = Math.floorMod(this, other) + +/** + * @return the greatest common factor of [a] and [b] using the + * [Euclidean algorithm](https://en.wikipedia.org/wiki/Euclidean_algorithm). + */ +tailrec fun greatestCommonFactor(a: Int, b: Int): Int = + if (b == 0) a else greatestCommonFactor(b, a % b) \ No newline at end of file