Add posturing to position playback

This commit is contained in:
Heliosares
2023-03-04 11:37:48 -05:00
parent 31f28e6814
commit c0eb2f0044
10 changed files with 377 additions and 183 deletions

View File

@@ -34,6 +34,7 @@ public class APPlayer {
// hotbar, main, armor, offhand, echest
private List<ItemStack> 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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<PosPoint> getLocations(IAuxProtect plugin, List<DbEntry> entries, long startTime) throws SQLException {
if (plugin.getPlatform() != PlatformType.SPIGOT) throw new UnsupportedOperationException();
Map<String, DbEntry> lastEntries = new HashMap<>();
entries.sort(Comparator.comparingLong(DbEntry::getTime));
List<PosPoint> points = new ArrayList<>();
for (DbEntry entry : entries) {
DbEntry lastEntry = lastEntries.get(entry.getUser());
if (lastEntry != null && entry.getBlob() != null) {
List<PosEncoder.DecodedPositionIncrement> 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<PosPoint> getLocations(IAuxProtect plugin, List<DbEntry> entries, long startTime) throws SQLException {
if (plugin.getPlatform() != PlatformType.SPIGOT) throw new UnsupportedOperationException();
Map<String, DbEntry> lastEntries = new HashMap<>();
entries.sort(Comparator.comparingLong(DbEntry::getTime));
List<PosPoint> points = new ArrayList<>();
for (DbEntry entry : entries) {
DbEntry lastEntry = lastEntries.get(entry.getUser());
if (lastEntry != null && entry.getBlob() != null) {
List<PosEncoder.PositionIncrement> 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<PosPoint> 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<FakePlayer> 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<PosPoint> 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<FakePlayer> 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) {
}
}

View File

@@ -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:<br>Bit mask indicating the presence/length of values below<br>0-2 bytes representing dX<br>0-2 bytes representing dY<br>0-2 bytes representing dZ<br>1 byte representing pitch<br>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<DecodedPositionIncrement> decode(byte[] bytes) {
List<DecodedPositionIncrement> out = new ArrayList<>();
public static List<PositionIncrement> decode(byte[] bytes) {
List<PositionIncrement> 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<PosEncoder.PositionIncrement> decodeLegacy(byte[] bytes) {
List<PosEncoder.PositionIncrement> 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;
}
}