From 6d2051d47d508bba19e716fd9da93605aea41696 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Wed, 6 Dec 2023 20:34:44 -0600 Subject: [PATCH 01/29] Update to 1.20.3 --- gradle.properties | 10 ++-- .../songplayer/FakePlayerEntity.java | 2 +- .../com/github/hhhzzzsss/songplayer/Util.java | 8 +-- .../item/SongItemConfirmationScreen.java | 2 +- .../ClientCommonNetworkHandlerMixin.java | 55 +++++++++++++++++++ .../mixin/ClientPlayNetworkHandlerMixin.java | 35 ------------ src/main/resources/fabric.mod.json | 2 +- src/main/resources/songplayer.mixins.json | 1 + 8 files changed, 68 insertions(+), 47 deletions(-) create mode 100644 src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientCommonNetworkHandlerMixin.java diff --git a/gradle.properties b/gradle.properties index e74047f..5cbe8c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,15 +4,15 @@ org.gradle.parallel=true # Fabric Properties # check these on https://fabricmc.net/use - minecraft_version=1.20.1 - yarn_mappings=1.20.1+build.2 - loader_version=0.14.21 + minecraft_version=1.20.3 + yarn_mappings=1.20.3+build.1 + loader_version=0.15.0 # Mod Properties - mod_version = 3.1.1 + mod_version = 3.1.2 maven_group = com.github.hhhzzzsss archives_base_name = song-player # Dependencies # currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api - fabric_version=0.83.1+1.20.1 + fabric_version=0.91.1+1.20.3 diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/FakePlayerEntity.java b/src/main/java/com/github/hhhzzzsss/songplayer/FakePlayerEntity.java index 9abaae5..b3259f3 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/FakePlayerEntity.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/FakePlayerEntity.java @@ -41,7 +41,7 @@ public class FakePlayerEntity extends OtherClientPlayerEntity { capeY = getY(); capeZ = getZ(); - world.addEntity(getId(), this); + world.addEntity(this); } public void resetPlayerPosition() { diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/Util.java b/src/main/java/com/github/hhhzzzsss/songplayer/Util.java index effd97f..eb2453e 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/Util.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/Util.java @@ -6,7 +6,7 @@ import net.minecraft.command.CommandSource; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtList; import net.minecraft.nbt.NbtString; -import net.minecraft.text.LiteralTextContent; +import net.minecraft.text.PlainTextContent; import net.minecraft.text.MutableText; import net.minecraft.text.Style; import net.minecraft.text.Text; @@ -216,19 +216,19 @@ public class Util { } public static MutableText getStyledText(String str, Style style) { - MutableText text = MutableText.of(new LiteralTextContent(str)); + MutableText text = MutableText.of(PlainTextContent.of(str)); text.setStyle(style); return text; } public static void setItemName(ItemStack stack, Text text) { - stack.getOrCreateSubNbt(ItemStack.DISPLAY_KEY).putString(ItemStack.NAME_KEY, Text.Serializer.toJson(text)); + stack.getOrCreateSubNbt(ItemStack.DISPLAY_KEY).putString(ItemStack.NAME_KEY, Text.Serialization.toJsonString(text)); } public static void setItemLore(ItemStack stack, Text... loreLines) { NbtList lore = new NbtList(); for (Text line : loreLines) { - lore.add(NbtString.of(Text.Serializer.toJson(line))); + lore.add(NbtString.of(Text.Serialization.toJsonString(line))); } stack.getOrCreateSubNbt(ItemStack.DISPLAY_KEY).put(ItemStack.LORE_KEY, lore); } diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemConfirmationScreen.java b/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemConfirmationScreen.java index 237ab90..12c273e 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemConfirmationScreen.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemConfirmationScreen.java @@ -54,7 +54,7 @@ public class SongItemConfirmationScreen extends Screen { @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { - this.renderBackground(context); + this.renderBackground(context, mouseX, mouseY, delta); context.drawCenteredTextWithShadow(textRenderer, this.title, this.width / 2, 40, 0xFFFFFF); diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientCommonNetworkHandlerMixin.java b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientCommonNetworkHandlerMixin.java new file mode 100644 index 0000000..eaed91c --- /dev/null +++ b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientCommonNetworkHandlerMixin.java @@ -0,0 +1,55 @@ +package com.github.hhhzzzsss.songplayer.mixin; + +import com.github.hhhzzzsss.songplayer.Config; +import com.github.hhhzzzsss.songplayer.SongPlayer; +import com.github.hhhzzzsss.songplayer.playing.SongHandler; +import com.github.hhhzzzsss.songplayer.playing.Stage; +import net.minecraft.client.network.ClientCommonNetworkHandler; +import net.minecraft.entity.EntityPose; +import net.minecraft.network.ClientConnection; +import net.minecraft.network.packet.Packet; +import net.minecraft.network.packet.c2s.play.ClientCommandC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientCommonNetworkHandler.class) +public class ClientCommonNetworkHandlerMixin { + @Shadow + private final ClientConnection connection; + + public ClientCommonNetworkHandlerMixin() { + connection = null; + } + + @Inject(at = @At("HEAD"), method = "sendPacket(Lnet/minecraft/network/packet/Packet;)V", cancellable = true) + private void onSendPacket(Packet packet, CallbackInfo ci) { + Stage stage = SongHandler.getInstance().stage; + + if (stage != null && packet instanceof PlayerMoveC2SPacket) { + if (!Config.getConfig().rotate) { + connection.send(new PlayerMoveC2SPacket.Full(stage.position.getX() + 0.5, stage.position.getY(), stage.position.getZ() + 0.5, SongPlayer.MC.player.getYaw(), SongPlayer.MC.player.getPitch(), true)); + if (SongPlayer.fakePlayer != null) { + SongPlayer.fakePlayer.copyStagePosAndPlayerLook(); + } + } + ci.cancel(); + } + else if (packet instanceof ClientCommandC2SPacket) { + ClientCommandC2SPacket.Mode mode = ((ClientCommandC2SPacket) packet).getMode(); + if (SongPlayer.fakePlayer != null) { + if (mode == ClientCommandC2SPacket.Mode.PRESS_SHIFT_KEY) { + SongPlayer.fakePlayer.setSneaking(true); + SongPlayer.fakePlayer.setPose(EntityPose.CROUCHING); + } + else if (mode == ClientCommandC2SPacket.Mode.RELEASE_SHIFT_KEY) { + SongPlayer.fakePlayer.setSneaking(false); + SongPlayer.fakePlayer.setPose(EntityPose.STANDING); + } + } + } + } +} diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java index f3b9df2..2255d67 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java @@ -23,41 +23,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(ClientPlayNetworkHandler.class) public class ClientPlayNetworkHandlerMixin { - @Shadow - private final ClientConnection connection; - - public ClientPlayNetworkHandlerMixin() { - connection = null; - } - - @Inject(at = @At("HEAD"), method = "sendPacket(Lnet/minecraft/network/packet/Packet;)V", cancellable = true) - private void onSendPacket(Packet packet, CallbackInfo ci) { - Stage stage = SongHandler.getInstance().stage; - - if (stage != null && packet instanceof PlayerMoveC2SPacket) { - if (!Config.getConfig().rotate) { - connection.send(new PlayerMoveC2SPacket.Full(stage.position.getX() + 0.5, stage.position.getY(), stage.position.getZ() + 0.5, SongPlayer.MC.player.getYaw(), SongPlayer.MC.player.getPitch(), true)); - if (SongPlayer.fakePlayer != null) { - SongPlayer.fakePlayer.copyStagePosAndPlayerLook(); - } - } - ci.cancel(); - } - else if (packet instanceof ClientCommandC2SPacket) { - ClientCommandC2SPacket.Mode mode = ((ClientCommandC2SPacket) packet).getMode(); - if (SongPlayer.fakePlayer != null) { - if (mode == ClientCommandC2SPacket.Mode.PRESS_SHIFT_KEY) { - SongPlayer.fakePlayer.setSneaking(true); - SongPlayer.fakePlayer.setPose(EntityPose.CROUCHING); - } - else if (mode == ClientCommandC2SPacket.Mode.RELEASE_SHIFT_KEY) { - SongPlayer.fakePlayer.setSneaking(false); - SongPlayer.fakePlayer.setPose(EntityPose.STANDING); - } - } - } - } - @Inject(at = @At("HEAD"), method = "sendChatMessage(Ljava/lang/String;)V", cancellable=true) private void onSendChatMessage(String content, CallbackInfo ci) { boolean isCommand = CommandProcessor.processChatMessage(content); diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 1972620..f3938a7 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -28,7 +28,7 @@ "depends": { "fabricloader": ">=0.14.11", "fabric": "*", - "minecraft": "1.20.x", + "minecraft": "1.20.3", "java": ">=17" }, "suggests": { diff --git a/src/main/resources/songplayer.mixins.json b/src/main/resources/songplayer.mixins.json index ccce863..98094e1 100644 --- a/src/main/resources/songplayer.mixins.json +++ b/src/main/resources/songplayer.mixins.json @@ -7,6 +7,7 @@ ], "client": [ "ChatInputSuggestorMixin", + "ClientCommonNetworkHandlerMixin", "ClientPlayerInteractionManagerAccessor", "ClientPlayNetworkHandlerAccessor", "ClientPlayNetworkHandlerMixin", From b7eee5f35eee72bb65924f8272bcae65b6103395 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Wed, 6 Dec 2023 20:43:11 -0600 Subject: [PATCH 02/29] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e54d85..f17a399 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SongPlayer A Fabric mod for Minecraft that plays songs with noteblocks. -The current version is for Minecraft 1.20-1.20.1 +The current version is for Minecraft 1.20.3 # How to install You can grab the mod jar from releases section. From 36ab413e9985ec0675365c3d59d12549961fe33e Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Wed, 6 Dec 2023 20:43:35 -0600 Subject: [PATCH 03/29] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f17a399..abe8f9f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SongPlayer A Fabric mod for Minecraft that plays songs with noteblocks. -The current version is for Minecraft 1.20.3 +The current version is for Minecraft 1.20.3. # How to install You can grab the mod jar from releases section. From 06f3497c95fe2235c946ad4e47c9047a876af2a5 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Sat, 9 Dec 2023 15:19:26 -0600 Subject: [PATCH 04/29] Add support for 1.20.4 --- README.md | 2 +- gradle.properties | 6 +++--- src/main/resources/fabric.mod.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index abe8f9f..83a41dd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SongPlayer A Fabric mod for Minecraft that plays songs with noteblocks. -The current version is for Minecraft 1.20.3. +The current version is for Minecraft 1.20.4. # How to install You can grab the mod jar from releases section. diff --git a/gradle.properties b/gradle.properties index 5cbe8c5..194608e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,15 +4,15 @@ org.gradle.parallel=true # Fabric Properties # check these on https://fabricmc.net/use - minecraft_version=1.20.3 + minecraft_version=1.20.4 yarn_mappings=1.20.3+build.1 loader_version=0.15.0 # Mod Properties - mod_version = 3.1.2 + mod_version = 3.1.3 maven_group = com.github.hhhzzzsss archives_base_name = song-player # Dependencies # currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api - fabric_version=0.91.1+1.20.3 + fabric_version=0.91.1+1.20.4 diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index f3938a7..8b1d77f 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -28,7 +28,7 @@ "depends": { "fabricloader": ">=0.14.11", "fabric": "*", - "minecraft": "1.20.3", + "minecraft": "~1.20.3", "java": ">=17" }, "suggests": { From 51e365c30f8a13f0655f94ac4fe62937d3f499f8 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Thu, 2 May 2024 08:59:39 -0500 Subject: [PATCH 05/29] Add support for 1.20.5-1.20.6 --- README.md | 2 +- build.gradle | 23 ++++++++++--------- gradle.properties | 10 ++++---- gradle/wrapper/gradle-wrapper.properties | 2 +- .../songplayer/CommandProcessor.java | 4 +++- .../com/github/hhhzzzsss/songplayer/Util.java | 10 ++++---- .../item/SongItemConfirmationScreen.java | 4 +--- .../item/SongItemCreatorThread.java | 4 +++- .../songplayer/item/SongItemUtils.java | 11 +++++++-- .../songplayer/mixin/InGameHudMixin.java | 14 +++-------- .../songplayer/playing/ProgressDisplay.java | 8 +++---- .../songplayer/playing/SongHandler.java | 19 ++++++++------- src/main/resources/fabric.mod.json | 2 +- 13 files changed, 56 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 83a41dd..68c482f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SongPlayer A Fabric mod for Minecraft that plays songs with noteblocks. -The current version is for Minecraft 1.20.4. +The current version is for Minecraft 1.20.5 - 1.20.6. # How to install You can grab the mod jar from releases section. diff --git a/build.gradle b/build.gradle index 14f6f46..46a0781 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,15 @@ plugins { - id 'fabric-loom' version '1.0-SNAPSHOT' + id 'fabric-loom' version '1.6-SNAPSHOT' id 'maven-publish' } -sourceCompatibility = JavaVersion.VERSION_17 -targetCompatibility = JavaVersion.VERSION_17 - -archivesBaseName = project.archives_base_name version = project.mod_version group = project.maven_group +base { + archivesName = project.archives_base_name +} + repositories { // Add repositories to retrieve artifacts from in here. // You should only use this when depending on other mods because @@ -27,8 +27,6 @@ dependencies { // Fabric API. This is technically optional, but you probably want it anyway. modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" - // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs. - // You may need to force-disable transitiveness on them. } processResources { @@ -40,8 +38,7 @@ processResources { } tasks.withType(JavaCompile).configureEach { - // Minecraft 1.18 (1.18-pre2) upwards uses Java 17. - it.options.release = 17 + it.options.release = 21 } java { @@ -49,18 +46,22 @@ java { // if it is present. // If you remove this line, sources will not be generated. withSourcesJar() + + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } jar { from("LICENSE") { - rename { "${it}_${project.archivesBaseName}"} + rename { "${it}_${project.base.archivesName.get()}"} } } // configure the maven publication publishing { publications { - mavenJava(MavenPublication) { + create("mavenJava", MavenPublication) { + artifactId = project.archives_base_name from components.java } } diff --git a/gradle.properties b/gradle.properties index 194608e..d232237 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,15 +4,15 @@ org.gradle.parallel=true # Fabric Properties # check these on https://fabricmc.net/use - minecraft_version=1.20.4 - yarn_mappings=1.20.3+build.1 - loader_version=0.15.0 + minecraft_version=1.20.6 + yarn_mappings=1.20.6+build.1 + loader_version=0.15.10 # Mod Properties - mod_version = 3.1.3 + mod_version = 3.1.4 maven_group = com.github.hhhzzzsss archives_base_name = song-player # Dependencies # currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api - fabric_version=0.91.1+1.20.4 + fabric_version=0.97.8+1.20.6 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f398c33..20db9ad 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java index 953b483..6635032 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java @@ -10,6 +10,8 @@ import com.github.hhhzzzsss.songplayer.song.Song; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; import net.minecraft.command.CommandSource; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.NbtComponent; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; import net.minecraft.util.Hand; @@ -1030,7 +1032,7 @@ public class CommandProcessor { return true; } String name = String.join(" ", Arrays.copyOfRange(split, 1, split.length)); - songPlayerNBT.putString(SongItemUtils.DISPLAY_NAME_KEY, name); + NbtComponent.set(DataComponentTypes.CUSTOM_DATA, stack, nbt -> nbt.getCompound(SongItemUtils.SONG_ITEM_KEY).putString(SongItemUtils.DISPLAY_NAME_KEY, name)); SongItemUtils.addSongItemDisplay(stack); MC.player.setStackInHand(Hand.MAIN_HAND, stack); MC.interactionManager.clickCreativeStack(MC.player.getStackInHand(Hand.MAIN_HAND), 36 + MC.player.getInventory().selectedSlot); diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/Util.java b/src/main/java/com/github/hhhzzzsss/songplayer/Util.java index eb2453e..121c6d1 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/Util.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/Util.java @@ -3,6 +3,8 @@ package com.github.hhhzzzsss.songplayer; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; import net.minecraft.command.CommandSource; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.LoreComponent; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtList; import net.minecraft.nbt.NbtString; @@ -222,14 +224,10 @@ public class Util { } public static void setItemName(ItemStack stack, Text text) { - stack.getOrCreateSubNbt(ItemStack.DISPLAY_KEY).putString(ItemStack.NAME_KEY, Text.Serialization.toJsonString(text)); + stack.set(DataComponentTypes.CUSTOM_NAME, text); } public static void setItemLore(ItemStack stack, Text... loreLines) { - NbtList lore = new NbtList(); - for (Text line : loreLines) { - lore.add(NbtString.of(Text.Serialization.toJsonString(line))); - } - stack.getOrCreateSubNbt(ItemStack.DISPLAY_KEY).put(ItemStack.LORE_KEY, lore); + stack.set(DataComponentTypes.LORE, new LoreComponent(List.of(loreLines))); } } diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemConfirmationScreen.java b/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemConfirmationScreen.java index 12c273e..8814357 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemConfirmationScreen.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemConfirmationScreen.java @@ -54,7 +54,7 @@ public class SongItemConfirmationScreen extends Screen { @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { - this.renderBackground(context, mouseX, mouseY, delta); + super.render(context, mouseX, mouseY, delta); context.drawCenteredTextWithShadow(textRenderer, this.title, this.width / 2, 40, 0xFFFFFF); @@ -86,8 +86,6 @@ public class SongItemConfirmationScreen extends Screen { else { unloadedText.drawCenterWithShadow(context, this.width / 2, 60); } - - super.render(context, mouseX, mouseY, delta); } public String getNumberColor(double number) { diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemCreatorThread.java b/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemCreatorThread.java index d084db0..91fc017 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemCreatorThread.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemCreatorThread.java @@ -3,6 +3,8 @@ package com.github.hhhzzzsss.songplayer.item; import com.github.hhhzzzsss.songplayer.SongPlayer; import com.github.hhhzzzsss.songplayer.conversion.SPConverter; import com.github.hhhzzzsss.songplayer.song.SongLoaderThread; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.CustomModelDataComponent; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; import net.minecraft.text.Text; @@ -39,7 +41,7 @@ public class SongItemCreatorThread extends SongLoaderThread { ItemStack newStack; if (stack.isEmpty()) { newStack = Items.PAPER.getDefaultStack(); - newStack.getOrCreateNbt().putInt("CustomModelData", 751642938); + newStack.set(DataComponentTypes.CUSTOM_MODEL_DATA, new CustomModelDataComponent(751642938)); } else { newStack = stack.copy(); diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemUtils.java b/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemUtils.java index 72b8cc0..f3cf9de 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemUtils.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemUtils.java @@ -1,6 +1,8 @@ package com.github.hhhzzzsss.songplayer.item; import com.github.hhhzzzsss.songplayer.Util; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.NbtComponent; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtElement; @@ -18,10 +20,10 @@ public class SongItemUtils { public static ItemStack createSongItem(ItemStack stack, byte[] songData, String filename, String displayName) { NbtCompound songPlayerNbt = new NbtCompound(); - stack.setSubNbt(SONG_ITEM_KEY, songPlayerNbt); songPlayerNbt.putString(SONG_DATA_KEY, Base64.getEncoder().encodeToString(songData)); songPlayerNbt.putString(FILE_NAME_KEY, filename); songPlayerNbt.putString(DISPLAY_NAME_KEY, displayName); + NbtComponent.set(DataComponentTypes.CUSTOM_DATA, stack, nbt -> nbt.put(SONG_ITEM_KEY, songPlayerNbt)); addSongItemDisplay(stack); return stack; } @@ -43,7 +45,12 @@ public class SongItemUtils { } public static NbtCompound getSongItemTag(ItemStack stack) { - return stack.getSubNbt(SONG_ITEM_KEY); + NbtCompound nbt = stack.getOrDefault(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT).copyNbt(); + if (nbt.contains(SONG_ITEM_KEY, NbtElement.COMPOUND_TYPE)) { + return (NbtCompound)nbt.get(SONG_ITEM_KEY); + } else { + return null; + } } public static boolean isSongItem(ItemStack stack) { diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/InGameHudMixin.java b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/InGameHudMixin.java index 9f3e533..a6ac6cc 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/InGameHudMixin.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/InGameHudMixin.java @@ -3,7 +3,6 @@ package com.github.hhhzzzsss.songplayer.mixin; import com.github.hhhzzzsss.songplayer.playing.ProgressDisplay; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.hud.InGameHud; -import net.minecraft.client.util.math.MatrixStack; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; @@ -12,18 +11,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(InGameHud.class) public class InGameHudMixin { - @Shadow - private int scaledWidth; - - @Shadow - private int scaledHeight; - @Shadow private int heldItemTooltipFade; - @Inject(method = "render", - at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;enableBlend()V", ordinal = 3)) - private void onRender(DrawContext context, float tickDelta, CallbackInfo ci) { - ProgressDisplay.getInstance().onRenderHUD(context, scaledWidth, scaledHeight, heldItemTooltipFade); + @Inject(at = @At("TAIL"), method = "renderHeldItemTooltip(Lnet/minecraft/client/gui/DrawContext;)V") + private void onRenderHeldItemTooltip(DrawContext context, CallbackInfo ci) { + ProgressDisplay.getInstance().onRenderHUD(context, heldItemTooltipFade); } } diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/ProgressDisplay.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/ProgressDisplay.java index 973c8b2..b3a1fb5 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/ProgressDisplay.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/ProgressDisplay.java @@ -29,16 +29,16 @@ public class ProgressDisplay { fade = 100; } - public void onRenderHUD(DrawContext context, int scaledWidth, int scaledHeight, int heldItemTooltipFade) { + public void onRenderHUD(DrawContext context, int heldItemTooltipFade) { if (fade <= 0) { return; } int bottomTextWidth = SongPlayer.MC.textRenderer.getWidth(bottomText); int topTextWidth = SongPlayer.MC.textRenderer.getWidth(topText); - int bottomTextX = (scaledWidth - bottomTextWidth) / 2; - int topTextX = (scaledWidth - topTextWidth) / 2; - int bottomTextY = scaledHeight - 59; + int bottomTextX = (SongPlayer.MC.getWindow().getScaledWidth() - bottomTextWidth) / 2; + int topTextX = (SongPlayer.MC.getWindow().getScaledWidth() - topTextWidth) / 2; + int bottomTextY = SongPlayer.MC.getWindow().getScaledHeight() - 59; if (!SongPlayer.MC.interactionManager.hasStatusBars()) { bottomTextY += 14; } diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java index cbb5d16..0ffbbea 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java @@ -8,8 +8,11 @@ import com.github.hhhzzzsss.songplayer.mixin.ClientPlayerInteractionManagerAcces import com.github.hhhzzzsss.songplayer.song.*; import net.minecraft.block.Block; import net.minecraft.client.world.ClientWorld; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.BlockStateComponent; import net.minecraft.entity.player.PlayerInventory; import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; import net.minecraft.nbt.NbtCompound; import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket; import net.minecraft.text.MutableText; @@ -26,6 +29,7 @@ import net.minecraft.world.GameMode; import java.io.IOException; import java.nio.file.Path; import java.util.LinkedList; +import java.util.Map; public class SongHandler { private static SongHandler instance = null; @@ -457,16 +461,11 @@ public class SongHandler { ((ClientPlayerInteractionManagerAccessor) SongPlayer.MC.interactionManager).invokeSyncSelectedSlot(); int instrument = id/25; int note = id%25; - NbtCompound nbt = new NbtCompound(); - nbt.putString("id", "minecraft:note_block"); - nbt.putByte("Count", (byte) 1); - NbtCompound tag = new NbtCompound(); - NbtCompound bsTag = new NbtCompound(); - bsTag.putString("instrument", instrumentNames[instrument]); - bsTag.putString("note", Integer.toString(note)); - tag.put("BlockStateTag", bsTag); - nbt.put("tag", tag); - ItemStack noteblockStack = ItemStack.fromNbt(nbt); + ItemStack noteblockStack = Items.NOTE_BLOCK.getDefaultStack(); + noteblockStack.set(DataComponentTypes.BLOCK_STATE, new BlockStateComponent(Map.of( + "instrument", instrumentNames[instrument], + "note", Integer.toString(note) + ))); inventory.main.set(slot, noteblockStack); SongPlayer.MC.interactionManager.clickCreativeStack(noteblockStack, 36 + slot); } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 8b1d77f..72e74e7 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -28,7 +28,7 @@ "depends": { "fabricloader": ">=0.14.11", "fabric": "*", - "minecraft": "~1.20.3", + "minecraft": "~1.20.5", "java": ">=17" }, "suggests": { From 29b36ea4b60a1e5d37bdd16df8e1b1d3f89f54df Mon Sep 17 00:00:00 2001 From: Chipmunk <65827213+ChipmunkMC@users.noreply.github.com> Date: Thu, 23 May 2024 16:27:45 +0000 Subject: [PATCH 06/29] Make `gradlew` executable (#31) --- gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From 9b961dfabfd98d7bd6e0a3dbc0dc151b47125351 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Thu, 23 May 2024 13:12:19 -0500 Subject: [PATCH 07/29] Add support for txt format --- .../songplayer/conversion/TxtConverter.java | 40 +++++++++++++++++++ .../songplayer/song/SongLoaderThread.java | 8 ++++ 2 files changed, 48 insertions(+) create mode 100644 src/main/java/com/github/hhhzzzsss/songplayer/conversion/TxtConverter.java diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/conversion/TxtConverter.java b/src/main/java/com/github/hhhzzzsss/songplayer/conversion/TxtConverter.java new file mode 100644 index 0000000..2257e67 --- /dev/null +++ b/src/main/java/com/github/hhhzzzsss/songplayer/conversion/TxtConverter.java @@ -0,0 +1,40 @@ +package com.github.hhhzzzsss.songplayer.conversion; + +import com.github.hhhzzzsss.songplayer.song.Note; +import com.github.hhhzzzsss.songplayer.song.Song; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class TxtConverter { + public static Song getSongFromBytes(byte[] bytes, String fileName) throws IOException { + Song song = new Song(fileName); + String strContent = new String(bytes, StandardCharsets.UTF_8); + + String[] lines = strContent.split("\\r?\\n"); + for (int lineNum = 1; lineNum <= lines.length; lineNum++) { + String line = lines[lineNum-1].strip(); + + if (line.startsWith("#")) continue; + + String[] split = line.split(":"); + if (split.length != 3) throw new IOException("Invalid format at line " + lineNum); + int tick, pitch, instrument; + try { + tick = Integer.parseInt(split[0]); + pitch = Integer.parseInt(split[1]); + instrument = Integer.parseInt(split[2]); + } catch (NumberFormatException e) { + throw new IOException("Invalid format at line " + lineNum); + } + + int noteId = pitch + instrument*25; + song.add(new Note(noteId, tick*50)); + song.length = song.get(song.size()-1).time + 50; + } + + song.sort(); + + return song; + } +} diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/song/SongLoaderThread.java b/src/main/java/com/github/hhhzzzsss/songplayer/song/SongLoaderThread.java index cc89583..7717d84 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/song/SongLoaderThread.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/song/SongLoaderThread.java @@ -4,6 +4,7 @@ import com.github.hhhzzzsss.songplayer.SongPlayer; import com.github.hhhzzzsss.songplayer.Util; import com.github.hhhzzzsss.songplayer.conversion.MidiConverter; import com.github.hhhzzzsss.songplayer.conversion.NBSConverter; +import com.github.hhhzzzsss.songplayer.conversion.TxtConverter; import java.io.IOException; import java.net.URL; @@ -75,6 +76,13 @@ public class SongLoaderThread extends Thread{ catch (Exception e) {} } + if (song == null) { + try { + song = TxtConverter.getSongFromBytes(bytes, filename); + } + catch (Exception e) {} + } + if (song == null) { throw new IOException("Invalid song format"); } From ec56816612e1dd636e1a1806510ba410e45233fe Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Thu, 23 May 2024 13:13:39 -0500 Subject: [PATCH 08/29] Prevent prefix from starting with a slash --- .../com/github/hhhzzzsss/songplayer/CommandProcessor.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java index 6635032..8f4e4a4 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java @@ -185,6 +185,10 @@ public class CommandProcessor { SongPlayer.addChatMessage("§cPrefix cannot contain a space"); return true; } + else if (args.startsWith("/")) { + SongPlayer.addChatMessage("§cPrefix cannot start with a /"); + return true; + } else if (args.length() > 0) { Config.getConfig().prefix = args; SongPlayer.addChatMessage("§6Set prefix to " + args); From 95df01fdc1fe5620827ac1048042e31e451110b4 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Thu, 23 May 2024 15:24:33 -0500 Subject: [PATCH 09/29] Stop playing if player is moved too far from stage --- .../mixin/ClientPlayNetworkHandlerMixin.java | 19 +++++++++++-------- .../songplayer/playing/SongHandler.java | 5 ++++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java index 2255d67..95c592f 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java @@ -1,22 +1,16 @@ package com.github.hhhzzzsss.songplayer.mixin; import com.github.hhhzzzsss.songplayer.CommandProcessor; -import com.github.hhhzzzsss.songplayer.Config; import com.github.hhhzzzsss.songplayer.SongPlayer; import com.github.hhhzzzsss.songplayer.playing.SongHandler; import com.github.hhhzzzsss.songplayer.playing.Stage; import net.minecraft.client.network.ClientPlayNetworkHandler; -import net.minecraft.entity.EntityPose; -import net.minecraft.network.ClientConnection; -import net.minecraft.network.packet.Packet; -import net.minecraft.network.packet.c2s.play.ClickSlotC2SPacket; -import net.minecraft.network.packet.c2s.play.ClientCommandC2SPacket; -import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket; import net.minecraft.network.packet.s2c.play.GameJoinS2CPacket; import net.minecraft.network.packet.s2c.play.PlayerAbilitiesS2CPacket; +import net.minecraft.network.packet.s2c.play.PlayerPositionLookS2CPacket; import net.minecraft.network.packet.s2c.play.PlayerRespawnS2CPacket; +import net.minecraft.util.math.Vec3d; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -41,6 +35,15 @@ public class ClientPlayNetworkHandlerMixin { SongHandler.getInstance().cleanup(); } + @Inject(at = @At("TAIL"), method = "onPlayerPositionLook(Lnet/minecraft/network/packet/s2c/play/PlayerPositionLookS2CPacket;)V") + public void onOnPlayerPositionLook(PlayerPositionLookS2CPacket packet, CallbackInfo ci) { + Stage stage = SongHandler.getInstance().stage; + if (!SongHandler.getInstance().isIdle() && stage != null && Vec3d.ofBottomCenter(stage.position).squaredDistanceTo(SongPlayer.MC.player.getPos()) > 3*3) { + SongPlayer.addChatMessage("§6Stopped playing because the server moved the player too far from the stage!"); + SongHandler.getInstance().cleanup(); + } + } + @Inject(at = @At("TAIL"), method = "onPlayerAbilities(Lnet/minecraft/network/packet/s2c/play/PlayerAbilitiesS2CPacket;)V") public void onOnPlayerAbilities(PlayerAbilitiesS2CPacket packet, CallbackInfo ci) { SongHandler handler = SongHandler.getInstance(); diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java index 0ffbbea..ead3ca6 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java @@ -13,7 +13,6 @@ import net.minecraft.component.type.BlockStateComponent; import net.minecraft.entity.player.PlayerInventory; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; -import net.minecraft.nbt.NbtCompound; import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket; import net.minecraft.text.MutableText; import net.minecraft.text.Text; @@ -524,4 +523,8 @@ public class SongHandler { buildSlot = -1; } } + + public boolean isIdle() { + return currentSong == null && currentPlaylist == null && songQueue.isEmpty(); + } } \ No newline at end of file From fd2b089e90c6c79e27a0777cb8d172c5d064e6e3 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Thu, 23 May 2024 16:42:27 -0500 Subject: [PATCH 10/29] Cancel velocity while playing songs --- .../mixin/ClientPlayNetworkHandlerMixin.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java index 95c592f..d8843c3 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java @@ -5,10 +5,7 @@ import com.github.hhhzzzsss.songplayer.SongPlayer; import com.github.hhhzzzsss.songplayer.playing.SongHandler; import com.github.hhhzzzsss.songplayer.playing.Stage; import net.minecraft.client.network.ClientPlayNetworkHandler; -import net.minecraft.network.packet.s2c.play.GameJoinS2CPacket; -import net.minecraft.network.packet.s2c.play.PlayerAbilitiesS2CPacket; -import net.minecraft.network.packet.s2c.play.PlayerPositionLookS2CPacket; -import net.minecraft.network.packet.s2c.play.PlayerRespawnS2CPacket; +import net.minecraft.network.packet.s2c.play.*; import net.minecraft.util.math.Vec3d; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -51,4 +48,11 @@ public class ClientPlayNetworkHandlerMixin { SongPlayer.MC.player.getAbilities().flying = handler.wasFlying; } } + + @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;setVelocityClient(DDD)V"), method = "onEntityVelocityUpdate", cancellable = true) + public void onOnEntityVelocityUpdate(EntityVelocityUpdateS2CPacket packet, CallbackInfo ci) { + if (!SongHandler.getInstance().isIdle() && packet.getId() == SongPlayer.MC.player.getId()) { + ci.cancel(); + } + } } From 56d40cad327e8fe2ad9f48de57f0c25cb8b678f0 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Sat, 25 May 2024 00:39:26 -0500 Subject: [PATCH 11/29] Added stage cleanup feature --- .../songplayer/CommandProcessor.java | 142 +++++- .../github/hhhzzzsss/songplayer/Config.java | 1 + .../songplayer/FakePlayerEntity.java | 6 +- .../com/github/hhhzzzsss/songplayer/Util.java | 14 +- .../ClientCommonNetworkHandlerMixin.java | 6 +- .../mixin/ClientPlayNetworkHandlerMixin.java | 41 +- .../songplayer/playing/SongHandler.java | 442 ++++++++++++++---- .../hhhzzzsss/songplayer/playing/Stage.java | 4 + 8 files changed, 554 insertions(+), 102 deletions(-) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java index 8f4e4a4..3150b38 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java @@ -9,12 +9,17 @@ import com.github.hhhzzzsss.songplayer.song.Playlist; import com.github.hhhzzzsss.songplayer.song.Song; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.block.BlockState; import net.minecraft.command.CommandSource; import net.minecraft.component.DataComponentTypes; import net.minecraft.component.type.NbtComponent; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; +import net.minecraft.state.property.Property; +import net.minecraft.text.*; +import net.minecraft.util.Formatting; import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; import net.minecraft.world.GameMode; import java.io.IOException; @@ -52,9 +57,12 @@ public class CommandProcessor { commands.add(new toggleFakePlayerCommand()); commands.add(new setStageTypeCommand()); commands.add(new toggleMovementCommand()); + commands.add(new toggleAutoCleanup()); + commands.add(new cleanupLastStageCommand()); commands.add(new announcementCommand()); commands.add(new songItemCommand()); commands.add(new testSongCommand()); + commands.add(new testBlockStateCommand()); for (Command command : commands) { commandMap.put(command.getName().toLowerCase(Locale.ROOT), command); @@ -236,16 +244,21 @@ public class CommandProcessor { return "Stops playing"; } public boolean processCommand(String args) { - if (SongHandler.getInstance().currentSong == null && SongHandler.getInstance().songQueue.isEmpty()) { + if (SongHandler.getInstance().isIdle()) { SongPlayer.addChatMessage("§6No song is currently playing"); return true; } if (args.length() == 0) { - if (SongHandler.getInstance().stage != null) { - SongHandler.getInstance().stage.movePlayerToStagePosition(); + if (SongHandler.getInstance().cleaningUp) { + SongHandler.getInstance().restoreStateAndReset(); + SongPlayer.addChatMessage("§6Stopped cleanup"); + } else if (Config.getConfig().autoCleanup && SongHandler.getInstance().originalBlocks.size() != 0) { + SongHandler.getInstance().partionResetAndCleanup(); + SongPlayer.addChatMessage("§6Stopped playing and switched to cleanup"); + } else { + SongHandler.getInstance().restoreStateAndReset(); + SongPlayer.addChatMessage("§6Stopped playing"); } - SongHandler.getInstance().restoreStateAndCleanUp(); - SongPlayer.addChatMessage("§6Stopped playing"); return true; } else { @@ -935,6 +948,92 @@ public class CommandProcessor { } } + private static class toggleAutoCleanup extends Command { + public String getName() { + return "toggleAutoCleanup"; + } + public String[] getAliases() { + return new String[]{"autoCleanup"}; + } + public String[] getSyntax() { + return new String[0]; + } + public String getDescription() { + return "Toggles whether you automatically clean up your stage and restore the original blocks after playing"; + } + public boolean processCommand(String args) { + if (args.length() == 0) { + Config.getConfig().autoCleanup = !Config.getConfig().autoCleanup; + if (Config.getConfig().autoCleanup) { + SongPlayer.addChatMessage("§6Enabled automatic cleanup"); + } + else { + SongPlayer.addChatMessage("§6Disabled automatic cleanup"); + } + Config.saveConfigWithErrorHandling(); + return true; + } + else { + return false; + } + } + } + + private static class cleanupLastStageCommand extends Command { + public String getName() { + return "cleanupLastStage"; + } + public String[] getAliases() { + return new String[]{}; + } + public String[] getSyntax() { + return new String[0]; + } + public String getDescription() { + return "Cleans up your most recent stage and restores the original blocks"; + } + public boolean processCommand(String args) { + if (args.length() == 0) { + Stage lastStage = SongHandler.getInstance().lastStage; + if (!SongHandler.getInstance().isIdle()) { + SongPlayer.addChatMessage("§cYou cannot start cleanup if you are in the middle of another action"); + return true; + } + if (lastStage == null || SongHandler.getInstance().originalBlocks.size() == 0) { + SongPlayer.addChatMessage("§6There is nothing to clean up"); + return true; + } + if (SongPlayer.MC.player.getPos().squaredDistanceTo(lastStage.getOriginBottomCenter()) > 3*3) { + System.out.println(SongPlayer.MC.player.getPos().squaredDistanceTo(lastStage.getOriginBottomCenter())); + String coordStr = String.format( + "%d %d %d", + lastStage.position.getX(), lastStage.position.getY(), lastStage.position.getZ() + ); + SongPlayer.addChatMessage("§6You must be within §33 §6blocks of the center of your stage to start cleanup."); + MutableText coordText = Util.joinTexts(null, + Text.literal("This is at ").setStyle(Style.EMPTY.withColor(Formatting.GOLD)), + Text.literal(coordStr).setStyle( + Style.EMPTY + .withColor(Formatting.DARK_AQUA) + .withUnderline(true) + .withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, coordStr)) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal("Copy \"" + coordStr + "\""))) + ), + Text.literal(" (click to copy)").setStyle(Style.EMPTY.withColor(Formatting.GOLD)) + ); + SongPlayer.addChatMessage(coordText); + return true; + } + + SongHandler.getInstance().startCleanup(); + return true; + } + else { + return false; + } + } + } + private static class announcementCommand extends Command { public String getName() { return "announcement"; @@ -1093,6 +1192,39 @@ public class CommandProcessor { } } + private static class testBlockStateCommand extends Command { + public String getName() { + return "testBlockState"; + } + public String[] getSyntax() { + return new String[0]; + } + public String getDescription() { + return "for dev purposes"; + } + public boolean processCommand(String args) { + if (args.length() == 0) { + if (MC.crosshairTarget instanceof BlockHitResult) { + BlockHitResult hitResult = (BlockHitResult) MC.crosshairTarget; + BlockState bs = MC.world.getBlockState(hitResult.getBlockPos()); + ItemStack stack = new ItemStack(bs.getBlock()); + SongPlayer.addChatMessage(stack.toString()); + for (Map.Entry, Comparable> entry : bs.getEntries().entrySet()) { + Property property = entry.getKey(); + Comparable value = entry.getValue(); +// SongPlayer.addChatMessage(net.minecraft.util.Util.getValueAsString(property, comparable)); + System.out.println(property.getClass()); + System.out.println(value.getClass()); + } + } + return true; + } + else { + return false; + } + } + } + public static CompletableFuture handleSuggestions(String text, SuggestionsBuilder suggestionsBuilder) { if (!text.contains(" ")) { List names = commandCompletions diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/Config.java b/src/main/java/com/github/hhhzzzsss/songplayer/Config.java index fab497c..a71555b 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/Config.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/Config.java @@ -26,6 +26,7 @@ public class Config { public boolean rotate = false; public boolean doAnnouncement = false; public String announcementMessage = "&6Now playing: &3[name]"; + public boolean autoCleanup = false; public static Config getConfig() { if (config == null) { diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/FakePlayerEntity.java b/src/main/java/com/github/hhhzzzsss/songplayer/FakePlayerEntity.java index b3259f3..9d6c0fe 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/FakePlayerEntity.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/FakePlayerEntity.java @@ -49,9 +49,9 @@ public class FakePlayerEntity extends OtherClientPlayerEntity { } public void copyStagePosAndPlayerLook() { - Stage stage = SongHandler.getInstance().stage; - if (stage != null) { - refreshPositionAndAngles(stage.position.getX()+0.5, stage.position.getY(), stage.position.getZ()+0.5, player.getYaw(), player.getPitch()); + Stage lastStage = SongHandler.getInstance().lastStage; + if (lastStage != null) { + refreshPositionAndAngles(lastStage.position.getX()+0.5, lastStage.position.getY(), lastStage.position.getZ()+0.5, player.getYaw(), player.getPitch()); headYaw = player.headYaw; } else { diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/Util.java b/src/main/java/com/github/hhhzzzsss/songplayer/Util.java index 121c6d1..3c122c3 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/Util.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/Util.java @@ -6,10 +6,8 @@ import net.minecraft.command.CommandSource; import net.minecraft.component.DataComponentTypes; import net.minecraft.component.type.LoreComponent; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NbtList; -import net.minecraft.nbt.NbtString; -import net.minecraft.text.PlainTextContent; import net.minecraft.text.MutableText; +import net.minecraft.text.PlainTextContent; import net.minecraft.text.Style; import net.minecraft.text.Text; @@ -230,4 +228,14 @@ public class Util { public static void setItemLore(ItemStack stack, Text... loreLines) { stack.set(DataComponentTypes.LORE, new LoreComponent(List.of(loreLines))); } + + public static MutableText joinTexts(MutableText base, Text... children) { + if (base == null) { + base = Text.empty(); + } + for (Text child : children) { + base.append(child); + } + return base; + } } diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientCommonNetworkHandlerMixin.java b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientCommonNetworkHandlerMixin.java index eaed91c..1201372 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientCommonNetworkHandlerMixin.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientCommonNetworkHandlerMixin.java @@ -27,11 +27,11 @@ public class ClientCommonNetworkHandlerMixin { @Inject(at = @At("HEAD"), method = "sendPacket(Lnet/minecraft/network/packet/Packet;)V", cancellable = true) private void onSendPacket(Packet packet, CallbackInfo ci) { - Stage stage = SongHandler.getInstance().stage; + Stage lastStage = SongHandler.getInstance().lastStage; - if (stage != null && packet instanceof PlayerMoveC2SPacket) { + if (!SongHandler.getInstance().isIdle() && lastStage != null && packet instanceof PlayerMoveC2SPacket) { if (!Config.getConfig().rotate) { - connection.send(new PlayerMoveC2SPacket.Full(stage.position.getX() + 0.5, stage.position.getY(), stage.position.getZ() + 0.5, SongPlayer.MC.player.getYaw(), SongPlayer.MC.player.getPitch(), true)); + connection.send(new PlayerMoveC2SPacket.Full(lastStage.position.getX() + 0.5, lastStage.position.getY(), lastStage.position.getZ() + 0.5, SongPlayer.MC.player.getYaw(), SongPlayer.MC.player.getPitch(), true)); if (SongPlayer.fakePlayer != null) { SongPlayer.fakePlayer.copyStagePosAndPlayerLook(); } diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java index d8843c3..7a03fa8 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java @@ -24,27 +24,54 @@ public class ClientPlayNetworkHandlerMixin { @Inject(at = @At("TAIL"), method = "onGameJoin(Lnet/minecraft/network/packet/s2c/play/GameJoinS2CPacket;)V") public void onOnGameJoin(GameJoinS2CPacket packet, CallbackInfo ci) { - SongHandler.getInstance().cleanup(); + SongHandler.getInstance().reset(); } @Inject(at = @At("TAIL"), method = "onPlayerRespawn(Lnet/minecraft/network/packet/s2c/play/PlayerRespawnS2CPacket;)V") public void onOnPlayerRespawn(PlayerRespawnS2CPacket packet, CallbackInfo ci) { - SongHandler.getInstance().cleanup(); + SongHandler.getInstance().reset(); } @Inject(at = @At("TAIL"), method = "onPlayerPositionLook(Lnet/minecraft/network/packet/s2c/play/PlayerPositionLookS2CPacket;)V") public void onOnPlayerPositionLook(PlayerPositionLookS2CPacket packet, CallbackInfo ci) { - Stage stage = SongHandler.getInstance().stage; - if (!SongHandler.getInstance().isIdle() && stage != null && Vec3d.ofBottomCenter(stage.position).squaredDistanceTo(SongPlayer.MC.player.getPos()) > 3*3) { - SongPlayer.addChatMessage("§6Stopped playing because the server moved the player too far from the stage!"); - SongHandler.getInstance().cleanup(); + Stage lastStage = SongHandler.getInstance().lastStage; + if (!SongHandler.getInstance().isIdle() && lastStage != null && lastStage.getOriginBottomCenter().squaredDistanceTo(SongPlayer.MC.player.getPos()) > 3*3) { + Vec3d stageOriginBottomCenter = lastStage.getOriginBottomCenter(); + boolean xrel = packet.getFlags().contains(PositionFlag.X); + boolean yrel = packet.getFlags().contains(PositionFlag.Y); + boolean zrel = packet.getFlags().contains(PositionFlag.Z); + double dx = 0.0; + double dy = 0.0; + double dz = 0.0; + if (xrel) { + dx = packet.getX(); + } else { + dx = SongPlayer.MC.player.getX() - stageOriginBottomCenter.getX(); + } + if (yrel) { + dy = packet.getY(); + } else { + dy = SongPlayer.MC.player.getY() - stageOriginBottomCenter.getY(); + } + if (zrel) { + dz = packet.getZ(); + } else { + dz = SongPlayer.MC.player.getZ() - stageOriginBottomCenter.getZ(); + } + double dist = dx*dx + dy*dy + dz*dz; + if (dist > 3.0) { + SongPlayer.addChatMessage("§6Stopped playing/building because the server moved the player too far from the stage!"); + SongHandler.getInstance().reset(); + } else { + lastStage.movePlayerToStagePosition(); + } } } @Inject(at = @At("TAIL"), method = "onPlayerAbilities(Lnet/minecraft/network/packet/s2c/play/PlayerAbilitiesS2CPacket;)V") public void onOnPlayerAbilities(PlayerAbilitiesS2CPacket packet, CallbackInfo ci) { SongHandler handler = SongHandler.getInstance(); - if (handler.currentSong != null || handler.currentPlaylist != null || handler.songQueue.size() > 0) { + if (handler.wasFlying) { SongPlayer.MC.player.getAbilities().flying = handler.wasFlying; } } diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java index ead3ca6..33e7cf2 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java @@ -6,7 +6,7 @@ import com.github.hhhzzzsss.songplayer.SongPlayer; import com.github.hhhzzzsss.songplayer.Util; import com.github.hhhzzzsss.songplayer.mixin.ClientPlayerInteractionManagerAccessor; import com.github.hhhzzzsss.songplayer.song.*; -import net.minecraft.block.Block; +import net.minecraft.block.*; import net.minecraft.client.world.ClientWorld; import net.minecraft.component.DataComponentTypes; import net.minecraft.component.type.BlockStateComponent; @@ -14,8 +14,8 @@ import net.minecraft.entity.player.PlayerInventory; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket; -import net.minecraft.text.MutableText; -import net.minecraft.text.Text; +import net.minecraft.state.property.Property; +import net.minecraft.text.*; import net.minecraft.util.Formatting; import net.minecraft.util.Hand; import net.minecraft.util.hit.BlockHitResult; @@ -27,8 +27,8 @@ import net.minecraft.world.GameMode; import java.io.IOException; import java.nio.file.Path; -import java.util.LinkedList; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; public class SongHandler { private static SongHandler instance = null; @@ -44,8 +44,12 @@ public class SongHandler { public LinkedList songQueue = new LinkedList<>(); public Song currentSong = null; public Playlist currentPlaylist = null; - public Stage stage = null; + public Stage stage = null; // Only exists when playing + public Stage lastStage = null; // Stays around even after playing + public HashMap originalBlocks = new HashMap<>(); public boolean building = false; + public boolean cleaningUp = false; + public boolean dirty = false; public boolean wasFlying = false; public GameMode originalGamemode = GameMode.CREATIVE; @@ -53,66 +57,54 @@ public class SongHandler { boolean playlistChecked = false; public void onUpdate(boolean tick) { - // Check current playlist and load song from it if necessary - if (currentSong == null && currentPlaylist != null && currentPlaylist.loaded) { - if (!playlistChecked) { - playlistChecked = true; - if (currentPlaylist.songsFailedToLoad.size() > 0) { - SongPlayer.addChatMessage("§cFailed to load the following songs from the playlist: §4" + String.join(" ", currentPlaylist.songsFailedToLoad)); + if (!cleaningUp) { + // Check current playlist and load song from it if necessary + if (currentSong == null && currentPlaylist != null && currentPlaylist.loaded) { + if (!playlistChecked) { + playlistChecked = true; + if (currentPlaylist.songsFailedToLoad.size() > 0) { + SongPlayer.addChatMessage("§cFailed to load the following songs from the playlist: §4" + String.join(" ", currentPlaylist.songsFailedToLoad)); + } } - } - Song nextSong = currentPlaylist.getNext(); - if (currentPlaylist.songs.size() == 0) { - SongPlayer.addChatMessage("§cPlaylist has no playable songs"); - currentPlaylist = null; - } - else if (nextSong == null) { - SongPlayer.addChatMessage("§6Playlist has finished playing"); - currentPlaylist = null; - } - else { - nextSong.reset(); - setSong(nextSong); - } - } - - // Check queue and load song from it if necessary - if (currentSong == null && currentPlaylist == null && songQueue.size() > 0) { - setSong(songQueue.poll()); - } - - // Check if loader thread is finished and handle accordingly - if (loaderThread != null && !loaderThread.isAlive()) { - if (loaderThread.exception != null) { - SongPlayer.addChatMessage("§cFailed to load song: §4" + loaderThread.exception.getMessage()); - } else { - if (currentSong == null) { - setSong(loaderThread.song); + Song nextSong = currentPlaylist.getNext(); + if (currentPlaylist.songs.size() == 0) { + SongPlayer.addChatMessage("§cPlaylist has no playable songs"); + currentPlaylist = null; + } else if (nextSong == null) { + SongPlayer.addChatMessage("§6Playlist has finished playing"); + currentPlaylist = null; } else { - queueSong(loaderThread.song); + nextSong.reset(); + setSong(nextSong); } } - loaderThread = null; + + // Check queue and load song from it if necessary + if (currentSong == null && currentPlaylist == null && songQueue.size() > 0) { + setSong(songQueue.poll()); + } + + // Check if loader thread is finished and handle accordingly + if (loaderThread != null && !loaderThread.isAlive()) { + if (loaderThread.exception != null) { + SongPlayer.addChatMessage("§cFailed to load song: §4" + loaderThread.exception.getMessage()); + } else { + if (currentSong == null) { + setSong(loaderThread.song); + } else { + queueSong(loaderThread.song); + } + } + loaderThread = null; + } } // Run cached command if timeout reached checkCommandCache(); - // Check if no song is playing and, if necessary, handle cleanup - if (currentSong == null) { - if (stage != null || SongPlayer.fakePlayer != null) { - restoreStateAndCleanUp(); - } - else { - originalGamemode = SongPlayer.MC.interactionManager.getCurrentGameMode(); - } - } - // Otherwise, handle song playing - else { - if (stage == null) { - stage = new Stage(); - stage.movePlayerToStagePosition(); - } + // If either playing or doing cleanup + if (cleaningUp || currentSong != null) { + // Handle creating/removing fake player depending on settings if (Config.getConfig().showFakePlayer && SongPlayer.fakePlayer == null) { SongPlayer.fakePlayer = new FakePlayerEntity(); SongPlayer.fakePlayer.copyStagePosAndPlayerLook(); @@ -124,9 +116,31 @@ public class SongHandler { SongPlayer.fakePlayer.getInventory().clone(SongPlayer.MC.player.getInventory()); } + // Allow flying SongPlayer.MC.player.getAbilities().allowFlying = true; wasFlying = SongPlayer.MC.player.getAbilities().flying; + } + // Check if doing cleanup + if (cleaningUp) { + if (tick) { + // Allow flying while doing cleanup + SongPlayer.MC.player.getAbilities().allowFlying = true; + wasFlying = SongPlayer.MC.player.getAbilities().flying; + + handleCleanup(); + } + } + // Check if song is playing + else if (currentSong != null) { + // This should never happen, but I left this check in just in case. + if (stage == null) { + SongPlayer.addChatMessage("§cStage is null! This should not happen!"); + reset(); + return; + } + + // Run building or playing tick depending on state if (building) { if (tick) { handleBuilding(); @@ -135,6 +149,20 @@ public class SongHandler { handlePlaying(tick); } } + // Otherwise, handle cleanup if necessary + else { + if (dirty) { + if (Config.getConfig().autoCleanup && originalBlocks.size() != 0) { + partionResetAndCleanup(); + } else { + restoreStateAndReset(); + } + } + else { + // When doing nothing else, record original gamemode + originalGamemode = SongPlayer.MC.interactionManager.getCurrentGameMode(); + } + } } public void loadSong(String location) { @@ -167,23 +195,18 @@ public class SongHandler { } } + // Sets currentSong and sets everything up for building public void setSong(Song song) { + dirty = true; currentSong = song; building = true; setCreativeIfNeeded(); if (Config.getConfig().doAnnouncement) { sendMessage(Config.getConfig().announcementMessage.replaceAll("\\[name\\]", song.name)); } - if (stage == null) { - stage = new Stage(); - stage.movePlayerToStagePosition(); - } - else { - stage.sendMovementPacketToStagePosition(); - } + prepareStage(); getAndSaveBuildSlot(); SongPlayer.addChatMessage("§6Building noteblocks"); - } private void queueSong(Song song) { @@ -213,6 +236,14 @@ public class SongHandler { } } + public void startCleanup() { + dirty = true; + cleaningUp = true; + setCreativeIfNeeded(); + getAndSaveBuildSlot(); + lastStage.sendMovementPacketToStagePosition(); + } + // Runs every tick private int buildStartDelay = 0; private int buildEndDelay = 0; @@ -240,6 +271,7 @@ public class SongHandler { return; } else { stage.checkBuildStatus(currentSong); + recordStageBlocks(); stage.sendMovementPacketToStagePosition(); } } @@ -310,6 +342,7 @@ public class SongHandler { if (tick) { if (stage.hasBreakingModification()) { stage.checkBuildStatus(currentSong); + recordStageBlocks(); } if (!stage.nothingToBuild()) { // Switch to building building = true; @@ -350,8 +383,7 @@ public class SongHandler { currentSong = null; } } - - public void setPlayProgressDisplay() { + private void setPlayProgressDisplay() { long currentTime = Math.min(currentSong.time, currentSong.length); long totalTime = currentSong.length; MutableText songText = Text.empty() @@ -382,18 +414,201 @@ public class SongHandler { ProgressDisplay.getInstance().setText(songText, playlistText); } - public void cleanup() { + // Runs every tick + private int cleanupTotalBlocksToPlace = 0; + private LinkedList cleanupBreakList = new LinkedList<>(); + private LinkedList cleanupPlaceList = new LinkedList<>(); + private ArrayList cleanupUnplaceableBlocks = new ArrayList<>(); + private void handleCleanup() { + setCleanupProgressDisplay(); + + if (buildStartDelay > 0) { + buildStartDelay--; + return; + } + if (buildCooldown > 0) { + buildCooldown--; + return; + } + ClientWorld world = SongPlayer.MC.world; + if (SongPlayer.MC.interactionManager.getCurrentGameMode() != GameMode.CREATIVE) { + return; + } + + if (cleanupBreakList.isEmpty() && cleanupPlaceList.isEmpty()) { + if (buildEndDelay > 0) { + buildEndDelay--; + return; + } else { + checkCleanupStatus(); + lastStage.sendMovementPacketToStagePosition(); + } + } + + if (!cleanupBreakList.isEmpty()) { + for (int i=0; i<5; i++) { + if (cleanupBreakList.isEmpty()) break; + BlockPos bp = cleanupBreakList.poll(); + attackBlock(bp); + } + buildEndDelay = 20; + } else if (!cleanupPlaceList.isEmpty()) { + BlockPos bp = cleanupPlaceList.pollFirst(); + BlockState actualBlockState = world.getBlockState(bp); + BlockState desiredBlockState = originalBlocks.get(bp); + if (actualBlockState != desiredBlockState) { + holdBlock(desiredBlockState, buildSlot); + if (!actualBlockState.isAir() && !actualBlockState.isLiquid()) { + attackBlock(bp); + } + placeBlock(bp); + } + buildCooldown = 0; // No cooldown, so it places a block every tick + buildEndDelay = 20; + } else { + originalBlocks.clear(); + cleaningUp = false; + SongPlayer.addChatMessage("§6Finished restoring original blocks"); + if (!cleanupUnplaceableBlocks.isEmpty()) { + SongPlayer.addChatMessage(String.format("§3%d §6blocks were not successfully restored")); + } + } + } + private void checkCleanupStatus() { + ClientWorld world = SongPlayer.MC.world; + + cleanupPlaceList.clear(); + cleanupBreakList.clear(); + cleanupUnplaceableBlocks.clear(); + + for (BlockPos bp : originalBlocks.keySet()) { + BlockState actualBlockState = world.getBlockState(bp); + BlockState desiredBlockState = originalBlocks.get(bp); + if (actualBlockState != desiredBlockState) { + if (isPlaceable(desiredBlockState)) { + cleanupPlaceList.add(bp); + } + if (!actualBlockState.isAir() && !actualBlockState.isLiquid()) { + cleanupBreakList.add(bp); + } + } + } + + cleanupBreakList = cleanupBreakList.stream() + .sorted((a, b) -> { + // First sort by gravity + boolean a_grav = SongPlayer.MC.world.getBlockState(a).getBlock() instanceof FallingBlock; + boolean b_grav = SongPlayer.MC.world.getBlockState(b).getBlock() instanceof FallingBlock; + if (a_grav && !b_grav) { + return 1; + } else if (!a_grav && b_grav) { + return -1; + } + // Then sort by distance + int a_dx = a.getX() - lastStage.position.getX(); + int a_dy = a.getY() - lastStage.position.getY(); + int a_dz = a.getZ() - lastStage.position.getZ(); + int b_dx = b.getX() - lastStage.position.getX(); + int b_dy = b.getY() - lastStage.position.getY(); + int b_dz = b.getZ() - lastStage.position.getZ(); + int a_dist = a_dx*a_dx + a_dy*a_dy + a_dz*a_dz; + int b_dist = b_dx*b_dx + b_dy*b_dy + b_dz*b_dz; + if (a_dist < b_dist) { + return -1; + } else if (a_dist > b_dist) { + return 1; + } + // Finally sort by angle + double a_angle = Math.atan2(a_dz, a_dx); + double b_angle = Math.atan2(b_dz, b_dx); + if (a_angle < b_angle) { + return -1; + } else if (a_angle > b_angle) { + return 1; + } else { + return 0; + } + }) + .collect(Collectors.toCollection(LinkedList::new)); + + cleanupPlaceList = cleanupPlaceList.stream() + .sorted((a, b) -> { + // First sort by gravity + boolean a_grav = originalBlocks.get(a).getBlock() instanceof FallingBlock; + boolean b_grav = originalBlocks.get(b).getBlock() instanceof FallingBlock; + if (a_grav && !b_grav) { + return -1; + } else if (!a_grav && b_grav) { + return 1; + } + // Then sort by distance + int a_dx = a.getX() - lastStage.position.getX(); + int a_dy = a.getY() - lastStage.position.getY(); + int a_dz = a.getZ() - lastStage.position.getZ(); + int b_dx = b.getX() - lastStage.position.getX(); + int b_dy = b.getY() - lastStage.position.getY(); + int b_dz = b.getZ() - lastStage.position.getZ(); + int a_dist = a_dx*a_dx + a_dy*a_dy + a_dz*a_dz; + int b_dist = b_dx*b_dx + b_dy*b_dy + b_dz*b_dz; + if (a_dist < b_dist) { + return -1; + } else if (a_dist > b_dist) { + return 1; + } + // Finally sort by angle + double a_angle = Math.atan2(a_dz, a_dx); + double b_angle = Math.atan2(b_dz, b_dx); + if (a_angle < b_angle) { + return 1; + } else if (a_angle > b_angle) { + return -1; + } else { + return 0; + } + }) + .collect(Collectors.toCollection(LinkedList::new)); + + cleanupPlaceList = cleanupPlaceList.reversed(); + cleanupTotalBlocksToPlace = cleanupPlaceList.size(); + + for (BlockPos bp : cleanupPlaceList) { + System.out.println(world.getBlockState(bp).getBlock() + " " + originalBlocks.get(bp).getBlock()); + } + + boolean noNecessaryBreaks = cleanupBreakList.stream().allMatch( + bp -> world.getBlockState(bp).getBlock().getDefaultState().equals(originalBlocks.get(bp).getBlock().getDefaultState()) + ); + boolean noNecessaryPlacements = cleanupPlaceList.stream().allMatch( + bp -> bp.equals(lastStage.position) + || bp.equals(lastStage.position.up()) + || world.getBlockState(bp).getBlock().getDefaultState().equals(originalBlocks.get(bp).getBlock().getDefaultState()) + ); + if (noNecessaryBreaks && noNecessaryPlacements) { + cleanupUnplaceableBlocks.addAll(cleanupPlaceList); + cleanupPlaceList.clear(); + } + } + private void setCleanupProgressDisplay() { + MutableText buildText = Text.empty() + .append(Text.literal("Rebuilding original blocks | " ).formatted(Formatting.GOLD)) + .append(Text.literal((cleanupTotalBlocksToPlace - cleanupPlaceList.size()) + "/" + cleanupTotalBlocksToPlace).formatted(Formatting.DARK_AQUA)); + ProgressDisplay.getInstance().setText(buildText, Text.empty()); + } + + // Resets all internal states like currentSong, and songQueue, which stops all actions + public void reset() { currentSong = null; currentPlaylist = null; songQueue.clear(); stage = null; buildSlot = -1; SongPlayer.removeFakePlayer(); + cleaningUp = false; + dirty = false; } - - public void restoreStateAndCleanUp() { - if (stage != null) { - stage.movePlayerToStagePosition(); + public void restoreStateAndReset() { + if (lastStage != null) { + lastStage.movePlayerToStagePosition(); } if (originalGamemode != SongPlayer.MC.interactionManager.getCurrentGameMode()) { if (originalGamemode == GameMode.CREATIVE) { @@ -404,15 +619,38 @@ public class SongHandler { } } restoreBuildSlot(); - cleanup(); + reset(); + } + public void partionResetAndCleanup() { + restoreBuildSlot(); + currentSong = null; + currentPlaylist = null; + songQueue.clear(); + stage = null; + buildSlot = -1; + startCleanup(); } + // Runs every frame when player is not ingame public void onNotIngame() { currentSong = null; currentPlaylist = null; songQueue.clear(); } + // Create stage if it doesn't exist and move the player to it + private void prepareStage() { + if (stage == null) { + stage = new Stage(); + lastStage = stage; + originalBlocks.clear(); + stage.movePlayerToStagePosition(); + } + else { + stage.sendMovementPacketToStagePosition(); + } + } + private long lastCommandTime = System.currentTimeMillis(); private String cachedCommand = null; private String cachedMessage = null; @@ -468,10 +706,25 @@ public class SongHandler { inventory.main.set(slot, noteblockStack); SongPlayer.MC.interactionManager.clickCreativeStack(noteblockStack, 36 + slot); } + private void holdBlock(BlockState bs, int slot) { + PlayerInventory inventory = SongPlayer.MC.player.getInventory(); + inventory.selectedSlot = slot; + ((ClientPlayerInteractionManagerAccessor) SongPlayer.MC.interactionManager).invokeSyncSelectedSlot(); + ItemStack stack = new ItemStack(bs.getBlock()); + Map stateMap = new TreeMap<>(); + for (Map.Entry, Comparable> entry : bs.getEntries().entrySet()) { + Property property = entry.getKey(); + Comparable value = entry.getValue(); + stateMap.put(property.getName(), net.minecraft.util.Util.getValueAsString(property, value)); + } + stack.set(DataComponentTypes.BLOCK_STATE, new BlockStateComponent(stateMap)); + inventory.main.set(slot, stack); + SongPlayer.MC.interactionManager.clickCreativeStack(stack, 36 + slot); + } private void placeBlock(BlockPos bp) { - double fx = Math.max(0.0, Math.min(1.0, (stage.position.getX() + 0.5 - bp.getX()))); - double fy = Math.max(0.0, Math.min(1.0, (stage.position.getY() + 0.0 - bp.getY()))); - double fz = Math.max(0.0, Math.min(1.0, (stage.position.getZ() + 0.5 - bp.getZ()))); + double fx = Math.max(0.0, Math.min(1.0, (lastStage.position.getX() + 0.5 - bp.getX()))); + double fy = Math.max(0.0, Math.min(1.0, (lastStage.position.getY() + 0.0 - bp.getY()))); + double fz = Math.max(0.0, Math.min(1.0, (lastStage.position.getZ() + 0.5 - bp.getZ()))); fx += bp.getX(); fy += bp.getY(); fz += bp.getZ(); @@ -485,6 +738,33 @@ public class SongHandler { private void stopAttack() { SongPlayer.MC.interactionManager.cancelBlockBreaking(); } + private void recordBlocks(Iterable bpList) { + for (BlockPos bp : bpList) { + if (!originalBlocks.containsKey(bp)) { + BlockState bs = SongPlayer.MC.world.getBlockState(bp); + originalBlocks.put(bp, bs); + } + } + } + private void recordStageBlocks() { + recordBlocks(stage.requiredBreaks); + recordBlocks(stage.missingNotes + .stream() + .map(noteId -> stage.noteblockPositions.get(noteId)) + .filter(Objects::nonNull) + .toList() + ); + } + private boolean isPlaceable(BlockState bs) { + Block block = bs.getBlock(); + if (bs.isAir() || bs.isLiquid()) { + return false; + } else if (block instanceof DoorBlock || block instanceof BedBlock) { + return false; + } else { + return true; + } + } private void doMovements(double lookX, double lookY, double lookZ) { if (Config.getConfig().swing) { @@ -494,9 +774,9 @@ public class SongHandler { } } if (Config.getConfig().rotate) { - double d = lookX - (stage.position.getX() + 0.5); - double e = lookY - (stage.position.getY() + SongPlayer.MC.player.getStandingEyeHeight()); - double f = lookZ - (stage.position.getZ() + 0.5); + double d = lookX - (lastStage.position.getX() + 0.5); + double e = lookY - (lastStage.position.getY() + SongPlayer.MC.player.getStandingEyeHeight()); + double f = lookZ - (lastStage.position.getZ() + 0.5); double g = Math.sqrt(d * d + f * f); float pitch = MathHelper.wrapDegrees((float) (-(MathHelper.atan2(e, g) * 57.2957763671875))); float yaw = MathHelper.wrapDegrees((float) (MathHelper.atan2(f, d) * 57.2957763671875) - 90.0f); @@ -506,7 +786,7 @@ public class SongHandler { SongPlayer.fakePlayer.setHeadYaw(yaw); } SongPlayer.MC.player.networkHandler.getConnection().send(new PlayerMoveC2SPacket.Full( - stage.position.getX() + 0.5, stage.position.getY(), stage.position.getZ() + 0.5, + lastStage.position.getX() + 0.5, lastStage.position.getY(), lastStage.position.getZ() + 0.5, yaw, pitch, true)); } @@ -525,6 +805,6 @@ public class SongHandler { } public boolean isIdle() { - return currentSong == null && currentPlaylist == null && songQueue.isEmpty(); + return currentSong == null && currentPlaylist == null && songQueue.isEmpty() && cleaningUp == false; } } \ No newline at end of file diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java index 5ee12c2..ae2ef29 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java @@ -468,4 +468,8 @@ public class Stage { } return false; } + + public Vec3d getOriginBottomCenter() { + return Vec3d.ofBottomCenter(position); + } } From 282a7ea4519e8f448b8fd8fa82076f48d6261688 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Sat, 25 May 2024 23:31:55 -0500 Subject: [PATCH 12/29] Sort gravity blocks by y level during cleanup --- .../songplayer/playing/SongHandler.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java index 33e7cf2..86b7f01 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java @@ -470,7 +470,7 @@ public class SongHandler { cleaningUp = false; SongPlayer.addChatMessage("§6Finished restoring original blocks"); if (!cleanupUnplaceableBlocks.isEmpty()) { - SongPlayer.addChatMessage(String.format("§3%d §6blocks were not successfully restored")); + SongPlayer.addChatMessage(String.format("§3%d §6blocks could not be restored", cleanupUnplaceableBlocks.size())); } } } @@ -504,6 +504,14 @@ public class SongHandler { } else if (!a_grav && b_grav) { return -1; } + // If there's gravity, sort by y coordinate + if (a_grav && b_grav) { + if (a.getY() < b.getY()) { + return -1; + } else if (a.getY() > b.getY()) { + return 1; + } + } // Then sort by distance int a_dx = a.getX() - lastStage.position.getX(); int a_dy = a.getY() - lastStage.position.getY(); @@ -541,6 +549,14 @@ public class SongHandler { } else if (!a_grav && b_grav) { return 1; } + // If there's gravity, sort by y coordinate + if (a_grav && b_grav) { + if (a.getY() < b.getY()) { + return 1; + } else if (a.getY() > b.getY()) { + return -1; + } + } // Then sort by distance int a_dx = a.getX() - lastStage.position.getX(); int a_dy = a.getY() - lastStage.position.getY(); From d8f8f96610c4ae396b74f34e40714da6d3619939 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Sun, 26 May 2024 01:11:09 -0500 Subject: [PATCH 13/29] Fixed flying consistency issue --- .../songplayer/CommandProcessor.java | 2 +- .../mixin/ClientPlayNetworkHandlerMixin.java | 2 +- .../mixin/ClientPlayerEntityMixin.java | 21 +++++++++++++++++++ .../songplayer/playing/SongHandler.java | 13 ++++++------ src/main/resources/songplayer.mixins.json | 1 + 5 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayerEntityMixin.java diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java index 3150b38..1840b35 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java @@ -253,7 +253,7 @@ public class CommandProcessor { SongHandler.getInstance().restoreStateAndReset(); SongPlayer.addChatMessage("§6Stopped cleanup"); } else if (Config.getConfig().autoCleanup && SongHandler.getInstance().originalBlocks.size() != 0) { - SongHandler.getInstance().partionResetAndCleanup(); + SongHandler.getInstance().partialResetAndCleanup(); SongPlayer.addChatMessage("§6Stopped playing and switched to cleanup"); } else { SongHandler.getInstance().restoreStateAndReset(); diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java index 7a03fa8..40f92cb 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java @@ -71,7 +71,7 @@ public class ClientPlayNetworkHandlerMixin { @Inject(at = @At("TAIL"), method = "onPlayerAbilities(Lnet/minecraft/network/packet/s2c/play/PlayerAbilitiesS2CPacket;)V") public void onOnPlayerAbilities(PlayerAbilitiesS2CPacket packet, CallbackInfo ci) { SongHandler handler = SongHandler.getInstance(); - if (handler.wasFlying) { + if (!handler.isIdle()) { SongPlayer.MC.player.getAbilities().flying = handler.wasFlying; } } diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayerEntityMixin.java b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayerEntityMixin.java new file mode 100644 index 0000000..ea86428 --- /dev/null +++ b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayerEntityMixin.java @@ -0,0 +1,21 @@ +package com.github.hhhzzzsss.songplayer.mixin; + +import com.github.hhhzzzsss.songplayer.playing.SongHandler; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.entity.player.PlayerAbilities; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ClientPlayerEntity.class) +public class ClientPlayerEntityMixin { + @Redirect(method = "tickMovement()V", at = @At(value = "FIELD", target = "Lnet/minecraft/entity/player/PlayerAbilities;allowFlying:Z", opcode = Opcodes.GETFIELD)) + private boolean getAllowFlying(PlayerAbilities playerAbilities) { + if (!SongHandler.getInstance().isIdle()) { + return true; + } else { + return playerAbilities.allowFlying; + } + } +} diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java index 86b7f01..7830c72 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java @@ -116,16 +116,14 @@ public class SongHandler { SongPlayer.fakePlayer.getInventory().clone(SongPlayer.MC.player.getInventory()); } - // Allow flying - SongPlayer.MC.player.getAbilities().allowFlying = true; + // Maintain flying status wasFlying = SongPlayer.MC.player.getAbilities().flying; } // Check if doing cleanup if (cleaningUp) { if (tick) { - // Allow flying while doing cleanup - SongPlayer.MC.player.getAbilities().allowFlying = true; + // Maintain flying status wasFlying = SongPlayer.MC.player.getAbilities().flying; handleCleanup(); @@ -153,7 +151,7 @@ public class SongHandler { else { if (dirty) { if (Config.getConfig().autoCleanup && originalBlocks.size() != 0) { - partionResetAndCleanup(); + partialResetAndCleanup(); } else { restoreStateAndReset(); } @@ -634,10 +632,13 @@ public class SongHandler { sendGamemodeCommand(Config.getConfig().survivalCommand); } } + if (SongPlayer.MC.player.getAbilities().allowFlying == false) { + SongPlayer.MC.player.getAbilities().flying = false; + } restoreBuildSlot(); reset(); } - public void partionResetAndCleanup() { + public void partialResetAndCleanup() { restoreBuildSlot(); currentSong = null; currentPlaylist = null; diff --git a/src/main/resources/songplayer.mixins.json b/src/main/resources/songplayer.mixins.json index 98094e1..23d100f 100644 --- a/src/main/resources/songplayer.mixins.json +++ b/src/main/resources/songplayer.mixins.json @@ -8,6 +8,7 @@ "client": [ "ChatInputSuggestorMixin", "ClientCommonNetworkHandlerMixin", + "ClientPlayerEntityMixin", "ClientPlayerInteractionManagerAccessor", "ClientPlayNetworkHandlerAccessor", "ClientPlayNetworkHandlerMixin", From 6b933463cf1ea7b6e4c26670d021db4f260d07c7 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Sun, 26 May 2024 01:17:35 -0500 Subject: [PATCH 14/29] Add upper half blocks to cleanup exclusion --- .../hhhzzzsss/songplayer/playing/SongHandler.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java index 7830c72..dc4c5cd 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java @@ -773,6 +773,16 @@ public class SongHandler { ); } private boolean isPlaceable(BlockState bs) { + Map, Comparable> entries = bs.getEntries(); + for (Map.Entry, Comparable> entry : entries.entrySet()) { + Property property = entry.getKey(); + Comparable value = entry.getValue(); + String propertyName = property.getName(); + String valueName = net.minecraft.util.Util.getValueAsString(property, value); + if (propertyName == "half" && valueName == "upper") { + return false; + } + } Block block = bs.getBlock(); if (bs.isAir() || bs.isLiquid()) { return false; From cbd27508efaace14bde19b88a82c91e280cba99f Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Mon, 27 May 2024 00:34:21 -0500 Subject: [PATCH 15/29] Add comment to clarify debug mixin --- .../com/github/hhhzzzsss/songplayer/mixin/ClientWorldMixin.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientWorldMixin.java b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientWorldMixin.java index cb74438..1f05648 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientWorldMixin.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientWorldMixin.java @@ -11,6 +11,8 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +// Used for debugging purposes + @Mixin(ClientWorld.class) public class ClientWorldMixin { @Inject(at = @At("HEAD"), method = "handleBlockUpdate(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;I)V", cancellable = true) From 44c614d6f0bafe6314b01ea0d5f1b0939ca76d6b Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Mon, 27 May 2024 00:35:30 -0500 Subject: [PATCH 16/29] Remove testBlockState command --- .../songplayer/CommandProcessor.java | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java index 1840b35..71b8f9d 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java @@ -62,7 +62,6 @@ public class CommandProcessor { commands.add(new announcementCommand()); commands.add(new songItemCommand()); commands.add(new testSongCommand()); - commands.add(new testBlockStateCommand()); for (Command command : commands) { commandMap.put(command.getName().toLowerCase(Locale.ROOT), command); @@ -1192,39 +1191,6 @@ public class CommandProcessor { } } - private static class testBlockStateCommand extends Command { - public String getName() { - return "testBlockState"; - } - public String[] getSyntax() { - return new String[0]; - } - public String getDescription() { - return "for dev purposes"; - } - public boolean processCommand(String args) { - if (args.length() == 0) { - if (MC.crosshairTarget instanceof BlockHitResult) { - BlockHitResult hitResult = (BlockHitResult) MC.crosshairTarget; - BlockState bs = MC.world.getBlockState(hitResult.getBlockPos()); - ItemStack stack = new ItemStack(bs.getBlock()); - SongPlayer.addChatMessage(stack.toString()); - for (Map.Entry, Comparable> entry : bs.getEntries().entrySet()) { - Property property = entry.getKey(); - Comparable value = entry.getValue(); -// SongPlayer.addChatMessage(net.minecraft.util.Util.getValueAsString(property, comparable)); - System.out.println(property.getClass()); - System.out.println(value.getClass()); - } - } - return true; - } - else { - return false; - } - } - } - public static CompletableFuture handleSuggestions(String text, SuggestionsBuilder suggestionsBuilder) { if (!text.contains(" ")) { List names = commandCompletions From e0c87ffda04039b32a2dc5c148b772c7bf9e93e6 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Thu, 30 May 2024 22:11:38 -0500 Subject: [PATCH 17/29] Survival only mode --- .../songplayer/CommandProcessor.java | 36 +++++- .../github/hhhzzzsss/songplayer/Config.java | 1 + .../songplayer/playing/SongHandler.java | 119 +++++++++++------- .../hhhzzzsss/songplayer/playing/Stage.java | 114 ++++++++++++++++- .../hhhzzzsss/songplayer/song/Instrument.java | 36 +++--- 5 files changed, 237 insertions(+), 69 deletions(-) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java index 71b8f9d..cc155ba 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java @@ -9,17 +9,14 @@ import com.github.hhhzzzsss.songplayer.song.Playlist; import com.github.hhhzzzsss.songplayer.song.Song; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; -import net.minecraft.block.BlockState; import net.minecraft.command.CommandSource; import net.minecraft.component.DataComponentTypes; import net.minecraft.component.type.NbtComponent; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; -import net.minecraft.state.property.Property; import net.minecraft.text.*; import net.minecraft.util.Formatting; import net.minecraft.util.Hand; -import net.minecraft.util.hit.BlockHitResult; import net.minecraft.world.GameMode; import java.io.IOException; @@ -61,6 +58,7 @@ public class CommandProcessor { commands.add(new cleanupLastStageCommand()); commands.add(new announcementCommand()); commands.add(new songItemCommand()); + commands.add(new toggleSurvivalOnly()); commands.add(new testSongCommand()); for (Command command : commands) { @@ -1003,7 +1001,6 @@ public class CommandProcessor { return true; } if (SongPlayer.MC.player.getPos().squaredDistanceTo(lastStage.getOriginBottomCenter()) > 3*3) { - System.out.println(SongPlayer.MC.player.getPos().squaredDistanceTo(lastStage.getOriginBottomCenter())); String coordStr = String.format( "%d %d %d", lastStage.position.getX(), lastStage.position.getY(), lastStage.position.getZ() @@ -1165,6 +1162,37 @@ public class CommandProcessor { } } + private static class toggleSurvivalOnly extends Command { + public String getName() { + return "toggleSurvivalOnly"; + } + public String[] getAliases() { + return new String[]{"survivalOnly"}; + } + public String[] getSyntax() { + return new String[0]; + } + public String getDescription() { + return "Enables or disables survival-only mode, in which automatic noteblock placement is disabled and automatic tuning is done by right-clicking.."; + } + public boolean processCommand(String args) { + if (args.length() == 0) { + Config.getConfig().survivalOnly = !Config.getConfig().survivalOnly; + if (Config.getConfig().survivalOnly) { + SongPlayer.addChatMessage("§6Enabled survival only mode"); + } + else { + SongPlayer.addChatMessage("§6Disabled survival only mode"); + } + Config.saveConfigWithErrorHandling(); + return true; + } + else { + return false; + } + } + } + private static class testSongCommand extends Command { public String getName() { return "testSong"; diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/Config.java b/src/main/java/com/github/hhhzzzsss/songplayer/Config.java index a71555b..9b15391 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/Config.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/Config.java @@ -27,6 +27,7 @@ public class Config { public boolean doAnnouncement = false; public String announcementMessage = "&6Now playing: &3[name]"; public boolean autoCleanup = false; + public boolean survivalOnly = false; public static Config getConfig() { if (config == null) { diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java index dc4c5cd..e0e17a6 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java @@ -150,7 +150,7 @@ public class SongHandler { // Otherwise, handle cleanup if necessary else { if (dirty) { - if (Config.getConfig().autoCleanup && originalBlocks.size() != 0) { + if (Config.getConfig().autoCleanup && originalBlocks.size() != 0 && !Config.getConfig().survivalOnly) { partialResetAndCleanup(); } else { restoreStateAndReset(); @@ -198,12 +198,12 @@ public class SongHandler { dirty = true; currentSong = song; building = true; - setCreativeIfNeeded(); + if (!Config.getConfig().survivalOnly) setCreativeIfNeeded(); if (Config.getConfig().doAnnouncement) { sendMessage(Config.getConfig().announcementMessage.replaceAll("\\[name\\]", song.name)); } prepareStage(); - getAndSaveBuildSlot(); + if (!Config.getConfig().survivalOnly) getAndSaveBuildSlot(); SongPlayer.addChatMessage("§6Building noteblocks"); } @@ -259,52 +259,73 @@ public class SongHandler { return; } ClientWorld world = SongPlayer.MC.world; - if (SongPlayer.MC.interactionManager.getCurrentGameMode() != GameMode.CREATIVE) { + if (!Config.getConfig().survivalOnly && SongPlayer.MC.interactionManager.getCurrentGameMode() != GameMode.CREATIVE) { return; } - if (stage.nothingToBuild()) { - if (buildEndDelay > 0) { + if (stage.nothingToBuild()) { // If there's nothing to build, wait for end delay then check build status + if (buildEndDelay > 0) { // Wait for end delay buildEndDelay--; return; - } else { - stage.checkBuildStatus(currentSong); - recordStageBlocks(); + } else { // Check build status when end delay is over + if (!Config.getConfig().survivalOnly) { + stage.checkBuildStatus(currentSong); + recordStageBlocks(); + } else { + try { + stage.checkSurvivalBuildStatus(currentSong); + } catch (Stage.NotEnoughInstrumentsException e) { + e.giveInstrumentSummary(); + reset(); + return; + } + } stage.sendMovementPacketToStagePosition(); } } - if (!stage.requiredBreaks.isEmpty()) { - for (int i=0; i<5; i++) { - if (stage.requiredBreaks.isEmpty()) break; - BlockPos bp = stage.requiredBreaks.poll(); - attackBlock(bp); - } - buildEndDelay = 20; - } else if (!stage.missingNotes.isEmpty()) { - int desiredNoteId = stage.missingNotes.pollFirst(); - BlockPos bp = stage.noteblockPositions.get(desiredNoteId); - if (bp == null) { - return; - } - int blockId = Block.getRawIdFromState(world.getBlockState(bp)); - int currentNoteId = (blockId-SongPlayer.NOTEBLOCK_BASE_ID)/2; - if (currentNoteId != desiredNoteId) { - holdNoteblock(desiredNoteId, buildSlot); - if (blockId != 0) { - attackBlock(bp); - } - placeBlock(bp); - } - buildCooldown = 0; // No cooldown, so it places a block every tick - buildEndDelay = 20; - } else { // Switch to playing - restoreBuildSlot(); + if (stage.nothingToBuild()) { // If there's still nothing to build after checking build status, switch to playing + if (!Config.getConfig().survivalOnly) restoreBuildSlot(); building = false; - setSurvivalIfNeeded(); stage.sendMovementPacketToStagePosition(); SongPlayer.addChatMessage("§6Now playing §3" + currentSong.name); } + + if (!Config.getConfig().survivalOnly) { // Regular mode + if (!stage.requiredBreaks.isEmpty()) { + for (int i = 0; i < 5; i++) { + if (stage.requiredBreaks.isEmpty()) break; + BlockPos bp = stage.requiredBreaks.poll(); + attackBlock(bp); + } + buildEndDelay = 20; + } else if (!stage.missingNotes.isEmpty()) { + int desiredNoteId = stage.missingNotes.pollFirst(); + BlockPos bp = stage.noteblockPositions.get(desiredNoteId); + if (bp == null) { + return; + } + int blockId = Block.getRawIdFromState(world.getBlockState(bp)); + int currentNoteId = (blockId - SongPlayer.NOTEBLOCK_BASE_ID) / 2; + if (currentNoteId != desiredNoteId) { + holdNoteblock(desiredNoteId, buildSlot); + if (blockId != 0) { + attackBlock(bp); + } + placeBlock(bp); + } + buildCooldown = 0; // No cooldown, so it places a block every tick + buildEndDelay = 20; + } + } else { // Survival only mode + if (!stage.requiredClicks.isEmpty()) { + BlockPos bp = stage.requiredClicks.pollFirst(); + if (SongPlayer.MC.world.getBlockState(bp).getBlock() == Blocks.NOTE_BLOCK) { + placeBlock(bp); + } + buildEndDelay = 20; + } + } } private void setBuildProgressDisplay() { MutableText buildText = Text.empty() @@ -339,12 +360,22 @@ public class SongHandler { if (tick) { if (stage.hasBreakingModification()) { - stage.checkBuildStatus(currentSong); - recordStageBlocks(); + if (!Config.getConfig().survivalOnly) { + stage.checkBuildStatus(currentSong); + recordStageBlocks(); + } else { + try { + stage.checkSurvivalBuildStatus(currentSong); + } catch (Stage.NotEnoughInstrumentsException e) { + SongPlayer.addChatMessage("§6Stopped because stage is missing instruments required for song."); + reset(); + return; + } + } } if (!stage.nothingToBuild()) { // Switch to building building = true; - setCreativeIfNeeded(); + if (!Config.getConfig().survivalOnly) setCreativeIfNeeded(); stage.sendMovementPacketToStagePosition(); currentSong.pause(); buildStartDelay = 20; @@ -354,7 +385,7 @@ public class SongHandler { int instrumentId = note / 25; System.out.println("Missing note: " + Instrument.getInstrumentFromId(instrumentId).name() + ":" + pitch); } - getAndSaveBuildSlot(); + if (!Config.getConfig().survivalOnly) getAndSaveBuildSlot(); SongPlayer.addChatMessage("§6Stage was altered. Rebuilding!"); return; } @@ -585,10 +616,6 @@ public class SongHandler { cleanupPlaceList = cleanupPlaceList.reversed(); cleanupTotalBlocksToPlace = cleanupPlaceList.size(); - for (BlockPos bp : cleanupPlaceList) { - System.out.println(world.getBlockState(bp).getBlock() + " " + originalBlocks.get(bp).getBlock()); - } - boolean noNecessaryBreaks = cleanupBreakList.stream().allMatch( bp -> world.getBlockState(bp).getBlock().getDefaultState().equals(originalBlocks.get(bp).getBlock().getDefaultState()) ); @@ -624,7 +651,7 @@ public class SongHandler { if (lastStage != null) { lastStage.movePlayerToStagePosition(); } - if (originalGamemode != SongPlayer.MC.interactionManager.getCurrentGameMode()) { + if (originalGamemode != SongPlayer.MC.interactionManager.getCurrentGameMode() && !Config.getConfig().survivalOnly) { if (originalGamemode == GameMode.CREATIVE) { sendGamemodeCommand(Config.getConfig().creativeCommand); } @@ -635,7 +662,7 @@ public class SongHandler { if (SongPlayer.MC.player.getAbilities().allowFlying == false) { SongPlayer.MC.player.getAbilities().flying = false; } - restoreBuildSlot(); + if (!Config.getConfig().survivalOnly) restoreBuildSlot(); reset(); } public void partialResetAndCleanup() { diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java index ae2ef29..be5b7fe 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java @@ -2,6 +2,7 @@ package com.github.hhhzzzsss.songplayer.playing; import com.github.hhhzzzsss.songplayer.Config; import com.github.hhhzzzsss.songplayer.SongPlayer; +import com.github.hhhzzzsss.songplayer.song.Instrument; import com.github.hhhzzzsss.songplayer.song.Song; import net.minecraft.block.Block; import net.minecraft.block.BlockState; @@ -25,10 +26,16 @@ public class Stage { public BlockPos position; public HashMap noteblockPositions = new HashMap<>(); + // Not used in survival-only mode public LinkedList requiredBreaks = new LinkedList<>(); public TreeSet missingNotes = new TreeSet<>(); public int totalMissingNotes = 0; - + + // Only used in survival-only mode + public LinkedList untunedNoteblocks = new LinkedList<>(); + public LinkedList requiredClicks = new LinkedList<>(); + public int totalUntunedNoteblocks = 0; + public Stage() { position = player.getBlockPos(); } @@ -195,6 +202,82 @@ public class Stage { totalMissingNotes = missingNotes.size(); } + public void checkSurvivalBuildStatus(Song song) throws NotEnoughInstrumentsException { + noteblockPositions.clear(); + + Map[] instrumentMap = loadSurvivalBlocks(); + + int[] requiredInstruments = new int[16]; + boolean hasMissing = false; + for (int instrumentId = 0; instrumentId < 16; instrumentId++) { + for (int pitch = 0; pitch < 25; pitch++) { + int noteId = instrumentId*25 + pitch; + if (song.requiredNotes[noteId]) { + requiredInstruments[instrumentId]++; + } + } + if (requiredInstruments[instrumentId] > instrumentMap[instrumentId].size()) { + hasMissing = true; + } + } + + if (hasMissing) { + int[] foundInstruments = new int[16]; + for (int i = 0; i < 16; i++) { + foundInstruments[i] = instrumentMap[i].size(); + } + throw new NotEnoughInstrumentsException(requiredInstruments, foundInstruments); + } + + for (int noteid=0; noteid<400; noteid++) { + if (song.requiredNotes[noteid]) { + int instrumentId = noteid / 25; + int targetPitch = noteid % 25; + Map.Entry closest = instrumentMap[instrumentId].entrySet() + .stream() + .min((a, b) -> { + int adist = (targetPitch - a.getValue() + 25) % 25; + int bdist = (targetPitch - b.getValue() + 25) % 25; + return Integer.compare(adist, bdist); + }) + .get(); + BlockPos bp = closest.getKey(); + int closestPitch = closest.getValue(); + instrumentMap[instrumentId].remove(bp); + noteblockPositions.put(noteid, bp); + int repetitions = (targetPitch - closestPitch + 25) % 25; + for (int i = 0; i < repetitions; i++) { + requiredClicks.add(bp); + } + } + } + } + + public class NotEnoughInstrumentsException extends Exception { + public int[] requiredInstruments; + public int[] foundInstruments; + public NotEnoughInstrumentsException(int[] requiredInstruments, int[] foundInstruments) { + this.requiredInstruments = requiredInstruments; + this.foundInstruments = foundInstruments; + } + public void giveInstrumentSummary() { + SongPlayer.addChatMessage("§c------------------------------"); + SongPlayer.addChatMessage("§cMissing instruments required to play song:"); + for (int instrumentId = 0; instrumentId < 16; instrumentId++) { + if (requiredInstruments[instrumentId] > 0) { + Instrument instrument = Instrument.getInstrumentFromId(instrumentId); + SongPlayer.addChatMessage(String.format( + " §3%s (%s): §%s%d/%d", + instrument.name(), instrument.material, + foundInstruments[instrumentId] < requiredInstruments[instrumentId] ? "c" : "a", + foundInstruments[instrumentId], requiredInstruments[instrumentId] + )); + } + } + SongPlayer.addChatMessage("§c------------------------------"); + } + } + void loadDefaultBlocks(Collection noteblockLocations, Collection breakLocations) { for (int dx = -4; dx <= 4; dx++) { for (int dz = -4; dz <= 4; dz++) { @@ -425,6 +508,29 @@ public class Stage { } } + Map[] loadSurvivalBlocks() { + Map[] instrumentMap = new Map[16]; + for (int i = 0; i < 16; i++) { + instrumentMap[i] = new TreeMap<>(); + } + for (int dx = -5; dx <= 5; dx++) { + for (int dz = -5; dz <= 5; dz++) { + for (int dy : new int[]{-1, 0, 1, 2, -2, 3, -3, 4, -4, 5, 6}) { + BlockPos bp = position.add(dx, dy, dz); + BlockState bs = SongPlayer.MC.world.getBlockState(bp); + int blockId = Block.getRawIdFromState(bs); + if (blockId >= SongPlayer.NOTEBLOCK_BASE_ID && blockId < SongPlayer.NOTEBLOCK_BASE_ID + 800) { + int noteId = (blockId - SongPlayer.NOTEBLOCK_BASE_ID) / 2; + int instrument = noteId / 25; + int pitch = noteId % 25; + instrumentMap[instrument].put(bp, pitch); + } + } + } + } + return instrumentMap; + } + // This doesn't check for whether the block above the noteblock position is also reachable // Usually there is sky above you though so hopefully this doesn't cause a problem most of the time boolean withinBreakingDist(int dx, int dy, int dz) { @@ -434,7 +540,11 @@ public class Stage { } public boolean nothingToBuild() { - return requiredBreaks.isEmpty() && missingNotes.isEmpty(); + if (!Config.getConfig().survivalOnly) { + return requiredBreaks.isEmpty() && missingNotes.isEmpty(); + } else { + return requiredClicks.isEmpty(); + } } private static final int WRONG_INSTRUMENT_TOLERANCE = 3; diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/song/Instrument.java b/src/main/java/com/github/hhhzzzsss/songplayer/song/Instrument.java index 2047466..bdd8bc3 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/song/Instrument.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/song/Instrument.java @@ -1,29 +1,31 @@ package com.github.hhhzzzsss.songplayer.song; public enum Instrument { - HARP(0, 54), - BASEDRUM(1, 0), - SNARE(2, 0), - HAT(3, 0), - BASS(4, 30), - FLUTE(5, 66), - BELL(6, 78), - GUITAR(7, 42), - CHIME(8, 78), - XYLOPHONE(9, 78), - IRON_XYLOPHONE(10, 54), - COW_BELL(11, 66), - DIDGERIDOO(12, 30), - BIT(13, 54), - BANJO(14, 54), - PLING(15, 54); + HARP(0, 54, "Dirt/Other"), + BASEDRUM(1, 0, "Any Stone"), + SNARE(2, 0, "Sand/Gravel"), + HAT(3, 0, "Glass"), + BASS(4, 30, "Any Wood"), + FLUTE(5, 66, "Clay"), + BELL(6, 78, "Block of Gold"), + GUITAR(7, 42, "Wool"), + CHIME(8, 78, "Packed Ice"), + XYLOPHONE(9, 78, "Bone Block"), + IRON_XYLOPHONE(10, 54, "Block of Iron"), + COW_BELL(11, 66, "Soul Sand"), + DIDGERIDOO(12, 30, "Pumpkin"), + BIT(13, 54, "Block of Emerald"), + BANJO(14, 54, "Hay Bale"), + PLING(15, 54, "Glowstone"); public final int instrumentId; public final int offset; + public final String material; - Instrument(int instrumentId, int offset) { + Instrument(int instrumentId, int offset, String material) { this.instrumentId = instrumentId; this.offset = offset; + this.material = material; } private static Instrument[] values = values(); From f4075075f2b3fee486082f525ba34423d475991b Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Thu, 30 May 2024 22:38:21 -0500 Subject: [PATCH 18/29] Minor fixes and cleanup --- .../hhhzzzsss/songplayer/playing/SongHandler.java | 11 +++++++---- .../github/hhhzzzsss/songplayer/playing/Stage.java | 1 - 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java index e0e17a6..a4a1278 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java @@ -276,7 +276,7 @@ public class SongHandler { stage.checkSurvivalBuildStatus(currentSong); } catch (Stage.NotEnoughInstrumentsException e) { e.giveInstrumentSummary(); - reset(); + restoreStateAndReset(); return; } } @@ -285,8 +285,11 @@ public class SongHandler { } if (stage.nothingToBuild()) { // If there's still nothing to build after checking build status, switch to playing - if (!Config.getConfig().survivalOnly) restoreBuildSlot(); building = false; + if (!Config.getConfig().survivalOnly) { + setSurvivalIfNeeded(); + restoreBuildSlot(); + } stage.sendMovementPacketToStagePosition(); SongPlayer.addChatMessage("§6Now playing §3" + currentSong.name); } @@ -806,7 +809,7 @@ public class SongHandler { Comparable value = entry.getValue(); String propertyName = property.getName(); String valueName = net.minecraft.util.Util.getValueAsString(property, value); - if (propertyName == "half" && valueName == "upper") { + if (propertyName.equals("half") && valueName.equals("upper")) { return false; } } @@ -859,6 +862,6 @@ public class SongHandler { } public boolean isIdle() { - return currentSong == null && currentPlaylist == null && songQueue.isEmpty() && cleaningUp == false; + return currentSong == null && currentPlaylist == null && songQueue.isEmpty() && !cleaningUp; } } \ No newline at end of file diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java index be5b7fe..286f7e4 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java @@ -41,7 +41,6 @@ public class Stage { } public void movePlayerToStagePosition() { - player.getAbilities().allowFlying = true; player.getAbilities().flying = true; player.refreshPositionAndAngles(position.getX() + 0.5, position.getY() + 0.0, position.getZ() + 0.5, player.getYaw(), player.getPitch()); player.setVelocity(Vec3d.ZERO); From e40f0919617bd274fa83c6881a28b9d1291da8a0 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Thu, 30 May 2024 22:53:31 -0500 Subject: [PATCH 19/29] Add checks for survival only mode --- .../github/hhhzzzsss/songplayer/CommandProcessor.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java index cc155ba..7eb7c25 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java @@ -218,6 +218,11 @@ public class CommandProcessor { } public boolean processCommand(String args) { if (args.length() > 0) { + if (Config.getConfig().survivalOnly && SongPlayer.MC.interactionManager.getCurrentGameMode() != GameMode.SURVIVAL) { + SongPlayer.addChatMessage("§cTo play in survival only mode, you must be in survival mode to start with."); + return true; + } + SongHandler.getInstance().loadSong(args); return true; } @@ -1177,6 +1182,11 @@ public class CommandProcessor { } public boolean processCommand(String args) { if (args.length() == 0) { + if (!SongHandler.getInstance().isIdle()) { + SongPlayer.addChatMessage("§cYou cannot change this setting while playing or building"); + return true; + } + Config.getConfig().survivalOnly = !Config.getConfig().survivalOnly; if (Config.getConfig().survivalOnly) { SongPlayer.addChatMessage("§6Enabled survival only mode"); From 072caac26a5ff4acaee9a9caadd90636e7121d20 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Thu, 30 May 2024 23:02:06 -0500 Subject: [PATCH 20/29] Remove wrong instrument tolerance --- .../com/github/hhhzzzsss/songplayer/playing/Stage.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java index 286f7e4..681968b 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java @@ -546,9 +546,7 @@ public class Stage { } } - private static final int WRONG_INSTRUMENT_TOLERANCE = 3; public boolean hasBreakingModification() { - int wrongInstruments = 0; for (Map.Entry entry : noteblockPositions.entrySet()) { BlockState bs = SongPlayer.MC.world.getBlockState(entry.getValue()); int blockId = Block.getRawIdFromState(bs); @@ -564,10 +562,7 @@ public class Stage { return true; } if (targetInstrument != actualInstrument) { - wrongInstruments++; - if (wrongInstruments > WRONG_INSTRUMENT_TOLERANCE) { - return true; - } + return true; } BlockState aboveBs = SongPlayer.MC.world.getBlockState(entry.getValue().up()); From 2363900c05c9e123a62dccecf24d2ae11dec1693 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Fri, 31 May 2024 19:27:48 -0500 Subject: [PATCH 21/29] Add out-of-range wrapping to nbs conversion --- .../hhhzzzsss/songplayer/conversion/NBSConverter.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/conversion/NBSConverter.java b/src/main/java/com/github/hhhzzzsss/songplayer/conversion/NBSConverter.java index dad97c8..f81f2da 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/conversion/NBSConverter.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/conversion/NBSConverter.java @@ -147,8 +147,11 @@ public class NBSConverter { continue; } - if (note.key < 33 || note.key > 57) { - continue; + while (note.key < 33) { + note.key += 12; + } + while (note.key > 57) { + note.key -= 12; } byte layerVolume = 100; From 707b5e6f0c4251f419d66b4c6c75018c6cc50b83 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Fri, 31 May 2024 19:53:28 -0500 Subject: [PATCH 22/29] Fixed glitch with sending extra movement packet after cleanup --- .../mixin/ClientCommonNetworkHandlerMixin.java | 12 +++++++----- .../hhhzzzsss/songplayer/playing/SongHandler.java | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientCommonNetworkHandlerMixin.java b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientCommonNetworkHandlerMixin.java index 1201372..a2bb466 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientCommonNetworkHandlerMixin.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientCommonNetworkHandlerMixin.java @@ -29,11 +29,13 @@ public class ClientCommonNetworkHandlerMixin { private void onSendPacket(Packet packet, CallbackInfo ci) { Stage lastStage = SongHandler.getInstance().lastStage; - if (!SongHandler.getInstance().isIdle() && lastStage != null && packet instanceof PlayerMoveC2SPacket) { - if (!Config.getConfig().rotate) { - connection.send(new PlayerMoveC2SPacket.Full(lastStage.position.getX() + 0.5, lastStage.position.getY(), lastStage.position.getZ() + 0.5, SongPlayer.MC.player.getYaw(), SongPlayer.MC.player.getPitch(), true)); - if (SongPlayer.fakePlayer != null) { - SongPlayer.fakePlayer.copyStagePosAndPlayerLook(); + if (!SongHandler.getInstance().isIdle() && packet instanceof PlayerMoveC2SPacket) { + if (lastStage != null) { + if (!Config.getConfig().rotate) { + connection.send(new PlayerMoveC2SPacket.Full(lastStage.position.getX() + 0.5, lastStage.position.getY(), lastStage.position.getZ() + 0.5, SongPlayer.MC.player.getYaw(), SongPlayer.MC.player.getPitch(), true)); + if (SongPlayer.fakePlayer != null) { + SongPlayer.fakePlayer.copyStagePosAndPlayerLook(); + } } } ci.cancel(); diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java index a4a1278..29fd86e 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java @@ -862,6 +862,6 @@ public class SongHandler { } public boolean isIdle() { - return currentSong == null && currentPlaylist == null && songQueue.isEmpty() && !cleaningUp; + return currentSong == null && currentPlaylist == null && songQueue.isEmpty() && !cleaningUp && !dirty; } } \ No newline at end of file From 3d2f89e10f58cea39a8975411c19440f0bd282bc Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Fri, 31 May 2024 22:38:00 -0500 Subject: [PATCH 23/29] Cut short the starting delay of midis if too long --- .../songplayer/conversion/MidiConverter.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/conversion/MidiConverter.java b/src/main/java/com/github/hhhzzzsss/songplayer/conversion/MidiConverter.java index f36b74a..89fb227 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/conversion/MidiConverter.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/conversion/MidiConverter.java @@ -126,6 +126,17 @@ public class MidiConverter { } song.sort(); + + // Shift to beginning if delay is too long + if (!song.notes.isEmpty()) { + long shift = song.notes.get(0).time - 1000; + if (song.notes.get(0).time > 1000) { + for (Note note : song.notes) { + note.time -= shift; + } + } + song.length -= shift; + } return song; } From 92a982b2f0cec876adda3dd23e243ddc9c51f727 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Sun, 2 Jun 2024 12:28:24 -0500 Subject: [PATCH 24/29] Added velocity threshold --- .../songplayer/CommandProcessor.java | 35 +++++++++++++++++++ .../github/hhhzzzsss/songplayer/Config.java | 1 + .../songplayer/conversion/MidiConverter.java | 15 ++++---- .../songplayer/conversion/NBSConverter.java | 2 +- .../songplayer/conversion/SPConverter.java | 3 +- .../songplayer/playing/SongHandler.java | 10 +++--- .../hhhzzzsss/songplayer/song/Note.java | 8 +++++ 7 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java index 7eb7c25..d744738 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java @@ -54,6 +54,7 @@ public class CommandProcessor { commands.add(new toggleFakePlayerCommand()); commands.add(new setStageTypeCommand()); commands.add(new toggleMovementCommand()); + commands.add(new setVelocityThresholdCommand()); commands.add(new toggleAutoCleanup()); commands.add(new cleanupLastStageCommand()); commands.add(new announcementCommand()); @@ -950,6 +951,40 @@ public class CommandProcessor { } } + private static class setVelocityThresholdCommand extends Command { + public String getName() { + return "setVelocityThreshold"; + } + public String[] getAliases() { + return new String[]{"velocityThreshold", "threshold"}; + } + public String[] getSyntax() { + return new String[] {""}; + } + public String getDescription() { + return "Sets the minimum velocity below which notes won't be played (applies to midi and nbs). This must be a number from 0 to 100. For song items, the threshold is baked in upon item creation."; + } + public boolean processCommand(String args) { + if (args.length() > 0) { + try { + int threshold = Integer.parseInt(args); + if (threshold < 0 || threshold > 100) { + SongPlayer.addChatMessage("§cVelocity threshold must be a value between 0 and 100"); + return true; + } + Config.getConfig().velocityThreshold = threshold; + SongPlayer.addChatMessage("§6Set velocity threshold to " + threshold); + Config.saveConfigWithErrorHandling(); + return true; + } catch (NumberFormatException e) { + return false; + } + } else { + return false; + } + } + } + private static class toggleAutoCleanup extends Command { public String getName() { return "toggleAutoCleanup"; diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/Config.java b/src/main/java/com/github/hhhzzzsss/songplayer/Config.java index 9b15391..96b9373 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/Config.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/Config.java @@ -24,6 +24,7 @@ public class Config { public Stage.StageType stageType = Stage.StageType.DEFAULT; public boolean swing = false; public boolean rotate = false; + public int velocityThreshold = 0; public boolean doAnnouncement = false; public String announcementMessage = "&6Now playing: &3[name]"; public boolean autoCleanup = false; diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/conversion/MidiConverter.java b/src/main/java/com/github/hhhzzzsss/songplayer/conversion/MidiConverter.java index 89fb227..5e996fd 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/conversion/MidiConverter.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/conversion/MidiConverter.java @@ -90,18 +90,19 @@ public class MidiConverter { instrumentIds[sm.getChannel()] = sm.getData1(); } else if (sm.getCommand() == NOTE_ON) { - if (sm.getData2() == 0) continue; int pitch = sm.getData1(); + int velocity = sm.getData2(); + if (velocity == 0) continue; // Just ignore notes with velocity 0 long deltaTick = event.getTick() - prevTick; prevTick = event.getTick(); microTime += (mpq/tpq) * deltaTick; Note note; if (sm.getChannel() == 9) { - note = getMidiPercussionNote(pitch, microTime); + note = getMidiPercussionNote(pitch, velocity, microTime); } else { - note = getMidiInstrumentNote(instrumentIds[sm.getChannel()], pitch, microTime); + note = getMidiInstrumentNote(instrumentIds[sm.getChannel()], pitch, velocity, microTime); } if (note != null) { song.add(note); @@ -141,7 +142,7 @@ public class MidiConverter { return song; } - public static Note getMidiInstrumentNote(int midiInstrument, int midiPitch, long microTime) { + public static Note getMidiInstrumentNote(int midiInstrument, int midiPitch, int velocity, long microTime) { com.github.hhhzzzsss.songplayer.song.Instrument instrument = null; com.github.hhhzzzsss.songplayer.song.Instrument[] instrumentList = instrumentMap.get(midiInstrument); if (instrumentList != null) { @@ -161,15 +162,15 @@ public class MidiConverter { int noteId = pitch + instrument.instrumentId*25; long time = microTime / 1000L; - return new Note(noteId, time); + return new Note(noteId, time, velocity); } - private static Note getMidiPercussionNote(int midiPitch, long microTime) { + private static Note getMidiPercussionNote(int midiPitch, int velocity, long microTime) { if (percussionMap.containsKey(midiPitch)) { int noteId = percussionMap.get(midiPitch); long time = microTime / 1000L; - return new Note(noteId, time); + return new Note(noteId, time, velocity); } return null; } diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/conversion/NBSConverter.java b/src/main/java/com/github/hhhzzzsss/songplayer/conversion/NBSConverter.java index f81f2da..7572947 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/conversion/NBSConverter.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/conversion/NBSConverter.java @@ -161,7 +161,7 @@ public class NBSConverter { int pitch = note.key-33; int noteId = pitch + instrument.instrumentId*25; - song.add(new Note(noteId, getMilliTime(note.tick, tempo))); + song.add(new Note(noteId, getMilliTime(note.tick, tempo), layerVolume)); } song.length = song.get(song.size()-1).time + 50; diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/conversion/SPConverter.java b/src/main/java/com/github/hhhzzzsss/songplayer/conversion/SPConverter.java index 41da087..3a2ff56 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/conversion/SPConverter.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/conversion/SPConverter.java @@ -1,5 +1,6 @@ package com.github.hhhzzzsss.songplayer.conversion; +import com.github.hhhzzzsss.songplayer.Config; import com.github.hhhzzzsss.songplayer.Util; import com.github.hhhzzzsss.songplayer.song.Note; import com.github.hhhzzzsss.songplayer.song.Song; @@ -81,7 +82,7 @@ public class SPConverter { song.sort(); long prevTime = 0; - for (Note note : song.notes) { + for (Note note : song.notes) if (note.velocity >= Config.getConfig().velocityThreshold) { writeShort(os, note.noteId); writeVarLong(os, note.time - prevTime); prevTime = note.time; diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java index 29fd86e..2a807dc 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java @@ -400,10 +400,12 @@ public class SongHandler { currentSong.advanceTime(); while (currentSong.reachedNextNote()) { Note note = currentSong.getNextNote(); - BlockPos bp = stage.noteblockPositions.get(note.noteId); - if (bp != null) { - attackBlock(bp); - somethingPlayed = true; + if (note.velocity >= Config.getConfig().velocityThreshold) { + BlockPos bp = stage.noteblockPositions.get(note.noteId); + if (bp != null) { + attackBlock(bp); + somethingPlayed = true; + } } } if (somethingPlayed) { diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/song/Note.java b/src/main/java/com/github/hhhzzzsss/songplayer/song/Note.java index e596958..aa67d08 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/song/Note.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/song/Note.java @@ -3,9 +3,17 @@ package com.github.hhhzzzsss.songplayer.song; public class Note implements Comparable { public int noteId; public long time; + public int velocity; public Note(int note, long time) { this.noteId = note; this.time = time; + this.velocity = 100; + } + + public Note(int note, long time, int velocity) { + this.noteId = note; + this.time = time; + this.velocity = velocity; } @Override From 8d28ed8832afec67fde13e0a05c77ac7387da7dd Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Tue, 4 Jun 2024 18:59:30 -0500 Subject: [PATCH 25/29] Fix midi velocity range --- .../github/hhhzzzsss/songplayer/conversion/MidiConverter.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/conversion/MidiConverter.java b/src/main/java/com/github/hhhzzzsss/songplayer/conversion/MidiConverter.java index 5e996fd..2816e2c 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/conversion/MidiConverter.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/conversion/MidiConverter.java @@ -93,6 +93,7 @@ public class MidiConverter { int pitch = sm.getData1(); int velocity = sm.getData2(); if (velocity == 0) continue; // Just ignore notes with velocity 0 + velocity = (velocity * 100) / 127; // Midi velocity goes from 0-127 long deltaTick = event.getTick() - prevTick; prevTick = event.getTick(); microTime += (mpq/tpq) * deltaTick; From 2e08a32ffcab68acf65a70ae2d5e8db53641ae11 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Sat, 3 Aug 2024 22:49:40 -0500 Subject: [PATCH 26/29] Update to 1.21 --- build.gradle | 2 +- gradle.properties | 8 ++--- gradle/wrapper/gradle-wrapper.jar | Bin 61574 -> 43453 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 29 ++++++++++-------- gradlew.bat | 20 ++++++------ .../item/SongItemConfirmationScreen.java | 5 ++- .../mixin/ClientPlayNetworkHandlerMixin.java | 2 +- .../hhhzzzsss/songplayer/playing/Stage.java | 3 +- src/main/resources/fabric.mod.json | 6 ++-- 10 files changed, 41 insertions(+), 37 deletions(-) diff --git a/build.gradle b/build.gradle index 46a0781..14de5c2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'fabric-loom' version '1.6-SNAPSHOT' + id 'fabric-loom' version '1.7-SNAPSHOT' id 'maven-publish' } diff --git a/gradle.properties b/gradle.properties index d232237..7f14159 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,9 +4,9 @@ org.gradle.parallel=true # Fabric Properties # check these on https://fabricmc.net/use - minecraft_version=1.20.6 - yarn_mappings=1.20.6+build.1 - loader_version=0.15.10 + minecraft_version=1.21 + yarn_mappings=1.21+build.9 + loader_version=0.15.11 # Mod Properties mod_version = 3.1.4 @@ -15,4 +15,4 @@ org.gradle.parallel=true # Dependencies # currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api - fabric_version=0.97.8+1.20.6 + fabric_version=0.100.8+1.21 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 943f0cbfa754578e88a3dae77fce6e3dea56edbf..e6441136f3d4ba8a0da8d277868979cfbc8ad796 100644 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 ScNgau(U?ep-K_E5zy1%ZQTdPn diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 20db9ad..09523c0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d4..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -83,10 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..25da30d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemConfirmationScreen.java b/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemConfirmationScreen.java index 8814357..f64f2b0 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemConfirmationScreen.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemConfirmationScreen.java @@ -6,7 +6,6 @@ import net.minecraft.client.font.MultilineText; 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.math.MatrixStack; import net.minecraft.item.ItemStack; import net.minecraft.text.Text; @@ -70,8 +69,8 @@ public class SongItemConfirmationScreen extends Screen { String.format("§7Max notes per second: %s%d", getNumberColor(loaderThread.maxNotesPerSecond), loaderThread.maxNotesPerSecond), String.format("§7Avg notes per second: %s%.2f", getNumberColor(loaderThread.avgNotesPerSecond), loaderThread.avgNotesPerSecond), }; - List messageList = Arrays.stream(loadedMessages).map(Text::literal).collect(Collectors.toList()); - this.loadedText = MultilineText.createFromTexts(this.textRenderer, messageList); + Text[] messageList = Arrays.stream(loadedMessages).map(Text::literal).toArray(Text[]::new); + this.loadedText = MultilineText.create(this.textRenderer, messageList); int loadedTextHeight = this.loadedText.count() * this.textRenderer.fontHeight; addButtons(60 + loadedTextHeight + 12); diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java index 40f92cb..4984f37 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/mixin/ClientPlayNetworkHandlerMixin.java @@ -78,7 +78,7 @@ public class ClientPlayNetworkHandlerMixin { @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;setVelocityClient(DDD)V"), method = "onEntityVelocityUpdate", cancellable = true) public void onOnEntityVelocityUpdate(EntityVelocityUpdateS2CPacket packet, CallbackInfo ci) { - if (!SongHandler.getInstance().isIdle() && packet.getId() == SongPlayer.MC.player.getId()) { + if (!SongHandler.getInstance().isIdle() && packet.getEntityId() == SongPlayer.MC.player.getId()) { ci.cancel(); } } diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java b/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java index 681968b..e134146 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/playing/Stage.java @@ -32,9 +32,7 @@ public class Stage { public int totalMissingNotes = 0; // Only used in survival-only mode - public LinkedList untunedNoteblocks = new LinkedList<>(); public LinkedList requiredClicks = new LinkedList<>(); - public int totalUntunedNoteblocks = 0; public Stage() { position = player.getBlockPos(); @@ -508,6 +506,7 @@ public class Stage { } Map[] loadSurvivalBlocks() { + @SuppressWarnings("unchecked") Map[] instrumentMap = new Map[16]; for (int i = 0; i < 16; i++) { instrumentMap[i] = new TreeMap<>(); diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 72e74e7..f8b2054 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -26,10 +26,10 @@ ], "depends": { - "fabricloader": ">=0.14.11", + "fabricloader": ">=0.15.0", "fabric": "*", - "minecraft": "~1.20.5", - "java": ">=17" + "minecraft": "~1.21", + "java": ">=21" }, "suggests": { "flamingo": "*" From ccb4c8f637226b17c80eba99e9b72735212cb8cc Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Sat, 3 Aug 2024 23:35:01 -0500 Subject: [PATCH 27/29] Updated command descriptions --- README.md | 49 ++++++++++++++++--- .../songplayer/CommandProcessor.java | 12 ++--- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 68c482f..ce444ac 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SongPlayer A Fabric mod for Minecraft that plays songs with noteblocks. -The current version is for Minecraft 1.20.5 - 1.20.6. +The current version is for Minecraft 1.21 # How to install You can grab the mod jar from releases section. @@ -25,6 +25,11 @@ All the commands are case insensitive. If no arguments are given, lists all SongPlayer commands. Otherwise, explains the specified command and shows its syntax. +### $setPrefix \ +*aliases: `$prefix`* + +Sets the prefix used for all SongPlayer commands (by default: `$`) + ### $play \ Plays a particular midi from the .minecraft/songs folder, or, if a url is specified, downloads the song at that url and tries to play it. @@ -52,6 +57,12 @@ Gets the status of the current song that is playing. Shows all the songs in the queue. +### $songs +### $songs \ +*aliases: `$list`* + +If no arguments are given, lists songs in the `songs` folder. Otherwise, lists songs in the specified subdirectory. + ### $playlist play \ ### $playlist create \ ### $playlist list \[\] @@ -64,12 +75,6 @@ Shows all the songs in the queue. Create, edit, delete, or play playlists. You can also toggle looping or shuffling. -### $songs -### $songs \ -*aliases: `$list`* - -If no arguments are given, lists songs in the `songs` folder. Otherwise, lists songs in the specified subdirectory. - ### $setCreativeCommand \ *aliases: `$sc`* @@ -86,7 +91,7 @@ However, /gms does not work on all servers. If the survival command is different, set it with this command. For example, if the server uses vanilla commands, do `$setSurvivalCommand /gamemode survival`. -### $useVanillaCommands +### $useEssentialsCommands *aliases: `$essentials`, `$useEssentials`, `$essentialsCommands`* Switch to using Essentials gamemode commands. @@ -118,6 +123,25 @@ Sets the type of noteblock stage to build. Thanks Sk8kman and Lizard16 for the s Toggles whether you swing your arm when hitting a noteblock and rotate to look at the noteblocks you are hitting. +### $setVelocityThreshold +*aliases: `$velocityThreshold` `$threshold`* + +Sets the minimum velocity below which notes won't be played (applies to midi and nbs). This must be a number from 0 to 100. For song items, the threshold is baked in upon item creation. + +### $toggleAutoCleanup +*aliases: `$autoCleanup`* + +Toggles whether you automatically clean up your stage and restore the original blocks after playing. + +### $cleanupLastStage + +Cleans up the most recent stage that you made and does its best to restore the blocks to their original state. + +If you stop playing and start playing again, the recorded modifications gets reset. + +Will not replace fluids or double blocks such as doors, and does not replace tile entity data. +May not properly handle blocks that rest on other blocks such as torches, either. + ### $announcement \ ### $announcement setMessage @@ -134,6 +158,15 @@ Encodes song data into an item. When you right click on such an item, SongPlayer It will automatically generate custom item names and lore, but these can be modified or deleted without affecting the song data, so feel free to edit the items as you wish. SongPlayer only looks at the `SongItemData` tag. +### $toggleSurvivalOnly +*aliases: `$survivalOnly`* + +Enables or disables survival-only mode, in which automatic noteblock placement is disabled and automatic tuning is done by right-clicking. + +In this mode, you must place the necessary instruments yourself. + +If you try to play a song and the requirements are not met, it will tell you how many instruments of each type you need. + ### $testSong A command I used for testing during development. It plays all 400 possible noteblock sounds in order. diff --git a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java index d744738..ea140a5 100644 --- a/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java +++ b/src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java @@ -55,11 +55,11 @@ public class CommandProcessor { commands.add(new setStageTypeCommand()); commands.add(new toggleMovementCommand()); commands.add(new setVelocityThresholdCommand()); - commands.add(new toggleAutoCleanup()); + commands.add(new toggleAutoCleanupCommand()); commands.add(new cleanupLastStageCommand()); commands.add(new announcementCommand()); commands.add(new songItemCommand()); - commands.add(new toggleSurvivalOnly()); + commands.add(new toggleSurvivalOnlyCommand()); commands.add(new testSongCommand()); for (Command command : commands) { @@ -959,7 +959,7 @@ public class CommandProcessor { return new String[]{"velocityThreshold", "threshold"}; } public String[] getSyntax() { - return new String[] {""}; + return new String[] {""}; } public String getDescription() { return "Sets the minimum velocity below which notes won't be played (applies to midi and nbs). This must be a number from 0 to 100. For song items, the threshold is baked in upon item creation."; @@ -985,7 +985,7 @@ public class CommandProcessor { } } - private static class toggleAutoCleanup extends Command { + private static class toggleAutoCleanupCommand extends Command { public String getName() { return "toggleAutoCleanup"; } @@ -1202,7 +1202,7 @@ public class CommandProcessor { } } - private static class toggleSurvivalOnly extends Command { + private static class toggleSurvivalOnlyCommand extends Command { public String getName() { return "toggleSurvivalOnly"; } @@ -1213,7 +1213,7 @@ public class CommandProcessor { return new String[0]; } public String getDescription() { - return "Enables or disables survival-only mode, in which automatic noteblock placement is disabled and automatic tuning is done by right-clicking.."; + return "Enables or disables survival-only mode, in which automatic noteblock placement is disabled and automatic tuning is done by right-clicking."; } public boolean processCommand(String args) { if (args.length() == 0) { From 006d4b9cba188e684f67b6b326b2d4454af4ab63 Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Sat, 3 Aug 2024 23:36:34 -0500 Subject: [PATCH 28/29] Increment version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7f14159..9413cd5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ org.gradle.parallel=true loader_version=0.15.11 # Mod Properties - mod_version = 3.1.4 + mod_version = 3.2.0 maven_group = com.github.hhhzzzsss archives_base_name = song-player From 3ef19074f3c5af025657f8d23254c892c286473b Mon Sep 17 00:00:00 2001 From: hhhzzzsss Date: Sat, 3 Aug 2024 23:44:59 -0500 Subject: [PATCH 29/29] Remove trailing newline --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ce444ac..1e0c663 100644 --- a/README.md +++ b/README.md @@ -184,4 +184,3 @@ When playing a song, freecam is enabled. You will be able to move around freely, **Sk8kman**: Several of Songplayer 3.0's changes were inspired by their fork of SongPlayer. Most notably was their alternate stage designs, but it also motivated me to implement playlists and togglable movements. **Lizard16**: Cited by Sk8kman as the person who made the spherical stage design. -