Players spawn, player skins, scoreboard, bossbar and updated protocol lib

This commit is contained in:
Tim203 2019-09-25 23:52:28 +02:00
parent 81d2f6aed4
commit 0b193c04e7
34 changed files with 871 additions and 786 deletions

View File

@ -74,7 +74,7 @@
<dependency>
<groupId>com.nukkitx.protocol</groupId>
<artifactId>bedrock-v361</artifactId>
<version>2.1.2</version>
<version>2.1.3</version>
<scope>compile</scope>
</dependency>
<dependency>

View File

@ -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 extends Entity> I as(Class<I> entityClass) {
return entityClass.isInstance(this) ? (I) this : null;
}
public <I extends Entity> boolean is(Class<I> entityClass) {
return entityClass.isInstance(this);
}
}

View File

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

View File

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

View File

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

View File

@ -46,6 +46,7 @@ public class EntityCache {
private Map<Long, Entity> entities = new HashMap<>();
private Map<Long, Long> entityIdTranslations = new HashMap<>();
private Map<UUID, PlayerEntity> playerEntities = new HashMap<>();
private Map<UUID, Long> 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);
}
}

View File

@ -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<Objective> objectives = scoreboard.getObjectives().values();
scoreboard = new Scoreboard(session);
for (Objective objective : objectives) {
scoreboard.despawnObjective(objective);
}
}
}
}

View File

@ -34,7 +34,6 @@ import java.util.HashMap;
import java.util.Map;
public class Registry<T> {
private final Map<Class<? extends T>, PacketTranslator<? extends T>> MAP = new HashMap<>();
public static final Registry<Packet> JAVA = new Registry<>();
@ -48,14 +47,15 @@ public class Registry<T> {
BEDROCK.MAP.put(clazz, translator);
}
public <P extends T> void translate(Class<? extends P> clazz, P packet, GeyserSession session) {
public <P extends T> boolean translate(Class<? extends P> clazz, P packet, GeyserSession session) {
try {
if (MAP.containsKey(clazz)) {
((PacketTranslator<P>) 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;
}
}

View File

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

View File

@ -49,15 +49,17 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
return;
}
ClientPlayerPositionRotationPacket playerPositionRotationPacket = new ClientPlayerPositionRotationPacket(
packet.isOnGround(), packet.getPosition().getX(), Math.ceil((packet.getPosition().getY() - EntityType.PLAYER.getOffset()) * 2) / 2,
packet.getPosition().getZ(), packet.getRotation().getY(), packet.getRotation().getX());
double javaY = packet.getPosition().getY() - EntityType.PLAYER.getOffset();
entity.moveAbsolute(packet.getPosition(), packet.getRotation());
ClientPlayerPositionRotationPacket playerPositionRotationPacket = new ClientPlayerPositionRotationPacket(
packet.isOnGround(), packet.getPosition().getX(), Math.ceil(javaY * 2) / 2,
packet.getPosition().getZ(), packet.getRotation().getX(), packet.getRotation().getY());
entity.moveAbsolute(packet.getPosition().sub(0, javaY, 0), packet.getRotation());
boolean colliding = false;
Position position = new Position((int) packet.getPosition().getX(),
(int) Math.ceil((packet.getPosition().getY() - EntityType.PLAYER.getOffset()) * 2) / 2, (int) packet.getPosition().getZ());
(int) Math.ceil(javaY * 2) / 2, (int) packet.getPosition().getZ());
BedrockItem block = session.getChunkCache().getBlockAt(position);
if (!block.getIdentifier().contains("air"))

View File

@ -0,0 +1,77 @@
package org.geysermc.connector.network.translators.java;
import com.flowpowered.math.vector.Vector3f;
import com.github.steveice10.mc.protocol.packet.ingame.server.ServerBossBarPacket;
import com.nukkitx.protocol.bedrock.data.EntityData;
import com.nukkitx.protocol.bedrock.packet.AddEntityPacket;
import com.nukkitx.protocol.bedrock.packet.BossEventPacket;
import com.nukkitx.protocol.bedrock.packet.RemoveEntityPacket;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.utils.MessageUtils;
public class JavaBossBarTranslator extends PacketTranslator<ServerBossBarPacket> {
@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.<br>
* 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);
}
}

