Merge remote-tracking branch 'original/master'

# Conflicts:
#	gradle.properties
#	src/main/java/com/github/hhhzzzsss/songplayer/CommandProcessor.java
#	src/main/java/com/github/hhhzzzsss/songplayer/Config.java
#	src/main/java/com/github/hhhzzzsss/songplayer/conversion/NBSConverter.java
#	src/main/java/com/github/hhhzzzsss/songplayer/conversion/SPConverter.java
#	src/main/java/com/github/hhhzzzsss/songplayer/item/SongItemConfirmationScreen.java
#	src/main/java/com/github/hhhzzzsss/songplayer/playing/SongHandler.java
#	src/main/java/com/github/hhhzzzsss/songplayer/song/SongLoaderThread.java
This commit is contained in:
Chayapak Supasakul 2024-10-19 17:52:33 +07:00
commit 4e6c135992
30 changed files with 1176 additions and 310 deletions

View file

@ -5,7 +5,7 @@ My fork of Harry Zhou's SongPlayer
# 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.21
# How to install
You can grab the mod jar from releases section.
@ -30,6 +30,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 \<prefix>
*aliases: `$prefix`*
Sets the prefix used for all SongPlayer commands (by default: `$`)
### $play \<filename or url>
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.
@ -57,6 +62,12 @@ Gets the status of the current song that is playing.
Shows all the songs in the queue.
### $songs
### $songs \<subdirectory>
*aliases: `$list`*
If no arguments are given, lists songs in the `songs` folder. Otherwise, lists songs in the specified subdirectory.
### $playlist play \<playlist>
### $playlist create \<playlist>
### $playlist list \[\<playlist>]
@ -69,12 +80,6 @@ Shows all the songs in the queue.
Create, edit, delete, or play playlists. You can also toggle looping or shuffling.
### $songs
### $songs \<subdirectory>
*aliases: `$list`*
If no arguments are given, lists songs in the `songs` folder. Otherwise, lists songs in the specified subdirectory.
### $setCreativeCommand \<command>
*aliases: `$sc`*
@ -91,7 +96,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.
@ -123,6 +128,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 <threshold>
*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 \<enable | disable | getMessage>
### $announcement setMessage
@ -139,6 +163,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.
@ -156,4 +189,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.

View file

@ -1,15 +1,15 @@
plugins {
id 'fabric-loom' version '1.0-SNAPSHOT'
id 'fabric-loom' version '1.7-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
}
}

View file

@ -4,15 +4,15 @@ org.gradle.parallel=true
# Fabric Properties
# check these on https://fabricmc.net/use
minecraft_version=1.20.2
yarn_mappings=1.20.2+build.1
loader_version=0.14.22
minecraft_version=1.21
yarn_mappings=1.21+build.9
loader_version=0.15.11
# Mod Properties
mod_version = 3.1.1
mod_version = 3.2.0
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.89.2+1.20.2
fabric_version=0.100.8+1.21

Binary file not shown.

View file

@ -1,6 +1,7 @@
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.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

29
gradlew vendored Normal file → Executable file
View file

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

20
gradlew.bat vendored
View file

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

View file

