437 lines
22 KiB
Java
437 lines
22 KiB
Java
package semmiedev.disc_jockey;
|
||
|
||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||
import net.minecraft.block.Block;
|
||
import net.minecraft.block.BlockState;
|
||
import net.minecraft.block.Blocks;
|
||
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 boolean warned;
|
||
public boolean running;
|
||
public Song song;
|
||
|
||
private int index;
|
||
private double tick; // Aka song position
|
||
private HashMap<Instrument, HashMap<Byte, BlockPos>> noteBlocks = null;
|
||
public boolean tuned;
|
||
private long lastPlaybackTickAt = -1L;
|
||
|
||
// 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;
|
||
return;
|
||
}
|
||
if (running) stop();
|
||
this.song = song;
|
||
//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 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) {
|
||
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 = client.player;
|
||
|
||
// 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()))
|
||
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()) {
|
||
ChatHud chatHud = MinecraftClient.getInstance().inGameHud.getChatHud();
|
||
chatHud.addMessage(Text.translatable(Main.MOD_ID+".player.invalid_note_blocks").formatted(Formatting.RED));
|
||
|
||
HashMap<Block, Integer> missing = new HashMap<>();
|
||
for (Note note : missingNotes) {
|
||
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) {
|
||
//tuned = true;
|
||
|
||
int ping = 0;
|
||
{
|
||
PlayerListEntry playerListEntry;
|
||
if (client.getNetworkHandler() != null && (playerListEntry = client.getNetworkHandler().getPlayerListEntry(client.player.getGameProfile().getId())) != null)
|
||
ping = playerListEntry.getLatency();
|
||
}
|
||
|
||
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(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;
|
||
}
|
||
untunedNotes.put(blockPos, blockState.get(Properties.NOTE));
|
||
}
|
||
} else {
|
||
noteBlocks = null;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if(tuneInitialUntunedBlocks == -1 || tuneInitialUntunedBlocks < untunedNotes.size())
|
||
tuneInitialUntunedBlocks = untunedNotes.size();
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
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 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;
|
||
}
|
||
}
|