View File

@ -36,7 +36,10 @@ public class JavaEntityDestroyTranslator extends PacketTranslator<ServerEntityDe
public void translate(ServerEntityDestroyPacket packet, GeyserSession session) {
for (int entityId : packet.getEntityIds()) {
Entity entity = session.getEntityCache().getEntityByJavaId(entityId);
session.getEntityCache().removeEntity(entity);
if (entity != null) {
session.getEntityCache().removeEntity(entity);
}
}
}
}

View File

@ -40,8 +40,8 @@ public class JavaEntityHeadLookTranslator extends PacketTranslator<ServerEntityH
if (packet.getEntityId() == session.getPlayerEntity().getEntityId()) {
entity = session.getPlayerEntity();
}
if (entity == null)
return;
if (entity == null) return;
entity.setRotation(new Vector3f(entity.getRotation().getX(), entity.getRotation().getY(), packet.getHeadYaw()));

View File

@ -39,8 +39,7 @@ public class JavaEntityMetadataTranslator extends PacketTranslator<ServerEntityM
if (packet.getEntityId() == session.getPlayerEntity().getEntityId()) {
entity = session.getPlayerEntity();
}
if (entity == null)
return;
if (entity == null) return;
if (entity.isValid()) {
// TODO: Make this actually useful lol

View File

@ -39,8 +39,7 @@ public class JavaEntityPositionRotationTranslator extends PacketTranslator<Serve
if (packet.getEntityId() == session.getPlayerEntity().getEntityId()) {
entity = session.getPlayerEntity();
}
if (entity == null)
return;
if (entity == null) return;
entity.moveRelative(packet.getMovementX(), packet.getMovementY(), packet.getMovementZ(), packet.getPitch(), packet.getYaw());

View File

@ -39,8 +39,7 @@ public class JavaEntityPositionTranslator extends PacketTranslator<ServerEntityP
if (packet.getEntityId() == session.getPlayerEntity().getEntityId()) {
entity = session.getPlayerEntity();
}
if (entity == null)
return;
if (entity == null) return;
entity.moveRelative(packet.getMovementX(), packet.getMovementY(), packet.getMovementZ(), packet.getPitch(), packet.getYaw());

View File

@ -40,11 +40,7 @@ public class JavaEntityPropertiesTranslator extends PacketTranslator<ServerEntit
if (packet.getEntityId() == session.getPlayerEntity().getEntityId()) {
entity = session.getPlayerEntity();
}
if (entity == null)
return;
if (!entity.isValid())
return;
if (entity == null || !entity.isValid()) return;
for (Attribute attribute : packet.getAttributes()) {
switch (attribute.getType()) {

View File

@ -40,8 +40,7 @@ public class JavaEntityRotationTranslator extends PacketTranslator<ServerEntityR
if (packet.getEntityId() == session.getPlayerEntity().getEntityId()) {
entity = session.getPlayerEntity();
}
if (entity == null)
return;
if (entity == null) return;
entity.moveRelative(packet.getMovementX(), packet.getMovementY(), packet.getMovementZ(), packet.getPitch(), packet.getYaw());

View File

@ -40,8 +40,7 @@ public class JavaEntityVelocityTranslator extends PacketTranslator<ServerEntityV
if (packet.getEntityId() == session.getPlayerEntity().getEntityId()) {
entity = session.getPlayerEntity();
}
if (entity == null)
return;
if (entity == null) return;
entity.setMotion(new Vector3f(packet.getMotionX(), packet.getMotionY(), packet.getMotionZ()));

View File

@ -5,14 +5,13 @@ import com.github.steveice10.mc.protocol.data.game.PlayerListEntry;
import com.github.steveice10.mc.protocol.data.game.PlayerListEntryAction;
import com.github.steveice10.mc.protocol.packet.ingame.server.ServerPlayerListEntryPacket;
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import com.nukkitx.protocol.bedrock.packet.SetLocalPlayerAsInitializedPacket;
import org.geysermc.connector.entity.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.utils.ProvidedSkin;
import org.geysermc.connector.utils.ProvidedSkinData;
public class JavaPlayerListEntryTranslator extends PacketTranslator<ServerPlayerListEntryPacket> {
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<ServerPlayer
PlayerListPacket.Entry entry1 = new PlayerListPacket.Entry(entry.getProfile().getId());
if (packet.getAction() == PlayerListEntryAction.ADD_PLAYER) {
if (session.getPlayerEntity().getUuid().equals(entry.getProfile().getId())) continue;
long geyserId = session.getEntityCache().getNextEntityId().incrementAndGet();
SetLocalPlayerAsInitializedPacket initPacket = new SetLocalPlayerAsInitializedPacket();
initPacket.setRuntimeEntityId(geyserId);
session.getUpstream().sendPacket(initPacket);
session.getEntityCache().addPlayerEntity(new PlayerEntity(
entry.getProfile(),
-1,
@ -39,19 +43,23 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator<ServerPlayer
entry1.setName(entry.getProfile().getName());
entry1.setEntityId(geyserId);
entry1.setSkinId(providedSkinData.getSkinId());
entry1.setSkinId(entry.getProfile().getIdAsString());
entry1.setSkinData(providedSkin);
entry1.setCapeData(new byte[0]);
entry1.setGeometryName(providedSkinData.getGeometryId());
entry1.setGeometryData(providedSkinData.getGeometryDataEncoded());
entry1.setGeometryName("geometry.humanoid");
entry1.setGeometryData("");
entry1.setXuid("");
entry1.setPlatformChatId("WIN10");
entry1.setPlatformChatId("");
} else {
session.getEntityCache().removePlayerEntity(entry.getProfile().getId());
PlayerEntity entity = session.getEntityCache().getPlayerEntity(entry.getProfile().getId());
if (entity != null && entity.isValid()) { // we'll despawn it manually ;-)
session.getEntityCache().removeEntity(entity);
} else { // just remove it from caching
session.getEntityCache().removePlayerEntity(entry.getProfile().getId());
}
}
translate.getEntries().add(entry1);
}
session.getUpstream().sendPacket(translate);
}
}

View File

@ -26,11 +26,18 @@
package org.geysermc.connector.network.translators.java.entity.spawn;
import com.flowpowered.math.vector.Vector3f;
import com.github.steveice10.mc.auth.data.GameProfile;
import com.github.steveice10.mc.protocol.packet.ingame.server.entity.spawn.ServerSpawnPlayerPacket;
import com.google.gson.JsonObject;
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import org.apache.commons.codec.Charsets;
import org.geysermc.api.Geyser;
import org.geysermc.connector.entity.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.utils.SkinProvider;
import java.util.Base64;
public class JavaSpawnPlayerTranslator extends PacketTranslator<ServerSpawnPlayerPacket> {
@ -41,7 +48,7 @@ public class JavaSpawnPlayerTranslator extends PacketTranslator<ServerSpawnPlaye
PlayerEntity entity = session.getEntityCache().getPlayerEntity(packet.getUUID());
if (entity == null) {
Geyser.getLogger().error("Haven't received PlayerListEntry packet before spawning player! We ignore the player");
Geyser.getLogger().error("Haven't received PlayerListEntry packet before spawning player! We ignore the player " + packet.getUUID());
return;
}
@ -50,5 +57,57 @@ public class JavaSpawnPlayerTranslator extends PacketTranslator<ServerSpawnPlaye
entity.setRotation(rotation);
session.getEntityCache().spawnEntity(entity);
// request skin and cape
Geyser.getGeneralThreadPool().execute(() -> {
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();
});
}
}

View File

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

View File

@ -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<ServerScoreboardObjectivePacket> {
@ -39,18 +39,21 @@ public class JavaScoreboardObjectiveTranslator extends PacketTranslator<ServerSc
public void translate(ServerScoreboardObjectivePacket packet, GeyserSession session) {
try {
ScoreboardCache cache = session.getScoreboardCache();
Scoreboard scoreboard = new Scoreboard(session);
if (cache.getScoreboard() != null)
scoreboard = cache.getScoreboard();
Scoreboard scoreboard = cache.getScoreboard();
if (scoreboard.getObjective(packet.getName()) == null) {
// whoops sent objective to early, ignore it
return;
}
switch (packet.getAction()) {
case ADD:
ScoreboardObjective objective = scoreboard.registerNewObjective(packet.getName());
objective.setDisplaySlot(ScoreboardObjective.DisplaySlot.SIDEBAR);
Objective objective = scoreboard.getObjective(packet.getName());
objective.setDisplayName(MessageUtils.getBedrockMessage(packet.getDisplayName()));
objective.setType(packet.getType().ordinal());
break;
case UPDATE:
ScoreboardObjective updateObj = scoreboard.getObjective(packet.getName());
Objective updateObj = scoreboard.getObjective(packet.getName());
updateObj.setDisplayName(MessageUtils.getBedrockMessage(packet.getDisplayName()));
break;
case REMOVE:
@ -59,7 +62,6 @@ public class JavaScoreboardObjectiveTranslator extends PacketTranslator<ServerSc
}
scoreboard.onUpdate();
cache.setScoreboard(scoreboard);
} catch (Exception ex) {
ex.printStackTrace();
}

View File

@ -26,13 +26,52 @@
package org.geysermc.connector.network.translators.java.scoreboard;
import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerTeamPacket;
import org.geysermc.api.Geyser;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.scoreboard.Scoreboard;
import org.geysermc.connector.scoreboard.UpdateType;
import org.geysermc.connector.utils.MessageUtils;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class JavaTeamTranslator extends PacketTranslator<ServerTeamPacket> {
@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<String> toPlayerSet(String[] players) {
return new HashSet<>(Arrays.asList(players));
}
}

View File

@ -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<ServerUpdateScorePacket> {
@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();

View File

@ -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<String, Score> 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;
}
}

View File

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

View File

@ -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<String, ScoreboardObjective> objectiveMap = new HashMap<String, ScoreboardObjective>();
private Map<String, Objective> objectives = new HashMap<>();
private Map<String, Team> 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<String> 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<String, Score> fakeMap = new HashMap<String, Score>();
for (Map.Entry<String, Score> 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<Objective> changedObjectives = new HashSet<>();
List<ScoreInfo> addScores = new ArrayList<>();
List<ScoreInfo> 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<ScoreInfo> 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;
}
}

View File

@ -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<String, Score> scores = new HashMap<String, Score>();
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<String, Score> 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;
}
}

View File

@ -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<String> entities = new HashSet<>();
public Team(Scoreboard scoreboard, String id) {
this.scoreboard = scoreboard;
this.id = id;
}
public void addEntities(String... names) {
List<String> 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<String> 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);
}
}
}
}
}

View File

@ -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!<br>
* Only used in {@link Objective Objective}
*/
UPDATE
}

View File

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

View File

@ -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<UUID, Skin> cachedSkins = new ConcurrentHashMap<>();
private static Map<String, Cape> 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<UUID, CompletableFuture<Skin>> requestedSkins = new ConcurrentHashMap<>();
private static Map<String, CompletableFuture<Cape>> 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<SkinAndCape> 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<Skin> 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<Skin> 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<Cape> 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<Cape> 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> T getOrDefault(CompletableFuture<T> future, T defaultValue, int timeoutInSeconds) {
try {
return future.get(timeoutInSeconds, TimeUnit.SECONDS);
} catch (Exception ignored) {}
return defaultValue;
}
}

View File

@ -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"
}
]
}
}
}