@ -10,8 +10,12 @@ 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.text.*;
import net.minecraft.util.Formatting;
import net.minecraft.util.Hand;
import net.minecraft.world.GameMode;
@ -50,8 +54,12 @@ public class CommandProcessor {
commands.add(new toggleFakePlayerCommand());
commands.add(new setStageTypeCommand());
commands.add(new toggleMovementCommand());
commands.add(new setVelocityThresholdCommand());
commands.add(new toggleAutoCleanupCommand());
commands.add(new cleanupLastStageCommand());
commands.add(new announcementCommand());
commands.add(new songItemCommand());
commands.add(new toggleSurvivalOnlyCommand());
commands.add(new testSongCommand());
commands.add(new pitchCommand());
commands.add(new speedCommand());
@ -131,7 +139,7 @@ public class CommandProcessor {
if (args.length() == 0) {
StringBuilder helpMessage = new StringBuilder("§6Commands -");
for (Command c : commands) {
helpMessage.append(" ").append(Config.getConfig().prefix).append(c.getName());
helpMessage.append(" " + Config.getConfig().prefix + c.getName());
}
SongPlayer.addChatMessage(helpMessage.toString());
}
@ -185,6 +193,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);
@ -209,6 +221,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;
}
@ -232,16 +249,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().partialResetAndCleanup();
SongPlayer.addChatMessage("§6Stopped playing and switched to cleanup");
} else {
SongHandler.getInstance().restoreStateAndReset();
SongPlayer.addChatMessage("§6Stopped playing", true);
}
SongHandler.getInstance().restoreStateAndCleanUp();
SongPlayer.addChatMessage("§6Stopped playing", true);
return true;
}
else {
@ -327,10 +349,10 @@ public class CommandProcessor {
SongHandler.getInstance().currentSong.looping = !SongHandler.getInstance().currentSong.looping;
SongHandler.getInstance().currentSong.loopCount = 0;
if (SongHandler.getInstance().currentSong.looping) {
SongPlayer.addChatMessage("§6Enabled looping", true);
SongPlayer.addChatMessage("§6Enabled looping");
}
else {
SongPlayer.addChatMessage("§6Disabled looping", true);
SongPlayer.addChatMessage("§6Disabled looping");
}
return true;
}
@ -434,8 +456,8 @@ public class CommandProcessor {
}
}
List<String> subdirectories;
List<String> songs;
List<String> subdirectories = null;
List<String> songs = null;
try {
subdirectories = Files.list(dir)
.filter(Files::isDirectory)
@ -594,9 +616,9 @@ public class CommandProcessor {
Config.getConfig().loopPlaylists = !Config.getConfig().loopPlaylists;
SongHandler.getInstance().setPlaylistLoop(Config.getConfig().loopPlaylists);
if (Config.getConfig().loopPlaylists) {
SongPlayer.addChatMessage("§6Enabled playlist looping", true);
SongPlayer.addChatMessage("§6Enabled playlist looping");
} else {
SongPlayer.addChatMessage("§6Disabled playlist looping", true);
SongPlayer.addChatMessage("§6Disabled playlist looping");
}
Config.saveConfigWithErrorHandling();
return true;
@ -606,9 +628,9 @@ public class CommandProcessor {
Config.getConfig().shufflePlaylists = !Config.getConfig().shufflePlaylists;
SongHandler.getInstance().setPlaylistShuffle(Config.getConfig().shufflePlaylists);
if (Config.getConfig().shufflePlaylists) {
SongPlayer.addChatMessage("§6Enabled playlist shuffling", true);
SongPlayer.addChatMessage("§6Enabled playlist shuffling");
} else {
SongPlayer.addChatMessage("§6Disabled playlist shuffling", true);
SongPlayer.addChatMessage("§6Disabled playlist shuffling");
}
Config.saveConfigWithErrorHandling();
return true;
@ -687,7 +709,7 @@ public class CommandProcessor {
if (playlistFiles == null) {
return null;
}
int max = (int) playlistFiles.count();
int max = playlistFiles.collect(Collectors.toList()).size();
Stream<String> suggestions = IntStream.range(1, max+1).mapToObj(Integer::toString);
return CommandSource.suggestMatching(suggestions, suggestionsBuilder);
}
@ -860,7 +882,7 @@ public class CommandProcessor {
try {
Stage.StageType stageType = Stage.StageType.valueOf(args.toUpperCase(Locale.ROOT));
Config.getConfig().stageType = stageType;
SongPlayer.addChatMessage("§6Set stage type to §3" + stageType.name(), true);
SongPlayer.addChatMessage("§6Set stage type to §3" + stageType.name());
Config.saveConfigWithErrorHandling();
}
catch (IllegalArgumentException e) {
@ -931,6 +953,125 @@ 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[] {"<threshold>"};
}
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 toggleAutoCleanupCommand 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) {
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";
@ -938,11 +1079,13 @@ public class CommandProcessor {
public String[] getSyntax() {
return new String[] {
"enable",
"disable"
"disable",
"getMessage",
"setMessage <message>",
};
}
public String getDescription() {
return "Set an announcement message that is sent when you start playing a song.";
return "Set an announcement message that is sent when you start playing a song. With setMessage, write [name] where the song name should go.";
}
public boolean processCommand(String args) {
String[] split = args.split(" ", 2);
@ -950,13 +1093,23 @@ public class CommandProcessor {
case "enable":
if (split.length != 1) return false;
Config.getConfig().doAnnouncement = true;
SongPlayer.addChatMessage("§6Enabled song announcements", true);
SongPlayer.addChatMessage("§6Enabled song announcements");
Config.saveConfigWithErrorHandling();
return true;
case "disable":
if (split.length != 1) return false;
Config.getConfig().doAnnouncement = false;
SongPlayer.addChatMessage("§6Disabled song announcements", true);
SongPlayer.addChatMessage("§6Disabled song announcements");
Config.saveConfigWithErrorHandling();
return true;
case "getmessage":
if (split.length != 1) return false;
SongPlayer.addChatMessage("§6Current announcement message is §r" + Config.getConfig().announcementMessage);
return true;
case "setmessage":
if (split.length != 2) return false;
Config.getConfig().announcementMessage = split[1];
SongPlayer.addChatMessage("§6Set announcement message to §r" + split[1]);
Config.saveConfigWithErrorHandling();
return true;
default:
@ -965,7 +1118,7 @@ public class CommandProcessor {
}
public CompletableFuture<Suggestions> getSuggestions(String args, SuggestionsBuilder suggestionsBuilder) {
if (!args.contains(" ")) {
return CommandSource.suggestMatching(new String[]{"enable", "disable"}, suggestionsBuilder);
return CommandSource.suggestMatching(new String[]{"enable", "disable", "getMessage", "setMessage"}, suggestionsBuilder);
}
else {
return null;
@ -1020,7 +1173,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);
@ -1051,6 +1204,42 @@ public class CommandProcessor {
}
}
private static class toggleSurvivalOnlyCommand 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) {
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");
}
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";

View file

@ -24,7 +24,11 @@ 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;
public boolean survivalOnly = false;
public static Config getConfig() {
if (config == null) {

View file

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

View file

@ -3,11 +3,11 @@ 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;
import net.minecraft.text.LiteralTextContent;
import net.minecraft.text.MutableText;
import net.minecraft.text.PlainTextContent;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
@ -216,20 +216,26 @@ 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.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.Serializer.toJson(line)));
stack.set(DataComponentTypes.LORE, new LoreComponent(List.of(loreLines)));
}
public static MutableText joinTexts(MutableText base, Text... children) {
if (base == null) {
base = Text.empty();
}
stack.getOrCreateSubNbt(ItemStack.DISPLAY_KEY).put(ItemStack.LORE_KEY, lore);
for (Text child : children) {
base.append(child);
}
return base;
}
}

View file

@ -92,18 +92,20 @@ 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
velocity = (velocity * 100) / 127; // Midi velocity goes from 0-127
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);
@ -128,11 +130,22 @@ 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;
}
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) {
@ -182,15 +195,15 @@ public class MidiConverter {
int noteId = (pitch - 33) + 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;
}

View file

@ -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.playing.SongHandler;
import com.github.hhhzzzsss.songplayer.song.Note;
@ -82,7 +83,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 + SongHandler.getInstance().pitch);
writeVarLong(os, note.time - prevTime);
prevTime = note.time;

View file

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

View file

@ -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;
@ -54,7 +53,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);
@ -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<Text> 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);
@ -86,8 +85,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) {

View file

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

View file

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

View file

@ -0,0 +1,57 @@
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 lastStage = SongHandler.getInstance().lastStage;
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();
}
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);
}
}
}
}
}

