From 0b193c04e74cf46f41688fbeff38e2e5b0151cc7 Mon Sep 17 00:00:00 2001 From: Tim203 Date: Wed, 25 Sep 2019 23:52:28 +0200 Subject: [PATCH] Players spawn, player skins, scoreboard, bossbar and updated protocol lib --- connector/pom.xml | 2 +- .../org/geysermc/connector/entity/Entity.java | 32 +- .../connector/entity/PlayerEntity.java | 21 +- .../network/UpstreamPacketHandler.java | 31 +- .../network/session/GeyserSession.java | 2 +- .../network/session/cache/EntityCache.java | 21 +- .../session/cache/ScoreboardCache.java | 24 +- .../network/translators/Registry.java | 8 +- .../network/translators/TranslatorsInit.java | 10 +- .../bedrock/BedrockMovePlayerTranslator.java | 12 +- .../java/JavaBossBarTranslator.java | 77 ++++ .../entity/JavaEntityDestroyTranslator.java | 5 +- .../entity/JavaEntityHeadLookTranslator.java | 4 +- .../entity/JavaEntityMetadataTranslator.java | 3 +- .../JavaEntityPositionRotationTranslator.java | 3 +- .../entity/JavaEntityPositionTranslator.java | 3 +- .../JavaEntityPropertiesTranslator.java | 6 +- .../entity/JavaEntityRotationTranslator.java | 3 +- .../entity/JavaEntityVelocityTranslator.java | 3 +- .../spawn/JavaPlayerListEntryTranslator.java | 24 +- .../spawn/JavaSpawnPlayerTranslator.java | 61 ++- .../JavaDisplayScoreboardTranslator.java | 12 +- .../JavaScoreboardObjectiveTranslator.java | 18 +- .../java/scoreboard/JavaTeamTranslator.java | 39 ++ .../scoreboard/JavaUpdateScoreTranslator.java | 20 +- .../connector/scoreboard/Objective.java | 126 ++++++ .../geysermc/connector/scoreboard/Score.java | 47 +-- .../connector/scoreboard/Scoreboard.java | 219 ++++++---- .../scoreboard/ScoreboardObjective.java | 130 ------ .../geysermc/connector/scoreboard/Team.java | 64 +++ .../connector/scoreboard/UpdateType.java | 15 + .../connector/utils/ProvidedSkinData.java | 39 -- .../connector/utils/SkinProvider.java | 190 +++++++++ .../resources/bedrock/skin/model_steve.json | 383 ------------------ 34 files changed, 871 insertions(+), 786 deletions(-) create mode 100644 connector/src/main/java/org/geysermc/connector/network/translators/java/JavaBossBarTranslator.java create mode 100644 connector/src/main/java/org/geysermc/connector/scoreboard/Objective.java delete mode 100644 connector/src/main/java/org/geysermc/connector/scoreboard/ScoreboardObjective.java create mode 100644 connector/src/main/java/org/geysermc/connector/scoreboard/Team.java create mode 100644 connector/src/main/java/org/geysermc/connector/scoreboard/UpdateType.java delete mode 100644 connector/src/main/java/org/geysermc/connector/utils/ProvidedSkinData.java create mode 100644 connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java delete mode 100644 connector/src/main/resources/bedrock/skin/model_steve.json diff --git a/connector/pom.xml b/connector/pom.xml index cea7aebfc..e4b0b358f 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -74,7 +74,7 @@ com.nukkitx.protocol bedrock-v361 - 2.1.2 + 2.1.3 compile diff --git a/connector/src/main/java/org/geysermc/connector/entity/Entity.java b/connector/src/main/java/org/geysermc/connector/entity/Entity.java index 2b2ad0544..735d5ac65 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/Entity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/Entity.java @@ -49,7 +49,6 @@ import java.util.*; @Getter @Setter public class Entity { - protected long entityId; protected long geyserId; @@ -111,24 +110,21 @@ public class Entity { } public void moveRelative(double relX, double relY, double relZ, float pitch, float yaw) { - moveRelative(relX, relY, relZ, new Vector3f(pitch, yaw, 0)); + moveRelative(relX, relY, relZ, new Vector3f(pitch, yaw, yaw)); } public void moveRelative(double relX, double relY, double relZ, Vector3f rotation) { - if (relX == 0 && relY == 0 && relZ == 0 && position.getX() == 0 && position.getY() == 0) - return; - this.rotation = rotation; this.position = new Vector3f(position.getX() + relX, position.getY() + relY, position.getZ() + relZ); this.movePending = true; } public void moveAbsolute(Vector3f position, float pitch, float yaw) { - moveAbsolute(position, new Vector3f(pitch, yaw, 0)); + moveAbsolute(position, new Vector3f(pitch, yaw, yaw)); } public void moveAbsolute(Vector3f position, Vector3f rotation) { - this.position = position; + setPosition(position); this.rotation = rotation; this.movePending = true; } @@ -138,14 +134,13 @@ public class Entity { flags.setFlag(EntityFlag.HAS_GRAVITY, true); flags.setFlag(EntityFlag.HAS_COLLISION, true); flags.setFlag(EntityFlag.CAN_SHOW_NAME, true); - flags.setFlag(EntityFlag.NO_AI, false); + flags.setFlag(EntityFlag.CAN_CLIMB, true); EntityDataDictionary dictionary = new EntityDataDictionary(); - dictionary.put(EntityData.NAMETAG, ""); - dictionary.put(EntityData.ENTITY_AGE, 0); dictionary.put(EntityData.SCALE, 1f); dictionary.put(EntityData.MAX_AIR, (short) 400); dictionary.put(EntityData.AIR, (short) 0); + dictionary.put(EntityData.LEAD_HOLDER_EID, -1L); dictionary.put(EntityData.BOUNDING_BOX_HEIGHT, entityType.getHeight()); dictionary.put(EntityData.BOUNDING_BOX_WIDTH, entityType.getWidth()); dictionary.putFlags(flags); @@ -185,4 +180,21 @@ public class Entity { ServerEntityPropertiesPacket entityPropertiesPacket = new ServerEntityPropertiesPacket((int) entityId, attributes); session.getDownstream().getSession().send(entityPropertiesPacket); } + + public void setPosition(Vector3f position) { + if (is(PlayerEntity.class)) { + this.position = position.add(0, entityType.getOffset(), 0); + return; + } + this.position = position; + } + + @SuppressWarnings("unchecked") + public I as(Class entityClass) { + return entityClass.isInstance(this) ? (I) this : null; + } + + public boolean is(Class entityClass) { + return entityClass.isInstance(this); + } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java index ff3a2ed4d..4c65fdabe 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java @@ -39,10 +39,12 @@ import java.util.UUID; @Getter @Setter public class PlayerEntity extends Entity { + private GameProfile profile; private UUID uuid; private String username; + private long lastSkinUpdate = -1; - private ItemData hand; + private ItemData hand = ItemData.of(0, (short) 0, 0); private ItemData helmet; private ItemData chestplate; @@ -52,8 +54,10 @@ public class PlayerEntity extends Entity { public PlayerEntity(GameProfile gameProfile, long entityId, long geyserId, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, EntityType.PLAYER, position, motion, rotation); + profile = gameProfile; uuid = gameProfile.getId(); username = gameProfile.getName(); + if (geyserId == 1) valid = true; } // TODO: Break this into an EquippableEntity class @@ -72,25 +76,28 @@ public class PlayerEntity extends Entity { @Override public void spawnEntity(GeyserSession session) { + if (geyserId == 1) return; + AddPlayerPacket addPlayerPacket = new AddPlayerPacket(); - addPlayerPacket.setRuntimeEntityId(geyserId); - addPlayerPacket.setUniqueEntityId(geyserId); addPlayerPacket.setUuid(uuid); addPlayerPacket.setUsername(username); - addPlayerPacket.setPlatformChatId(""); + addPlayerPacket.setRuntimeEntityId(geyserId); + addPlayerPacket.setUniqueEntityId(geyserId); addPlayerPacket.setPosition(position); - addPlayerPacket.setMotion(motion); addPlayerPacket.setRotation(rotation); + addPlayerPacket.setMotion(motion); addPlayerPacket.setHand(hand); - addPlayerPacket.getMetadata().putAll(getMetadata()); addPlayerPacket.setPlayerFlags(0); addPlayerPacket.setCommandPermission(0); addPlayerPacket.setWorldFlags(0); addPlayerPacket.setPlayerPermission(0); addPlayerPacket.setCustomFlags(0); - addPlayerPacket.setDeviceId("WIN10"); + addPlayerPacket.setDeviceId(""); + addPlayerPacket.setPlatformChatId(""); + addPlayerPacket.getMetadata().putAll(getMetadata()); valid = true; session.getUpstream().sendPacket(addPlayerPacket); +// System.out.println("Spawned player "+uuid+" "+username+" "+geyserId); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java index 929ed9d5b..b11652430 100644 --- a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java @@ -40,8 +40,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { } private boolean translateAndDefault(BedrockPacket packet) { - Registry.BEDROCK.translate(packet.getClass(), packet, session); - return defaultHandler(packet); + return Registry.BEDROCK.translate(packet.getClass(), packet, session); } @Override @@ -66,7 +65,6 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { @Override public boolean handle(ResourcePackClientResponsePacket textPacket) { - connector.getLogger().debug("Handled " + textPacket.getClass().getSimpleName()); switch (textPacket.getStatus()) { case COMPLETED: session.connect(connector.getRemoteServer()); @@ -88,7 +86,6 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { @Override public boolean handle(ModalFormResponsePacket packet) { - connector.getLogger().debug("Handled packet: " + packet.getClass().getSimpleName()); return LoginEncryptionUtils.authenticateFromForm(session, connector, packet.getFormData()); } @@ -111,7 +108,6 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { @Override public boolean handle(MovePlayerPacket packet) { - connector.getLogger().debug("Handled packet: " + packet.getClass().getSimpleName()); if (!session.isLoggedIn() && !session.isLoggingIn()) { // TODO it is safer to key authentication on something that won't change (UUID, not username) if (!couldLoginUserByName(session.getAuthenticationData().getName())) { @@ -119,7 +115,8 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { } // else we were able to log the user in return true; - } else if (session.isLoggingIn()) { + } + if (session.isLoggingIn()) { session.sendMessage("Please wait until you are logged in..."); } @@ -127,27 +124,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { } @Override - public boolean handle(AnimatePacket packet) { - return translateAndDefault(packet); - } - - @Override - public boolean handle(CommandRequestPacket packet) { - return translateAndDefault(packet); - } - - @Override - public boolean handle(TextPacket packet) { - return translateAndDefault(packet); - } - - @Override - public boolean handle(MobEquipmentPacket packet) { - return translateAndDefault(packet); - } - - @Override - public boolean handle(PlayerActionPacket packet) { + boolean defaultHandler(BedrockPacket packet) { return translateAndDefault(packet); } } \ No newline at end of file diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index 0ad983b53..d84f61a71 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -103,7 +103,7 @@ public class GeyserSession implements Player { this.scoreboardCache = new ScoreboardCache(this); this.windowCache = new WindowCache(this); - this.playerEntity = new PlayerEntity(new GameProfile(UUID.randomUUID(), "unknown"), -1, 1, new Vector3f(0, 0, 0), new Vector3f(0, 0, 0), new Vector3f(0, 0, 0)); + this.playerEntity = new PlayerEntity(new GameProfile(UUID.randomUUID(), "unknown"), 1, 1, Vector3f.ZERO, Vector3f.ZERO, Vector3f.ZERO); this.inventory = new PlayerInventory(); this.javaPacketCache = new DataCache<>(); diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java index a619533d3..1d8c15d48 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java @@ -46,6 +46,7 @@ public class EntityCache { private Map entities = new HashMap<>(); private Map entityIdTranslations = new HashMap<>(); private Map playerEntities = new HashMap<>(); + private Map bossbars = new HashMap<>(); @Getter private AtomicLong nextEntityId = new AtomicLong(2L); @@ -62,13 +63,13 @@ public class EntityCache { } public void removeEntity(Entity entity) { - if (entity == null) return; + if (entity == null || !entity.isValid()) return; Long geyserId = entityIdTranslations.remove(entity.getEntityId()); if (geyserId != null) { entities.remove(geyserId); - if (entity instanceof PlayerEntity) { - playerEntities.remove(((PlayerEntity) entity).getUuid()); + if (entity.is(PlayerEntity.class)) { + playerEntities.remove(entity.as(PlayerEntity.class).getUuid()); } } entity.despawnEntity(session); @@ -93,4 +94,18 @@ public class EntityCache { public void removePlayerEntity(UUID uuid) { playerEntities.remove(uuid); } + + public long addBossBar(UUID uuid) { + long entityId = getNextEntityId().incrementAndGet(); + bossbars.put(uuid, entityId); + return entityId; + } + + public long getBossBar(UUID uuid) { + return bossbars.containsKey(uuid) ? bossbars.get(uuid) : -1; + } + + public long removeBossBar(UUID uuid) { + return bossbars.remove(uuid); + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/ScoreboardCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/ScoreboardCache.java index 147a5355d..c24a0a9d5 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/ScoreboardCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/ScoreboardCache.java @@ -25,27 +25,31 @@ package org.geysermc.connector.network.session.cache; -import com.nukkitx.protocol.bedrock.packet.RemoveObjectivePacket; import lombok.Getter; -import lombok.Setter; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.scoreboard.Objective; import org.geysermc.connector.scoreboard.Scoreboard; -public class ScoreboardCache { +import java.util.Collection; +@Getter +public class ScoreboardCache { private GeyserSession session; + private Scoreboard scoreboard; public ScoreboardCache(GeyserSession session) { this.session = session; + this.scoreboard = new Scoreboard(session); } - @Getter - @Setter - private Scoreboard scoreboard; - public void removeScoreboard() { - RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket(); - removeObjectivePacket.setObjectiveId(scoreboard.getObjective().getObjectiveName()); - session.getUpstream().sendPacket(removeObjectivePacket); + if (scoreboard != null) { + Collection objectives = scoreboard.getObjectives().values(); + scoreboard = new Scoreboard(session); + + for (Objective objective : objectives) { + scoreboard.despawnObjective(objective); + } + } } } \ No newline at end of file diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/Registry.java b/connector/src/main/java/org/geysermc/connector/network/translators/Registry.java index a4efde575..e13ce9748 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/Registry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/Registry.java @@ -34,7 +34,6 @@ import java.util.HashMap; import java.util.Map; public class Registry { - private final Map, PacketTranslator> MAP = new HashMap<>(); public static final Registry JAVA = new Registry<>(); @@ -48,14 +47,15 @@ public class Registry { BEDROCK.MAP.put(clazz, translator); } - public

void translate(Class clazz, P packet, GeyserSession session) { + public

boolean translate(Class clazz, P packet, GeyserSession session) { try { if (MAP.containsKey(clazz)) { ((PacketTranslator

) MAP.get(clazz)).translate(packet, session); + return true; } } catch (NullPointerException ex) { - GeyserLogger.DEFAULT.debug("Could not translate packet " + packet.getClass().getSimpleName()); - ex.printStackTrace(); + GeyserLogger.DEFAULT.error("Could not translate packet " + packet.getClass().getSimpleName(), ex); } + return false; } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java b/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java index 88e142cd0..b48483a41 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java @@ -34,6 +34,7 @@ import com.github.steveice10.mc.protocol.packet.ingame.server.entity.player.Serv import com.github.steveice10.mc.protocol.packet.ingame.server.entity.spawn.*; import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerDisplayScoreboardPacket; import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerScoreboardObjectivePacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerTeamPacket; import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerUpdateScorePacket; import com.github.steveice10.mc.protocol.packet.ingame.server.window.ServerOpenWindowPacket; import com.github.steveice10.mc.protocol.packet.ingame.server.window.ServerSetSlotPacket; @@ -50,11 +51,7 @@ import org.geysermc.connector.network.translators.block.BlockTranslator; import org.geysermc.connector.network.translators.inventory.GenericInventoryTranslator; import org.geysermc.connector.network.translators.inventory.InventoryTranslator; import org.geysermc.connector.network.translators.item.ItemTranslator; -import org.geysermc.connector.network.translators.java.JavaChatTranslator; -import org.geysermc.connector.network.translators.java.JavaDifficultyTranslator; -import org.geysermc.connector.network.translators.java.JavaJoinGameTranslator; -import org.geysermc.connector.network.translators.java.JavaRespawnTranslator; -import org.geysermc.connector.network.translators.java.JavaTitleTranslator; +import org.geysermc.connector.network.translators.java.*; import org.geysermc.connector.network.translators.java.entity.*; import org.geysermc.connector.network.translators.java.entity.player.JavaPlayerHealthTranslator; import org.geysermc.connector.network.translators.java.entity.player.JavaPlayerPositionRotationTranslator; @@ -62,6 +59,7 @@ import org.geysermc.connector.network.translators.java.entity.player.JavaPlayerS import org.geysermc.connector.network.translators.java.entity.spawn.*; import org.geysermc.connector.network.translators.java.scoreboard.JavaDisplayScoreboardTranslator; import org.geysermc.connector.network.translators.java.scoreboard.JavaScoreboardObjectiveTranslator; +import org.geysermc.connector.network.translators.java.scoreboard.JavaTeamTranslator; import org.geysermc.connector.network.translators.java.scoreboard.JavaUpdateScoreTranslator; import org.geysermc.connector.network.translators.java.window.JavaOpenWindowTranslator; import org.geysermc.connector.network.translators.java.window.JavaSetSlotTranslator; @@ -119,6 +117,7 @@ public class TranslatorsInit { Registry.registerJava(ServerEntityRotationPacket.class, new JavaEntityRotationTranslator()); Registry.registerJava(ServerEntityHeadLookPacket.class, new JavaEntityHeadLookTranslator()); Registry.registerJava(ServerEntityMetadataPacket.class, new JavaEntityMetadataTranslator()); + Registry.registerJava(ServerBossBarPacket.class, new JavaBossBarTranslator()); Registry.registerJava(ServerSpawnExpOrbPacket.class, new JavaSpawnExpOrbTranslator()); Registry.registerJava(ServerSpawnGlobalEntityPacket.class, new JavaSpawnGlobalEntityTranslator()); @@ -141,6 +140,7 @@ public class TranslatorsInit { Registry.registerJava(ServerScoreboardObjectivePacket.class, new JavaScoreboardObjectiveTranslator()); Registry.registerJava(ServerDisplayScoreboardPacket.class, new JavaDisplayScoreboardTranslator()); Registry.registerJava(ServerUpdateScorePacket.class, new JavaUpdateScoreTranslator()); + Registry.registerJava(ServerTeamPacket.class, new JavaTeamTranslator()); Registry.registerJava(ServerBlockChangePacket.class, new JavaBlockChangeTranslator()); Registry.registerJava(ServerMultiBlockChangePacket.class, new JavaMultiBlockChangeTranslator()); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMovePlayerTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMovePlayerTranslator.java index d74bb8337..a09d0daa3 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMovePlayerTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMovePlayerTranslator.java @@ -49,15 +49,17 @@ public class BedrockMovePlayerTranslator extends PacketTranslator { + @Override + public void translate(ServerBossBarPacket packet, GeyserSession session) { + BossEventPacket bossEventPacket = new BossEventPacket(); + bossEventPacket.setBossUniqueEntityId(session.getEntityCache().getBossBar(packet.getUUID())); + + switch (packet.getAction()) { + case ADD: + long entityId = session.getEntityCache().addBossBar(packet.getUUID()); + addBossEntity(session, entityId); + + bossEventPacket.setType(BossEventPacket.Type.SHOW); + bossEventPacket.setBossUniqueEntityId(entityId); + bossEventPacket.setTitle(MessageUtils.getBedrockMessage(packet.getTitle())); + bossEventPacket.setHealthPercentage(packet.getHealth()); + bossEventPacket.setColor(0); //ignored by client + bossEventPacket.setOverlay(1); + bossEventPacket.setDarkenSky(0); + break; + case UPDATE_TITLE: + bossEventPacket.setType(BossEventPacket.Type.TITLE); + bossEventPacket.setTitle(MessageUtils.getBedrockMessage(packet.getTitle())); + break; + case UPDATE_HEALTH: + bossEventPacket.setType(BossEventPacket.Type.HEALTH_PERCENTAGE); + bossEventPacket.setHealthPercentage(packet.getHealth()); + break; + case REMOVE: + bossEventPacket.setType(BossEventPacket.Type.HIDE); + removeBossEntity(session, session.getEntityCache().removeBossBar(packet.getUUID())); + break; + case UPDATE_STYLE: + case UPDATE_FLAGS: + //todo + return; + } + + session.getUpstream().sendPacket(bossEventPacket); + } + + /** + * Bedrock still needs an entity to display the BossBar.
+ * Just like 1.8 but it doesn't care about which entity + */ + private void addBossEntity(GeyserSession session, long entityId) { + AddEntityPacket addEntityPacket = new AddEntityPacket(); + addEntityPacket.setUniqueEntityId(entityId); + addEntityPacket.setRuntimeEntityId(entityId); + addEntityPacket.setIdentifier("minecraft:creeper"); + addEntityPacket.setEntityType(33); + addEntityPacket.setPosition(session.getPlayerEntity().getPosition()); + addEntityPacket.setRotation(Vector3f.ZERO); + addEntityPacket.setMotion(Vector3f.ZERO); + addEntityPacket.getMetadata().put(EntityData.SCALE, 0.01F); // scale = 0 doesn't work? + + session.getUpstream().sendPacket(addEntityPacket); + } + + private void removeBossEntity(GeyserSession session, long entityId) { + RemoveEntityPacket removeEntityPacket = new RemoveEntityPacket(); + removeEntityPacket.setUniqueEntityId(entityId); + + session.getUpstream().sendPacket(removeEntityPacket); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityDestroyTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityDestroyTranslator.java index 55da3c733..065463235 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityDestroyTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityDestroyTranslator.java @@ -36,7 +36,10 @@ public class JavaEntityDestroyTranslator extends PacketTranslator { - private static ProvidedSkinData providedSkinData = ProvidedSkinData.getProvidedSkin("bedrock/skin/model_steve.json"); private static byte[] providedSkin = new ProvidedSkin("bedrock/skin/skin_steve.png").getSkin(); @Override @@ -26,8 +25,13 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator { @@ -41,7 +48,7 @@ public class JavaSpawnPlayerTranslator extends PacketTranslator { + GameProfile.Property skinProperty = entity.getProfile().getProperty("textures"); + + JsonObject skinObject = SkinProvider.getGson().fromJson(new String(Base64.getDecoder().decode(skinProperty.getValue()), Charsets.UTF_8), JsonObject.class); + JsonObject textures = skinObject.getAsJsonObject("textures"); + + JsonObject skinTexture = textures.getAsJsonObject("SKIN"); + String skinUrl = skinTexture.get("url").getAsString(); + + boolean isAlex = skinTexture.has("metadata"); + + String capeUrl = null; + if (textures.has("CAPE")) { + JsonObject capeTexture = textures.getAsJsonObject("CAPE"); + capeUrl = capeTexture.get("url").getAsString(); + } + + SkinProvider.requestAndHandleSkinAndCape(entity.getUuid(), skinUrl, capeUrl) + .whenCompleteAsync((skinAndCape, throwable) -> { + SkinProvider.Skin skin = skinAndCape.getSkin(); + SkinProvider.Cape cape = skinAndCape.getCape(); + + if (entity.getLastSkinUpdate() < skin.getRequestedOn()) { + Geyser.getLogger().debug("Received Skin for " + entity.getUuid() + ", updating player.."); + entity.setLastSkinUpdate(skin.getRequestedOn()); + + PlayerListPacket.Entry updatedEntry = new PlayerListPacket.Entry(skin.getSkinOwner()); + updatedEntry.setName(entity.getUsername()); + updatedEntry.setEntityId(entity.getGeyserId()); + updatedEntry.setSkinId(entity.getUuid().toString()); + updatedEntry.setSkinData(skin.getSkinData()); + updatedEntry.setCapeData(cape.getCapeData()); + updatedEntry.setGeometryName("geometry.humanoid.custom" + (isAlex ? "Slim" : "")); + updatedEntry.setGeometryData(""); + updatedEntry.setXuid(""); + updatedEntry.setPlatformChatId(""); + + PlayerListPacket playerRemovePacket = new PlayerListPacket(); + playerRemovePacket.setType(PlayerListPacket.Type.REMOVE); + playerRemovePacket.getEntries().add(updatedEntry); + session.getUpstream().sendPacket(playerRemovePacket); + + PlayerListPacket playerAddPacket = new PlayerListPacket(); + playerAddPacket.setType(PlayerListPacket.Type.ADD); + playerAddPacket.getEntries().add(updatedEntry); + session.getUpstream().sendPacket(playerAddPacket); + } + }).isCompletedExceptionally(); + }); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaDisplayScoreboardTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaDisplayScoreboardTranslator.java index 429857aa3..cc4a04757 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaDisplayScoreboardTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaDisplayScoreboardTranslator.java @@ -27,20 +27,14 @@ package org.geysermc.connector.network.translators.java.scoreboard; import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerDisplayScoreboardPacket; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.session.cache.ScoreboardCache; import org.geysermc.connector.network.translators.PacketTranslator; -import org.geysermc.connector.scoreboard.Scoreboard; public class JavaDisplayScoreboardTranslator extends PacketTranslator { @Override public void translate(ServerDisplayScoreboardPacket packet, GeyserSession session) { - try { - ScoreboardCache cache = session.getScoreboardCache(); - Scoreboard scoreboard = new Scoreboard(session); - cache.setScoreboard(scoreboard); - } catch (Exception ex) { - ex.printStackTrace(); - } + session.getScoreboardCache().getScoreboard().registerNewObjective( + packet.getScoreboardName(), packet.getPosition() + ); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaScoreboardObjectiveTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaScoreboardObjectiveTranslator.java index fb60217cd..89fdec79c 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaScoreboardObjectiveTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaScoreboardObjectiveTranslator.java @@ -30,7 +30,7 @@ import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.cache.ScoreboardCache; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.scoreboard.Scoreboard; -import org.geysermc.connector.scoreboard.ScoreboardObjective; +import org.geysermc.connector.scoreboard.Objective; import org.geysermc.connector.utils.MessageUtils; public class JavaScoreboardObjectiveTranslator extends PacketTranslator { @@ -39,18 +39,21 @@ public class JavaScoreboardObjectiveTranslator extends PacketTranslator { @Override public void translate(ServerTeamPacket packet, GeyserSession session) { + Geyser.getLogger().debug("Team packet " + packet.getTeamName() + " " + packet.getAction()+" "+ Arrays.toString(packet.getPlayers())); + Scoreboard scoreboard = session.getScoreboardCache().getScoreboard(); + switch (packet.getAction()) { + case CREATE: + scoreboard.registerNewTeam(packet.getTeamName(), toPlayerSet(packet.getPlayers())) + .setName(MessageUtils.getBedrockMessage(packet.getDisplayName())) + .setPrefix(MessageUtils.getBedrockMessage(packet.getPrefix())) + .setSuffix(MessageUtils.getBedrockMessage(packet.getSuffix())); + break; + case UPDATE: + scoreboard.getTeam(packet.getTeamName()) + .setName(MessageUtils.getBedrockMessage(packet.getDisplayName())) + .setPrefix(MessageUtils.getBedrockMessage(packet.getPrefix())) + .setSuffix(MessageUtils.getBedrockMessage(packet.getSuffix())) + .setUpdateType(UpdateType.UPDATE); + break; + case ADD_PLAYER: + scoreboard.getTeam(packet.getTeamName()).addEntities(packet.getPlayers()); + break; + case REMOVE_PLAYER: + scoreboard.getTeam(packet.getTeamName()).removeEntities(packet.getPlayers()); + break; + case REMOVE: + scoreboard.removeTeam(packet.getTeamName()); + break; + } + scoreboard.onUpdate(); + } + + private Set toPlayerSet(String[] players) { + return new HashSet<>(Arrays.asList(players)); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaUpdateScoreTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaUpdateScoreTranslator.java index 83b01ae02..dae080a3b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaUpdateScoreTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaUpdateScoreTranslator.java @@ -26,37 +26,33 @@ package org.geysermc.connector.network.translators.java.scoreboard; import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerUpdateScorePacket; -import com.nukkitx.protocol.bedrock.packet.SetScorePacket; +import org.geysermc.api.Geyser; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.session.cache.ScoreboardCache; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.scoreboard.Scoreboard; -import org.geysermc.connector.scoreboard.ScoreboardObjective; +import org.geysermc.connector.scoreboard.Objective; public class JavaUpdateScoreTranslator extends PacketTranslator { @Override public void translate(ServerUpdateScorePacket packet, GeyserSession session) { try { - ScoreboardCache cache = session.getScoreboardCache(); - Scoreboard scoreboard = new Scoreboard(session); - if (cache.getScoreboard() != null) - scoreboard = cache.getScoreboard(); + Scoreboard scoreboard = session.getScoreboardCache().getScoreboard(); - ScoreboardObjective objective = scoreboard.getObjective(packet.getObjective()); + Objective objective = scoreboard.getObjective(packet.getObjective()); if (objective == null) { - objective = scoreboard.registerNewObjective(packet.getObjective()); + Geyser.getLogger().info("Tried to update score without the existence of its requested objective"); + return; } switch (packet.getAction()) { case REMOVE: - objective.registerScore(packet.getEntry(), packet.getEntry(), packet.getValue(), SetScorePacket.Action.REMOVE); + objective.resetScore(packet.getEntry()); break; case ADD_OR_UPDATE: - objective.registerScore(packet.getEntry(), packet.getEntry(), packet.getValue(), SetScorePacket.Action.SET); + objective.setScore(packet.getEntry(), packet.getValue()); break; } - cache.setScoreboard(scoreboard); scoreboard.onUpdate(); } catch (Exception ex) { ex.printStackTrace(); diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Objective.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Objective.java new file mode 100644 index 000000000..d48391813 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Objective.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2019 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.scoreboard; + +import com.github.steveice10.mc.protocol.data.game.scoreboard.ScoreboardPosition; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; + +@Getter +public class Objective { + private Scoreboard scoreboard; + private long id; + + @Setter + private UpdateType updateType = UpdateType.ADD; + private String objectiveName; + private String displaySlot; + private String displayName; + private int type; // 0 = integer, 1 = heart + + private Map scores = new HashMap<>(); + + public Objective(Scoreboard scoreboard, String objectiveName, ScoreboardPosition displaySlot, String displayName, int type) { + this(scoreboard, objectiveName, displaySlot.name().toLowerCase(), displayName, type); + } + + public Objective(Scoreboard scoreboard, String objectiveName, String displaySlot, String displayName, int type) { + this.scoreboard = scoreboard; + this.id = scoreboard.getNextId().getAndIncrement(); + this.objectiveName = objectiveName; + this.displaySlot = displaySlot; + this.displayName = displayName; + this.type = type; + } + + public void registerScore(String id, int score) { + if (!scores.containsKey(id)) { + Score score1 = new Score(this, id).setScore(score); + Team team = scoreboard.getTeamFor(id); + if (team != null) score1.setTeam(team); + scores.put(id, score1); + } + } + + public void setScore(String id, int score) { + if (scores.containsKey(id)) { + scores.get(id).setScore(score).setUpdateType(UpdateType.ADD); + } else { + registerScore(id, score); + } + } + + public void setScoreText(String oldText, String newText) { + if (!scores.containsKey(oldText) || oldText.equals(newText)) return; + Score oldScore = scores.get(oldText); + + Score newScore = new Score(this, newText).setScore(oldScore.getScore()); + Team team = scoreboard.getTeamFor(newText); + if (team != null) newScore.setTeam(team); + + scores.put(newText, newScore); + oldScore.setUpdateType(UpdateType.REMOVE); + } + + public int getScore(String id) { + if (scores.containsKey(id)) { + return scores.get(id).getScore(); + } + return 0; + } + + public Score getScore(int line) { + for (Score score : scores.values()) { + if (score.getScore() == line) return score; + } + return null; + } + + public void resetScore(String id) { + if (scores.containsKey(id)) { + scores.get(id).setUpdateType(UpdateType.REMOVE); + } + } + + public void removeScore(String id) { + scores.remove(id); + } + + public Objective setDisplayName(String displayName) { + this.displayName = displayName; + if (updateType == UpdateType.NOTHING) updateType = UpdateType.UPDATE; + return this; + } + + public Objective setType(int type) { + this.type = type; + if (updateType == UpdateType.NOTHING) updateType = UpdateType.UPDATE; + return this; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Score.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Score.java index 9535b0947..9e4966582 100644 --- a/connector/src/main/java/org/geysermc/connector/scoreboard/Score.java +++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Score.java @@ -25,43 +25,32 @@ package org.geysermc.connector.scoreboard; -import com.nukkitx.protocol.bedrock.packet.SetScorePacket; import lombok.Getter; import lombok.Setter; +import lombok.experimental.Accessors; -import java.util.Random; - -/** - * Adapted from: https://github.com/Ragnok123/GTScoreboard - */ +@Getter @Setter +@Accessors(chain = true) public class Score { + private Objective objective; + private long id; - @Getter - @Setter + private UpdateType updateType = UpdateType.ADD; + private String name; + private Team team; private int score; - @Getter - private long scoreboardId; - private ScoreboardObjective objective; - - @Getter - @Setter - private String fakePlayer; - - @Getter - @Setter - private SetScorePacket.Action action = SetScorePacket.Action.SET; - - private boolean modified = false; - - @Getter - @Setter - private String fakeId; - - public Score(ScoreboardObjective objective, String fakePlayer) { - this.scoreboardId = -new Random().nextLong(); + public Score(Objective objective, String name) { + this.id = objective.getScoreboard().getNextId().getAndIncrement(); this.objective = objective; - this.fakePlayer = fakePlayer; + this.name = name; + } + + public String getDisplayName() { + if (team != null) { + return team.getPrefix() + name + team.getSuffix(); + } + return name; } } diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java index 6b6337993..22b080cb6 100644 --- a/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java +++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java @@ -25,111 +25,178 @@ package org.geysermc.connector.scoreboard; +import com.github.steveice10.mc.protocol.data.game.scoreboard.ScoreboardPosition; import com.nukkitx.protocol.bedrock.data.ScoreInfo; import com.nukkitx.protocol.bedrock.packet.RemoveObjectivePacket; import com.nukkitx.protocol.bedrock.packet.SetDisplayObjectivePacket; import com.nukkitx.protocol.bedrock.packet.SetScorePacket; import lombok.Getter; -import lombok.Setter; +import org.geysermc.api.Geyser; +import org.geysermc.connector.console.GeyserLogger; import org.geysermc.connector.network.session.GeyserSession; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; -/** - * Adapted from: https://github.com/Ragnok123/GTScoreboard - */ +import static org.geysermc.connector.scoreboard.UpdateType.*; + +@Getter public class Scoreboard { - - @Getter - private ScoreboardObjective objective; - private GeyserSession session; + private AtomicLong nextId = new AtomicLong(0); - @Getter - @Setter - private long id; - - private Map objectiveMap = new HashMap(); + private Map objectives = new HashMap<>(); + private Map teams = new HashMap<>(); public Scoreboard(GeyserSession session) { this.session = session; - - id = new Random().nextLong(); } - public ScoreboardObjective registerNewObjective(String objectiveName) { - ScoreboardObjective objective = new ScoreboardObjective(); - objective.setObjectiveName(objectiveName); - this.objective = objective; - if (!objectiveMap.containsKey(objectiveName)) { - objectiveMap.put(objectiveName, objective); - } - + public Objective registerNewObjective(String objectiveId, ScoreboardPosition displaySlot) { + Objective objective = new Objective(this, objectiveId, displaySlot, "unknown", 0); + if (objectives.containsKey(objectiveId)) despawnObjective(objectives.get(objectiveId)); + objectives.put(objectiveId, objective); return objective; } - public ScoreboardObjective getObjective(String objectiveName) { - ScoreboardObjective objective = null; - if (objectiveMap.containsKey(objectiveName) && this.objective.getObjectiveName().contains(objectiveName)) { - objective = this.objective; + public Team registerNewTeam(String teamName, Set players) { + if (teams.containsKey(teamName)) { + Geyser.getLogger().info("Ignoring team " + teamName + ". It overrides without removing old team."); + return getTeam(teamName); } - return objective; + Team team = new Team(this, teamName).setEntities(players); + teams.put(teamName, team); + + for (Objective objective : objectives.values()) { + for (Score score : objective.getScores().values()) { + if (players.contains(score.getName())) { + score.setTeam(team).setUpdateType(ADD); + } + } + } + return team; } - public void setObjective(String objectiveName) { - if (objectiveMap.containsKey(objectiveName)) - objective = objectiveMap.get(objectiveName); + public Objective getObjective(String objectiveName) { + return objectives.get(objectiveName); + } + + public Team getTeam(String teamName) { + return teams.get(teamName); } public void unregisterObjective(String objectiveName) { - if (!objectiveMap.containsKey(objectiveName)) - return; - - if (objective.getObjectiveName().equals(objectiveName)) { - objective = null; - } - - objectiveMap.remove(objectiveName); + Objective objective = getObjective(objectiveName); + if (objective != null) objective.setUpdateType(REMOVE); } - public void onUpdate() { - if (objective == null) - return; - - RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket(); - removeObjectivePacket.setObjectiveId(objective.getObjectiveName()); - session.getUpstream().sendPacket(removeObjectivePacket); - - SetDisplayObjectivePacket displayObjectivePacket = new SetDisplayObjectivePacket(); - displayObjectivePacket.setObjectiveId(objective.getObjectiveName()); - displayObjectivePacket.setDisplayName(objective.getDisplayName()); - displayObjectivePacket.setCriteria("dummy"); - displayObjectivePacket.setDisplaySlot("sidebar"); - displayObjectivePacket.setSortOrder(1); - session.getUpstream().sendPacket(displayObjectivePacket); - - Map fakeMap = new HashMap(); - for (Map.Entry entry : objective.getScores().entrySet()) { - fakeMap.put(entry.getKey(), entry.getValue()); - } - - for (String string : fakeMap.keySet()) { - Score score = fakeMap.get(string); - ScoreInfo scoreInfo = new ScoreInfo(score.getScoreboardId(), objective.getObjectiveName(), score.getScore(), score.getFakePlayer()); - - SetScorePacket setScorePacket = new SetScorePacket(); - setScorePacket.setAction(score.getAction()); - setScorePacket.setInfos(Arrays.asList(scoreInfo)); - session.getUpstream().sendPacket(setScorePacket); - - if (score.getAction() == SetScorePacket.Action.REMOVE) { - String id = score.getFakeId(); - objective.getScores().remove(id); + public void removeTeam(String teamName) { + if (teams.remove(teamName) != null) { + for (Objective objective : objectives.values()) { + for (Score score : objective.getScores().values()) { + if (score.getName().equals(teamName)) { + score.setTeam(null).setUpdateType(ADD); + } + } } } } + + public void onUpdate() { + Set changedObjectives = new HashSet<>(); + List addScores = new ArrayList<>(); + List removeScores = new ArrayList<>(); + + for (String objectiveId : new ArrayList<>(objectives.keySet())) { + Objective objective = objectives.get(objectiveId); + if (objective.getUpdateType() != NOTHING) changedObjectives.add(objective); + + for (String identifier : new HashSet<>(objective.getScores().keySet())) { + Score score = objective.getScores().get(identifier); + + boolean add = (objective.getUpdateType() != NOTHING && objective.getUpdateType() != REMOVE) && score.getUpdateType() != REMOVE || score.getUpdateType() == ADD; + boolean remove = (add && score.getUpdateType() != ADD && objective.getUpdateType() != ADD) || objective.getUpdateType() == REMOVE || score.getUpdateType() == REMOVE; + + ScoreInfo info = new ScoreInfo(score.getId(), score.getObjective().getObjectiveName(), score.getScore(), score.getDisplayName()); + if (add || (score.getTeam() != null && (score.getTeam().getUpdateType() == ADD || score.getTeam().getUpdateType() == UPDATE))) addScores.add(info); + if (remove || (score.getTeam() != null && score.getTeam().getUpdateType() != NOTHING)) removeScores.add(info); + + if (score.getUpdateType() == REMOVE) { + objective.removeScore(score.getName()); + } + + if (addScores.contains(info) || removeScores.contains(info)) changedObjectives.add(objective); + score.setUpdateType(NOTHING); + } + } + + for (Objective objective : changedObjectives) { + boolean update = objective.getUpdateType() == NOTHING || objective.getUpdateType() == UPDATE; + if (objective.getUpdateType() == REMOVE || update) { + RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket(); + removeObjectivePacket.setObjectiveId(objective.getObjectiveName()); + session.getUpstream().sendPacket(removeObjectivePacket); + if (objective.getUpdateType() == REMOVE) { + objectives.remove(objective.getObjectiveName()); // now we can deregister + } + } + if (objective.getUpdateType() == ADD || update) { + SetDisplayObjectivePacket displayObjectivePacket = new SetDisplayObjectivePacket(); + displayObjectivePacket.setObjectiveId(objective.getObjectiveName()); + displayObjectivePacket.setDisplayName(objective.getDisplayName()); + displayObjectivePacket.setCriteria("dummy"); + displayObjectivePacket.setDisplaySlot(objective.getDisplaySlot()); + displayObjectivePacket.setSortOrder(1); // ?? + session.getUpstream().sendPacket(displayObjectivePacket); + } + objective.setUpdateType(NOTHING); + } + + if (!removeScores.isEmpty()) { + SetScorePacket setScorePacket = new SetScorePacket(); + setScorePacket.setAction(SetScorePacket.Action.REMOVE); + setScorePacket.setInfos(removeScores); + session.getUpstream().sendPacket(setScorePacket); + } + + if (!addScores.isEmpty()) { + SetScorePacket setScorePacket = new SetScorePacket(); + setScorePacket.setAction(SetScorePacket.Action.SET); + setScorePacket.setInfos(addScores); + session.getUpstream().sendPacket(setScorePacket); + } + } + + public void despawnObjective(Objective objective) { + RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket(); + removeObjectivePacket.setObjectiveId(objective.getObjectiveName()); + session.getUpstream().sendPacket(removeObjectivePacket); + objectives.remove(objective.getDisplayName()); + + List toRemove = new ArrayList<>(); + for (String identifier : objective.getScores().keySet()) { + Score score = objective.getScores().get(identifier); + toRemove.add(new ScoreInfo( + score.getId(), score.getObjective().getObjectiveName(), + 0, "" + )); + } + + if (!toRemove.isEmpty()) { + SetScorePacket setScorePacket = new SetScorePacket(); + setScorePacket.setAction(SetScorePacket.Action.REMOVE); + setScorePacket.setInfos(toRemove); + session.getUpstream().sendPacket(setScorePacket); + } + } + + public Team getTeamFor(String entity) { + for (Team team : teams.values()) { + if (team.getEntities().contains(entity)) { + return team; + } + } + return null; + } } diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/ScoreboardObjective.java b/connector/src/main/java/org/geysermc/connector/scoreboard/ScoreboardObjective.java deleted file mode 100644 index f90c474e5..000000000 --- a/connector/src/main/java/org/geysermc/connector/scoreboard/ScoreboardObjective.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (c) 2019 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.connector.scoreboard; - -import com.nukkitx.protocol.bedrock.packet.SetScorePacket; -import lombok.Getter; -import lombok.Setter; - -import java.util.HashMap; -import java.util.Map; - -/** - * Adapted from: https://github.com/Ragnok123/GTScoreboard - */ -public class ScoreboardObjective { - - @Getter - @Setter - private int scoreboardTick = 0; - - @Getter - @Setter - private String objectiveName; - - @Getter - @Setter - private DisplaySlot displaySlot; - - @Getter - @Setter - private String displayName; - - @Getter - private Map scores = new HashMap(); - - public void registerScore(String id, String fake, int value) { - registerScore(id, fake, value, SetScorePacket.Action.SET); - } - - public void registerScore(String id, String fake, int value, SetScorePacket.Action action) { - Score score = new Score(this, fake); - score.setScore(value); - score.setFakeId(id); - score.setAction(action); - if (!scores.containsKey(id)) { - scores.put(id, score); - } else { - setScore(id, value); - } - } - - public void setScore(String id, int value) { - if (scores.containsKey(id)) { - Score modifiedScore = scores.get(id); - modifiedScore.setScore(value); - scores.remove(id); - scores.put(id, modifiedScore); - } - } - - public void setScoreText(String id, String text) { - if (scores.containsKey(id)) { - Score newScore = new Score(this, text); - newScore.setScore(scores.get(id).getScore()); - newScore.setFakeId(id); - scores.remove(id); - scores.put(id, newScore); - } - } - - public int getScore(String id) { - int i = 0; - if (scores.containsKey(id)) { - Score score = scores.get(id); - i = score.getScore(); - } - - return i; - } - - public Score getScore(int line) { - Score score = null; - for (Map.Entry entry : scores.entrySet()) { - if (entry.getValue().getScore() == line) - return entry.getValue(); - } - - return null; - } - - public void resetScore(String id) { - if (scores.containsKey(id)) { - Score modifiedScore = scores.get(id); - modifiedScore.setAction(SetScorePacket.Action.REMOVE); - scores.remove(id); - scores.put(id, modifiedScore); - } - } - - public enum DisplaySlot { - - SIDEBAR, - LIST, - BELOWNAME; - - } -} diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java new file mode 100644 index 000000000..2106787ea --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java @@ -0,0 +1,64 @@ +package org.geysermc.connector.scoreboard; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Getter +@Setter +@Accessors(chain = true) +public class Team { + private final Scoreboard scoreboard; + private final String id; + + private UpdateType updateType = UpdateType.ADD; + private String name; + private String prefix; + private String suffix; + private Set entities = new HashSet<>(); + + + public Team(Scoreboard scoreboard, String id) { + this.scoreboard = scoreboard; + this.id = id; + } + + public void addEntities(String... names) { + List added = new ArrayList<>(); + for (String name : names) { + if (!entities.contains(name)) { + entities.add(name); + added.add(name); + } + } + for (Objective objective : scoreboard.getObjectives().values()) { + for (Score score : objective.getScores().values()) { + if (added.contains(score.getName())) { + score.setTeam(this).setUpdateType(UpdateType.ADD); + } + } + } + } + + public void removeEntities(String... names) { + List removed = new ArrayList<>(); + for (String name : names) { + if (entities.contains(name)) { + entities.remove(name); + removed.add(name); + } + } + for (Objective objective : scoreboard.getObjectives().values()) { + for (Score score : objective.getScores().values()) { + if (removed.contains(score.getName())) { + score.setTeam(null).setUpdateType(UpdateType.ADD); + } + } + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/UpdateType.java b/connector/src/main/java/org/geysermc/connector/scoreboard/UpdateType.java new file mode 100644 index 000000000..e0465db3f --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/scoreboard/UpdateType.java @@ -0,0 +1,15 @@ +package org.geysermc.connector.scoreboard; + +public enum UpdateType { + REMOVE, + /** + * Nothing has changed, it's cool + */ + NOTHING, + ADD, + /** + * Hey, something has been updated!
+ * Only used in {@link Objective Objective} + */ + UPDATE +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/ProvidedSkinData.java b/connector/src/main/java/org/geysermc/connector/utils/ProvidedSkinData.java deleted file mode 100644 index 9618edcd9..000000000 --- a/connector/src/main/java/org/geysermc/connector/utils/ProvidedSkinData.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.geysermc.connector.utils; - -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import lombok.Getter; -import org.apache.commons.codec.Charsets; - -import java.util.Base64; - -@Getter -public class ProvidedSkinData { - private static final Gson gson = new GsonBuilder().create(); - private String skinId; - private String skinName; - private String geometryId; - private ObjectNode geometryData; - - public static ProvidedSkinData getProvidedSkin(String skinName) { - try { - ObjectMapper objectMapper = new ObjectMapper(new JsonFactory()); - return objectMapper.readValue(ProvidedSkinData.class.getClassLoader().getResource(skinName), ProvidedSkinData.class); - } catch (Exception ex) { - ex.printStackTrace(); - return null; - } - } - - public String getGeometryDataEncoded() { - try { - return new String(Base64.getEncoder().encode(geometryData.toString().getBytes(Charsets.UTF_8))); - } catch (Exception ex) { - ex.printStackTrace(); - return null; - } - } -} diff --git a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java new file mode 100644 index 000000000..aab8574ac --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java @@ -0,0 +1,190 @@ +package org.geysermc.connector.utils; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.geysermc.api.Geyser; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URL; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.*; + +public class SkinProvider { + private static final ExecutorService executorService = Executors.newFixedThreadPool(14); + @Getter private static final Gson gson = new GsonBuilder().create(); + + private static Map cachedSkins = new ConcurrentHashMap<>(); + private static Map cachedCapes = new ConcurrentHashMap<>(); + + public static final Skin EMPTY_SKIN = new Skin(-1, ""); + public static final Cape EMPTY_CAPE = new Cape("", new byte[0]); + private static final int CACHE_INTERVAL = 8 * 60 * 1000; // 8 minutes + + private static Map> requestedSkins = new ConcurrentHashMap<>(); + private static Map> requestedCapes = new ConcurrentHashMap<>(); + + public static boolean hasSkinCached(UUID uuid) { + return cachedSkins.containsKey(uuid); + } + + public static boolean hasCapeCached(String capeUrl) { + return cachedCapes.containsKey(capeUrl); + } + + public static Skin getCachedSkin(UUID uuid) { + return cachedSkins.get(uuid); + } + + public static Cape getCachedCape(String capeUrl) { + return cachedCapes.get(capeUrl); + } + + public static CompletableFuture requestAndHandleSkinAndCape(UUID playerId, String skinUrl, String capeUrl) { + return CompletableFuture.supplyAsync(() -> { + long time = System.currentTimeMillis(); + + SkinAndCape skinAndCape = new SkinAndCape( + getOrDefault(requestAndHandleSkin(playerId, skinUrl, false), EMPTY_SKIN, 5), + getOrDefault(requestAndHandleCape(capeUrl, false), EMPTY_CAPE, 5) + ); + + Geyser.getLogger().info("Took " + (System.currentTimeMillis() - time) + "ms for " + playerId); + return skinAndCape; + }, executorService); + } + + public static CompletableFuture requestAndHandleSkin(UUID playerId, String textureUrl, boolean newThread) { + if (textureUrl == null || textureUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_SKIN); + if (requestedSkins.containsKey(playerId)) return requestedSkins.get(playerId); // already requested + + if ((System.currentTimeMillis() - CACHE_INTERVAL) < cachedSkins.getOrDefault(playerId, EMPTY_SKIN).getRequestedOn()) { + // no need to update, still cached + return CompletableFuture.completedFuture(cachedSkins.get(playerId)); + } + + CompletableFuture future; + if (newThread) { + future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), executorService) + .whenCompleteAsync((skin, throwable) -> { + if (!cachedSkins.getOrDefault(playerId, EMPTY_SKIN).getTextureUrl().equals(textureUrl)) { + skin.updated = true; + cachedSkins.put(playerId, skin); + } + requestedSkins.remove(skin.getSkinOwner()); + }); + requestedSkins.put(playerId, future); + } else { + Skin skin = supplySkin(playerId, textureUrl); + future = CompletableFuture.completedFuture(skin); + cachedSkins.put(playerId, skin); + } + return future; + } + + public static CompletableFuture requestAndHandleCape(String capeUrl, boolean newThread) { + if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE); + if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested + + if (cachedCapes.containsKey(capeUrl)) { + // no need to update the cache, capes are static :D + return CompletableFuture.completedFuture(cachedCapes.get(capeUrl)); + } + + CompletableFuture future; + if (newThread) { + future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl), executorService) + .whenCompleteAsync((cape, throwable) -> { + cachedCapes.put(capeUrl, cape); + requestedCapes.remove(capeUrl); + }); + requestedCapes.put(capeUrl, future); + } else { + Cape cape = supplyCape(capeUrl); // blocking + future = CompletableFuture.completedFuture(cape); + cachedCapes.put(capeUrl, cape); + } + return future; + } + + private static Skin supplySkin(UUID uuid, String textureUrl) { + byte[] skin = EMPTY_SKIN.getSkinData(); + try { + skin = requestImage(textureUrl); + } catch (Exception ignored) {} // just ignore I guess + return new Skin(uuid, textureUrl, skin, System.currentTimeMillis(), false); + } + + private static Cape supplyCape(String capeUrl) { + byte[] cape = EMPTY_CAPE.getCapeData(); + try { + cape = requestImage(capeUrl); + } catch (Exception ignored) {} // just ignore I guess + return new Cape(capeUrl, cape); + } + + private static byte[] requestImage(String imageUrl) throws Exception { + BufferedImage image = ImageIO.read(new URL(imageUrl)); + Geyser.getLogger().debug("Downloaded " + imageUrl); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(image.getWidth() * 4 + image.getHeight() * 4); + try { + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + int rgba = image.getRGB(x, y); + outputStream.write((rgba >> 16) & 0xFF); + outputStream.write((rgba >> 8) & 0xFF); + outputStream.write(rgba & 0xFF); + outputStream.write((rgba >> 24) & 0xFF); + } + } + image.flush(); + return outputStream.toByteArray(); + } finally { + try { + outputStream.close(); + } catch (IOException ignored) {} + } + } + + @AllArgsConstructor + @Getter + public static class SkinAndCape { + private Skin skin; + private Cape cape; + } + + @AllArgsConstructor + @Getter + public static class Skin { + private UUID skinOwner; + private String textureUrl; + private byte[] skinData = new byte[0]; + private long requestedOn; + private boolean updated; + + private Skin(long requestedOn, String textureUrl) { + this.requestedOn = requestedOn; + this.textureUrl = textureUrl; + } + } + + @AllArgsConstructor + @Getter + public static class Cape { + private String textureUrl; + private byte[] capeData; + } + + private static T getOrDefault(CompletableFuture future, T defaultValue, int timeoutInSeconds) { + try { + return future.get(timeoutInSeconds, TimeUnit.SECONDS); + } catch (Exception ignored) {} + return defaultValue; + } +} diff --git a/connector/src/main/resources/bedrock/skin/model_steve.json b/connector/src/main/resources/bedrock/skin/model_steve.json deleted file mode 100644 index 514308fe4..000000000 --- a/connector/src/main/resources/bedrock/skin/model_steve.json +++ /dev/null @@ -1,383 +0,0 @@ -{ - "skinId" : "c18e65aa-7b21-4637-9b63-8ad63622ef01_Custom", - "skinName" : "skin.Standard.Custom", - "geometryId" : "geometry.humanoid.custom", - "geometryData" : - { - "geometry.humanoid": { - "bones": [ - { - "name": "body", - "pivot": [ 0.0, 24.0, 0.0 ], - "cubes": [ - { - "origin": [ -4.0, 12.0, -2.0 ], - "size": [ 8, 12, 4 ], - "uv": [ 16, 16 ] - } - ] - }, - - { - "name": "waist", - "neverRender": true, - "pivot": [ 0.0, 12.0, 0.0 ] - }, - - { - "name": "head", - "pivot": [ 0.0, 24.0, 0.0 ], - "cubes": [ - { - "origin": [ -4.0, 24.0, -4.0 ], - "size": [ 8, 8, 8 ], - "uv": [ 0, 0 ] - } - ] - }, - - { - "name": "hat", - "pivot": [ 0.0, 24.0, 0.0 ], - "cubes": [ - { - "origin": [ -4.0, 24.0, -4.0 ], - "size": [ 8, 8, 8 ], - "uv": [ 32, 0 ], - "inflate": 0.5 - } - ], - "neverRender": true - }, - - { - "name": "rightArm", - "pivot": [ -5.0, 22.0, 0.0 ], - "cubes": [ - { - "origin": [ -8.0, 12.0, -2.0 ], - "size": [ 4, 12, 4 ], - "uv": [ 40, 16 ] - } - ] - }, - - { - "name": "leftArm", - "pivot": [ 5.0, 22.0, 0.0 ], - "cubes": [ - { - "origin": [ 4.0, 12.0, -2.0 ], - "size": [ 4, 12, 4 ], - "uv": [ 40, 16 ] - } - ], - "mirror": true - }, - - { - "name": "rightLeg", - "pivot": [ -1.9, 12.0, 0.0 ], - "cubes": [ - { - "origin": [ -3.9, 0.0, -2.0 ], - "size": [ 4, 12, 4 ], - "uv": [ 0, 16 ] - } - ] - }, - - { - "name": "leftLeg", - "pivot": [ 1.9, 12.0, 0.0 ], - "cubes": [ - { - "origin": [ -0.1, 0.0, -2.0 ], - "size": [ 4, 12, 4 ], - "uv": [ 0, 16 ] - } - ], - "mirror": true - } - ] - }, - - "geometry.cape": { - "texturewidth": 64, - "textureheight": 32, - - "bones": [ - { - "name": "cape", - "pivot": [ 0.0, 24.0, -3.0 ], - "cubes": [ - { - "origin": [ -5.0, 8.0, -3.0 ], - "size": [ 10, 16, 1 ], - "uv": [ 0, 0 ] - } - ], - "material": "alpha" - } - ] - }, - "geometry.humanoid.custom:geometry.humanoid": { - "bones": [ - { - "name": "hat", - "neverRender": false, - "material": "alpha", - "pivot": [ 0.0, 24.0, 0.0 ] - }, - { - "name": "leftArm", - "reset": true, - "mirror": false, - "pivot": [ 5.0, 22.0, 0.0 ], - "cubes": [ - { - "origin": [ 4.0, 12.0, -2.0 ], - "size": [ 4, 12, 4 ], - "uv": [ 32, 48 ] - } - ] - }, - - { - "name": "rightArm", - "reset": true, - "pivot": [ -5.0, 22.0, 0.0 ], - "cubes": [ - { - "origin": [ -8.0, 12.0, -2.0 ], - "size": [ 4, 12, 4 ], - "uv": [ 40, 16 ] - } - ] - }, - - { - "name": "rightItem", - "pivot": [ -6, 15, 1 ], - "neverRender": true, - "parent": "rightArm" - }, - - { - "name": "leftSleeve", - "pivot": [ 5.0, 22.0, 0.0 ], - "cubes": [ - { - "origin": [ 4.0, 12.0, -2.0 ], - "size": [ 4, 12, 4 ], - "uv": [ 48, 48 ], - "inflate": 0.25 - } - ], - "material": "alpha" - }, - - { - "name": "rightSleeve", - "pivot": [ -5.0, 22.0, 0.0 ], - "cubes": [ - { - "origin": [ -8.0, 12.0, -2.0 ], - "size": [ 4, 12, 4 ], - "uv": [ 40, 32 ], - "inflate": 0.25 - } - ], - "material": "alpha" - }, - - { - "name": "leftLeg", - "reset": true, - "mirror": false, - "pivot": [ 1.9, 12.0, 0.0 ], - "cubes": [ - { - "origin": [ -0.1, 0.0, -2.0 ], - "size": [ 4, 12, 4 ], - "uv": [ 16, 48 ] - } - ] - }, - - { - "name": "leftPants", - "pivot": [ 1.9, 12.0, 0.0 ], - "cubes": [ - { - "origin": [ -0.1, 0.0, -2.0 ], - "size": [ 4, 12, 4 ], - "uv": [ 0, 48 ], - "inflate": 0.25 - } - ], - "pos": [ 1.9, 12, 0 ], - "material": "alpha" - }, - - { - "name": "rightPants", - "pivot": [ -1.9, 12.0, 0.0 ], - "cubes": [ - { - "origin": [ -3.9, 0.0, -2.0 ], - "size": [ 4, 12, 4 ], - "uv": [ 0, 32 ], - "inflate": 0.25 - } - ], - "pos": [ -1.9, 12, 0 ], - "material": "alpha" - }, - - { - "name": "jacket", - "pivot": [ 0.0, 24.0, 0.0 ], - "cubes": [ - { - "origin": [ -4.0, 12.0, -2.0 ], - "size": [ 8, 12, 4 ], - "uv": [ 16, 32 ], - "inflate": 0.25 - } - ], - "material": "alpha" - } - ] - }, - "geometry.humanoid.customSlim:geometry.humanoid": { - - "bones": [ - { - "name": "hat", - "neverRender": false, - "material": "alpha" - }, - { - "name": "leftArm", - "reset": true, - "mirror": false, - "pivot": [ 5.0, 21.5, 0.0 ], - "cubes": [ - { - "origin": [ 4.0, 11.5, -2.0 ], - "size": [ 3, 12, 4 ], - "uv": [ 32, 48 ] - } - ] - }, - - { - "name": "rightArm", - "reset": true, - "pivot": [ -5.0, 21.5, 0.0 ], - "cubes": [ - { - "origin": [ -7.0, 11.5, -2.0 ], - "size": [ 3, 12, 4 ], - "uv": [ 40, 16 ] - } - ] - }, - - { - "pivot": [ -6, 14.5, 1 ], - "neverRender": true, - "name": "rightItem", - "parent": "rightArm" - }, - - { - "name": "leftSleeve", - "pivot": [ 5.0, 21.5, 0.0 ], - "cubes": [ - { - "origin": [ 4.0, 11.5, -2.0 ], - "size": [ 3, 12, 4 ], - "uv": [ 48, 48 ], - "inflate": 0.25 - } - ], - "material": "alpha" - }, - - { - "name": "rightSleeve", - "pivot": [ -5.0, 21.5, 0.0 ], - "cubes": [ - { - "origin": [ -7.0, 11.5, -2.0 ], - "size": [ 3, 12, 4 ], - "uv": [ 40, 32 ], - "inflate": 0.25 - } - ], - "material": "alpha" - }, - - { - "name": "leftLeg", - "reset": true, - "mirror": false, - "pivot": [ 1.9, 12.0, 0.0 ], - "cubes": [ - { - "origin": [ -0.1, 0.0, -2.0 ], - "size": [ 4, 12, 4 ], - "uv": [ 16, 48 ] - } - ] - }, - - { - "name": "leftPants", - "pivot": [ 1.9, 12.0, 0.0 ], - "cubes": [ - { - "origin": [ -0.1, 0.0, -2.0 ], - "size": [ 4, 12, 4 ], - "uv": [ 0, 48 ], - "inflate": 0.25 - } - ], - "material": "alpha" - }, - - { - "name": "rightPants", - "pivot": [ -1.9, 12.0, 0.0 ], - "cubes": [ - { - "origin": [ -3.9, 0.0, -2.0 ], - "size": [ 4, 12, 4 ], - "uv": [ 0, 32 ], - "inflate": 0.25 - } - ], - "material": "alpha" - }, - - { - "name": "jacket", - "pivot": [ 0.0, 24.0, 0.0 ], - "cubes": [ - { - "origin": [ -4.0, 12.0, -2.0 ], - "size": [ 8, 12, 4 ], - "uv": [ 16, 32 ], - "inflate": 0.25 - } - ], - "material": "alpha" - } - ] - } - - } - -} \ No newline at end of file