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 # SongPlayer
A Fabric mod for Minecraft that plays songs with noteblocks. 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 # How to install
You can grab the mod jar from releases section. 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. If no arguments are given, lists all SongPlayer commands.
Otherwise, explains the specified command and shows its syntax. 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> ### $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. 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. 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 play \<playlist>
### $playlist create \<playlist> ### $playlist create \<playlist>
### $playlist list \[\<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. 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> ### $setCreativeCommand \<command>
*aliases: `$sc`* *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. If the survival command is different, set it with this command.
For example, if the server uses vanilla commands, do `$setSurvivalCommand /gamemode survival`. For example, if the server uses vanilla commands, do `$setSurvivalCommand /gamemode survival`.
### $useVanillaCommands ### $useEssentialsCommands
*aliases: `$essentials`, `$useEssentials`, `$essentialsCommands`* *aliases: `$essentials`, `$useEssentials`, `$essentialsCommands`*
Switch to using Essentials gamemode commands. 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. 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 \<enable | disable | getMessage>
### $announcement setMessage ### $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. 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 ### $testSong
A command I used for testing during development. A command I used for testing during development.
It plays all 400 possible noteblock sounds in order. 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. **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. **Lizard16**: Cited by Sk8kman as the person who made the spherical stage design.

View file

@ -1,15 +1,15 @@
plugins { plugins {
id 'fabric-loom' version '1.0-SNAPSHOT' id 'fabric-loom' version '1.7-SNAPSHOT'
id 'maven-publish' id 'maven-publish'
} }
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
archivesBaseName = project.archives_base_name
version = project.mod_version version = project.mod_version
group = project.maven_group group = project.maven_group
base {
archivesName = project.archives_base_name
}
repositories { repositories {
// Add repositories to retrieve artifacts from in here. // Add repositories to retrieve artifacts from in here.
// You should only use this when depending on other mods because // 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. // Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" 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 { processResources {
@ -40,8 +38,7 @@ processResources {
} }
tasks.withType(JavaCompile).configureEach { tasks.withType(JavaCompile).configureEach {
// Minecraft 1.18 (1.18-pre2) upwards uses Java 17. it.options.release = 21
it.options.release = 17
} }
java { java {
@ -49,18 +46,22 @@ java {
// if it is present. // if it is present.
// If you remove this line, sources will not be generated. // If you remove this line, sources will not be generated.
withSourcesJar() withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
} }
jar { jar {
from("LICENSE") { from("LICENSE") {
rename { "${it}_${project.archivesBaseName}"} rename { "${it}_${project.base.archivesName.get()}"}
} }
} }
// configure the maven publication // configure the maven publication
publishing { publishing {
publications { publications {
mavenJava(MavenPublication) { create("mavenJava", MavenPublication) {
artifactId = project.archives_base_name
from components.java from components.java
} }
} }

View file

@ -4,15 +4,15 @@ org.gradle.parallel=true
# Fabric Properties # Fabric Properties
# check these on https://fabricmc.net/use # check these on https://fabricmc.net/use
minecraft_version=1.20.2 minecraft_version=1.21
yarn_mappings=1.20.2+build.1 yarn_mappings=1.21+build.9
loader_version=0.14.22 loader_version=0.15.11
# Mod Properties # Mod Properties
mod_version = 3.1.1 mod_version = 3.2.0
maven_group = com.github.hhhzzzsss maven_group = com.github.hhhzzzsss
archives_base_name = song-player archives_base_name = song-player
# Dependencies # Dependencies
# currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api # 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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

29
gradlew vendored Normal file → Executable file
View file

@ -83,10 +83,8 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # 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
# 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"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -133,10 +131,13 @@ location of your Java installation."
fi fi
else else
JAVACMD=java 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 Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # 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 ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # 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" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
# shell script including quotes and variable substitutions, so put them in DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded. # 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 -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-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 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail 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.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder; import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import net.minecraft.command.CommandSource; import net.minecraft.command.CommandSource;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.NbtComponent;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtCompound;
import net.minecraft.text.*;
import net.minecraft.util.Formatting;
import net.minecraft.util.Hand; import net.minecraft.util.Hand;
import net.minecraft.world.GameMode; import net.minecraft.world.GameMode;
@ -50,8 +54,12 @@ public class CommandProcessor {
commands.add(new toggleFakePlayerCommand()); commands.add(new toggleFakePlayerCommand());
commands.add(new setStageTypeCommand()); commands.add(new setStageTypeCommand());
commands.add(new toggleMovementCommand()); commands.add(new toggleMovementCommand());
commands.add(new setVelocityThresholdCommand());
commands.add(new toggleAutoCleanupCommand());
commands.add(new cleanupLastStageCommand());
commands.add(new announcementCommand()); commands.add(new announcementCommand());
commands.add(new songItemCommand()); commands.add(new songItemCommand());
commands.add(new toggleSurvivalOnlyCommand());
commands.add(new testSongCommand()); commands.add(new testSongCommand());
commands.add(new pitchCommand()); commands.add(new pitchCommand());
commands.add(new speedCommand()); commands.add(new speedCommand());
@ -131,7 +139,7 @@ public class CommandProcessor {
if (args.length() == 0) { if (args.length() == 0) {
StringBuilder helpMessage = new StringBuilder("§6Commands -"); StringBuilder helpMessage = new StringBuilder("§6Commands -");
for (Command c : commands) { for (Command c : commands) {
helpMessage.append(" ").append(Config.getConfig().prefix).append(c.getName()); helpMessage.append(" " + Config.getConfig().prefix + c.getName());
} }
SongPlayer.addChatMessage(helpMessage.toString()); SongPlayer.addChatMessage(helpMessage.toString());
} }
@ -185,6 +193,10 @@ public class CommandProcessor {
SongPlayer.addChatMessage("§cPrefix cannot contain a space"); SongPlayer.addChatMessage("§cPrefix cannot contain a space");
return true; return true;
} }
else if (args.startsWith("/")) {
SongPlayer.addChatMessage("§cPrefix cannot start with a /");
return true;
}
else if (args.length() > 0) { else if (args.length() > 0) {
Config.getConfig().prefix = args; Config.getConfig().prefix = args;
SongPlayer.addChatMessage("§6Set prefix to " + args); SongPlayer.addChatMessage("§6Set prefix to " + args);
@ -209,6 +221,11 @@ public class CommandProcessor {
} }
public boolean processCommand(String args) { public boolean processCommand(String args) {
if (args.length() > 0) { 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); SongHandler.getInstance().loadSong(args);
return true; return true;
} }
@ -232,16 +249,21 @@ public class CommandProcessor {
return "Stops playing"; return "Stops playing";
} }
public boolean processCommand(String args) { 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"); SongPlayer.addChatMessage("§6No song is currently playing");
return true; return true;
} }
if (args.length() == 0) { if (args.length() == 0) {
if (SongHandler.getInstance().stage != null) { if (SongHandler.getInstance().cleaningUp) {
SongHandler.getInstance().stage.movePlayerToStagePosition(); SongHandler.getInstance().restoreStateAndReset();
} SongPlayer.addChatMessage("§6Stopped cleanup");
SongHandler.getInstance().restoreStateAndCleanUp(); } 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); SongPlayer.addChatMessage("§6Stopped playing", true);
}
return true; return true;
} }
else { else {
@ -327,10 +349,10 @@ public class CommandProcessor {
SongHandler.getInstance().currentSong.looping = !SongHandler.getInstance().currentSong.looping; SongHandler.getInstance().currentSong.looping = !SongHandler.getInstance().currentSong.looping;
SongHandler.getInstance().currentSong.loopCount = 0; SongHandler.getInstance().currentSong.loopCount = 0;
if (SongHandler.getInstance().currentSong.looping) { if (SongHandler.getInstance().currentSong.looping) {
SongPlayer.addChatMessage("§6Enabled looping", true); SongPlayer.addChatMessage("§6Enabled looping");
} }
else { else {
SongPlayer.addChatMessage("§6Disabled looping", true); SongPlayer.addChatMessage("§6Disabled looping");
} }
return true; return true;
} }
@ -434,8 +456,8 @@ public class CommandProcessor {
} }
} }
List<String> subdirectories; List<String> subdirectories = null;
List<String> songs; List<String> songs = null;
try { try {
subdirectories = Files.list(dir) subdirectories = Files.list(dir)
.filter(Files::isDirectory) .filter(Files::isDirectory)
@ -594,9 +616,9 @@ public class CommandProcessor {
Config.getConfig().loopPlaylists = !Config.getConfig().loopPlaylists; Config.getConfig().loopPlaylists = !Config.getConfig().loopPlaylists;
SongHandler.getInstance().setPlaylistLoop(Config.getConfig().loopPlaylists); SongHandler.getInstance().setPlaylistLoop(Config.getConfig().loopPlaylists);
if (Config.getConfig().loopPlaylists) { if (Config.getConfig().loopPlaylists) {
SongPlayer.addChatMessage("§6Enabled playlist looping", true); SongPlayer.addChatMessage("§6Enabled playlist looping");
} else { } else {
SongPlayer.addChatMessage("§6Disabled playlist looping", true); SongPlayer.addChatMessage("§6Disabled playlist looping");
} }
Config.saveConfigWithErrorHandling(); Config.saveConfigWithErrorHandling();
return true; return true;
@ -606,9 +628,9 @@ public class CommandProcessor {
Config.getConfig().shufflePlaylists = !Config.getConfig().shufflePlaylists; Config.getConfig().shufflePlaylists = !Config.getConfig().shufflePlaylists;
SongHandler.getInstance().setPlaylistShuffle(Config.getConfig().shufflePlaylists); SongHandler.getInstance().setPlaylistShuffle(Config.getConfig().shufflePlaylists);
if (Config.getConfig().shufflePlaylists) { if (Config.getConfig().shufflePlaylists) {
SongPlayer.addChatMessage("§6Enabled playlist shuffling", true); SongPlayer.addChatMessage("§6Enabled playlist shuffling");
} else { } else {
SongPlayer.addChatMessage("§6Disabled playlist shuffling", true); SongPlayer.addChatMessage("§6Disabled playlist shuffling");
} }
Config.saveConfigWithErrorHandling(); Config.saveConfigWithErrorHandling();
return true; return true;
@ -687,7 +709,7 @@ public class CommandProcessor {
if (playlistFiles == null) { if (playlistFiles == null) {
return 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); Stream<String> suggestions = IntStream.range(1, max+1).mapToObj(Integer::toString);
return CommandSource.suggestMatching(suggestions, suggestionsBuilder); return CommandSource.suggestMatching(suggestions, suggestionsBuilder);
} }
@ -860,7 +882,7 @@ public class CommandProcessor {
try { try {
Stage.StageType stageType = Stage.StageType.valueOf(args.toUpperCase(Locale.ROOT)); Stage.StageType stageType = Stage.StageType.valueOf(args.toUpperCase(Locale.ROOT));
Config.getConfig().stageType = stageType; 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(); Config.saveConfigWithErrorHandling();
} }
catch (IllegalArgumentException e) { 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 { private static class announcementCommand extends Command {
public String getName() { public String getName() {
return "announcement"; return "announcement";
@ -938,11 +1079,13 @@ public class CommandProcessor {
public String[] getSyntax() { public String[] getSyntax() {
return new String[] { return new String[] {
"enable", "enable",
"disable" "disable",
"getMessage",
"setMessage <message>",
}; };
} }
public String getDescription() { 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) { public boolean processCommand(String args) {
String[] split = args.split(" ", 2); String[] split = args.split(" ", 2);
@ -950,13 +1093,23 @@ public class CommandProcessor {
case "enable": case "enable":
if (split.length != 1) return false; if (split.length != 1) return false;
Config.getConfig().doAnnouncement = true; Config.getConfig().doAnnouncement = true;
SongPlayer.addChatMessage("§6Enabled song announcements", true); SongPlayer.addChatMessage("§6Enabled song announcements");
Config.saveConfigWithErrorHandling(); Config.saveConfigWithErrorHandling();
return true; return true;
case "disable": case "disable":
if (split.length != 1) return false; if (split.length != 1) return false;
Config.getConfig().doAnnouncement = 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(); Config.saveConfigWithErrorHandling();
return true; return true;
default: default:
@ -965,7 +1118,7 @@ public class CommandProcessor {
} }
public CompletableFuture<Suggestions> getSuggestions(String args, SuggestionsBuilder suggestionsBuilder) { public CompletableFuture<Suggestions> getSuggestions(String args, SuggestionsBuilder suggestionsBuilder) {
if (!args.contains(" ")) { if (!args.contains(" ")) {
return CommandSource.suggestMatching(new String[]{"enable", "disable"}, suggestionsBuilder); return CommandSource.suggestMatching(new String[]{"enable", "disable", "getMessage", "setMessage"}, suggestionsBuilder);
} }
else { else {
return null; return null;
@ -1020,7 +1173,7 @@ public class CommandProcessor {
return true; return true;
} }
String name = String.join(" ", Arrays.copyOfRange(split, 1, split.length)); 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); SongItemUtils.addSongItemDisplay(stack);
MC.player.setStackInHand(Hand.MAIN_HAND, stack); MC.player.setStackInHand(Hand.MAIN_HAND, stack);
MC.interactionManager.clickCreativeStack(MC.player.getStackInHand(Hand.MAIN_HAND), 36 + MC.player.getInventory().selectedSlot); 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 { private static class testSongCommand extends Command {
public String getName() { public String getName() {
return "testSong"; return "testSong";

View file

@ -24,7 +24,11 @@ public class Config {
public Stage.StageType stageType = Stage.StageType.DEFAULT; public Stage.StageType stageType = Stage.StageType.DEFAULT;
public boolean swing = false; public boolean swing = false;
public boolean rotate = false; public boolean rotate = false;
public int velocityThreshold = 0;
public boolean doAnnouncement = false; public boolean doAnnouncement = false;
public String announcementMessage = "&6Now playing: &3[name]";
public boolean autoCleanup = false;
public boolean survivalOnly = false;
public static Config getConfig() { public static Config getConfig() {
if (config == null) { if (config == null) {

View file

@ -49,9 +49,9 @@ public class FakePlayerEntity extends OtherClientPlayerEntity {
} }
public void copyStagePosAndPlayerLook() { public void copyStagePosAndPlayerLook() {
Stage stage = SongHandler.getInstance().stage; Stage lastStage = SongHandler.getInstance().lastStage;
if (stage != null) { if (lastStage != null) {
refreshPositionAndAngles(stage.position.getX()+0.5, stage.position.getY(), stage.position.getZ()+0.5, player.getYaw(), player.getPitch()); refreshPositionAndAngles(lastStage.position.getX()+0.5, lastStage.position.getY(), lastStage.position.getZ()+0.5, player.getYaw(), player.getPitch());
headYaw = player.headYaw; headYaw = player.headYaw;
} }
else { else {

View file

@ -3,11 +3,11 @@ package com.github.hhhzzzsss.songplayer;
import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder; import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import net.minecraft.command.CommandSource; import net.minecraft.command.CommandSource;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.LoreComponent;
import net.minecraft.item.ItemStack; 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.MutableText;
import net.minecraft.text.PlainTextContent;
import net.minecraft.text.Style; import net.minecraft.text.Style;
import net.minecraft.text.Text; import net.minecraft.text.Text;
@ -216,20 +216,26 @@ public class Util {
} }
public static MutableText getStyledText(String str, Style style) { 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); text.setStyle(style);
return text; return text;
} }
public static void setItemName(ItemStack stack, Text 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) { public static void setItemLore(ItemStack stack, Text... loreLines) {
NbtList lore = new NbtList(); stack.set(DataComponentTypes.LORE, new LoreComponent(List.of(loreLines)));
for (Text line : loreLines) {
lore.add(NbtString.of(Text.Serializer.toJson(line)));
} }
stack.getOrCreateSubNbt(ItemStack.DISPLAY_KEY).put(ItemStack.LORE_KEY, lore);
public static MutableText joinTexts(MutableText base, Text... children) {
if (base == null) {
base = Text.empty();
}
for (Text child : children) {
base.append(child);
}
return base;
} }
} }

View file

@ -92,18 +92,20 @@ public class MidiConverter {
instrumentIds[sm.getChannel()] = sm.getData1(); instrumentIds[sm.getChannel()] = sm.getData1();
} }
else if (sm.getCommand() == NOTE_ON) { else if (sm.getCommand() == NOTE_ON) {
if (sm.getData2() == 0) continue;
int pitch = sm.getData1(); 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; long deltaTick = event.getTick() - prevTick;
prevTick = event.getTick(); prevTick = event.getTick();
microTime += (mpq/tpq) * deltaTick; microTime += (mpq/tpq) * deltaTick;
Note note; Note note;
if (sm.getChannel() == 9) { if (sm.getChannel() == 9) {
note = getMidiPercussionNote(pitch, microTime); note = getMidiPercussionNote(pitch, velocity, microTime);
} }
else { else {
note = getMidiInstrumentNote(instrumentIds[sm.getChannel()], pitch, microTime); note = getMidiInstrumentNote(instrumentIds[sm.getChannel()], pitch, velocity, microTime);
} }
if (note != null) { if (note != null) {
song.add(note); song.add(note);
@ -129,10 +131,21 @@ public class MidiConverter {
song.sort(); 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; 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 instrument = null;
com.github.hhhzzzsss.songplayer.song.Instrument[] instrumentList = instrumentMap.get(midiInstrument); com.github.hhhzzzsss.songplayer.song.Instrument[] instrumentList = instrumentMap.get(midiInstrument);
if (instrumentList != null) { if (instrumentList != null) {
@ -182,15 +195,15 @@ public class MidiConverter {
int noteId = (pitch - 33) + instrument.instrumentId*25; int noteId = (pitch - 33) + instrument.instrumentId*25;
long time = microTime / 1000L; 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)) { if (percussionMap.containsKey(midiPitch)) {
int noteId = percussionMap.get(midiPitch); int noteId = percussionMap.get(midiPitch);
long time = microTime / 1000L; long time = microTime / 1000L;
return new Note(noteId, time); return new Note(noteId, time, velocity);
} }
return null; return null;
} }

View file

@ -1,5 +1,6 @@
package com.github.hhhzzzsss.songplayer.conversion; package com.github.hhhzzzsss.songplayer.conversion;
import com.github.hhhzzzsss.songplayer.Config;
import com.github.hhhzzzsss.songplayer.Util; import com.github.hhhzzzsss.songplayer.Util;
import com.github.hhhzzzsss.songplayer.playing.SongHandler; import com.github.hhhzzzsss.songplayer.playing.SongHandler;
import com.github.hhhzzzsss.songplayer.song.Note; import com.github.hhhzzzsss.songplayer.song.Note;
@ -82,7 +83,7 @@ public class SPConverter {
song.sort(); song.sort();
long prevTime = 0; 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); writeShort(os, note.noteId + SongHandler.getInstance().pitch);
writeVarLong(os, note.time - prevTime); writeVarLong(os, note.time - prevTime);
prevTime = note.time; 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.DrawContext;
import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.text.Text; import net.minecraft.text.Text;
@ -54,7 +53,7 @@ public class SongItemConfirmationScreen extends Screen {
@Override @Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) { 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); 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("§7Max notes per second: %s%d", getNumberColor(loaderThread.maxNotesPerSecond), loaderThread.maxNotesPerSecond),
String.format("§7Avg notes per second: %s%.2f", getNumberColor(loaderThread.avgNotesPerSecond), loaderThread.avgNotesPerSecond), 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()); Text[] messageList = Arrays.stream(loadedMessages).map(Text::literal).toArray(Text[]::new);
this.loadedText = MultilineText.createFromTexts(this.textRenderer, messageList); this.loadedText = MultilineText.create(this.textRenderer, messageList);
int loadedTextHeight = this.loadedText.count() * this.textRenderer.fontHeight; int loadedTextHeight = this.loadedText.count() * this.textRenderer.fontHeight;
addButtons(60 + loadedTextHeight + 12); addButtons(60 + loadedTextHeight + 12);
@ -86,8 +85,6 @@ public class SongItemConfirmationScreen extends Screen {
else { else {
unloadedText.drawCenterWithShadow(context, this.width / 2, 60); unloadedText.drawCenterWithShadow(context, this.width / 2, 60);
} }
super.render(context, mouseX, mouseY, delta);
} }
public String getNumberColor(double number) { 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.SongPlayer;
import com.github.hhhzzzsss.songplayer.conversion.SPConverter; import com.github.hhhzzzsss.songplayer.conversion.SPConverter;
import com.github.hhhzzzsss.songplayer.song.SongLoaderThread; 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.ItemStack;
import net.minecraft.item.Items; import net.minecraft.item.Items;
import net.minecraft.text.Text; import net.minecraft.text.Text;
@ -39,7 +41,7 @@ public class SongItemCreatorThread extends SongLoaderThread {
ItemStack newStack; ItemStack newStack;
if (stack.isEmpty()) { if (stack.isEmpty()) {
newStack = Items.PAPER.getDefaultStack(); newStack = Items.PAPER.getDefaultStack();
newStack.getOrCreateNbt().putInt("CustomModelData", 751642938); newStack.set(DataComponentTypes.CUSTOM_MODEL_DATA, new CustomModelDataComponent(751642938));
} }
else { else {
newStack = stack.copy(); newStack = stack.copy();

View file

@ -1,6 +1,8 @@
package com.github.hhhzzzsss.songplayer.item; package com.github.hhhzzzsss.songplayer.item;
import com.github.hhhzzzsss.songplayer.Util; 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.item.ItemStack;
import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtElement; import net.minecraft.nbt.NbtElement;
@ -18,10 +20,10 @@ public class SongItemUtils {
public static ItemStack createSongItem(ItemStack stack, byte[] songData, String filename, String displayName) { public static ItemStack createSongItem(ItemStack stack, byte[] songData, String filename, String displayName) {
NbtCompound songPlayerNbt = new NbtCompound(); NbtCompound songPlayerNbt = new NbtCompound();
stack.setSubNbt(SONG_ITEM_KEY, songPlayerNbt);
songPlayerNbt.putString(SONG_DATA_KEY, Base64.getEncoder().encodeToString(songData)); songPlayerNbt.putString(SONG_DATA_KEY, Base64.getEncoder().encodeToString(songData));
songPlayerNbt.putString(FILE_NAME_KEY, filename); songPlayerNbt.putString(FILE_NAME_KEY, filename);
songPlayerNbt.putString(DISPLAY_NAME_KEY, displayName); songPlayerNbt.putString(DISPLAY_NAME_KEY, displayName);
NbtComponent.set(DataComponentTypes.CUSTOM_DATA, stack, nbt -> nbt.put(SONG_ITEM_KEY, songPlayerNbt));
addSongItemDisplay(stack); addSongItemDisplay(stack);
return stack; return stack;
} }
@ -43,7 +45,12 @@ public class SongItemUtils {
} }
public static NbtCompound getSongItemTag(ItemStack stack) { 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) { 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; package com.github.hhhzzzsss.songplayer.mixin;
import com.github.hhhzzzsss.songplayer.CommandProcessor; import com.github.hhhzzzsss.songplayer.CommandProcessor;
import com.github.hhhzzzsss.songplayer.Config;
import com.github.hhhzzzsss.songplayer.SongPlayer; import com.github.hhhzzzsss.songplayer.SongPlayer;
import com.github.hhhzzzsss.songplayer.playing.SongHandler; import com.github.hhhzzzsss.songplayer.playing.SongHandler;
import com.github.hhhzzzsss.songplayer.playing.Stage; import com.github.hhhzzzsss.songplayer.playing.Stage;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayNetworkHandler; import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.entity.EntityPose; import net.minecraft.network.packet.s2c.play.*;
import net.minecraft.network.ClientConnection; import net.minecraft.util.math.Vec3d;
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 org.spongepowered.asm.mixin.Mixin; 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.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 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") @Inject(at = @At("TAIL"), method = "onGameJoin(Lnet/minecraft/network/packet/s2c/play/GameJoinS2CPacket;)V")
public void onOnGameJoin(GameJoinS2CPacket packet, CallbackInfo ci) { 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") @Inject(at = @At("TAIL"), method = "onPlayerRespawn(Lnet/minecraft/network/packet/s2c/play/PlayerRespawnS2CPacket;)V")
public void onOnPlayerRespawn(PlayerRespawnS2CPacket packet, CallbackInfo ci) { 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") @Inject(at = @At("TAIL"), method = "onPlayerAbilities(Lnet/minecraft/network/packet/s2c/play/PlayerAbilitiesS2CPacket;)V")
public void onOnPlayerAbilities(PlayerAbilitiesS2CPacket packet, CallbackInfo ci) { public void onOnPlayerAbilities(PlayerAbilitiesS2CPacket packet, CallbackInfo ci) {
SongHandler handler = SongHandler.getInstance(); 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; 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.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
// Used for debugging purposes
@Mixin(ClientWorld.class) @Mixin(ClientWorld.class)
public class ClientWorldMixin { public class ClientWorldMixin {
@Inject(at = @At("HEAD"), method = "handleBlockUpdate(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;I)V", cancellable = true) @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 com.github.hhhzzzsss.songplayer.playing.ProgressDisplay;
import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.InGameHud; 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.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
@ -12,18 +11,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(InGameHud.class) @Mixin(InGameHud.class)
public class InGameHudMixin { public class InGameHudMixin {
@Shadow
private int scaledWidth;
@Shadow
private int scaledHeight;
@Shadow @Shadow
private int heldItemTooltipFade; private int heldItemTooltipFade;
@Inject(method = "render", @Inject(at = @At("TAIL"), method = "renderHeldItemTooltip(Lnet/minecraft/client/gui/DrawContext;)V")
at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;enableBlend()V", ordinal = 3)) private void onRenderHeldItemTooltip(DrawContext context, CallbackInfo ci) {
private void onRender(DrawContext context, float tickDelta, CallbackInfo ci) { ProgressDisplay.getInstance().onRenderHUD(context, heldItemTooltipFade);
ProgressDisplay.getInstance().onRenderHUD(context, scaledWidth, scaledHeight, heldItemTooltipFade);
} }
} }

View file

@ -29,16 +29,16 @@ public class ProgressDisplay {
fade = 100; fade = 100;
} }
public void onRenderHUD(DrawContext context, int scaledWidth, int scaledHeight, int heldItemTooltipFade) { public void onRenderHUD(DrawContext context, int heldItemTooltipFade) {
if (fade <= 0) { if (fade <= 0) {
return; return;
} }
int bottomTextWidth = SongPlayer.MC.textRenderer.getWidth(bottomText); int bottomTextWidth = SongPlayer.MC.textRenderer.getWidth(bottomText);
int topTextWidth = SongPlayer.MC.textRenderer.getWidth(topText); int topTextWidth = SongPlayer.MC.textRenderer.getWidth(topText);
int bottomTextX = (scaledWidth - bottomTextWidth) / 2; int bottomTextX = (SongPlayer.MC.getWindow().getScaledWidth() - bottomTextWidth) / 2;
int topTextX = (scaledWidth - topTextWidth) / 2; int topTextX = (SongPlayer.MC.getWindow().getScaledWidth() - topTextWidth) / 2;
int bottomTextY = scaledHeight - 59; int bottomTextY = SongPlayer.MC.getWindow().getScaledHeight() - 59;
if (!SongPlayer.MC.interactionManager.hasStatusBars()) { if (!SongPlayer.MC.interactionManager.hasStatusBars()) {
bottomTextY += 14; 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.Util;
import com.github.hhhzzzsss.songplayer.mixin.ClientPlayerInteractionManagerAccessor; import com.github.hhhzzzsss.songplayer.mixin.ClientPlayerInteractionManagerAccessor;
import com.github.hhhzzzsss.songplayer.song.*; import com.github.hhhzzzsss.songplayer.song.*;
import net.minecraft.block.Block; import net.minecraft.block.*;
import net.minecraft.client.world.ClientWorld; 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.entity.player.PlayerInventory;
import net.minecraft.item.ItemStack; 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.network.packet.c2s.play.PlayerMoveC2SPacket;
import net.minecraft.text.MutableText; import net.minecraft.state.property.Property;
import net.minecraft.text.Text; import net.minecraft.text.*;
import net.minecraft.util.Formatting; import net.minecraft.util.Formatting;
import net.minecraft.util.Hand; import net.minecraft.util.Hand;
import net.minecraft.util.hit.BlockHitResult; import net.minecraft.util.hit.BlockHitResult;
@ -25,6 +27,8 @@ import net.minecraft.world.GameMode;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -43,8 +47,12 @@ public class SongHandler {
public LinkedList<Song> songQueue = new LinkedList<>(); public LinkedList<Song> songQueue = new LinkedList<>();
public Song currentSong = null; public Song currentSong = null;
public Playlist currentPlaylist = 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 building = false;
public boolean cleaningUp = false;
public boolean dirty = false;
public boolean wasFlying = false; public boolean wasFlying = false;
public GameMode originalGamemode = GameMode.CREATIVE; public GameMode originalGamemode = GameMode.CREATIVE;
@ -56,6 +64,7 @@ public class SongHandler {
boolean playlistChecked = false; boolean playlistChecked = false;
public void onUpdate(boolean tick) { public void onUpdate(boolean tick) {
if (!cleaningUp) {
// Check current playlist and load song from it if necessary // Check current playlist and load song from it if necessary
if (currentSong == null && currentPlaylist != null && currentPlaylist.loaded) { if (currentSong == null && currentPlaylist != null && currentPlaylist.loaded) {
if (!playlistChecked) { if (!playlistChecked) {
@ -68,12 +77,10 @@ public class SongHandler {
if (currentPlaylist.songs.size() == 0) { if (currentPlaylist.songs.size() == 0) {
SongPlayer.addChatMessage("§cPlaylist has no playable songs"); SongPlayer.addChatMessage("§cPlaylist has no playable songs");
currentPlaylist = null; currentPlaylist = null;
} } else if (nextSong == null) {
else if (nextSong == null) {
SongPlayer.addChatMessage("§6Playlist has finished playing"); SongPlayer.addChatMessage("§6Playlist has finished playing");
currentPlaylist = null; currentPlaylist = null;
} } else {
else {
nextSong.reset(); nextSong.reset();
setSong(nextSong); setSong(nextSong);
} }
@ -97,25 +104,14 @@ public class SongHandler {
} }
loaderThread = null; loaderThread = null;
} }
}
// Run cached command if timeout reached // Run cached command if timeout reached
checkCommandCache(); checkCommandCache();
// Check if no song is playing and, if necessary, handle cleanup // If either playing or doing cleanup
if (currentSong == null) { if (cleaningUp || currentSong != null) {
if (stage != null || SongPlayer.fakePlayer != null) { // Handle creating/removing fake player depending on settings
restoreStateAndCleanUp();
}
else {
originalGamemode = SongPlayer.MC.interactionManager.getCurrentGameMode();
}
}
// Otherwise, handle song playing
else {
if (stage == null) {
stage = new Stage();
stage.movePlayerToStagePosition();
}
if (Config.getConfig().showFakePlayer && SongPlayer.fakePlayer == null) { if (Config.getConfig().showFakePlayer && SongPlayer.fakePlayer == null) {
SongPlayer.fakePlayer = new FakePlayerEntity(); SongPlayer.fakePlayer = new FakePlayerEntity();
SongPlayer.fakePlayer.copyStagePosAndPlayerLook(); SongPlayer.fakePlayer.copyStagePosAndPlayerLook();
@ -127,9 +123,29 @@ public class SongHandler {
SongPlayer.fakePlayer.getInventory().clone(SongPlayer.MC.player.getInventory()); 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; 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 (building) {
if (tick) { if (tick) {
handleBuilding(); handleBuilding();
@ -138,6 +154,20 @@ public class SongHandler {
handlePlaying(tick); 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) { 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) { public void setSong(Song song) {
dirty = true;
currentSong = song; currentSong = song;
building = true; building = true;
setCreativeIfNeeded(); if (!Config.getConfig().survivalOnly) setCreativeIfNeeded();
if (stage == null) { if (Config.getConfig().doAnnouncement) {
stage = new Stage(); sendMessage(Config.getConfig().announcementMessage.replaceAll("\\[name\\]", song.name));
stage.movePlayerToStagePosition();
} }
else { prepareStage();
stage.sendMovementPacketToStagePosition(); if (!Config.getConfig().survivalOnly) getAndSaveBuildSlot();
}
getAndSaveBuildSlot();
SongPlayer.addChatMessage("§6Building noteblocks", true); SongPlayer.addChatMessage("§6Building noteblocks", true);
} }
private void queueSong(Song song) { 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 // Runs every tick
private int buildStartDelay = 0; private int buildStartDelay = 0;
private int buildEndDelay = 0; private int buildEndDelay = 0;
@ -230,22 +266,44 @@ public class SongHandler {
return; return;
} }
ClientWorld world = SongPlayer.MC.world; ClientWorld world = SongPlayer.MC.world;
if (SongPlayer.MC.interactionManager.getCurrentGameMode() != GameMode.CREATIVE) { if (!Config.getConfig().survivalOnly && SongPlayer.MC.interactionManager.getCurrentGameMode() != GameMode.CREATIVE) {
return; return;
} }
if (stage.nothingToBuild()) { if (stage.nothingToBuild()) { // If there's nothing to build, wait for end delay then check build status
if (buildEndDelay > 0) { if (buildEndDelay > 0) { // Wait for end delay
buildEndDelay--; buildEndDelay--;
return; return;
} else { } else { // Check build status when end delay is over
if (!Config.getConfig().survivalOnly) {
stage.checkBuildStatus(currentSong); stage.checkBuildStatus(currentSong);
recordStageBlocks();
} else {
try {
stage.checkSurvivalBuildStatus(currentSong);
} catch (Stage.NotEnoughInstrumentsException e) {
e.giveInstrumentSummary();
restoreStateAndReset();
return;
}
}
stage.sendMovementPacketToStagePosition(); stage.sendMovementPacketToStagePosition();
} }
} }
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();
}
stage.sendMovementPacketToStagePosition();
SongPlayer.addChatMessage("§6Now playing §3" + currentSong.name);
}
if (!Config.getConfig().survivalOnly) { // Regular mode
if (!stage.requiredBreaks.isEmpty()) { if (!stage.requiredBreaks.isEmpty()) {
for (int i=0; i<5; i++) { for (int i = 0; i < 5; i++) {
if (stage.requiredBreaks.isEmpty()) break; if (stage.requiredBreaks.isEmpty()) break;
BlockPos bp = stage.requiredBreaks.poll(); BlockPos bp = stage.requiredBreaks.poll();
attackBlock(bp); attackBlock(bp);
@ -258,7 +316,7 @@ public class SongHandler {
return; return;
} }
int blockId = Block.getRawIdFromState(world.getBlockState(bp)); int blockId = Block.getRawIdFromState(world.getBlockState(bp));
int currentNoteId = (blockId-SongPlayer.NOTEBLOCK_BASE_ID)/2; int currentNoteId = (blockId - SongPlayer.NOTEBLOCK_BASE_ID) / 2;
if (currentNoteId != desiredNoteId) { if (currentNoteId != desiredNoteId) {
holdNoteblock(desiredNoteId, buildSlot); holdNoteblock(desiredNoteId, buildSlot);
if (blockId != 0) { if (blockId != 0) {
@ -268,12 +326,15 @@ public class SongHandler {
} }
buildCooldown = 0; // No cooldown, so it places a block every tick buildCooldown = 0; // No cooldown, so it places a block every tick
buildEndDelay = 20; buildEndDelay = 20;
} else { // Switch to playing }
restoreBuildSlot(); } else { // Survival only mode
building = false; if (!stage.requiredClicks.isEmpty()) {
setSurvivalIfNeeded(); BlockPos bp = stage.requiredClicks.pollFirst();
stage.sendMovementPacketToStagePosition(); if (SongPlayer.MC.world.getBlockState(bp).getBlock() == Blocks.NOTE_BLOCK) {
SongPlayer.addChatMessage("§6Now playing §3" + currentSong.name, true); placeBlock(bp);
}
buildEndDelay = 20;
}
} }
} }
private void setBuildProgressDisplay() { private void setBuildProgressDisplay() {
@ -309,11 +370,22 @@ public class SongHandler {
if (tick) { if (tick) {
if (stage.hasBreakingModification()) { if (stage.hasBreakingModification()) {
if (!Config.getConfig().survivalOnly) {
stage.checkBuildStatus(currentSong); 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 if (!stage.nothingToBuild()) { // Switch to building
building = true; building = true;
setCreativeIfNeeded(); if (!Config.getConfig().survivalOnly) setCreativeIfNeeded();
stage.sendMovementPacketToStagePosition(); stage.sendMovementPacketToStagePosition();
currentSong.pause(); currentSong.pause();
buildStartDelay = 20; buildStartDelay = 20;
@ -323,7 +395,7 @@ public class SongHandler {
int instrumentId = note / 25; int instrumentId = note / 25;
System.out.println("Missing note: " + Instrument.getInstrumentFromId(instrumentId).name() + ":" + pitch); System.out.println("Missing note: " + Instrument.getInstrumentFromId(instrumentId).name() + ":" + pitch);
} }
getAndSaveBuildSlot(); if (!Config.getConfig().survivalOnly) getAndSaveBuildSlot();
SongPlayer.addChatMessage("§6Stage was altered. Rebuilding!", true); SongPlayer.addChatMessage("§6Stage was altered. Rebuilding!", true);
return; return;
} }
@ -335,12 +407,14 @@ public class SongHandler {
currentSong.advanceTime(); currentSong.advanceTime();
while (currentSong.reachedNextNote()) { while (currentSong.reachedNextNote()) {
Note note = currentSong.getNextNote(); Note note = currentSong.getNextNote();
if (note.velocity >= Config.getConfig().velocityThreshold) {
BlockPos bp = stage.noteblockPositions.get(note.noteId); BlockPos bp = stage.noteblockPositions.get(note.noteId);
if (bp != null) { if (bp != null) {
attackBlock(bp); attackBlock(bp);
somethingPlayed = true; somethingPlayed = true;
} }
} }
}
if (somethingPlayed) { if (somethingPlayed) {
stopAttack(); stopAttack();
} }
@ -350,8 +424,7 @@ public class SongHandler {
currentSong = null; currentSong = null;
} }
} }
private void setPlayProgressDisplay() {
public void setPlayProgressDisplay() {
long currentTime = Math.min(currentSong.time, currentSong.length); long currentTime = Math.min(currentSong.time, currentSong.length);
long totalTime = currentSong.length; long totalTime = currentSong.length;
MutableText songText = Text.empty() MutableText songText = Text.empty()
@ -382,20 +455,215 @@ public class SongHandler {
ProgressDisplay.getInstance().setText(songText, playlistText); 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; currentSong = null;
currentPlaylist = null; currentPlaylist = null;
songQueue.clear(); songQueue.clear();
stage = null; stage = null;
buildSlot = -1; buildSlot = -1;
SongPlayer.removeFakePlayer(); SongPlayer.removeFakePlayer();
cleaningUp = false;
dirty = false;
} }
public void restoreStateAndReset() {
public void restoreStateAndCleanUp() { if (lastStage != null) {
if (stage != null) { lastStage.movePlayerToStagePosition();
stage.movePlayerToStagePosition();
} }
if (originalGamemode != SongPlayer.MC.interactionManager.getCurrentGameMode()) { if (originalGamemode != SongPlayer.MC.interactionManager.getCurrentGameMode() && !Config.getConfig().survivalOnly) {
if (originalGamemode == GameMode.CREATIVE) { if (originalGamemode == GameMode.CREATIVE) {
sendGamemodeCommand(Config.getConfig().creativeCommand); sendGamemodeCommand(Config.getConfig().creativeCommand);
} }
@ -403,51 +671,77 @@ public class SongHandler {
sendGamemodeCommand(Config.getConfig().survivalCommand); 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(); restoreBuildSlot();
cleanup(); currentSong = null;
currentPlaylist = null;
songQueue.clear();
stage = null;
buildSlot = -1;
startCleanup();
} }
// Runs every frame when player is not ingame
public void onNotIngame() { public void onNotIngame() {
currentSong = null; currentSong = null;
currentPlaylist = null; currentPlaylist = null;
songQueue.clear(); songQueue.clear();
} }
private long lastCommandTime = System.currentTimeMillis(); // Create stage if it doesn't exist and move the player to it
private List<String> cachedCommands = new ArrayList<>(); private void prepareStage() {
private List<String> cachedMessages = new ArrayList<>(); if (stage == null) {
private void sendGamemodeCommand(String command) { stage = new Stage();
cachedCommands.add(command); lastStage = stage;
originalBlocks.clear();
stage.movePlayerToStagePosition();
} }
public void sendMessage(String message) { else {
cachedMessages.add(message); stage.sendMovementPacketToStagePosition();
}
}
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() { private void checkCommandCache() {
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
if (currentTime >= lastCommandTime + 1500 && !cachedCommands.isEmpty()) { if (currentTime >= lastCommandTime + 1500 && cachedCommand != null) {
SongPlayer.MC.getNetworkHandler().sendCommand(cachedCommands.get(0)); SongPlayer.MC.getNetworkHandler().sendCommand(cachedCommand);
cachedCommands.remove(0); cachedCommand = null;
lastCommandTime = currentTime; lastCommandTime = currentTime;
} }
else if (currentTime >= lastCommandTime + 500 && !cachedMessages.isEmpty()) { else if (currentTime >= lastCommandTime + 500 && cachedMessage != null) {
if (cachedMessages.get(0).startsWith("/")) { if (cachedMessage.startsWith("/")) {
SongPlayer.MC.getNetworkHandler().sendCommand(cachedMessages.get(0).substring(1)); SongPlayer.MC.getNetworkHandler().sendCommand(cachedMessage.substring(1));
} }
else { else {
SongPlayer.MC.getNetworkHandler().sendChatMessage(cachedMessages.get(0)); SongPlayer.MC.getNetworkHandler().sendChatMessage(cachedMessage);
} }
cachedMessages.remove(0); cachedMessage = null;
lastCommandTime = currentTime; lastCommandTime = currentTime;
} }
} }
private void setCreativeIfNeeded() { private void setCreativeIfNeeded() {
cachedCommands.clear(); cachedCommand = null;
if (SongPlayer.MC.interactionManager.getCurrentGameMode() != GameMode.CREATIVE) { if (SongPlayer.MC.interactionManager.getCurrentGameMode() != GameMode.CREATIVE) {
sendGamemodeCommand(Config.getConfig().creativeCommand); sendGamemodeCommand(Config.getConfig().creativeCommand);
} }
} }
private void setSurvivalIfNeeded() { private void setSurvivalIfNeeded() {
cachedCommands.clear(); cachedCommand = null;
if (SongPlayer.MC.interactionManager.getCurrentGameMode() != GameMode.SURVIVAL) { if (SongPlayer.MC.interactionManager.getCurrentGameMode() != GameMode.SURVIVAL) {
sendGamemodeCommand(Config.getConfig().survivalCommand); sendGamemodeCommand(Config.getConfig().survivalCommand);
} }
@ -460,23 +754,33 @@ public class SongHandler {
((ClientPlayerInteractionManagerAccessor) SongPlayer.MC.interactionManager).invokeSyncSelectedSlot(); ((ClientPlayerInteractionManagerAccessor) SongPlayer.MC.interactionManager).invokeSyncSelectedSlot();
int instrument = id/25; int instrument = id/25;
int note = id%25; int note = id%25;
NbtCompound nbt = new NbtCompound(); ItemStack noteblockStack = Items.NOTE_BLOCK.getDefaultStack();
nbt.putString("id", "minecraft:note_block"); noteblockStack.set(DataComponentTypes.BLOCK_STATE, new BlockStateComponent(Map.of(
nbt.putByte("Count", (byte) 1); "instrument", instrumentNames[instrument],
NbtCompound tag = new NbtCompound(); "note", Integer.toString(note)
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);
inventory.main.set(slot, noteblockStack); inventory.main.set(slot, noteblockStack);
SongPlayer.MC.interactionManager.clickCreativeStack(noteblockStack, 36 + slot); 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) { private void placeBlock(BlockPos bp) {
double fx = Math.max(0.0, Math.min(1.0, (stage.position.getX() + 0.5 - bp.getX()))); 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, (stage.position.getY() + 0.0 - bp.getY()))); 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, (stage.position.getZ() + 0.5 - bp.getZ()))); double fz = Math.max(0.0, Math.min(1.0, (lastStage.position.getZ() + 0.5 - bp.getZ())));
fx += bp.getX(); fx += bp.getX();
fy += bp.getY(); fy += bp.getY();
fz += bp.getZ(); fz += bp.getZ();
@ -490,6 +794,43 @@ public class SongHandler {
private void stopAttack() { private void stopAttack() {
SongPlayer.MC.interactionManager.cancelBlockBreaking(); 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) { private void doMovements(double lookX, double lookY, double lookZ) {
if (Config.getConfig().swing) { if (Config.getConfig().swing) {
@ -499,9 +840,9 @@ public class SongHandler {
} }
} }
if (Config.getConfig().rotate) { if (Config.getConfig().rotate) {
double d = lookX - (stage.position.getX() + 0.5); double d = lookX - (lastStage.position.getX() + 0.5);
double e = lookY - (stage.position.getY() + SongPlayer.MC.player.getStandingEyeHeight()); double e = lookY - (lastStage.position.getY() + SongPlayer.MC.player.getStandingEyeHeight());
double f = lookZ - (stage.position.getZ() + 0.5); double f = lookZ - (lastStage.position.getZ() + 0.5);
double g = Math.sqrt(d * d + f * f); double g = Math.sqrt(d * d + f * f);
float pitch = MathHelper.wrapDegrees((float) (-(MathHelper.atan2(e, g) * 57.2957763671875))); float pitch = MathHelper.wrapDegrees((float) (-(MathHelper.atan2(e, g) * 57.2957763671875)));
float yaw = MathHelper.wrapDegrees((float) (MathHelper.atan2(f, d) * 57.2957763671875) - 90.0f); 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.fakePlayer.setHeadYaw(yaw);
} }
SongPlayer.MC.player.networkHandler.getConnection().send(new PlayerMoveC2SPacket.Full( 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, yaw, pitch,
true)); true));
} }
@ -528,4 +869,8 @@ public class SongHandler {
buildSlot = -1; 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.Config;
import com.github.hhhzzzsss.songplayer.SongPlayer; import com.github.hhhzzzsss.songplayer.SongPlayer;
import com.github.hhhzzzsss.songplayer.song.Instrument;
import com.github.hhhzzzsss.songplayer.song.Song; import com.github.hhhzzzsss.songplayer.song.Song;
import net.minecraft.block.Block; import net.minecraft.block.Block;
import net.minecraft.block.BlockState; import net.minecraft.block.BlockState;
@ -25,16 +26,19 @@ public class Stage {
public BlockPos position; public BlockPos position;
public HashMap<Integer, BlockPos> noteblockPositions = new HashMap<>(); public HashMap<Integer, BlockPos> noteblockPositions = new HashMap<>();
// Not used in survival-only mode
public LinkedList<BlockPos> requiredBreaks = new LinkedList<>(); public LinkedList<BlockPos> requiredBreaks = new LinkedList<>();
public TreeSet<Integer> missingNotes = new TreeSet<>(); public TreeSet<Integer> missingNotes = new TreeSet<>();
public int totalMissingNotes = 0; public int totalMissingNotes = 0;
// Only used in survival-only mode
public LinkedList<BlockPos> requiredClicks = new LinkedList<>();
public Stage() { public Stage() {
position = player.getBlockPos(); position = player.getBlockPos();
} }
public void movePlayerToStagePosition() { public void movePlayerToStagePosition() {
player.getAbilities().allowFlying = true;
player.getAbilities().flying = true; player.getAbilities().flying = true;
player.refreshPositionAndAngles(position.getX() + 0.5, position.getY() + 0.0, position.getZ() + 0.5, player.getYaw(), player.getPitch()); player.refreshPositionAndAngles(position.getX() + 0.5, position.getY() + 0.0, position.getZ() + 0.5, player.getYaw(), player.getPitch());
player.setVelocity(Vec3d.ZERO); player.setVelocity(Vec3d.ZERO);
@ -195,6 +199,82 @@ public class Stage {
totalMissingNotes = missingNotes.size(); 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) { void loadDefaultBlocks(Collection<BlockPos> noteblockLocations, Collection<BlockPos> breakLocations) {
for (int dx = -4; dx <= 4; dx++) { for (int dx = -4; dx <= 4; dx++) {
for (int dz = -4; dz <= 4; dz++) { 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 // 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 // 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) { boolean withinBreakingDist(int dx, int dy, int dz) {
@ -434,12 +538,14 @@ public class Stage {
} }
public boolean nothingToBuild() { public boolean nothingToBuild() {
if (!Config.getConfig().survivalOnly) {
return requiredBreaks.isEmpty() && missingNotes.isEmpty(); return requiredBreaks.isEmpty() && missingNotes.isEmpty();
} else {
return requiredClicks.isEmpty();
}
} }
private static final int WRONG_INSTRUMENT_TOLERANCE = 3;
public boolean hasBreakingModification() { public boolean hasBreakingModification() {
int wrongInstruments = 0;
for (Map.Entry<Integer, BlockPos> entry : noteblockPositions.entrySet()) { for (Map.Entry<Integer, BlockPos> entry : noteblockPositions.entrySet()) {
BlockState bs = SongPlayer.MC.world.getBlockState(entry.getValue()); BlockState bs = SongPlayer.MC.world.getBlockState(entry.getValue());
int blockId = Block.getRawIdFromState(bs); int blockId = Block.getRawIdFromState(bs);
@ -455,11 +561,8 @@ public class Stage {
return true; return true;
} }
if (targetInstrument != actualInstrument) { if (targetInstrument != actualInstrument) {
wrongInstruments++;
if (wrongInstruments > WRONG_INSTRUMENT_TOLERANCE) {
return true; return true;
} }
}
BlockState aboveBs = SongPlayer.MC.world.getBlockState(entry.getValue().up()); BlockState aboveBs = SongPlayer.MC.world.getBlockState(entry.getValue().up());
if (!aboveBs.isAir() && !aboveBs.isLiquid()) { if (!aboveBs.isAir() && !aboveBs.isLiquid()) {
@ -468,4 +571,8 @@ public class Stage {
} }
return false; return false;
} }
public Vec3d getOriginBottomCenter() {
return Vec3d.ofBottomCenter(position);
}
} }

View file

@ -1,29 +1,31 @@
package com.github.hhhzzzsss.songplayer.song; package com.github.hhhzzzsss.songplayer.song;
public enum Instrument { public enum Instrument {
HARP(0, 54), HARP(0, 54, "Dirt/Other"),
BASEDRUM(1, 0), BASEDRUM(1, 0, "Any Stone"),
SNARE(2, 0), SNARE(2, 0, "Sand/Gravel"),
HAT(3, 0), HAT(3, 0, "Glass"),
BASS(4, 30), BASS(4, 30, "Any Wood"),
FLUTE(5, 66), FLUTE(5, 66, "Clay"),
BELL(6, 78), BELL(6, 78, "Block of Gold"),
GUITAR(7, 42), GUITAR(7, 42, "Wool"),
CHIME(8, 78), CHIME(8, 78, "Packed Ice"),
XYLOPHONE(9, 78), XYLOPHONE(9, 78, "Bone Block"),
IRON_XYLOPHONE(10, 54), IRON_XYLOPHONE(10, 54, "Block of Iron"),
COW_BELL(11, 66), COW_BELL(11, 66, "Soul Sand"),
DIDGERIDOO(12, 30), DIDGERIDOO(12, 30, "Pumpkin"),
BIT(13, 54), BIT(13, 54, "Block of Emerald"),
BANJO(14, 54), BANJO(14, 54, "Hay Bale"),
PLING(15, 54); PLING(15, 54, "Glowstone");
public final int instrumentId; public final int instrumentId;
public final int offset; public final int offset;
public final String material;
Instrument(int instrumentId, int offset) { Instrument(int instrumentId, int offset, String material) {
this.instrumentId = instrumentId; this.instrumentId = instrumentId;
this.offset = offset; this.offset = offset;
this.material = material;
} }
private static Instrument[] values = values(); private static Instrument[] values = values();

View file

@ -3,9 +3,17 @@ package com.github.hhhzzzsss.songplayer.song;
public class Note implements Comparable<Note> { public class Note implements Comparable<Note> {
public int noteId; public int noteId;
public long time; public long time;
public int velocity;
public Note(int note, long time) { public Note(int note, long time) {
this.noteId = note; this.noteId = note;
this.time = time; this.time = time;
this.velocity = 100;
}
public Note(int note, long time, int velocity) {
this.noteId = note;
this.time = time;
this.velocity = velocity;
} }
@Override @Override

View file

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

View file

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

View file

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