View file

@ -1,23 +1,14 @@
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.MinecraftClient;
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.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.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@ -34,19 +25,62 @@ 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 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.isIdle()) {
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.getEntityId() == SongPlayer.MC.player.getId()) {
ci.cancel();
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -6,14 +6,16 @@ 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;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
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;
@ -25,6 +27,8 @@ import net.minecraft.world.GameMode;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@ -43,8 +47,12 @@ public class SongHandler {
public LinkedList<Song> 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<BlockPos, BlockState> originalBlocks = new HashMap<>();
public boolean building = false;
public boolean cleaningUp = false;
public boolean dirty = false;
public boolean wasFlying = false;
public GameMode originalGamemode = GameMode.CREATIVE;
@ -56,66 +64,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(), true);
} 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(), true);
} 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();
@ -127,9 +123,29 @@ public class SongHandler {
SongPlayer.fakePlayer.getInventory().clone(SongPlayer.MC.player.getInventory());
}
SongPlayer.MC.player.getAbilities().allowFlying = true;
// Maintain flying status
wasFlying = SongPlayer.MC.player.getAbilities().flying;
}
// Check if doing cleanup
if (cleaningUp) {
if (tick) {
// Maintain flying status
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();
@ -138,6 +154,20 @@ public class SongHandler {
handlePlaying(tick);
}
}
// Otherwise, handle cleanup if necessary
else {
if (dirty) {
if (Config.getConfig().autoCleanup && originalBlocks.size() != 0 && !Config.getConfig().survivalOnly) {
partialResetAndCleanup();
} else {
restoreStateAndReset();
}
}
else {
// When doing nothing else, record original gamemode
originalGamemode = SongPlayer.MC.interactionManager.getCurrentGameMode();
}
}
}
public void loadSong(String location) {
@ -170,20 +200,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 (stage == null) {
stage = new Stage();
stage.movePlayerToStagePosition();
if (!Config.getConfig().survivalOnly) setCreativeIfNeeded();
if (Config.getConfig().doAnnouncement) {
sendMessage(Config.getConfig().announcementMessage.replaceAll("\\[name\\]", song.name));
}
else {
stage.sendMovementPacketToStagePosition();
}
getAndSaveBuildSlot();
prepareStage();
if (!Config.getConfig().survivalOnly) getAndSaveBuildSlot();
SongPlayer.addChatMessage("§6Building noteblocks", true);
}
private void queueSong(Song song) {
@ -213,6 +241,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;
@ -230,50 +266,75 @@ 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);
} 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();
restoreStateAndReset();
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);
if (stage.nothingToBuild()) { // If there's still nothing to build after checking build status, switch to playing
building = false;
if (!Config.getConfig().survivalOnly) {
setSurvivalIfNeeded();
restoreBuildSlot();
}
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) {
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);
}
placeBlock(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;
}
buildCooldown = 0; // No cooldown, so it places a block every tick
buildEndDelay = 20;
} else { // Switch to playing
restoreBuildSlot();
building = false;
setSurvivalIfNeeded();
stage.sendMovementPacketToStagePosition();
SongPlayer.addChatMessage("§6Now playing §3" + currentSong.name, true);
}
}
private void setBuildProgressDisplay() {
@ -309,11 +370,22 @@ public class SongHandler {
if (tick) {
if (stage.hasBreakingModification()) {
stage.checkBuildStatus(currentSong);
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;
@ -323,7 +395,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!", true);
return;
}
@ -335,10 +407,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) {
@ -350,8 +424,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,20 +455,215 @@ public class SongHandler {
ProgressDisplay.getInstance().setText(songText, playlistText);
}
public void cleanup() {
// Runs every tick
private int cleanupTotalBlocksToPlace = 0;
private LinkedList<BlockPos> cleanupBreakList = new LinkedList<>();
private LinkedList<BlockPos> cleanupPlaceList = new LinkedList<>();
private ArrayList<BlockPos> 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 could not be restored", cleanupUnplaceableBlocks.size()));
}
}
}
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;
}
// 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();
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;
}
// 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();
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();
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 != SongPlayer.MC.interactionManager.getCurrentGameMode() && !Config.getConfig().survivalOnly) {
if (originalGamemode == GameMode.CREATIVE) {
sendGamemodeCommand(Config.getConfig().creativeCommand);
}
@ -403,51 +671,77 @@ public class SongHandler {
sendGamemodeCommand(Config.getConfig().survivalCommand);
}
}
if (SongPlayer.MC.player.getAbilities().allowFlying == false) {
SongPlayer.MC.player.getAbilities().flying = false;
}
if (!Config.getConfig().survivalOnly) restoreBuildSlot();
reset();
}
public void partialResetAndCleanup() {
restoreBuildSlot();
cleanup();
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();
}
private long lastCommandTime = System.currentTimeMillis();
private List<String> cachedCommands = new ArrayList<>();
private List<String> cachedMessages = new ArrayList<>();
private void sendGamemodeCommand(String command) {
cachedCommands.add(command);
// 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();
}
}
public void sendMessage(String message) {
cachedMessages.add(message);
private long lastCommandTime = System.currentTimeMillis();
private String cachedCommand = null;
private String cachedMessage = null;
private void sendGamemodeCommand(String command) {
cachedCommand = command;
}
private void sendMessage(String message) {
cachedMessage = message;
}
private void checkCommandCache() {
long currentTime = System.currentTimeMillis();
if (currentTime >= lastCommandTime + 1500 && !cachedCommands.isEmpty()) {
SongPlayer.MC.getNetworkHandler().sendCommand(cachedCommands.get(0));
cachedCommands.remove(0);
if (currentTime >= lastCommandTime + 1500 && cachedCommand != null) {
SongPlayer.MC.getNetworkHandler().sendCommand(cachedCommand);
cachedCommand = null;
lastCommandTime = currentTime;
}
else if (currentTime >= lastCommandTime + 500 && !cachedMessages.isEmpty()) {
if (cachedMessages.get(0).startsWith("/")) {
SongPlayer.MC.getNetworkHandler().sendCommand(cachedMessages.get(0).substring(1));
else if (currentTime >= lastCommandTime + 500 && cachedMessage != null) {
if (cachedMessage.startsWith("/")) {
SongPlayer.MC.getNetworkHandler().sendCommand(cachedMessage.substring(1));
}
else {
SongPlayer.MC.getNetworkHandler().sendChatMessage(cachedMessages.get(0));
SongPlayer.MC.getNetworkHandler().sendChatMessage(cachedMessage);
}
cachedMessages.remove(0);
cachedMessage = null;
lastCommandTime = currentTime;
}
}
private void setCreativeIfNeeded() {
cachedCommands.clear();
cachedCommand = null;
if (SongPlayer.MC.interactionManager.getCurrentGameMode() != GameMode.CREATIVE) {
sendGamemodeCommand(Config.getConfig().creativeCommand);
}
}
private void setSurvivalIfNeeded() {
cachedCommands.clear();
cachedCommand = null;
if (SongPlayer.MC.interactionManager.getCurrentGameMode() != GameMode.SURVIVAL) {
sendGamemodeCommand(Config.getConfig().survivalCommand);
}
@ -460,23 +754,33 @@ 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);
}
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<String, String> stateMap = new TreeMap<>();
for (Map.Entry<Property<?>, 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();
@ -490,6 +794,43 @@ public class SongHandler {
private void stopAttack() {
SongPlayer.MC.interactionManager.cancelBlockBreaking();
}
private void recordBlocks(Iterable<BlockPos> 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) {
Map<Property<?>, Comparable<?>> entries = bs.getEntries();
for (Map.Entry<Property<?>, 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.equals("half") && valueName.equals("upper")) {
return false;
}
}
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) {
@ -499,9 +840,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);
@ -511,7 +852,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));
}
@ -528,4 +869,8 @@ public class SongHandler {
buildSlot = -1;
}
}
public boolean isIdle() {
return currentSong == null && currentPlaylist == null && songQueue.isEmpty() && !cleaningUp && !dirty;
}
}

