Update Song Player with a lot of new features

**Async and, client tps and lag independent playback**

Playback should work in any conditions. No matter how fast/slow or laggy or
inconsistent the client is. The client itself may not hear it in the correct
timing, but other players will.

The way song ticks are progressed was overhauled and simplified as well. This
enables changing the speed of the song or seeking it.

**Overhauled tuning**

Tuning got overhauled and is now much quicker. It also will try to tune the
least amount possible, which makes tuning when changing songs a lot quicker.

It assumes changes going through and should work on any ping. Though initial
wrong ping information combined with very bad or fluctuating ping can cause
the tuning to overshoot and never finish (should be very unlikely and resolve
itself after some time).

**Added a few variables to control playback better**

Variables like didSongReachEnd, missingInstrumentBlocks and some others are
used to have more fine grained control from other mods (for implementing a
playback queue or getting more info about tuning issues). I'd like them
preserved but can understand if they do not make much sense.

speed is also not changed in the mod self right now, but i think there
could be some UI element added to have fun with it. Same for instrumentMap.

**Tick listener is always enabled**

I stumbled upon some crashes when it was removing / adding it, so I made the
tick listener permanent. It should not have any significant performance
implications anyway.

**Added packet rate limit**

When playing back, the own packets are being kept track of. If they get very
high, the client will stop sending "cosmetic" packets and if they get close to
the packet rate limit of servers, it'll interrupt playback for brief periods
to prevent getting packet kicked.

This was added to not get kicked for playing "Rush E" or setting very high "speed"s.

**Reduce amount of swining**
Does not swing arm for every note played but roughly every serverside tick.
This reduces wasted packets no one is going to see anyway.

**Use server side distance check**

Use the same check the server uses to determine if a noteblock can be reached
or not. Increases range to the max allowed.

**Other changes**

I probably forgot some other changes here as well. Many were added a while back
and there were probably numerous other fixes for stability’s sake. So excuse me
for not recounting every single one.
This commit is contained in:
EnderKill98 2023-06-16 18:03:31 +02:00
parent 5fbb54de7f
commit ad8f94ee27
2 changed files with 347 additions and 84 deletions

View File

@ -22,4 +22,22 @@ public class Song {
public String toString() {
return displayName;
}
public double millisecondsToTicks(long milliseconds) {
// From NBS Format: The tempo of the song multiplied by 100 (for example, 1225 instead of 12.25). Measured in ticks per second.
double songSpeed = (tempo / 100.0) / 20.0; // 20 Ticks per second (temp / 100 = 20) would be 1x speed
double oneMsTo20TickFraction = 1.0 / 50.0;
return milliseconds * oneMsTo20TickFraction * songSpeed;
}
public double ticksToMilliseconds(double ticks) {
double songSpeed = (tempo / 100.0) / 20.0;
double oneMsTo20TickFraction = 1.0 / 50.0;
return ticks / oneMsTo20TickFraction / songSpeed;
}
public double getLengthInSeconds() {
return ticksToMilliseconds(length) / 1000.0;
}
}

View File

