From c0eb2f004495c18c7174eb905765bebf5967bfb5 Mon Sep 17 00:00:00 2001 From: Heliosares Date: Sat, 4 Mar 2023 11:37:48 -0500 Subject: [PATCH] Add posturing to position playback --- .../heliosares/auxprotect/core/APPlayer.java | 5 +- .../core/commands/LookupCommand.java | 2 + .../auxprotect/database/ConnectionPool.java | 14 +- .../auxprotect/database/MigrationManager.java | 2 +- .../auxprotect/database/PosEntry.java | 36 ++- .../auxprotect/database/SQLManager.java | 13 +- .../auxprotect/spigot/AuxProtectSpigot.java | 7 +- .../auxprotect/utils/FakePlayer.java | 67 +++++- .../auxprotect/utils/PlaybackSolver.java | 187 ++++++++------- .../auxprotect/utils/PosEncoder.java | 227 ++++++++++++------ 10 files changed, 377 insertions(+), 183 deletions(-) diff --git a/src/dev/heliosares/auxprotect/core/APPlayer.java b/src/dev/heliosares/auxprotect/core/APPlayer.java index ee8eafb..bdad9b1 100644 --- a/src/dev/heliosares/auxprotect/core/APPlayer.java +++ b/src/dev/heliosares/auxprotect/core/APPlayer.java @@ -34,6 +34,7 @@ public class APPlayer { // hotbar, main, armor, offhand, echest private List invDiffItems; private Location lastLocationDiff; + private PosEncoder.Posture lastPosture; public APPlayer(IAuxProtect plugin, Player player) { this.player = player; @@ -144,9 +145,11 @@ public class APPlayer { public void tickDiffPos() { if (lastLocationDiff != null) { synchronized (posBlob) { - for (byte b : PosEncoder.encode(lastLocationDiff, player.getLocation())) { + PosEncoder.Posture posture = PosEncoder.Posture.fromPlayer(player); + for (byte b : PosEncoder.encode(lastLocationDiff, player.getLocation(), posture, lastPosture)) { posBlob.add(b); } + lastPosture = posture; } } lastLocationDiff = player.getLocation().clone(); diff --git a/src/dev/heliosares/auxprotect/core/commands/LookupCommand.java b/src/dev/heliosares/auxprotect/core/commands/LookupCommand.java index 87e0728..d2ebcd2 100644 --- a/src/dev/heliosares/auxprotect/core/commands/LookupCommand.java +++ b/src/dev/heliosares/auxprotect/core/commands/LookupCommand.java @@ -425,6 +425,8 @@ public class LookupCommand extends Command { sender.sendMessageRaw(e.getMessage()); } catch (Exception e) { sender.sendLang(Language.L.ERROR); + plugin.warning("Error during lookup:"); + plugin.print(e); } } diff --git a/src/dev/heliosares/auxprotect/database/ConnectionPool.java b/src/dev/heliosares/auxprotect/database/ConnectionPool.java index aaf43ea..50578ec 100644 --- a/src/dev/heliosares/auxprotect/database/ConnectionPool.java +++ b/src/dev/heliosares/auxprotect/database/ConnectionPool.java @@ -372,18 +372,16 @@ public class ConnectionPool { } } - protected int count(String table) throws SQLException { + protected int count(Connection connection, String table) throws SQLException { String stmtStr = getCountStmt(table); plugin.debug(stmtStr, 5); - return executeReturn(connection -> { - try (PreparedStatement pstmt = connection.prepareStatement(stmtStr)) { - try (ResultSet rs = pstmt.executeQuery()) { - if (rs.next()) return rs.getInt(1); - return -1; - } + try (PreparedStatement pstmt = connection.prepareStatement(stmtStr)) { + try (ResultSet rs = pstmt.executeQuery()) { + if (rs.next()) return rs.getInt(1); + return -1; } - }, 30000L, Integer.class); + } } protected String getCountStmt(String table) { diff --git a/src/dev/heliosares/auxprotect/database/MigrationManager.java b/src/dev/heliosares/auxprotect/database/MigrationManager.java index faf0101..742221f 100644 --- a/src/dev/heliosares/auxprotect/database/MigrationManager.java +++ b/src/dev/heliosares/auxprotect/database/MigrationManager.java @@ -54,7 +54,7 @@ public class MigrationManager { plugin.info("Tables renamed"); }, () -> { for (Table table : migrateTablesV3[0]) { - total += sql.count(table + "_temp"); + total += sql.count(connection, table + "_temp"); } if (plugin.getPlatform() == PlatformType.BUNGEE) { migrateTablesV3[0] = new Table[]{Table.AUXPROTECT_MAIN, Table.AUXPROTECT_LONGTERM}; diff --git a/src/dev/heliosares/auxprotect/database/PosEntry.java b/src/dev/heliosares/auxprotect/database/PosEntry.java index 329e1bd..aad9b38 100644 --- a/src/dev/heliosares/auxprotect/database/PosEntry.java +++ b/src/dev/heliosares/auxprotect/database/PosEntry.java @@ -21,7 +21,7 @@ public class PosEntry extends DbEntry { protected PosEntry(long time, int uid, EntryAction action, boolean state, String world, int x, int y, int z, byte increment, int pitch, int yaw, String target, int target_id, String data) { super(time, uid, action, state, world, x, y, z, pitch, yaw, target, target_id, data); - double[] dInc = PosEncoder.byteToFractions(increment); + double[] dInc = byteToFractions(increment); this.x = x + dInc[0]; this.y = y + dInc[1]; this.z = z + dInc[2]; @@ -36,6 +36,38 @@ public class PosEntry extends DbEntry { this.z = location.getZ(); } + /** + * Stores the fraction of the x/y/z values into a single byte. The structure is as follows + * 0b X X X Y Y Z Z Z + * X and Z are stored in 8ths, Y is stored in 4ths. + */ + public static byte getFractionalByte(double dx, double dy, double dz) { + dx %= 1; + dy %= 1; + dz %= 1; + if (dx < 0) dx++; + if (dy < 0) dy++; + if (dz < 0) dz++; + int x = (int) Math.min(Math.round(dx * 8), 7) << 5; + int y = (int) Math.min(Math.round(dy * 4), 3) << 3; + int z = (int) Math.min(Math.round(dz * 8), 7); + + return (byte) (x | y | z); + } + + /** + * Retrieves the fractional values from the increment byte generated in {@link PosEncoder#getFractionalByte(double, double, double)} + * + * @return An array of doubles of length 3, containing the x, y, and z fractions respectively. + */ + public static double[] byteToFractions(byte b) { + int x = (b >> 5) & 0b111; + int y = (b >> 3) & 0b11; + int z = b & 0b111; + + return new double[]{x / 8D, y / 4D, z / 8D}; + } + public double getDoubleX() { return x; } @@ -64,6 +96,6 @@ public class PosEntry extends DbEntry { } public byte getIncrement() { - return PosEncoder.getFractionalByte(x, y, z); + return getFractionalByte(x, y, z); } } diff --git a/src/dev/heliosares/auxprotect/database/SQLManager.java b/src/dev/heliosares/auxprotect/database/SQLManager.java index 6673b85..38e46fd 100644 --- a/src/dev/heliosares/auxprotect/database/SQLManager.java +++ b/src/dev/heliosares/auxprotect/database/SQLManager.java @@ -287,6 +287,9 @@ public class SQLManager extends ConnectionPool { invblobmanager.init(connection); } + if (getLast(LastKeys.LEGACY_POSITIONS, connection) == 0) + setLast(LastKeys.LEGACY_POSITIONS, System.currentTimeMillis(), connection); + plugin.debug("init done."); } @@ -570,7 +573,7 @@ public class SQLManager extends ConnectionPool { } public int count(Table table) throws SQLException { - return count(table.toString()); + return executeReturn(connection -> count(connection, table.toString()), 30000L, Integer.class); } public byte[] getBlob(DbEntry entry) throws SQLException { @@ -629,6 +632,7 @@ public class SQLManager extends ConnectionPool { } public void tick() { + if (!isConnected()) return; try { execute(connection -> { Arrays.asList(Table.values()).forEach(t -> { @@ -652,18 +656,22 @@ public class SQLManager extends ConnectionPool { //TODO implement public void setLast(LastKeys key, long value) throws SQLException { + key.value = value; execute(connection -> setLast(key, value, connection), 30000L); } public void setLast(LastKeys key, long value, Connection connection) throws SQLException { + key.value = value; execute("UPDATE " + Table.AUXPROTECT_LASTS + " SET value=? WHERE name=?", connection, value, key.id); } public long getLast(LastKeys key) throws SQLException { + if (key.value != null) return key.value; return executeReturn(connection -> getLast(key, connection), 30000L, Long.class); } public long getLast(LastKeys key, Connection connection) throws SQLException { + if (key.value != null) return key.value; try (PreparedStatement stmt = connection.prepareStatement("SELECT value FROM " + Table.AUXPROTECT_LASTS + " WHERE name=?")) { stmt.setShort(1, key.id); try (ResultSet rs = stmt.executeQuery()) { @@ -681,9 +689,10 @@ public class SQLManager extends ConnectionPool { public enum LastKeys { - AUTO_PURGE(1), VACUUM(2), TELEMETRY(3); + AUTO_PURGE(1), VACUUM(2), TELEMETRY(3), LEGACY_POSITIONS(4); public final short id; + private Long value; LastKeys(int id) { this.id = (short) id; diff --git a/src/dev/heliosares/auxprotect/spigot/AuxProtectSpigot.java b/src/dev/heliosares/auxprotect/spigot/AuxProtectSpigot.java index 097cb67..b7b353d 100644 --- a/src/dev/heliosares/auxprotect/spigot/AuxProtectSpigot.java +++ b/src/dev/heliosares/auxprotect/spigot/AuxProtectSpigot.java @@ -10,10 +10,7 @@ import dev.heliosares.auxprotect.database.*; import dev.heliosares.auxprotect.exceptions.BusyException; import dev.heliosares.auxprotect.spigot.listeners.*; import dev.heliosares.auxprotect.towny.TownyListener; -import dev.heliosares.auxprotect.utils.Pane; -import dev.heliosares.auxprotect.utils.StackUtil; -import dev.heliosares.auxprotect.utils.Telemetry; -import dev.heliosares.auxprotect.utils.UpdateChecker; +import dev.heliosares.auxprotect.utils.*; import net.milkbowl.vault.economy.Economy; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -224,6 +221,7 @@ public class AuxProtectSpigot extends JavaPlugin implements IAuxProtect { 3); delay = (1000 * 60 * 60 - (System.currentTimeMillis() - lastloaded)) / 50; } + getServer().getScheduler().runTaskLater(AuxProtectSpigot.this, () -> Telemetry.init(AuxProtectSpigot.this, 14232), delay); /* @@ -626,6 +624,7 @@ public class AuxProtectSpigot extends JavaPlugin implements IAuxProtect { sqlManager = null; } Pane.shutdown(); + PlaybackSolver.shutdown(); info("Done disabling."); } diff --git a/src/dev/heliosares/auxprotect/utils/FakePlayer.java b/src/dev/heliosares/auxprotect/utils/FakePlayer.java index 7d0e787..1611e28 100644 --- a/src/dev/heliosares/auxprotect/utils/FakePlayer.java +++ b/src/dev/heliosares/auxprotect/utils/FakePlayer.java @@ -4,8 +4,11 @@ import com.comphenix.protocol.PacketType; import com.comphenix.protocol.ProtocolManager; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.wrappers.*; +import com.google.common.collect.Lists; import org.bukkit.Location; +import org.bukkit.Material; import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; @@ -18,6 +21,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.Collections; +import java.util.List; import java.util.UUID; public class FakePlayer { @@ -26,10 +30,9 @@ public class FakePlayer { private final String name; private final ProtocolManager protocol; private final Player audience; - private Location loc; - private long lastMoved; + private PosEncoder.Posture currentPosture = PosEncoder.Posture.STANDING; public FakePlayer(String name, ProtocolManager protocol, Player audience) { this.uuid = generateNPCUUID(); @@ -62,7 +65,6 @@ public class FakePlayer { public void spawn(Location loc_, @Nullable Skin skin) { this.loc = loc_; - // Sends player info, creates the player PacketContainer packet = new PacketContainer(PacketType.Play.Server.PLAYER_INFO); @@ -81,7 +83,7 @@ public class FakePlayer { // Set initial location packet = new PacketContainer(PacketType.Play.Server.NAMED_ENTITY_SPAWN); - packet.getIntegers().write(0, id); + setIdInPacket(packet); packet.getUUIDs().write(0, uuid); packet.getDoubles().write(0, loc.getX()); packet.getDoubles().write(1, loc.getY()); @@ -91,23 +93,23 @@ public class FakePlayer { protocol.sendServerPacket(audience, packet); } - public void setLocation(Location loc) { - + public void setLocation(Location loc, boolean onGround) { // Move entity PacketContainer packet = new PacketContainer(PacketType.Play.Server.REL_ENTITY_MOVE_LOOK); - packet.getIntegers().write(0, id); + setIdInPacket(packet); packet.getShorts().write(0, (short) ((loc.getX() - this.loc.getX()) * 4096)); packet.getShorts().write(1, (short) ((loc.getY() - this.loc.getY()) * 4096)); packet.getShorts().write(2, (short) ((loc.getZ() - this.loc.getZ()) * 4096)); packet.getBytes().write(0, (byte) (loc.getYaw() * 256f / 360f)); packet.getBytes().write(1, (byte) (loc.getPitch() * 256f / 360f)); + packet.getBooleans().write(0, onGround); protocol.sendServerPacket(audience, packet); // Update head packet = new PacketContainer(PacketType.Play.Server.ENTITY_HEAD_ROTATION); - packet.getIntegers().write(0, id); + setIdInPacket(packet); packet.getBytes().write(0, (byte) (loc.getYaw() * 256f / 360f)); protocol.sendServerPacket(audience, packet); @@ -115,8 +117,51 @@ public class FakePlayer { this.loc = loc; } - public void remove() { + public void setPosture(PosEncoder.Posture posture) { + if (currentPosture == posture) return; + PacketContainer packet = new PacketContainer(PacketType.Play.Server.ENTITY_METADATA); + setIdInPacket(packet); + + final List wrappedDataValueList = Lists.newArrayList(); + wrappedDataValueList.add(new WrappedDataValue(0, WrappedDataWatcher.Registry.get(Byte.class), (byte) switch (posture) { + case STANDING, SITTING, SLEEPING -> 0; + case SNEAKING -> 0x02; + case SWIMMING, CRAWLING -> 0x10; + case GLIDING -> 0x80; + })); + wrappedDataValueList.add(new WrappedDataValue(6, WrappedDataWatcher.Registry.get(EnumWrappers.getEntityPoseClass()), switch (posture) { + case STANDING -> EnumWrappers.EntityPose.STANDING; + case SNEAKING -> EnumWrappers.EntityPose.CROUCHING; + case SWIMMING, CRAWLING -> EnumWrappers.EntityPose.SWIMMING; + case GLIDING -> EnumWrappers.EntityPose.FALL_FLYING; + case SITTING -> EnumWrappers.EntityPose.SITTING; + case SLEEPING -> EnumWrappers.EntityPose.SLEEPING; + })); + packet.getDataValueCollectionModifier().write(0, wrappedDataValueList); + + protocol.sendServerPacket(audience, packet); + + if (posture == PosEncoder.Posture.GLIDING) { + setEquipment(EnumWrappers.ItemSlot.CHEST, new ItemStack(Material.ELYTRA)); + } else if (currentPosture == PosEncoder.Posture.GLIDING) { + setEquipment(EnumWrappers.ItemSlot.CHEST, new ItemStack(Material.AIR)); + } + + currentPosture = posture; + } + + public void setEquipment(EnumWrappers.ItemSlot slot, ItemStack item) { + PacketContainer packet = new PacketContainer(PacketType.Play.Server.ENTITY_EQUIPMENT); + + setIdInPacket(packet); + + packet.getSlotStackPairLists().write(0, List.of(new Pair<>(slot,item))); + + protocol.sendServerPacket(audience, packet); + } + + public void remove() { // Removes player info PacketContainer packet = new PacketContainer(PacketType.Play.Server.PLAYER_INFO_REMOVE); @@ -136,6 +181,10 @@ public class FakePlayer { protocol.sendServerPacket(audience, packet); } + private void setIdInPacket(PacketContainer packet) { + packet.getIntegers().write(0, id); + } + public long getLastMoved() { return lastMoved; } diff --git a/src/dev/heliosares/auxprotect/utils/PlaybackSolver.java b/src/dev/heliosares/auxprotect/utils/PlaybackSolver.java index 861a3f3..0adb699 100644 --- a/src/dev/heliosares/auxprotect/utils/PlaybackSolver.java +++ b/src/dev/heliosares/auxprotect/utils/PlaybackSolver.java @@ -8,6 +8,7 @@ import dev.heliosares.auxprotect.core.Language; import dev.heliosares.auxprotect.core.PlatformType; import dev.heliosares.auxprotect.database.DbEntry; import dev.heliosares.auxprotect.database.DbEntryBukkit; +import dev.heliosares.auxprotect.database.SQLManager; import dev.heliosares.auxprotect.exceptions.LookupException; import dev.heliosares.auxprotect.spigot.AuxProtectSpigot; import net.md_5.bungee.api.ChatMessageType; @@ -17,6 +18,7 @@ import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; import org.json.simple.parser.ParseException; +import javax.annotation.Nullable; import java.io.IOException; import java.sql.SQLException; import java.util.*; @@ -88,45 +90,10 @@ public class PlaybackSolver extends BukkitRunnable { runTaskTimer((AuxProtectSpigot) plugin, 1, 1); } - public static List getLocations(IAuxProtect plugin, List entries, long startTime) throws SQLException { - if (plugin.getPlatform() != PlatformType.SPIGOT) throw new UnsupportedOperationException(); - Map lastEntries = new HashMap<>(); - entries.sort(Comparator.comparingLong(DbEntry::getTime)); - List points = new ArrayList<>(); - for (DbEntry entry : entries) { - DbEntry lastEntry = lastEntries.get(entry.getUser()); - if (lastEntry != null && entry.getBlob() != null) { - List decoded = PosEncoder.decode(entry.getBlob()); - Location lastLoc = DbEntryBukkit.getLocation(lastEntry); - final long incrementBy = (entry.getTime() - lastEntry.getTime()) / (decoded.size() + 1); - for (int i = 0; i < decoded.size(); i++) { - PosEncoder.DecodedPositionIncrement inc = decoded.get(i); - long time = lastEntry.getTime() + (i + 1) * incrementBy; - if (time < startTime) continue; - org.bukkit.util.Vector add = new org.bukkit.util.Vector(inc.x(), inc.y(), inc.z()); - Location incLoc = lastLoc.clone().add(add); - if (inc.hasPitch()) incLoc.setPitch(inc.pitch()); - if (inc.hasYaw()) incLoc.setYaw(inc.yaw()); - lastLoc = incLoc.clone(); - PosPoint point = new PosPoint(time, UUID.fromString(entry.getUserUUID().substring(1)), entry.getUser(), entry.getUid(), incLoc.clone(), true); - points.add(point); - } - } - Location entryLoc = DbEntryBukkit.getLocation(entry); - entryLoc.setYaw(entry.getYaw()); - entryLoc.setPitch(entry.getPitch()); - PosPoint point = new PosPoint(entry.getTime(), UUID.fromString(entry.getUserUUID().substring(1)), entry.getUser(), entry.getUid(), entryLoc, false); - points.add(point); - lastEntries.put(entry.getUser(), entry); - } - points.sort(Comparator.comparingLong(a -> a.time)); - return points; - } - - public static void close(UUID uuid) { + public static void shutdown() { synchronized (instances) { - PlaybackSolver instance = instances.get(uuid); - if (instance != null) instance.close(); + instances.values().forEach(PlaybackSolver::close); + instances.clear(); } } @@ -136,57 +103,112 @@ public class PlaybackSolver extends BukkitRunnable { } } + public static void close(UUID uuid) { + synchronized (instances) { + PlaybackSolver instance = instances.get(uuid); + if (instance != null) instance.close(); + } + } + + + public static List getLocations(IAuxProtect plugin, List entries, long startTime) throws SQLException { + if (plugin.getPlatform() != PlatformType.SPIGOT) throw new UnsupportedOperationException(); + Map lastEntries = new HashMap<>(); + entries.sort(Comparator.comparingLong(DbEntry::getTime)); + List points = new ArrayList<>(); + for (DbEntry entry : entries) { + DbEntry lastEntry = lastEntries.get(entry.getUser()); + if (lastEntry != null && entry.getBlob() != null) { + List decoded; + if (entry.getTime() < plugin.getSqlManager().getLast(SQLManager.LastKeys.LEGACY_POSITIONS)) { + decoded = PosEncoder.decodeLegacy(entry.getBlob()); + } else { + decoded = PosEncoder.decode(entry.getBlob()); + } + Location lastLoc = DbEntryBukkit.getLocation(lastEntry); + final long incrementBy = (entry.getTime() - lastEntry.getTime()) / (decoded.size() + 1); + for (int i = 0; i < decoded.size(); i++) { + PosEncoder.PositionIncrement inc = decoded.get(i); + long time = lastEntry.getTime() + (i + 1) * incrementBy; + if (time < startTime) continue; + org.bukkit.util.Vector add = new org.bukkit.util.Vector(inc.x(), inc.y(), inc.z()); + Location incLoc = lastLoc.clone().add(add); + if (inc.hasLook()) { + incLoc.setPitch(inc.pitch()); + incLoc.setYaw(inc.yaw()); + } + lastLoc = incLoc.clone(); + PosPoint point = new PosPoint(time, UUID.fromString(entry.getUserUUID().substring(1)), entry.getUser(), entry.getUid(), incLoc.clone(), true, inc.posture()); + points.add(point); + } + } + Location entryLoc = DbEntryBukkit.getLocation(entry); + entryLoc.setYaw(entry.getYaw()); + entryLoc.setPitch(entry.getPitch()); + PosPoint point = new PosPoint(entry.getTime(), UUID.fromString(entry.getUserUUID().substring(1)), entry.getUser(), entry.getUid(), entryLoc, false, null); + points.add(point); + lastEntries.put(entry.getUser(), entry); + } + points.sort(Comparator.comparingLong(a -> a.time)); + return points; + } + @Override public void run() { - if (closed || isCancelled() || !audience.isOnline()) { - close(); + synchronized (this) { + if (closed || isCancelled() || !audience.isOnline()) { + close(); + cancel(); + return; + } + final long timeNow = System.currentTimeMillis() - realReferenceTime + startTime; + + audience.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(TimeUtil.format(timeNow, TimeUtil.entryTimeFormat) + " §7- " + TimeUtil.millisToString(System.currentTimeMillis() - timeNow) + " ago")); + + + for (Iterator it = points.iterator(); it.hasNext(); ) { + PosPoint point = it.next(); + if (timeNow > point.time()) { + FakePlayer actor = actors.get(point.name()); + Location loc = point.location.clone(); + assert loc.getWorld() != null; + + if (actor == null) { + String name = "~" + point.name; + if (name.length() > 16) name = name.substring(0, 16); + actor = new FakePlayer(name, protocol, audience); + actor.spawn(point.location(), skins.get(point.uuid)); + } + actors.put(point.name(), actor); + actor.setLocation(loc, false); + if (point.posture != null) actor.setPosture(point.posture); + + it.remove(); + } else break; + } + Iterator it = actors.values().iterator(); + while (it.hasNext()) { + FakePlayer pl = it.next(); + if (System.currentTimeMillis() - pl.getLastMoved() > 1000) { + pl.remove(); + it.remove(); + } + } + if (points.isEmpty()) close(); + } + } + + public void close() { + synchronized (this) { + if (closed) return; + closed = true; if (audience.isOnline()) { audience.sendMessage(Language.translate(Language.L.COMMAND__LOOKUP__PLAYBACK__STOPPED)); audience.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(Language.translate(Language.L.COMMAND__LOOKUP__PLAYBACK__STOPPED))); actors.values().forEach(FakePlayer::remove); - } - actors.clear(); - cancel(); - return; - } - final long timeNow = System.currentTimeMillis() - realReferenceTime + startTime; - - audience.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(TimeUtil.format(timeNow, TimeUtil.entryTimeFormat) + " §7- " + TimeUtil.millisToString(System.currentTimeMillis() - timeNow) + " ago")); - - - for (Iterator it = points.iterator(); it.hasNext(); ) { - PosPoint point = it.next(); - if (timeNow > point.time()) { - FakePlayer actor = actors.get(point.name()); - Location loc = point.location.clone(); - assert loc.getWorld() != null; - - if (actor == null) { - String name = "~" + point.name; - if (name.length() > 16) name = name.substring(0, 16); - actor = new FakePlayer(name, protocol, audience); - actor.spawn(point.location(), skins.get(point.uuid)); - } - actors.put(point.name(), actor); - actor.setLocation(loc); - - it.remove(); - } else break; - } - Iterator it = actors.values().iterator(); - while (it.hasNext()) { - FakePlayer pl = it.next(); - if (System.currentTimeMillis() - pl.getLastMoved() > 1000) { - pl.remove(); - it.remove(); + actors.clear(); } } - if (points.isEmpty()) close(); - } - - public void close() { - if (closed) return; - closed = true; cleanup(); } @@ -194,6 +216,7 @@ public class PlaybackSolver extends BukkitRunnable { return closed; } - public record PosPoint(long time, UUID uuid, String name, int uid, Location location, boolean inc) { + public record PosPoint(long time, UUID uuid, String name, int uid, Location location, boolean inc, + @Nullable PosEncoder.Posture posture) { } } diff --git a/src/dev/heliosares/auxprotect/utils/PosEncoder.java b/src/dev/heliosares/auxprotect/utils/PosEncoder.java index 949addd..b5837e3 100644 --- a/src/dev/heliosares/auxprotect/utils/PosEncoder.java +++ b/src/dev/heliosares/auxprotect/utils/PosEncoder.java @@ -1,7 +1,9 @@ package dev.heliosares.auxprotect.utils; import org.bukkit.Location; +import org.bukkit.entity.Player; +import javax.annotation.Nullable; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; @@ -14,39 +16,57 @@ public class PosEncoder { * * @return The incremental byte array representing:
Bit mask indicating the presence/length of values below
0-2 bytes representing dX
0-2 bytes representing dY
0-2 bytes representing dZ
1 byte representing pitch
1 byte representing yaw */ - public static byte[] encode(Location from, Location to) { - IncrementalByte diffX = simplify(to.getX() - from.getX()); - IncrementalByte diffY = simplify(to.getY() - from.getY()); - IncrementalByte diffZ = simplify(to.getZ() - from.getZ()); - byte pitch = (byte) to.getPitch(); - boolean doPitch = to.getPitch() != from.getPitch(); - byte yaw = (byte) ((to.getYaw() / 180.0) * 127); - boolean doYaw = to.getYaw() != from.getYaw(); + public static byte[] encode(Location from, Location to, Posture posture, @Nullable Posture lastPosture) { + return encode( + to.getX() - from.getX(), + to.getY() - from.getY(), + to.getZ() - from.getZ(), + to.getPitch() != from.getPitch() || to.getYaw() != from.getYaw(), to.getPitch(), to.getYaw(), + posture.equals(lastPosture) ? null : posture); + } + + private static byte[] encode(double diffX_, double diffY_, double diffZ_, boolean doLook, float pitch_, float yaw_, @Nullable Posture posture) { + IncrementalByte diffX = simplify(diffX_); + IncrementalByte diffY = simplify(diffY_); + IncrementalByte diffZ = simplify(diffZ_); + byte pitch = (byte) pitch_; + byte yaw = (byte) ((yaw_ / 180.0) * 127); // bitMask indicates the presence of various values // 0-1 represent number of bytes (0-2) representing X. Value of 0b11 indicates fine // 2-3 represent number of bytes (0-2) representing Y. Value of 0b11 indicates fine // 4-5 represent number of bytes (0-2) representing Z. Value of 0b11 indicates fine - // 6 represents whether there is pitch - // 7 represents whether there is yaw + // 6 represents whether there is look (pitch/yaw) + // 7 represents whether there is posture data (sneak, gliding, etc.) byte bitMask = 0; + int len = 1 + diffX.array.length + diffY.array.length + diffZ.array.length; bitMask |= diffX.getBytesNeeded(); bitMask |= diffY.getBytesNeeded() << 2; bitMask |= diffZ.getBytesNeeded() << 4; - if (doPitch) bitMask |= 1 << 6; - if (doYaw) bitMask -= 128; - int len = 1 + diffX.array.length + diffY.array.length + diffZ.array.length; - if (doPitch) len++; - if (doYaw) len++; + if (doLook) { + bitMask = setBit(bitMask, 6, true); + len += 2; + } + if (posture != null) { + bitMask = setBit(bitMask, 7, true); + len++; + } + + ByteBuffer bb = ByteBuffer.allocate(len); bb.order(ByteOrder.LITTLE_ENDIAN); bb.put(bitMask); bb.put(diffX.array); bb.put(diffY.array); bb.put(diffZ.array); - if (doPitch) bb.put(pitch); - if (doYaw) bb.put(yaw); + if (doLook) { + bb.put(pitch); + bb.put(yaw); + } + if (posture != null) { + bb.put(posture.getID()); + } return bb.array(); } @@ -57,10 +77,10 @@ public class PosEncoder { * @param bytes The incremental byte array * @return A list of records representing the presence and value of each component of position. */ - public static List decode(byte[] bytes) { - List out = new ArrayList<>(); + public static List decode(byte[] bytes) { + List out = new ArrayList<>(); for (int i = 0, safety = 0; i < bytes.length && safety < bytes.length; safety++) { - DecodedPositionIncrement decoded = decodeSingle(bytes, i); + PositionIncrement decoded = decodeSingle(bytes, i); i += decoded.bytes; out.add(decoded); } @@ -74,35 +94,48 @@ public class PosEncoder { * @param offset Where to start looking in the data * @return A record representing the presence and value of each component of position */ - public static DecodedPositionIncrement decodeSingle(byte[] bytes, int offset) { + private static PositionIncrement decodeSingle(byte[] bytes, int offset) { double[] out = new double[5]; byte bitMask = bytes[offset]; - boolean yaw = bitMask < 0; - if (yaw) bitMask += 128; - + int index = 1; int xLen = bitMask & 0b11; - int yLen = (bitMask >> 2) & 0b11; - int zLen = (bitMask >> 4) & 0b11; - - if (xLen > 0) out[0] = toDouble(bytes, offset + 1, xLen); + if (xLen > 0) out[0] = toDouble(bytes, offset + index, xLen); if (xLen == 3) xLen = 1; - if (yLen > 0) out[1] = toDouble(bytes, offset + 1 + xLen, yLen); + index += xLen; + + int yLen = (bitMask >> 2) & 0b11; + if (yLen > 0) out[1] = toDouble(bytes, offset + index, yLen); if (yLen == 3) yLen = 1; - if (zLen > 0) out[2] = toDouble(bytes, offset + 1 + xLen + yLen, zLen); + index += yLen; + + int zLen = (bitMask >> 4) & 0b11; + if (zLen > 0) out[2] = toDouble(bytes, offset + index, zLen); if (zLen == 3) zLen = 1; + index += zLen; - boolean pitch = (bitMask >> 6 & 1) == 1; - if (pitch) out[3] = bytes[offset + 1 + xLen + yLen + zLen]; - if (yaw) out[4] = (double) bytes[offset + 1 + xLen + yLen + zLen + (pitch ? 1 : 0)] / 127.0 * 180; - return new DecodedPositionIncrement( + boolean look = getBit(bitMask, 6); + boolean hasPosture = getBit(bitMask, 7); + + if (look) { + out[3] = bytes[offset + index++]; + out[4] = (double) bytes[offset + index++] / 127.0 * 180; + } + + Posture posture = null; + if (hasPosture) { + posture = Posture.fromID(bytes[offset + index++]); + } + + + return new PositionIncrement( xLen > 0, out[0], yLen > 0, out[1], zLen > 0, out[2], - pitch, (float) out[3], - yaw, (float) out[4], - 1 + xLen + yLen + zLen + (yaw ? 1 : 0) + (pitch ? 1 : 0) + look, (float) out[3], (float) out[4], + hasPosture, posture, + index ); } @@ -115,6 +148,7 @@ public class PosEncoder { * @return The double retrieved from the byte array */ private static double toDouble(byte[] bytes, int index, int bitMask) { + if (bytes.length == 0) return 0; double sig; if (bitMask == 3) { bitMask = 1; @@ -153,38 +187,6 @@ public class PosEncoder { return new IncrementalByte(new byte[]{(byte) (s >> 8), lower}, false); } - /** - * Stores the fraction of the x/y/z values into a single byte. The structure is as follows - * 0b X X X Y Y Z Z Z - * X and Z are stored in 8ths, Y is stored in 4ths. - */ - public static byte getFractionalByte(double dx, double dy, double dz) { - dx %= 1; - dy %= 1; - dz %= 1; - if (dx < 0) dx++; - if (dy < 0) dy++; - if (dz < 0) dz++; - int x = (int) Math.min(Math.round(dx * 8), 7) << 5; - int y = (int) Math.min(Math.round(dy * 4), 3) << 3; - int z = (int) Math.min(Math.round(dz * 8), 7); - - return (byte) (x | y | z); - } - - /** - * Retrieves the fractional values from the increment byte generated in {@link PosEncoder#getFractionalByte(double, double, double)} - * - * @return An array of doubles of length 3, containing the x, y, and z fractions respectively. - */ - public static double[] byteToFractions(byte b) { - int x = (b >> 5) & 0b111; - int y = (b >> 3) & 0b11; - int z = b & 0b111; - - return new double[]{x / 8D, y / 4D, z / 8D}; - } - /** * @param array The data * @param fine Whether the value is stored in hundredths or tenths. true indicates hundredths. @@ -195,11 +197,88 @@ public class PosEncoder { } } - public record DecodedPositionIncrement(boolean hasX, double x, boolean hasY, double y, boolean hasZ, double z, - boolean hasPitch, float pitch, boolean hasYaw, float yaw, int bytes) { - @Override - public String toString() { - return "X=" + x + " Y=" + y + " Z=" + z + " Pitch=" + pitch + " Yaw=" + yaw; + public enum Posture { + STANDING(0), SNEAKING(1), SWIMMING(2), GLIDING(3), SITTING(4), CRAWLING(5), SLEEPING(6); + private final byte id; + + Posture(int id) { + this.id = (byte) id; + } + + public byte getID() { + return id; + } + + public static Posture fromPlayer(Player player) { + if (player.isSwimming()) return SWIMMING; + if (player.isGliding()) return GLIDING; + if (player.isInsideVehicle()) return SITTING; + if (player.isSleeping()) return SLEEPING; + if (player.isSneaking()) return SNEAKING; + if (player.getBoundingBox().getHeight() < 1) return CRAWLING; + return STANDING; + } + + public static Posture fromID(byte id) { + for (Posture posture : values()) if (posture.id == id) return posture; + throw new IllegalArgumentException("Unknown posture: " + id); } } + + public record PositionIncrement(boolean hasX, double x, boolean hasY, double y, boolean hasZ, double z, + boolean hasLook, float pitch, float yaw, boolean hasPosture, Posture posture, + int bytes) { + @Override + public String toString() { + return "X=" + (hasX ? x : "none") + " Y=" + (hasY ? y : "none") + " Z=" + (hasZ ? z : "none") + " Pitch=" + pitch + " Yaw=" + yaw + " Posture=" + posture; + } + } + + public static byte setBit(byte b, int index, boolean value) { + if (index > 7 || index < 0) throw new IndexOutOfBoundsException(index + " is not a valid byte index."); + byte val = (byte) (1 << index); + if (value) b |= val; + else b &= ~val; + return b; + } + + public static boolean getBit(byte b, int index) { + return ((b >> index) & 1) == 1; + } + + public static List decodeLegacy(byte[] bytes) { + List out = new ArrayList<>(); + for (int offset = 0, safety = 0; offset < bytes.length && safety < bytes.length; safety++) { + double[] doubles = new double[5]; + byte hdr = bytes[offset]; + + boolean yaw = hdr < 0; + if (yaw) hdr += 128; + + int xlen = hdr & 0b11; + int ylen = (hdr >> 2) & 0b11; + int zlen = (hdr >> 4) & 0b11; + + if (xlen > 0) doubles[0] = toDouble(bytes, offset + 1, xlen); + if (xlen == 3) xlen = 1; + if (ylen > 0) doubles[1] = toDouble(bytes, offset + 1 + xlen, ylen); + if (ylen == 3) ylen = 1; + if (zlen > 0) doubles[2] = toDouble(bytes, offset + 1 + xlen + ylen, zlen); + if (zlen == 3) zlen = 1; + + boolean pitch = (hdr >> 6 & 1) == 1; + if (pitch) doubles[3] = bytes[offset + 1 + xlen + ylen + zlen]; + if (yaw) doubles[4] = (double) bytes[offset + 1 + xlen + ylen + zlen + (pitch ? 1 : 0)] / 127.0 * 180; + + PosEncoder.PositionIncrement decod = new PosEncoder.PositionIncrement( + xlen > 0, doubles[0], + ylen > 0, doubles[1], + zlen > 0, doubles[2], pitch || yaw, (float) doubles[3], (float) doubles[4], + false, null, + 1 + xlen + ylen + zlen + (yaw ? 1 : 0) + (pitch ? 1 : 0)); + offset += decod.bytes(); + out.add(decod); + } + return out; + } }