View file

@ -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,16 +26,19 @@ public class Stage {
public BlockPos position;
public HashMap<Integer, BlockPos> noteblockPositions = new HashMap<>();
// Not used in survival-only mode
public LinkedList<BlockPos> requiredBreaks = new LinkedList<>();
public TreeSet<Integer> missingNotes = new TreeSet<>();
public int totalMissingNotes = 0;
// Only used in survival-only mode
public LinkedList<BlockPos> requiredClicks = new LinkedList<>();
public Stage() {
position = player.getBlockPos();
}
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);
@ -195,6 +199,82 @@ public class Stage {
totalMissingNotes = missingNotes.size();
}
public void checkSurvivalBuildStatus(Song song) throws NotEnoughInstrumentsException {
noteblockPositions.clear();
Map<BlockPos, Integer>[] 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<BlockPos, Integer> 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<BlockPos> noteblockLocations, Collection<BlockPos> breakLocations) {
for (int dx = -4; dx <= 4; dx++) {
for (int dz = -4; dz <= 4; dz++) {
@ -425,6 +505,30 @@ public class Stage {
}
}
Map<BlockPos, Integer>[] loadSurvivalBlocks() {
@SuppressWarnings("unchecked")
Map<BlockPos, Integer>[] 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,12 +538,14 @@ 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;
public boolean hasBreakingModification() {
int wrongInstruments = 0;
for (Map.Entry<Integer, BlockPos> entry : noteblockPositions.entrySet()) {
BlockState bs = SongPlayer.MC.world.getBlockState(entry.getValue());
int blockId = Block.getRawIdFromState(bs);
@ -455,10 +561,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());
@ -468,4 +571,8 @@ public class Stage {
}
return false;
}
public Vec3d getOriginBottomCenter() {
return Vec3d.ofBottomCenter(position);
}
}

View file

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

View file

@ -3,9 +3,17 @@ package com.github.hhhzzzsss.songplayer.song;
public class Note implements Comparable<Note> {
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

View file

@ -4,7 +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 com.github.hhhzzzsss.songplayer.conversion.TxtConverter;
import java.io.IOException;
import java.net.URL;
@ -56,7 +56,7 @@ public class SongLoaderThread extends Thread{
try {
byte[] bytes;
if (isUrl) {
bytes = DownloadUtils.DownloadToByteArray(songUrl);
bytes = DownloadUtils.DownloadToByteArray(songUrl, 10*1024*1024);
filename = Paths.get(songUrl.toURI().getPath()).getFileName().toString();
}
else {
@ -78,11 +78,9 @@ public class SongLoaderThread extends Thread{
if (song == null) {
try {
song = TXTConverter.getSongFromBytes(bytes, filename);
}
catch (Exception e) {
e.printStackTrace();
song = TxtConverter.getSongFromBytes(bytes, filename);
}
catch (Exception e) {}
}
if (song == null) {

View file

@ -26,10 +26,10 @@
],
"depends": {
"fabricloader": ">=0.14.11",
"fabricloader": ">=0.15.0",
"fabric": "*",
"minecraft": "1.20.x",
"java": ">=17"
"minecraft": "~1.21",
"java": ">=21"
},
"suggests": {
"flamingo": "*"

View file

@ -7,6 +7,8 @@
],
"client": [
"ChatInputSuggestorMixin",
"ClientCommonNetworkHandlerMixin",
"ClientPlayerEntityMixin",
"ClientPlayerInteractionManagerAccessor",
"ClientPlayNetworkHandlerAccessor",
"ClientPlayNetworkHandlerMixin",