@ -8,33 +8,85 @@ import net.minecraft.block.enums.Instrument;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.hud.ChatHud;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.network.PlayerListEntry;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket;
import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket;
import net.minecraft.state.property.Properties;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.util.Hand;
import net.minecraft.util.Pair;
import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.math.*;
import net.minecraft.world.GameMode;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class SongPlayer implements ClientTickEvents.StartWorldTick {
private static final Box BOX = new Box(0, 0, 0, 1, 1, 1);
private static boolean warned;
public boolean running;
public Song song;
private int index;
private float tick;
private double tick; // Aka song position
private HashMap<Instrument, HashMap<Byte, BlockPos>> noteBlocks = null;
private boolean tuned;
private int tuneDelay = 5;
private long lastPlaybackTickAt = -1L;
public void start(Song song) {
// Used to check and enforce packet rate limits to not get kicked
private long last100MsSpanAt = -1L;
private int last100MsSpanEstimatedPackets = 0;
// At how many packets/100ms should the player just reduce / stop sending packets for a while
final private int last100MsReducePacketsAfter = 300 / 10, last100MsStopPacketsAfter = 450 / 10;
// If higher than current millis, don't send any packets of this kind (temp disable)
private long reducePacketsUntil = -1L, stopPacketsUntil = -1L;
// Use to limit swings and look to only each tick. More will not be visually visible anyway due to interpolation
private long lastLookSentAt = -1L, lastSwingSentAt = -1L;
// The thread executing the tickPlayback method
private Thread playbackThread = null;
public long playbackLoopDelay = 5;
// Just for external debugging purposes
public HashMap<Block, Integer> missingInstrumentBlocks = new HashMap<>();
public float speed = 1.0f; // Toy
private long lastInteractAt = -1;
private float availableInteracts = 8;
private int tuneInitialUntunedBlocks = -1;
private HashMap<BlockPos, Pair<Integer, Long>> notePredictions = new HashMap<>();
public boolean didSongReachEnd = false;
public SongPlayer() {
Main.TICK_LISTENERS.add(this);
}
public @NotNull HashMap<Instrument, Instrument> instrumentMap = new HashMap<>(); // Toy
public synchronized void startPlaybackThread() {
this.playbackThread = new Thread(() -> {
Thread ownThread = this.playbackThread;
while(ownThread == this.playbackThread) {
try {
// Accuracy doesn't really matter at this precision imo
Thread.sleep(playbackLoopDelay);
}catch (Exception ex) {
ex.printStackTrace();
}
tickPlayback();
}
});
this.playbackThread.start();
}
public synchronized void stopPlaybackThread() {
this.playbackThread = null; // Should stop on its own then
}
public synchronized void start(Song song) {
if (!Main.config.hideWarning && !warned) {
MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(Text.translatable("disc_jockey.warning").formatted(Formatting.BOLD, Formatting.RED));
warned = true;
@ -42,52 +94,205 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
}
if (running) stop();
this.song = song;
Main.TICK_LISTENERS.add(this);
//Main.LOGGER.info("Song length: " + song.length + " and tempo " + song.tempo);
//Main.TICK_LISTENERS.add(this);
if(this.playbackThread == null) startPlaybackThread();
running = true;
lastPlaybackTickAt = System.currentTimeMillis();
last100MsSpanAt = System.currentTimeMillis();
last100MsSpanEstimatedPackets = 0;
reducePacketsUntil = -1L;
stopPacketsUntil = -1L;
lastLookSentAt = -1L;
lastSwingSentAt = -1L;
missingInstrumentBlocks.clear();
didSongReachEnd = false;
}
public void stop() {
MinecraftClient.getInstance().send(() -> Main.TICK_LISTENERS.remove(this));
public synchronized void stop() {
//MinecraftClient.getInstance().send(() -> Main.TICK_LISTENERS.remove(this));
stopPlaybackThread();
running = false;
index = 0;
tick = 0;
noteBlocks = null;
notePredictions.clear();
tuned = false;
tuneInitialUntunedBlocks = -1;
lastPlaybackTickAt = -1L;
last100MsSpanAt = -1L;
last100MsSpanEstimatedPackets = 0;
reducePacketsUntil = -1L;
stopPacketsUntil = -1L;
lastLookSentAt = -1L;
lastSwingSentAt = -1L;
didSongReachEnd = false; // Change after running stop() if actually ended cleanly
}
public synchronized void tickPlayback() {
if (!running) {
lastPlaybackTickAt = -1L;
last100MsSpanAt = -1L;
return;
}
long previousPlaybackTickAt = lastPlaybackTickAt;
lastPlaybackTickAt = System.currentTimeMillis();
if(last100MsSpanAt != -1L && System.currentTimeMillis() - last100MsSpanAt >= 100) {
last100MsSpanEstimatedPackets = 0;
last100MsSpanAt = System.currentTimeMillis();
}else if (last100MsSpanAt == -1L) {
last100MsSpanAt = System.currentTimeMillis();
last100MsSpanEstimatedPackets = 0;
}
if(noteBlocks != null && tuned) {
while (running) {
MinecraftClient client = MinecraftClient.getInstance();
GameMode gameMode = client.interactionManager == null ? null : client.interactionManager.getCurrentGameMode();
// In the best case, gameMode would only be queried in sync Ticks, no here
if (gameMode == null || !gameMode.isSurvivalLike()) {
client.inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.invalid_game_mode", gameMode.getTranslatableName()).formatted(Formatting.RED));
stop();
return;
}
long note = song.notes[index];
final long now = System.currentTimeMillis();
if ((short)note <= Math.round(tick)) {
BlockPos blockPos = noteBlocks.get(Note.INSTRUMENTS[(byte)(note >> Note.INSTRUMENT_SHIFT)]).get((byte)(note >> Note.NOTE_SHIFT));
if (!canInteractWith(client.player, blockPos)) {
stop();
client.inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.to_far").formatted(Formatting.RED));
return;
}
Vec3d unit = Vec3d.ofCenter(blockPos, 0.5).subtract(client.player.getEyePos()).normalize();
if((lastLookSentAt == -1L || now - lastLookSentAt >= 50) && last100MsSpanEstimatedPackets < last100MsReducePacketsAfter && (reducePacketsUntil == -1L || reducePacketsUntil < now)) {
client.getNetworkHandler().sendPacket(new PlayerMoveC2SPacket.LookAndOnGround(MathHelper.wrapDegrees((float) (MathHelper.atan2(unit.z, unit.x) * 57.2957763671875) - 90.0f), MathHelper.wrapDegrees((float) (-(MathHelper.atan2(unit.y, Math.sqrt(unit.x * unit.x + unit.z * unit.z)) * 57.2957763671875))), true));
last100MsSpanEstimatedPackets++;
lastLookSentAt = now;
}else if(last100MsSpanEstimatedPackets >= last100MsReducePacketsAfter){
reducePacketsUntil = Math.max(reducePacketsUntil, now + 500);
}
if(last100MsSpanEstimatedPackets < last100MsStopPacketsAfter && (stopPacketsUntil == -1L || stopPacketsUntil < now)) {
// TODO: 5/30/2022 Check if the block needs tuning
//client.interactionManager.attackBlock(blockPos, Direction.UP);
client.player.networkHandler.sendPacket(new PlayerActionC2SPacket(PlayerActionC2SPacket.Action.START_DESTROY_BLOCK, blockPos, Direction.UP, 0));
last100MsSpanEstimatedPackets++;
}else if(last100MsSpanEstimatedPackets >= last100MsStopPacketsAfter) {
Main.LOGGER.info("Stopping all packets for a bit!");
stopPacketsUntil = Math.max(stopPacketsUntil, now + 250);
reducePacketsUntil = Math.max(reducePacketsUntil, now + 10000);
}
if(last100MsSpanEstimatedPackets < last100MsReducePacketsAfter && (reducePacketsUntil == -1L || reducePacketsUntil < now)) {
client.player.networkHandler.sendPacket(new PlayerActionC2SPacket(PlayerActionC2SPacket.Action.ABORT_DESTROY_BLOCK, blockPos, Direction.UP, 0));
last100MsSpanEstimatedPackets++;
}else if(last100MsSpanEstimatedPackets >= last100MsReducePacketsAfter){
reducePacketsUntil = Math.max(reducePacketsUntil, now + 500);
}
if((lastSwingSentAt == -1L || now - lastSwingSentAt >= 50) &&last100MsSpanEstimatedPackets < last100MsReducePacketsAfter && (reducePacketsUntil == -1L || reducePacketsUntil < now)) {
client.executeSync(() -> client.player.swingHand(Hand.MAIN_HAND));
lastSwingSentAt = now;
last100MsSpanEstimatedPackets++;
}else if(last100MsSpanEstimatedPackets >= last100MsReducePacketsAfter){
reducePacketsUntil = Math.max(reducePacketsUntil, now + 500);
}
index++;
if (index >= song.notes.length) {
stop();
didSongReachEnd = true;
break;
}
} else {
break;
}
}
if(running) { // Might not be running anymore (prevent small offset on song, even if that is not played anymore)
long elapsedMs = previousPlaybackTickAt != -1L && lastPlaybackTickAt != -1L ? lastPlaybackTickAt - previousPlaybackTickAt : (16); // Assume 16ms if unknown
tick += song.millisecondsToTicks(elapsedMs) * speed;
}
}
}
// TODO: 6/2/2022 Play note blocks every song tick, instead of every tick. That way the song will sound better
// 11/1/2023 Playback now done in separate thread. Not ideal but better especially when FPS are low.
@Override
public void onStartTick(ClientWorld world) {
if (!running) return;
MinecraftClient client = MinecraftClient.getInstance();
if(world == null || client.world == null || client.player == null) return;
if(song == null || !running) return;
// Clear outdated note predictions
ArrayList<BlockPos> outdatedPredictions = new ArrayList<>();
for(Map.Entry<BlockPos, Pair<Integer, Long>> entry : notePredictions.entrySet()) {
if(entry.getValue().getRight() < System.currentTimeMillis())
outdatedPredictions.add(entry.getKey());
}
for(BlockPos outdatedPrediction : outdatedPredictions) notePredictions.remove(outdatedPrediction);
if (noteBlocks == null) {
noteBlocks = new HashMap<>();
ClientPlayerEntity player = MinecraftClient.getInstance().player;
ClientPlayerEntity player = client.player;
ArrayList<Note> capturedNotes = new ArrayList<>();
Vec3d playerPos = player.getEyePos();
for (int x = -7; x <= 7; x++) {
for (int y = -7; y <= 7; y++) {
for (int z = -7; z <= 7; z++) {
Vec3d vec3d = playerPos.add(x, y, z);
BlockPos blockPos = new BlockPos(MathHelper.floor(vec3d.x), MathHelper.floor(vec3d.y), MathHelper.floor(vec3d.z));
if (intersect(playerPos, MinecraftClient.getInstance().interactionManager.getReachDistance(), BOX.offset(blockPos))) {
// Create list of available noteblock positions per used instrument
HashMap<Instrument, ArrayList<BlockPos>> noteblocksForInstrument = new HashMap<>();
for(Instrument instrument : Instrument.values())
noteblocksForInstrument.put(instrument, new ArrayList<>());
final Vec3d playerPos = player.getEyePos();
final int[] orderedOffsets = new int[] { 0, -1, 1, -2, 2, -3, 3, -4, 4, -5, 5, -6, 6, -7, 7 };
for(Instrument instrument : noteblocksForInstrument.keySet().toArray(new Instrument[0])) {
for (int y : orderedOffsets) {
for (int x : orderedOffsets) {
for (int z : orderedOffsets) {
Vec3d vec3d = playerPos.add(x, y, z);
BlockPos blockPos = new BlockPos(MathHelper.floor(vec3d.x), MathHelper.floor(vec3d.y), MathHelper.floor(vec3d.z));
if (!canInteractWith(player, blockPos))
continue;
BlockState blockState = world.getBlockState(blockPos);
if (blockState.isOf(Blocks.NOTE_BLOCK) && world.isAir(blockPos.up())) {
for (Note note : song.uniqueNotes) {
if (!capturedNotes.contains(note) && blockState.get(Properties.INSTRUMENT) == note.instrument) {
getNotes(note.instrument).put(note.note, blockPos);
capturedNotes.add(note);
break;
}
}
}
if (!blockState.isOf(Blocks.NOTE_BLOCK) || !world.isAir(blockPos.up()))
continue;
if (blockState.get(Properties.INSTRUMENT) == instrument)
noteblocksForInstrument.get(instrument).add(blockPos);
}
}
}
}
// Remap instruments for funzies
if(!instrumentMap.isEmpty()) {
HashMap<Instrument, ArrayList<BlockPos>> newNoteblocksForInstrument = new HashMap<>();
for(Instrument orig : noteblocksForInstrument.keySet()) {
newNoteblocksForInstrument.put(orig, noteblocksForInstrument.getOrDefault(instrumentMap.getOrDefault(orig, orig), new ArrayList<>()));
}
noteblocksForInstrument = newNoteblocksForInstrument;
}
// Find fitting noteblocks with the least amount of adjustments required (to reduce tuning time)
ArrayList<Note> capturedNotes = new ArrayList<>();
for(Note note : song.uniqueNotes) {
ArrayList<BlockPos> availableBlocks = noteblocksForInstrument.get(note.instrument);
BlockPos bestBlockPos = null;
int bestBlockTuningSteps = Integer.MAX_VALUE;
for(BlockPos blockPos : availableBlocks) {
int wantedNote = note.note;
int currentNote = client.world.getBlockState(blockPos).get(Properties.NOTE);
int tuningSteps = wantedNote >= currentNote ? wantedNote - currentNote : (25 - currentNote) + wantedNote;
if(tuningSteps < bestBlockTuningSteps) {
bestBlockPos = blockPos;
bestBlockTuningSteps = tuningSteps;
}
}
if(bestBlockPos != null) {
capturedNotes.add(note);
availableBlocks.remove(bestBlockPos);
getNotes(note.instrument).put(note.note, bestBlockPos);
} // else will be a missing note
}
ArrayList<Note> missingNotes = new ArrayList<>(song.uniqueNotes);
missingNotes.removeAll(capturedNotes);
if (!missingNotes.isEmpty()) {
@ -96,96 +301,136 @@ public class SongPlayer implements ClientTickEvents.StartWorldTick {
HashMap<Block, Integer> missing = new HashMap<>();
for (Note note : missingNotes) {
Block block = Note.INSTRUMENT_BLOCKS.get(note.instrument);
Block block = Note.INSTRUMENT_BLOCKS.get(instrumentMap.getOrDefault(note.instrument, note.instrument));
Integer got = missing.get(block);
if (got == null) got = 0;
missing.put(block, got + 1);
}
missingInstrumentBlocks = missing;
missing.forEach((block, integer) -> chatHud.addMessage(Text.literal(block.getName().getString()+" × "+integer).formatted(Formatting.RED)));
stop();
}
} else if (!tuned) {
if (tuneDelay > 0) {
tuneDelay--;
return;
//tuned = true;
int ping = 0;
{
PlayerListEntry playerListEntry;
if (client.getNetworkHandler() != null && (playerListEntry = client.getNetworkHandler().getPlayerListEntry(client.player.getGameProfile().getId())) != null)
ping = playerListEntry.getLatency();
}
tuned = true;
MinecraftClient client = MinecraftClient.getInstance();
int tuneAmount = 0;
if(lastInteractAt != -1L) {
// Paper allows 8 interacts per 300 ms
availableInteracts += ((System.currentTimeMillis() - lastInteractAt) / (310.0 / 8.0));
availableInteracts = Math.min(8f, Math.max(0f, availableInteracts));
}else {
availableInteracts = 8f;
lastInteractAt = System.currentTimeMillis();
}
int fullyTunedBlocks = 0;
HashMap<BlockPos, Integer> untunedNotes = new HashMap<>();
for (Note note : song.uniqueNotes) {
if(noteBlocks == null || noteBlocks.get(note.instrument) == null)
continue;
BlockPos blockPos = noteBlocks.get(note.instrument).get(note.note);
if(blockPos == null) continue;
BlockState blockState = world.getBlockState(blockPos);
int assumedNote = notePredictions.containsKey(blockPos) ? notePredictions.get(blockPos).getLeft() : blockState.get(Properties.NOTE);
if (blockState.contains(Properties.NOTE)) {
if (blockState.get(Properties.NOTE) != note.note) {
if (!intersect(client.player.getEyePos(), client.interactionManager.getReachDistance(), BOX.offset(blockPos))) {
if(assumedNote == note.note && blockState.get(Properties.NOTE) == note.note)
fullyTunedBlocks++;
if (assumedNote != note.note) {
if (!canInteractWith(client.player, blockPos)) {
stop();
client.inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.to_far").formatted(Formatting.RED));
return;
}
Vec3d unit = Vec3d.ofCenter(blockPos, 0.5).subtract(client.player.getEyePos()).normalize();
client.getNetworkHandler().sendPacket(new PlayerMoveC2SPacket.LookAndOnGround(MathHelper.wrapDegrees((float)(MathHelper.atan2(unit.z, unit.x) * 57.2957763671875) - 90.0f), MathHelper.wrapDegrees((float)(-(MathHelper.atan2(unit.y, Math.sqrt(unit.x * unit.x + unit.z * unit.z)) * 57.2957763671875))), true));
client.interactionManager.interactBlock(client.player, Hand.MAIN_HAND, new BlockHitResult(Vec3d.of(blockPos), Direction.UP, blockPos, false));
client.player.swingHand(Hand.MAIN_HAND);
tuned = false;
tuneDelay = 5;
if (++tuneAmount == 6) break;
untunedNotes.put(blockPos, blockState.get(Properties.NOTE));
}
} else {
noteBlocks = null;
break;
}
}
} else {
while (running) {
MinecraftClient client = MinecraftClient.getInstance();
GameMode gameMode = client.interactionManager.getCurrentGameMode();
if (!gameMode.isSurvivalLike()) {
client.inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.invalid_game_mode", gameMode.getTranslatableName()).formatted(Formatting.RED));
stop();
return;
}
long note = song.notes[index];
if ((short)note == Math.round(tick)) {
BlockPos blockPos = noteBlocks.get(Note.INSTRUMENTS[(byte)(note >> Note.INSTRUMENT_SHIFT)]).get((byte)(note >> Note.NOTE_SHIFT));
if (!intersect(client.player.getEyePos(), client.interactionManager.getReachDistance(), BOX.offset(blockPos))) {
stop();
client.inGameHud.getChatHud().addMessage(Text.translatable(Main.MOD_ID+".player.to_far").formatted(Formatting.RED));
return;
}
Vec3d unit = Vec3d.ofCenter(blockPos, 0.5).subtract(client.player.getEyePos()).normalize();
client.getNetworkHandler().sendPacket(new PlayerMoveC2SPacket.LookAndOnGround(MathHelper.wrapDegrees((float)(MathHelper.atan2(unit.z, unit.x) * 57.2957763671875) - 90.0f), MathHelper.wrapDegrees((float)(-(MathHelper.atan2(unit.y, Math.sqrt(unit.x * unit.x + unit.z * unit.z)) * 57.2957763671875))), true));
// TODO: 5/30/2022 Check if the block needs tuning
client.interactionManager.attackBlock(blockPos, Direction.UP);
client.player.swingHand(Hand.MAIN_HAND);
if(tuneInitialUntunedBlocks == -1 || tuneInitialUntunedBlocks < untunedNotes.size())
tuneInitialUntunedBlocks = untunedNotes.size();
index++;
if (index >= song.notes.length) {
stop();
break;
}
} else {
break;
if(untunedNotes.isEmpty() && fullyTunedBlocks == song.uniqueNotes.size()) {
// Wait roundrip + 100ms before considering tuned after changing notes (in case the server rejects an interact)
if(lastInteractAt == -1 || System.currentTimeMillis() - lastInteractAt >= ping * 2 + 100) {
tuned = true;
tuneInitialUntunedBlocks = -1;
}
}
tick += song.tempo / 100f / 20f;
BlockPos lastBlockPos = null;
int lastTunedNote = Integer.MIN_VALUE;
float roughTuneProgress = 1 - (untunedNotes.size() / Math.max(tuneInitialUntunedBlocks + 0f, 1f));
while(availableInteracts >= 1f && untunedNotes.size() > 0) {
BlockPos blockPos = null;
int searches = 0;
while(blockPos == null) {
searches++;
// Find higher note
for (Map.Entry<BlockPos, Integer> entry : untunedNotes.entrySet()) {
if (entry.getValue() > lastTunedNote) {
blockPos = entry.getKey();
break;
}
}
// Find higher note or equal
if (blockPos == null) {
for (Map.Entry<BlockPos, Integer> entry : untunedNotes.entrySet()) {
if (entry.getValue() >= lastTunedNote) {
blockPos = entry.getKey();
break;
}
}
}
// Not found. Reset last note
if(blockPos == null)
lastTunedNote = Integer.MIN_VALUE;
if(blockPos == null && searches > 1) {
// Something went wrong. Take any note (one should at least exist here)
blockPos = untunedNotes.keySet().toArray(new BlockPos[0])[0];
break;
}
}
if(blockPos == null) return; // Something went very, very wrong!
lastTunedNote = untunedNotes.get(blockPos);
untunedNotes.remove(blockPos);
int assumedNote = notePredictions.containsKey(blockPos) ? notePredictions.get(blockPos).getLeft() : client.world.getBlockState(blockPos).get(Properties.NOTE);
notePredictions.put(blockPos, new Pair((assumedNote + 1) % 25, System.currentTimeMillis() + ping * 2 + 100));
client.interactionManager.interactBlock(client.player, Hand.MAIN_HAND, new BlockHitResult(Vec3d.of(blockPos), Direction.UP, blockPos, false));
lastInteractAt = System.currentTimeMillis();
availableInteracts -= 1f;
lastBlockPos = blockPos;
}
if(lastBlockPos != null) {
// Turn head into spinning with time and lookup up further the further tuning is progressed
client.getNetworkHandler().sendPacket(new PlayerMoveC2SPacket.LookAndOnGround(((float) (System.currentTimeMillis() % 2000)) * (360f/2000f), (1 - roughTuneProgress) * 180 - 90, true));
client.player.swingHand(Hand.MAIN_HAND);
}
}
}
private boolean intersect(Vec3d pos, double radius, Box box) {
double x = Math.max(box.minX, Math.min(pos.x, box.maxX));
double y = Math.max(box.minY, Math.min(pos.y, box.maxY));
double z = Math.max(box.minZ, Math.min(pos.z, box.maxZ));
double distance = (x - pos.x) * (x - pos.x) + (y - pos.y) * (y - pos.y) + (z - pos.z) * (z - pos.z);
return distance < radius * radius;
}
private HashMap<Byte, BlockPos> getNotes(Instrument instrument) {
return noteBlocks.computeIfAbsent(instrument, k -> new HashMap<>());
}
// The server limits interacts to 6 Blocks from Player Eye to Block Center
private boolean canInteractWith(ClientPlayerEntity player, BlockPos blockPos) {
return player.getEyePos().squaredDistanceTo(new Vec3d(blockPos.getX() + 0.5, blockPos.getY() + 0.5, blockPos.getZ() + 0.5)) <= 6.0*6.0;
}
public double getSongElapsedSeconds() {
if(song == null) return 0;
return song.ticksToMilliseconds(tick) / 1000;
}
}