forked from GeyserMC/Geyser
Custom skull block support (#683)
Custom skulls are now implemented within the world when placed as a block. This is achieved by placing a fake player entity in the same spot. Co-authored-by: DoctorMacc <toy.fighter1@gmail.com> Co-authored-by: bundabrg <brendan@grieve.com.au> Co-authored-by: bundabrg <bundabrg@grieve.com.au> Co-authored-by: Camotoy <20743703+DoctorMacc@users.noreply.github.com>
This commit is contained in:
parent
2067143b57
commit
2c0f3ec84d
24 changed files with 663 additions and 134 deletions
|
@ -55,6 +55,7 @@ import org.geysermc.connector.network.translators.world.WorldManager;
|
||||||
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
|
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
|
||||||
import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
|
import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
|
||||||
import org.geysermc.connector.network.translators.collision.CollisionTranslator;
|
import org.geysermc.connector.network.translators.collision.CollisionTranslator;
|
||||||
|
import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator;
|
||||||
import org.geysermc.connector.utils.DimensionUtils;
|
import org.geysermc.connector.utils.DimensionUtils;
|
||||||
import org.geysermc.connector.utils.LanguageUtils;
|
import org.geysermc.connector.utils.LanguageUtils;
|
||||||
import org.geysermc.connector.utils.LocaleUtils;
|
import org.geysermc.connector.utils.LocaleUtils;
|
||||||
|
@ -78,7 +79,11 @@ import java.util.concurrent.TimeUnit;
|
||||||
@Getter
|
@Getter
|
||||||
public class GeyserConnector {
|
public class GeyserConnector {
|
||||||
|
|
||||||
public static final ObjectMapper JSON_MAPPER = new ObjectMapper().enable(JsonParser.Feature.IGNORE_UNDEFINED).enable(JsonParser.Feature.ALLOW_COMMENTS).disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
public static final ObjectMapper JSON_MAPPER = new ObjectMapper()
|
||||||
|
.enable(JsonParser.Feature.IGNORE_UNDEFINED)
|
||||||
|
.enable(JsonParser.Feature.ALLOW_COMMENTS)
|
||||||
|
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
|
||||||
|
.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES);
|
||||||
|
|
||||||
public static final String NAME = "Geyser";
|
public static final String NAME = "Geyser";
|
||||||
public static final String VERSION = "DEV"; // A fallback for running in IDEs
|
public static final String VERSION = "DEV"; // A fallback for running in IDEs
|
||||||
|
@ -182,6 +187,7 @@ public class GeyserConnector {
|
||||||
authType = AuthType.getByName(config.getRemote().getAuthType());
|
authType = AuthType.getByName(config.getRemote().getAuthType());
|
||||||
|
|
||||||
DimensionUtils.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether
|
DimensionUtils.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether
|
||||||
|
SkullBlockEntityTranslator.ALLOW_CUSTOM_SKULLS = config.isAllowCustomSkulls();
|
||||||
|
|
||||||
// https://github.com/GeyserMC/Geyser/issues/957
|
// https://github.com/GeyserMC/Geyser/issues/957
|
||||||
RakNetConstants.MAXIMUM_MTU_SIZE = (short) config.getMtu();
|
RakNetConstants.MAXIMUM_MTU_SIZE = (short) config.getMtu();
|
||||||
|
@ -255,7 +261,7 @@ public class GeyserConnector {
|
||||||
message += LanguageUtils.getLocaleStringLog("geyser.core.finish.console");
|
message += LanguageUtils.getLocaleStringLog("geyser.core.finish.console");
|
||||||
}
|
}
|
||||||
logger.info(message);
|
logger.info(message);
|
||||||
|
|
||||||
if (platformType == PlatformType.STANDALONE) {
|
if (platformType == PlatformType.STANDALONE) {
|
||||||
logger.warning(LanguageUtils.getLocaleStringLog("geyser.core.movement_warn"));
|
logger.warning(LanguageUtils.getLocaleStringLog("geyser.core.movement_warn"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,8 @@ public interface GeyserConfiguration {
|
||||||
|
|
||||||
int getCacheImages();
|
int getCacheImages();
|
||||||
|
|
||||||
|
boolean isAllowCustomSkulls();
|
||||||
|
|
||||||
IMetricsInfo getMetrics();
|
IMetricsInfo getMetrics();
|
||||||
|
|
||||||
interface IBedrockConfiguration {
|
interface IBedrockConfiguration {
|
||||||
|
@ -107,7 +109,7 @@ public interface GeyserConfiguration {
|
||||||
String getAddress();
|
String getAddress();
|
||||||
|
|
||||||
int getPort();
|
int getPort();
|
||||||
|
|
||||||
void setAddress(String address);
|
void setAddress(String address);
|
||||||
|
|
||||||
void setPort(int port);
|
void setPort(int port);
|
||||||
|
|
|
@ -101,6 +101,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
|
||||||
@JsonProperty("cache-images")
|
@JsonProperty("cache-images")
|
||||||
private int cacheImages = 0;
|
private int cacheImages = 0;
|
||||||
|
|
||||||
|
@JsonProperty("allow-custom-skulls")
|
||||||
|
private boolean allowCustomSkulls = true;
|
||||||
|
|
||||||
@JsonProperty("above-bedrock-nether-building")
|
@JsonProperty("above-bedrock-nether-building")
|
||||||
private boolean aboveBedrockNetherBuilding = false;
|
private boolean aboveBedrockNetherBuilding = false;
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,6 @@ public class PlayerEntity extends LivingEntity {
|
||||||
private GameProfile profile;
|
private GameProfile profile;
|
||||||
private UUID uuid;
|
private UUID uuid;
|
||||||
private String username;
|
private String username;
|
||||||
private long lastSkinUpdate = -1;
|
|
||||||
private boolean playerList = true; // Player is in the player list
|
private boolean playerList = true; // Player is in the player list
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2020 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.entity.player;
|
||||||
|
|
||||||
|
import com.github.steveice10.mc.auth.data.GameProfile;
|
||||||
|
import com.nukkitx.math.vector.Vector3f;
|
||||||
|
import com.nukkitx.math.vector.Vector3i;
|
||||||
|
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
|
||||||
|
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper to handle skulls more effectively - skulls have to be treated as entities since there are no
|
||||||
|
* custom player skulls in Bedrock.
|
||||||
|
*/
|
||||||
|
public class SkullPlayerEntity extends PlayerEntity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the block state that the skull is associated with. Used to determine if the block in the skull's position
|
||||||
|
* has changed
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private int blockState;
|
||||||
|
|
||||||
|
public SkullPlayerEntity(GameProfile gameProfile, long geyserId, Vector3f position, Vector3f rotation) {
|
||||||
|
super(gameProfile, 0, geyserId, position, Vector3f.ZERO, rotation);
|
||||||
|
setPlayerList(false);
|
||||||
|
|
||||||
|
//Set bounding box to almost nothing so the skull is able to be broken and not cause entity to cast a shadow
|
||||||
|
metadata.clear();
|
||||||
|
metadata.put(EntityData.SCALE, 1.08f);
|
||||||
|
metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.001f);
|
||||||
|
metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.001f);
|
||||||
|
metadata.getOrCreateFlags().setFlag(EntityFlag.CAN_SHOW_NAME, false);
|
||||||
|
metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true); // Until the skin is loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
public void despawnEntity(GeyserSession session, Vector3i position) {
|
||||||
|
this.despawnEntity(session);
|
||||||
|
session.getSkullCache().remove(position, this);
|
||||||
|
}
|
||||||
|
}
|
|
@ -67,6 +67,7 @@ import org.geysermc.connector.GeyserConnector;
|
||||||
import org.geysermc.connector.command.CommandSender;
|
import org.geysermc.connector.command.CommandSender;
|
||||||
import org.geysermc.connector.common.AuthType;
|
import org.geysermc.connector.common.AuthType;
|
||||||
import org.geysermc.connector.entity.Entity;
|
import org.geysermc.connector.entity.Entity;
|
||||||
|
import org.geysermc.connector.entity.player.SkullPlayerEntity;
|
||||||
import org.geysermc.connector.entity.player.SessionPlayerEntity;
|
import org.geysermc.connector.entity.player.SessionPlayerEntity;
|
||||||
import org.geysermc.connector.inventory.PlayerInventory;
|
import org.geysermc.connector.inventory.PlayerInventory;
|
||||||
import org.geysermc.connector.network.translators.chat.MessageTranslator;
|
import org.geysermc.connector.network.translators.chat.MessageTranslator;
|
||||||
|
@ -80,6 +81,7 @@ import org.geysermc.connector.network.translators.PacketTranslatorRegistry;
|
||||||
import org.geysermc.connector.network.translators.collision.CollisionManager;
|
import org.geysermc.connector.network.translators.collision.CollisionManager;
|
||||||
import org.geysermc.connector.network.translators.inventory.EnchantmentInventoryTranslator;
|
import org.geysermc.connector.network.translators.inventory.EnchantmentInventoryTranslator;
|
||||||
import org.geysermc.connector.network.translators.item.ItemRegistry;
|
import org.geysermc.connector.network.translators.item.ItemRegistry;
|
||||||
|
import org.geysermc.connector.skin.SkinManager;
|
||||||
import org.geysermc.connector.utils.*;
|
import org.geysermc.connector.utils.*;
|
||||||
import org.geysermc.floodgate.util.BedrockData;
|
import org.geysermc.floodgate.util.BedrockData;
|
||||||
import org.geysermc.floodgate.util.EncryptionUtil;
|
import org.geysermc.floodgate.util.EncryptionUtil;
|
||||||
|
@ -90,6 +92,7 @@ import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.spec.InvalidKeySpecException;
|
import java.security.spec.InvalidKeySpecException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
@ -121,7 +124,7 @@ public class GeyserSession implements CommandSender {
|
||||||
*/
|
*/
|
||||||
private final CollisionManager collisionManager;
|
private final CollisionManager collisionManager;
|
||||||
|
|
||||||
@Getter
|
private final Map<Vector3i, SkullPlayerEntity> skullCache = new ConcurrentHashMap<>();
|
||||||
private final Long2ObjectMap<ClientboundMapItemDataPacket> storedMaps = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>());
|
private final Long2ObjectMap<ClientboundMapItemDataPacket> storedMaps = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -531,7 +534,7 @@ public class GeyserSession implements CommandSender {
|
||||||
|
|
||||||
// Check if they are not using a linked account
|
// Check if they are not using a linked account
|
||||||
if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) {
|
if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) {
|
||||||
SkinUtils.handleBedrockSkin(playerEntity, clientData);
|
SkinManager.handleBedrockSkin(playerEntity, clientData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,13 +25,14 @@
|
||||||
|
|
||||||
package org.geysermc.connector.network.translators.bedrock;
|
package org.geysermc.connector.network.translators.bedrock;
|
||||||
|
|
||||||
|
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
||||||
|
import com.nukkitx.protocol.bedrock.packet.SetLocalPlayerAsInitializedPacket;
|
||||||
import org.geysermc.connector.entity.player.PlayerEntity;
|
import org.geysermc.connector.entity.player.PlayerEntity;
|
||||||
import org.geysermc.connector.network.session.GeyserSession;
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
import org.geysermc.connector.network.translators.PacketTranslator;
|
import org.geysermc.connector.network.translators.PacketTranslator;
|
||||||
import org.geysermc.connector.network.translators.Translator;
|
import org.geysermc.connector.network.translators.Translator;
|
||||||
import org.geysermc.connector.utils.SkinUtils;
|
import org.geysermc.connector.skin.SkinManager;
|
||||||
|
import org.geysermc.connector.skin.SkullSkinManager;
|
||||||
import com.nukkitx.protocol.bedrock.packet.SetLocalPlayerAsInitializedPacket;
|
|
||||||
|
|
||||||
@Translator(packet = SetLocalPlayerAsInitializedPacket.class)
|
@Translator(packet = SetLocalPlayerAsInitializedPacket.class)
|
||||||
public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslator<SetLocalPlayerAsInitializedPacket> {
|
public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslator<SetLocalPlayerAsInitializedPacket> {
|
||||||
|
@ -44,10 +45,20 @@ public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslat
|
||||||
|
|
||||||
for (PlayerEntity entity : session.getEntityCache().getEntitiesByType(PlayerEntity.class)) {
|
for (PlayerEntity entity : session.getEntityCache().getEntitiesByType(PlayerEntity.class)) {
|
||||||
if (!entity.isValid()) {
|
if (!entity.isValid()) {
|
||||||
SkinUtils.requestAndHandleSkinAndCape(entity, session, null);
|
SkinManager.requestAndHandleSkinAndCape(entity, session, null);
|
||||||
entity.sendPlayer(session);
|
entity.sendPlayer(session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send Skulls
|
||||||
|
for (PlayerEntity entity : session.getSkullCache().values()) {
|
||||||
|
entity.spawnEntity(session);
|
||||||
|
|
||||||
|
SkullSkinManager.requestAndHandleSkin(entity, session, (skin) -> {
|
||||||
|
entity.getMetadata().getFlags().setFlag(EntityFlag.INVISIBLE, false);
|
||||||
|
entity.updateBedrockMetadata(session);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2020 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.network.translators.item.translators.nbt;
|
||||||
|
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||||
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
|
import org.geysermc.connector.network.translators.ItemRemapper;
|
||||||
|
import org.geysermc.connector.network.translators.item.ItemEntry;
|
||||||
|
import org.geysermc.connector.network.translators.item.NbtItemStackTranslator;
|
||||||
|
import org.geysermc.connector.utils.LocaleUtils;
|
||||||
|
|
||||||
|
@ItemRemapper
|
||||||
|
public class PlayerHeadTranslator extends NbtItemStackTranslator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) {
|
||||||
|
if (!itemTag.contains("display") || !((CompoundTag) itemTag.get("display")).contains("name")) {
|
||||||
|
if (itemTag.contains("SkullOwner")) {
|
||||||
|
StringTag name;
|
||||||
|
Tag skullOwner = itemTag.get("SkullOwner");
|
||||||
|
if (skullOwner instanceof StringTag) {
|
||||||
|
name = (StringTag) skullOwner;
|
||||||
|
} else {
|
||||||
|
StringTag skullName;
|
||||||
|
if (skullOwner instanceof CompoundTag && (skullName = ((CompoundTag) skullOwner).get("Name")) != null) {
|
||||||
|
name = skullName;
|
||||||
|
} else {
|
||||||
|
session.getConnector().getLogger().debug("Not sure how to handle skull head item display. " + itemTag);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add correct name of player skull
|
||||||
|
// TODO: It's always yellow, even with a custom name. Handle?
|
||||||
|
String displayName = "\u00a7r\u00a7e" + LocaleUtils.getLocaleString("block.minecraft.player_head.named", session.getLocale()).replace("%s", name.getValue());
|
||||||
|
if (!itemTag.contains("display")) {
|
||||||
|
itemTag.put(new CompoundTag("display"));
|
||||||
|
}
|
||||||
|
((CompoundTag) itemTag.get("display")).put(new StringTag("Name", displayName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean acceptItem(ItemEntry itemEntry) {
|
||||||
|
return itemEntry.getJavaIdentifier().equals("minecraft:player_head");
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ import org.geysermc.connector.entity.player.PlayerEntity;
|
||||||
import org.geysermc.connector.network.session.GeyserSession;
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
import org.geysermc.connector.network.translators.PacketTranslator;
|
import org.geysermc.connector.network.translators.PacketTranslator;
|
||||||
import org.geysermc.connector.network.translators.Translator;
|
import org.geysermc.connector.network.translators.Translator;
|
||||||
import org.geysermc.connector.utils.SkinUtils;
|
import org.geysermc.connector.skin.SkinManager;
|
||||||
|
|
||||||
import com.github.steveice10.mc.protocol.data.game.PlayerListEntry;
|
import com.github.steveice10.mc.protocol.data.game.PlayerListEntry;
|
||||||
import com.github.steveice10.mc.protocol.data.game.PlayerListEntryAction;
|
import com.github.steveice10.mc.protocol.data.game.PlayerListEntryAction;
|
||||||
|
@ -57,7 +57,8 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator<ServerPlayer
|
||||||
if (self) {
|
if (self) {
|
||||||
// Entity is ourself
|
// Entity is ourself
|
||||||
playerEntity = session.getPlayerEntity();
|
playerEntity = session.getPlayerEntity();
|
||||||
SkinUtils.requestAndHandleSkinAndCape(playerEntity, session, skinAndCape ->
|
//TODO: playerEntity.setProfile(entry.getProfile()); seems to help with online mode skins but needs more testing to ensure Floodgate skins aren't overwritten
|
||||||
|
SkinManager.requestAndHandleSkinAndCape(playerEntity, session, skinAndCape ->
|
||||||
GeyserConnector.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data"));
|
GeyserConnector.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data"));
|
||||||
} else {
|
} else {
|
||||||
playerEntity = session.getEntityCache().getPlayerEntity(entry.getProfile().getId());
|
playerEntity = session.getEntityCache().getPlayerEntity(entry.getProfile().getId());
|
||||||
|
@ -81,7 +82,7 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator<ServerPlayer
|
||||||
playerEntity.setPlayerList(true);
|
playerEntity.setPlayerList(true);
|
||||||
playerEntity.setValid(true);
|
playerEntity.setValid(true);
|
||||||
|
|
||||||
PlayerListPacket.Entry playerListEntry = SkinUtils.buildCachedEntry(session, entry.getProfile(), playerEntity.getGeyserId());
|
PlayerListPacket.Entry playerListEntry = SkinManager.buildCachedEntry(session, playerEntity);
|
||||||
|
|
||||||
translate.getEntries().add(playerListEntry);
|
translate.getEntries().add(playerListEntry);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -33,7 +33,7 @@ import org.geysermc.connector.network.session.GeyserSession;
|
||||||
import org.geysermc.connector.network.translators.PacketTranslator;
|
import org.geysermc.connector.network.translators.PacketTranslator;
|
||||||
import org.geysermc.connector.network.translators.Translator;
|
import org.geysermc.connector.network.translators.Translator;
|
||||||
import org.geysermc.connector.utils.LanguageUtils;
|
import org.geysermc.connector.utils.LanguageUtils;
|
||||||
import org.geysermc.connector.utils.SkinUtils;
|
import org.geysermc.connector.skin.SkinManager;
|
||||||
|
|
||||||
@Translator(packet = ServerSpawnPlayerPacket.class)
|
@Translator(packet = ServerSpawnPlayerPacket.class)
|
||||||
public class JavaSpawnPlayerTranslator extends PacketTranslator<ServerSpawnPlayerPacket> {
|
public class JavaSpawnPlayerTranslator extends PacketTranslator<ServerSpawnPlayerPacket> {
|
||||||
|
@ -62,7 +62,7 @@ public class JavaSpawnPlayerTranslator extends PacketTranslator<ServerSpawnPlaye
|
||||||
|
|
||||||
if (session.getUpstream().isInitialized()) {
|
if (session.getUpstream().isInitialized()) {
|
||||||
entity.sendPlayer(session);
|
entity.sendPlayer(session);
|
||||||
SkinUtils.requestAndHandleSkinAndCape(entity, session, null);
|
SkinManager.requestAndHandleSkinAndCape(entity, session, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,11 +25,13 @@
|
||||||
|
|
||||||
package org.geysermc.connector.network.translators.java.world;
|
package org.geysermc.connector.network.translators.java.world;
|
||||||
|
|
||||||
|
import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerUnloadChunkPacket;
|
||||||
|
import com.nukkitx.math.vector.Vector3i;
|
||||||
import org.geysermc.connector.network.session.GeyserSession;
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
import org.geysermc.connector.network.translators.PacketTranslator;
|
import org.geysermc.connector.network.translators.PacketTranslator;
|
||||||
import org.geysermc.connector.network.translators.Translator;
|
import org.geysermc.connector.network.translators.Translator;
|
||||||
|
|
||||||
import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerUnloadChunkPacket;
|
import java.util.Iterator;
|
||||||
|
|
||||||
@Translator(packet = ServerUnloadChunkPacket.class)
|
@Translator(packet = ServerUnloadChunkPacket.class)
|
||||||
public class JavaUnloadChunkTranslator extends PacketTranslator<ServerUnloadChunkPacket> {
|
public class JavaUnloadChunkTranslator extends PacketTranslator<ServerUnloadChunkPacket> {
|
||||||
|
@ -37,5 +39,15 @@ public class JavaUnloadChunkTranslator extends PacketTranslator<ServerUnloadChun
|
||||||
@Override
|
@Override
|
||||||
public void translate(ServerUnloadChunkPacket packet, GeyserSession session) {
|
public void translate(ServerUnloadChunkPacket packet, GeyserSession session) {
|
||||||
session.getChunkCache().removeChunk(packet.getX(), packet.getZ());
|
session.getChunkCache().removeChunk(packet.getX(), packet.getZ());
|
||||||
|
|
||||||
|
//Checks if a skull is in an unloaded chunk then removes it
|
||||||
|
Iterator<Vector3i> iterator = session.getSkullCache().keySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Vector3i position = iterator.next();
|
||||||
|
if (Math.floor(position.getX() / 16) == packet.getX() && Math.floor(position.getZ() / 16) == packet.getZ()) {
|
||||||
|
session.getSkullCache().get(position).despawnEntity(session);
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import org.geysermc.connector.network.session.GeyserSession;
|
||||||
import org.geysermc.connector.network.translators.PacketTranslator;
|
import org.geysermc.connector.network.translators.PacketTranslator;
|
||||||
import org.geysermc.connector.network.translators.Translator;
|
import org.geysermc.connector.network.translators.Translator;
|
||||||
import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
|
import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
|
||||||
|
import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator;
|
||||||
import org.geysermc.connector.utils.BlockEntityUtils;
|
import org.geysermc.connector.utils.BlockEntityUtils;
|
||||||
import org.geysermc.connector.utils.ChunkUtils;
|
import org.geysermc.connector.utils.ChunkUtils;
|
||||||
|
|
||||||
|
@ -64,6 +65,10 @@ public class JavaUpdateTileEntityTranslator extends PacketTranslator<ServerUpdat
|
||||||
// Cache chunks is not enabled; use block entity cache
|
// Cache chunks is not enabled; use block entity cache
|
||||||
ChunkUtils.CACHED_BLOCK_ENTITIES.removeInt(packet.getPosition());
|
ChunkUtils.CACHED_BLOCK_ENTITIES.removeInt(packet.getPosition());
|
||||||
BlockEntityUtils.updateBlockEntity(session, translator.getBlockEntityTag(id, packet.getNbt(), blockState), packet.getPosition());
|
BlockEntityUtils.updateBlockEntity(session, translator.getBlockEntityTag(id, packet.getNbt(), blockState), packet.getPosition());
|
||||||
|
// Check for custom skulls.
|
||||||
|
if (SkullBlockEntityTranslator.ALLOW_CUSTOM_SKULLS && packet.getNbt().contains("SkullOwner")) {
|
||||||
|
SkullBlockEntityTranslator.spawnPlayer(session, packet.getNbt(), blockState);
|
||||||
|
}
|
||||||
|
|
||||||
// If block entity is command block, OP permission level is appropriate, player is in creative mode and the NBT is not empty
|
// If block entity is command block, OP permission level is appropriate, player is in creative mode and the NBT is not empty
|
||||||
if (packet.getType() == UpdatedTileType.COMMAND_BLOCK && session.getOpPermissionLevel() >= 2 &&
|
if (packet.getType() == UpdatedTileType.COMMAND_BLOCK && session.getOpPermissionLevel() >= 2 &&
|
||||||
|
|
|
@ -47,6 +47,7 @@ public class BlockStateValues {
|
||||||
private static final Int2BooleanMap PISTON_VALUES = new Int2BooleanOpenHashMap();
|
private static final Int2BooleanMap PISTON_VALUES = new Int2BooleanOpenHashMap();
|
||||||
private static final Int2ByteMap SKULL_VARIANTS = new Int2ByteOpenHashMap();
|
private static final Int2ByteMap SKULL_VARIANTS = new Int2ByteOpenHashMap();
|
||||||
private static final Int2ByteMap SKULL_ROTATIONS = new Int2ByteOpenHashMap();
|
private static final Int2ByteMap SKULL_ROTATIONS = new Int2ByteOpenHashMap();
|
||||||
|
private static final Int2IntMap SKULL_WALL_DIRECTIONS = new Int2IntOpenHashMap();
|
||||||
private static final Int2ByteMap SHULKERBOX_DIRECTIONS = new Int2ByteOpenHashMap();
|
private static final Int2ByteMap SHULKERBOX_DIRECTIONS = new Int2ByteOpenHashMap();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,6 +111,26 @@ public class BlockStateValues {
|
||||||
SKULL_ROTATIONS.put(javaBlockState, (byte) skullRotation.intValue());
|
SKULL_ROTATIONS.put(javaBlockState, (byte) skullRotation.intValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entry.getKey().contains("wall_skull") || entry.getKey().contains("wall_head")) {
|
||||||
|
String direction = entry.getKey().substring(entry.getKey().lastIndexOf("facing=") + 7);
|
||||||
|
int rotation = 0;
|
||||||
|
switch (direction.substring(0, direction.length() - 1)) {
|
||||||
|
case "north":
|
||||||
|
rotation = 180;
|
||||||
|
break;
|
||||||
|
case "south":
|
||||||
|
rotation = 0;
|
||||||
|
break;
|
||||||
|
case "west":
|
||||||
|
rotation = 90;
|
||||||
|
break;
|
||||||
|
case "east":
|
||||||
|
rotation = 270;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
SKULL_WALL_DIRECTIONS.put(javaBlockState, rotation);
|
||||||
|
}
|
||||||
|
|
||||||
JsonNode shulkerDirection = entry.getValue().get("shulker_direction");
|
JsonNode shulkerDirection = entry.getValue().get("shulker_direction");
|
||||||
if (shulkerDirection != null) {
|
if (shulkerDirection != null) {
|
||||||
BlockStateValues.SHULKERBOX_DIRECTIONS.put(javaBlockState, (byte) shulkerDirection.intValue());
|
BlockStateValues.SHULKERBOX_DIRECTIONS.put(javaBlockState, (byte) shulkerDirection.intValue());
|
||||||
|
@ -222,6 +243,15 @@ public class BlockStateValues {
|
||||||
return SKULL_ROTATIONS.getOrDefault(state, (byte) -1);
|
return SKULL_ROTATIONS.getOrDefault(state, (byte) -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skull rotations are part of the namespaced ID in Java Edition, but part of the block entity tag in Bedrock.
|
||||||
|
* This gives a integer rotation that Bedrock can use.
|
||||||
|
*
|
||||||
|
* @return Skull wall rotation value with the blockstate
|
||||||
|
*/
|
||||||
|
public static Int2IntMap getSkullWallDirections() {
|
||||||
|
return SKULL_WALL_DIRECTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shulker box directions are part of the namespaced ID in Java Edition, but part of the block entity tag in Bedrock.
|
* Shulker box directions are part of the namespaced ID in Java Edition, but part of the block entity tag in Bedrock.
|
||||||
|
|
|
@ -1,50 +1,163 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
|
* Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
* in the Software without restriction, including without limitation the rights
|
* in the Software without restriction, including without limitation the rights
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
* furnished to do so, subject to the following conditions:
|
* furnished to do so, subject to the following conditions:
|
||||||
*
|
*
|
||||||
* The above copyright notice and this permission notice shall be included in
|
* The above copyright notice and this permission notice shall be included in
|
||||||
* all copies or substantial portions of the Software.
|
* all copies or substantial portions of the Software.
|
||||||
*
|
*
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
* 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
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
* THE SOFTWARE.
|
* THE SOFTWARE.
|
||||||
*
|
*
|
||||||
* @author GeyserMC
|
* @author GeyserMC
|
||||||
* @link https://github.com/GeyserMC/Geyser
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.geysermc.connector.network.translators.world.block.entity;
|
package org.geysermc.connector.network.translators.world.block.entity;
|
||||||
|
|
||||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
import com.github.steveice10.mc.auth.data.GameProfile;
|
||||||
import com.nukkitx.nbt.NbtMapBuilder;
|
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||||
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
|
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
||||||
@BlockEntity(name = "Skull", regex = "skull")
|
import com.nukkitx.math.vector.Vector3f;
|
||||||
public class SkullBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState {
|
import com.nukkitx.math.vector.Vector3i;
|
||||||
@Override
|
import com.nukkitx.nbt.NbtMapBuilder;
|
||||||
public boolean isBlock(int blockState) {
|
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
||||||
return BlockStateValues.getSkullVariant(blockState) != -1;
|
import org.geysermc.connector.entity.player.SkullPlayerEntity;
|
||||||
}
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
|
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
|
||||||
@Override
|
import org.geysermc.connector.skin.SkinProvider;
|
||||||
public void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState) {
|
import org.geysermc.connector.skin.SkullSkinManager;
|
||||||
byte skullVariant = BlockStateValues.getSkullVariant(blockState);
|
|
||||||
float rotation = BlockStateValues.getSkullRotation(blockState) * 22.5f;
|
import java.util.ArrayList;
|
||||||
// Just in case...
|
import java.util.LinkedHashMap;
|
||||||
if (skullVariant == -1) {
|
import java.util.List;
|
||||||
skullVariant = 0;
|
import java.util.UUID;
|
||||||
}
|
import java.util.concurrent.CompletableFuture;
|
||||||
builder.put("Rotation", rotation);
|
import java.util.concurrent.TimeUnit;
|
||||||
builder.put("SkullType", skullVariant);
|
|
||||||
}
|
@BlockEntity(name = "Skull", regex = "skull")
|
||||||
}
|
public class SkullBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState {
|
||||||
|
public static boolean ALLOW_CUSTOM_SKULLS;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isBlock(int blockState) {
|
||||||
|
return BlockStateValues.getSkullVariant(blockState) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState) {
|
||||||
|
byte skullVariant = BlockStateValues.getSkullVariant(blockState);
|
||||||
|
float rotation = BlockStateValues.getSkullRotation(blockState) * 22.5f;
|
||||||
|
// Just in case...
|
||||||
|
if (skullVariant == -1) {
|
||||||
|
skullVariant = 0;
|
||||||
|
}
|
||||||
|
builder.put("Rotation", rotation);
|
||||||
|
builder.put("SkullType", skullVariant);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<GameProfile> getProfile(CompoundTag tag) {
|
||||||
|
if (tag.contains("SkullOwner")) {
|
||||||
|
CompoundTag owner = tag.get("SkullOwner");
|
||||||
|
CompoundTag properties = owner.get("Properties");
|
||||||
|
if (properties == null) {
|
||||||
|
return SkinProvider.requestTexturesFromUsername(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
ListTag textures = properties.get("textures");
|
||||||
|
LinkedHashMap<?,?> tag1 = (LinkedHashMap<?,?>) textures.get(0).getValue();
|
||||||
|
StringTag texture = (StringTag) tag1.get("Value");
|
||||||
|
|
||||||
|
List<GameProfile.Property> profileProperties = new ArrayList<>();
|
||||||
|
|
||||||
|
GameProfile gameProfile = new GameProfile(UUID.randomUUID(), "");
|
||||||
|
profileProperties.add(new GameProfile.Property("textures", texture.getValue()));
|
||||||
|
gameProfile.setProperties(profileProperties);
|
||||||
|
return CompletableFuture.completedFuture(gameProfile);
|
||||||
|
}
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void spawnPlayer(GeyserSession session, CompoundTag tag, int blockState) {
|
||||||
|
int posX = (int) tag.get("x").getValue();
|
||||||
|
int posY = (int) tag.get("y").getValue();
|
||||||
|
int posZ = (int) tag.get("z").getValue();
|
||||||
|
float x = posX + .5f;
|
||||||
|
float y = posY - .01f;
|
||||||
|
float z = posZ + .5f;
|
||||||
|
float rotation;
|
||||||
|
|
||||||
|
byte floorRotation = BlockStateValues.getSkullRotation(blockState);
|
||||||
|
if (floorRotation == -1) {
|
||||||
|
// Wall skull
|
||||||
|
y += 0.25f;
|
||||||
|
rotation = BlockStateValues.getSkullWallDirections().get(blockState);
|
||||||
|
switch ((int) rotation) {
|
||||||
|
case 180:
|
||||||
|
// North
|
||||||
|
z += 0.24f;
|
||||||
|
break;
|
||||||
|
case 0:
|
||||||
|
// South
|
||||||
|
z -= 0.24f;
|
||||||
|
break;
|
||||||
|
case 90:
|
||||||
|
// West
|
||||||
|
x += 0.24f;
|
||||||
|
break;
|
||||||
|
case 270:
|
||||||
|
// East
|
||||||
|
x -= 0.24f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rotation = (180f + (floorRotation * 22.5f)) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3i blockPosition = Vector3i.from(posX, posY, posZ);
|
||||||
|
Vector3f entityPosition = Vector3f.from(x, y, z);
|
||||||
|
Vector3f entityRotation = Vector3f.from(rotation, 0, rotation);
|
||||||
|
long geyserId = session.getEntityCache().getNextEntityId().incrementAndGet();
|
||||||
|
|
||||||
|
getProfile(tag).whenComplete((gameProfile, throwable) -> {
|
||||||
|
if (gameProfile == null) {
|
||||||
|
session.getConnector().getLogger().debug("Custom skull with invalid SkullOwner tag: " + blockPosition.toString() + " " + tag.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SkullPlayerEntity existingSkull = session.getSkullCache().get(blockPosition);
|
||||||
|
if (existingSkull != null) {
|
||||||
|
// Ensure that two skulls can't spawn on the same point
|
||||||
|
existingSkull.despawnEntity(session, blockPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
SkullPlayerEntity player = new SkullPlayerEntity(gameProfile, geyserId, entityPosition, entityRotation);
|
||||||
|
player.setBlockState(blockState);
|
||||||
|
|
||||||
|
// Cache entity
|
||||||
|
session.getSkullCache().put(blockPosition, player);
|
||||||
|
|
||||||
|
// Only send to session if we are initialized, otherwise it will happen then.
|
||||||
|
if (session.getUpstream().isInitialized()) {
|
||||||
|
player.spawnEntity(session);
|
||||||
|
|
||||||
|
SkullSkinManager.requestAndHandleSkin(player, session, (skin -> session.getConnector().getGeneralThreadPool().schedule(() -> {
|
||||||
|
// Delay to minimize split-second "player" pop-in
|
||||||
|
player.getMetadata().getFlags().setFlag(EntityFlag.INVISIBLE, false);
|
||||||
|
player.updateBedrockMetadata(session);
|
||||||
|
}, 250, TimeUnit.MILLISECONDS)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
* @link https://github.com/GeyserMC/Geyser
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.geysermc.connector.utils;
|
package org.geysermc.connector.skin;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
|
@ -23,10 +23,9 @@
|
||||||
* @link https://github.com/GeyserMC/Geyser
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.geysermc.connector.utils;
|
package org.geysermc.connector.skin;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.github.steveice10.mc.auth.data.GameProfile;
|
import com.github.steveice10.mc.auth.data.GameProfile;
|
||||||
import com.nukkitx.protocol.bedrock.data.skin.ImageData;
|
import com.nukkitx.protocol.bedrock.data.skin.ImageData;
|
||||||
import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
|
import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
|
||||||
|
@ -38,6 +37,7 @@ import org.geysermc.connector.common.AuthType;
|
||||||
import org.geysermc.connector.entity.player.PlayerEntity;
|
import org.geysermc.connector.entity.player.PlayerEntity;
|
||||||
import org.geysermc.connector.network.session.GeyserSession;
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
import org.geysermc.connector.network.session.auth.BedrockClientData;
|
import org.geysermc.connector.network.session.auth.BedrockClientData;
|
||||||
|
import org.geysermc.connector.utils.LanguageUtils;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
@ -45,12 +45,11 @@ import java.util.Collections;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class SkinUtils {
|
public class SkinManager {
|
||||||
|
|
||||||
public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, GameProfile profile, long geyserId) {
|
public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, PlayerEntity playerEntity) {
|
||||||
GameProfileData data = GameProfileData.from(profile);
|
GameProfileData data = GameProfileData.from(playerEntity.getProfile());
|
||||||
SkinProvider.Cape cape = SkinProvider.getCachedCape(data.getCapeUrl());
|
SkinProvider.Cape cape = SkinProvider.getCachedCape(data.getCapeUrl());
|
||||||
|
|
||||||
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
|
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
|
||||||
|
|
||||||
SkinProvider.Skin skin = SkinProvider.getCachedSkin(data.getSkinUrl());
|
SkinProvider.Skin skin = SkinProvider.getCachedSkin(data.getSkinUrl());
|
||||||
|
@ -60,25 +59,24 @@ public class SkinUtils {
|
||||||
|
|
||||||
return buildEntryManually(
|
return buildEntryManually(
|
||||||
session,
|
session,
|
||||||
profile.getId(),
|
playerEntity.getProfile().getId(),
|
||||||
profile.getName(),
|
playerEntity.getProfile().getName(),
|
||||||
geyserId,
|
playerEntity.getGeyserId(),
|
||||||
skin.getTextureUrl(),
|
skin.getTextureUrl(),
|
||||||
skin.getSkinData(),
|
skin.getSkinData(),
|
||||||
cape.getCapeId(),
|
cape.getCapeId(),
|
||||||
cape.getCapeData(),
|
cape.getCapeData(),
|
||||||
geometry.getGeometryName(),
|
geometry
|
||||||
geometry.getGeometryData()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId,
|
public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId,
|
||||||
String skinId, byte[] skinData,
|
String skinId, byte[] skinData,
|
||||||
String capeId, byte[] capeData,
|
String capeId, byte[] capeData,
|
||||||
String geometryName, String geometryData) {
|
SkinProvider.SkinGeometry geometry) {
|
||||||
SerializedSkin serializedSkin = SerializedSkin.of(
|
SerializedSkin serializedSkin = SerializedSkin.of(
|
||||||
skinId, geometryName, ImageData.of(skinData), Collections.emptyList(),
|
skinId, geometry.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
|
||||||
ImageData.of(capeData), geometryData, "", true, false, !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId
|
ImageData.of(capeData), geometry.getGeometryData(), "", true, false, !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId
|
||||||
);
|
);
|
||||||
|
|
||||||
// This attempts to find the xuid of the player so profile images show up for xbox accounts
|
// This attempts to find the xuid of the player so profile images show up for xbox accounts
|
||||||
|
@ -119,11 +117,11 @@ public class SkinUtils {
|
||||||
try {
|
try {
|
||||||
SkinProvider.Skin skin = skinAndCape.getSkin();
|
SkinProvider.Skin skin = skinAndCape.getSkin();
|
||||||
SkinProvider.Cape cape = skinAndCape.getCape();
|
SkinProvider.Cape cape = skinAndCape.getCape();
|
||||||
|
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
|
||||||
|
|
||||||
if (cape.isFailed()) {
|
if (cape.isFailed()) {
|
||||||
cape = SkinProvider.getOrDefault(SkinProvider.requestBedrockCape(
|
cape = SkinProvider.getOrDefault(SkinProvider.requestBedrockCape(entity.getUuid()),
|
||||||
entity.getUuid(), false
|
SkinProvider.EMPTY_CAPE, 3);
|
||||||
), SkinProvider.EMPTY_CAPE, 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cape.isFailed() && SkinProvider.ALLOW_THIRD_PARTY_CAPES) {
|
if (cape.isFailed() && SkinProvider.ALLOW_THIRD_PARTY_CAPES) {
|
||||||
|
@ -133,9 +131,8 @@ public class SkinUtils {
|
||||||
), SkinProvider.EMPTY_CAPE, SkinProvider.CapeProvider.VALUES.length * 3);
|
), SkinProvider.EMPTY_CAPE, SkinProvider.CapeProvider.VALUES.length * 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
|
|
||||||
geometry = SkinProvider.getOrDefault(SkinProvider.requestBedrockGeometry(
|
geometry = SkinProvider.getOrDefault(SkinProvider.requestBedrockGeometry(
|
||||||
geometry, entity.getUuid(), false
|
geometry, entity.getUuid()
|
||||||
), geometry, 3);
|
), geometry, 3);
|
||||||
|
|
||||||
// Not a bedrock player check for ears
|
// Not a bedrock player check for ears
|
||||||
|
@ -165,8 +162,6 @@ public class SkinUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.setLastSkinUpdate(skin.getRequestedOn());
|
|
||||||
|
|
||||||
if (session.getUpstream().isInitialized()) {
|
if (session.getUpstream().isInitialized()) {
|
||||||
PlayerListPacket.Entry updatedEntry = buildEntryManually(
|
PlayerListPacket.Entry updatedEntry = buildEntryManually(
|
||||||
session,
|
session,
|
||||||
|
@ -177,8 +172,7 @@ public class SkinUtils {
|
||||||
skin.getSkinData(),
|
skin.getSkinData(),
|
||||||
cape.getCapeId(),
|
cape.getCapeId(),
|
||||||
cape.getCapeData(),
|
cape.getCapeData(),
|
||||||
geometry.getGeometryName(),
|
geometry
|
||||||
geometry.getGeometryData()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
@ -252,7 +246,7 @@ public class SkinUtils {
|
||||||
GameProfile.Property skinProperty = profile.getProperty("textures");
|
GameProfile.Property skinProperty = profile.getProperty("textures");
|
||||||
|
|
||||||
// TODO: Remove try/catch here
|
// TODO: Remove try/catch here
|
||||||
JsonNode skinObject = new ObjectMapper().readTree(new String(Base64.getDecoder().decode(skinProperty.getValue()), StandardCharsets.UTF_8));
|
JsonNode skinObject = GeyserConnector.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(skinProperty.getValue()), StandardCharsets.UTF_8));
|
||||||
JsonNode textures = skinObject.get("textures");
|
JsonNode textures = skinObject.get("textures");
|
||||||
|
|
||||||
JsonNode skinTexture = textures.get("SKIN");
|
JsonNode skinTexture = textures.get("SKIN");
|
|
@ -23,10 +23,14 @@
|
||||||
* @link https://github.com/GeyserMC/Geyser
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.geysermc.connector.utils;
|
package org.geysermc.connector.skin;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.github.steveice10.mc.auth.data.GameProfile;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.IntArrayTag;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||||
import com.google.common.cache.Cache;
|
import com.google.common.cache.Cache;
|
||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.cache.CacheBuilder;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
@ -34,15 +38,20 @@ import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import org.geysermc.connector.GeyserConnector;
|
import org.geysermc.connector.GeyserConnector;
|
||||||
import org.geysermc.connector.network.session.GeyserSession;
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
|
import org.geysermc.connector.utils.FileUtils;
|
||||||
|
import org.geysermc.connector.utils.WebUtils;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.*;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
@ -76,40 +85,20 @@ public class SkinProvider {
|
||||||
public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserConnector.getInstance().getConfig().isAllowThirdPartyEars();
|
public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserConnector.getInstance().getConfig().isAllowThirdPartyEars();
|
||||||
public static String EARS_GEOMETRY;
|
public static String EARS_GEOMETRY;
|
||||||
public static String EARS_GEOMETRY_SLIM;
|
public static String EARS_GEOMETRY_SLIM;
|
||||||
|
public static SkinGeometry SKULL_GEOMETRY;
|
||||||
|
|
||||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
/* Load in the normal ears geometry */
|
/* Load in the normal ears geometry */
|
||||||
InputStream earsStream = FileUtils.getResource("bedrock/skin/geometry.humanoid.ears.json");
|
EARS_GEOMETRY = new String(FileUtils.readAllBytes(FileUtils.getResource("bedrock/skin/geometry.humanoid.ears.json")), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
StringBuilder earsDataBuilder = new StringBuilder();
|
|
||||||
try (Reader reader = new BufferedReader(new InputStreamReader(earsStream, Charset.forName(StandardCharsets.UTF_8.name())))) {
|
|
||||||
int c = 0;
|
|
||||||
while ((c = reader.read()) != -1) {
|
|
||||||
earsDataBuilder.append((char) c);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new AssertionError("Unable to load ears geometry", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
EARS_GEOMETRY = earsDataBuilder.toString();
|
|
||||||
|
|
||||||
|
|
||||||
/* Load in the slim ears geometry */
|
/* Load in the slim ears geometry */
|
||||||
earsStream = FileUtils.getResource("bedrock/skin/geometry.humanoid.earsSlim.json");
|
EARS_GEOMETRY_SLIM = new String(FileUtils.readAllBytes(FileUtils.getResource("bedrock/skin/geometry.humanoid.earsSlim.json")), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
earsDataBuilder = new StringBuilder();
|
/* Load in the custom skull geometry */
|
||||||
try (Reader reader = new BufferedReader(new InputStreamReader(earsStream, Charset.forName(StandardCharsets.UTF_8.name())))) {
|
String skullData = new String(FileUtils.readAllBytes(FileUtils.getResource("bedrock/skin/geometry.humanoid.customskull.json")), StandardCharsets.UTF_8);
|
||||||
int c = 0;
|
SKULL_GEOMETRY = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.customskull\"}}", skullData, false);
|
||||||
while ((c = reader.read()) != -1) {
|
|
||||||
earsDataBuilder.append((char) c);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new AssertionError("Unable to load ears geometry", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
EARS_GEOMETRY_SLIM = earsDataBuilder.toString();
|
|
||||||
|
|
||||||
// Schedule Daily Image Expiry if we are caching them
|
// Schedule Daily Image Expiry if we are caching them
|
||||||
if (GeyserConnector.getInstance().getConfig().getCacheImages() > 0) {
|
if (GeyserConnector.getInstance().getConfig().getCacheImages() > 0) {
|
||||||
|
@ -205,7 +194,6 @@ public class SkinProvider {
|
||||||
if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE);
|
if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE);
|
||||||
if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested
|
if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested
|
||||||
|
|
||||||
boolean officialCape = provider == CapeProvider.MINECRAFT;
|
|
||||||
Cape cachedCape = cachedCapes.getIfPresent(capeUrl);
|
Cape cachedCape = cachedCapes.getIfPresent(capeUrl);
|
||||||
if (cachedCape != null) {
|
if (cachedCape != null) {
|
||||||
return CompletableFuture.completedFuture(cachedCape);
|
return CompletableFuture.completedFuture(cachedCape);
|
||||||
|
@ -280,7 +268,7 @@ public class SkinProvider {
|
||||||
return CompletableFuture.completedFuture(officialSkin);
|
return CompletableFuture.completedFuture(officialSkin);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<Cape> requestBedrockCape(UUID playerID, boolean newThread) {
|
public static CompletableFuture<Cape> requestBedrockCape(UUID playerID) {
|
||||||
Cape bedrockCape = cachedCapes.getIfPresent(playerID.toString() + ".Bedrock");
|
Cape bedrockCape = cachedCapes.getIfPresent(playerID.toString() + ".Bedrock");
|
||||||
if (bedrockCape == null) {
|
if (bedrockCape == null) {
|
||||||
bedrockCape = EMPTY_CAPE;
|
bedrockCape = EMPTY_CAPE;
|
||||||
|
@ -288,7 +276,7 @@ public class SkinProvider {
|
||||||
return CompletableFuture.completedFuture(bedrockCape);
|
return CompletableFuture.completedFuture(bedrockCape);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<SkinGeometry> requestBedrockGeometry(SkinGeometry currentGeometry, UUID playerID, boolean newThread) {
|
public static CompletableFuture<SkinGeometry> requestBedrockGeometry(SkinGeometry currentGeometry, UUID playerID) {
|
||||||
SkinGeometry bedrockGeometry = cachedGeometry.getOrDefault(playerID, currentGeometry);
|
SkinGeometry bedrockGeometry = cachedGeometry.getOrDefault(playerID, currentGeometry);
|
||||||
return CompletableFuture.completedFuture(bedrockGeometry);
|
return CompletableFuture.completedFuture(bedrockGeometry);
|
||||||
}
|
}
|
||||||
|
@ -444,6 +432,60 @@ public class SkinProvider {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a skull has a username but no textures, request them.
|
||||||
|
* @param skullOwner the CompoundTag of the skull with no textures
|
||||||
|
* @return a completable GameProfile with textures included
|
||||||
|
*/
|
||||||
|
public static CompletableFuture<GameProfile> requestTexturesFromUsername(CompoundTag skullOwner) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
Tag uuidTag = skullOwner.get("Id");
|
||||||
|
String uuidToString = "";
|
||||||
|
JsonNode node;
|
||||||
|
GameProfile gameProfile = new GameProfile(UUID.randomUUID(), "");
|
||||||
|
boolean retrieveUuidFromInternet = !(uuidTag instanceof IntArrayTag); // also covers null check
|
||||||
|
|
||||||
|
if (!retrieveUuidFromInternet) {
|
||||||
|
int[] uuidAsArray = ((IntArrayTag) uuidTag).getValue();
|
||||||
|
// thank u viaversion
|
||||||
|
UUID uuid = new UUID((long) uuidAsArray[0] << 32 | ((long) uuidAsArray[1] & 0xFFFFFFFFL),
|
||||||
|
(long) uuidAsArray[2] << 32 | ((long) uuidAsArray[3] & 0xFFFFFFFFL));
|
||||||
|
retrieveUuidFromInternet = uuid.version() != 4;
|
||||||
|
uuidToString = uuid.toString().replace("-", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (retrieveUuidFromInternet) {
|
||||||
|
// Offline skin, or no present UUID
|
||||||
|
node = WebUtils.getJson("https://api.mojang.com/users/profiles/minecraft/" + skullOwner.get("Name").getValue());
|
||||||
|
JsonNode id = node.get("id");
|
||||||
|
if (id == null) {
|
||||||
|
GeyserConnector.getInstance().getLogger().debug("No UUID found in Mojang response for " + skullOwner.get("Name").getValue());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
uuidToString = id.asText();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get textures from UUID
|
||||||
|
node = WebUtils.getJson("https://sessionserver.mojang.com/session/minecraft/profile/" + uuidToString);
|
||||||
|
List<GameProfile.Property> profileProperties = new ArrayList<>();
|
||||||
|
JsonNode properties = node.get("properties");
|
||||||
|
if (properties == null) {
|
||||||
|
GeyserConnector.getInstance().getLogger().debug("No properties found in Mojang response for " + uuidToString);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
profileProperties.add(new GameProfile.Property("textures", node.get("properties").get(0).get("value").asText()));
|
||||||
|
gameProfile.setProperties(profileProperties);
|
||||||
|
return gameProfile;
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (GeyserConnector.getInstance().getConfig().isDebugMode()) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, EXECUTOR_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
private static BufferedImage downloadImage(String imageUrl, CapeProvider provider) throws IOException {
|
private static BufferedImage downloadImage(String imageUrl, CapeProvider provider) throws IOException {
|
||||||
if (provider == CapeProvider.FIVEZIG)
|
if (provider == CapeProvider.FIVEZIG)
|
||||||
return readFiveZigCape(imageUrl);
|
return readFiveZigCape(imageUrl);
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2020 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.skin;
|
||||||
|
|
||||||
|
import com.nukkitx.protocol.bedrock.data.skin.ImageData;
|
||||||
|
import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
|
||||||
|
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
|
||||||
|
import org.geysermc.connector.GeyserConnector;
|
||||||
|
import org.geysermc.connector.entity.player.PlayerEntity;
|
||||||
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
|
import org.geysermc.connector.utils.LanguageUtils;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class SkullSkinManager extends SkinManager {
|
||||||
|
|
||||||
|
public static PlayerListPacket.Entry buildSkullEntryManually(UUID uuid, String username, long geyserId,
|
||||||
|
String skinId, byte[] skinData) {
|
||||||
|
// Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png
|
||||||
|
skinId = skinId + "_skull";
|
||||||
|
SerializedSkin serializedSkin = SerializedSkin.of(
|
||||||
|
skinId, SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
|
||||||
|
ImageData.of(SkinProvider.EMPTY_CAPE.getCapeData()), SkinProvider.SKULL_GEOMETRY.getGeometryData(),
|
||||||
|
"", true, false, false, SkinProvider.EMPTY_CAPE.getCapeId(), skinId
|
||||||
|
);
|
||||||
|
|
||||||
|
PlayerListPacket.Entry entry = new PlayerListPacket.Entry(uuid);
|
||||||
|
entry.setName(username);
|
||||||
|
entry.setEntityId(geyserId);
|
||||||
|
entry.setSkin(serializedSkin);
|
||||||
|
entry.setXuid("");
|
||||||
|
entry.setPlatformChatId("");
|
||||||
|
entry.setTeacher(false);
|
||||||
|
entry.setTrustedSkin(true);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void requestAndHandleSkin(PlayerEntity entity, GeyserSession session,
|
||||||
|
Consumer<SkinProvider.Skin> skinConsumer) {
|
||||||
|
GameProfileData data = GameProfileData.from(entity.getProfile());
|
||||||
|
|
||||||
|
SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), false)
|
||||||
|
.whenCompleteAsync((skin, throwable) -> {
|
||||||
|
try {
|
||||||
|
if (session.getUpstream().isInitialized()) {
|
||||||
|
PlayerListPacket.Entry updatedEntry = buildSkullEntryManually(
|
||||||
|
entity.getUuid(),
|
||||||
|
entity.getUsername(),
|
||||||
|
entity.getGeyserId(),
|
||||||
|
skin.getTextureUrl(),
|
||||||
|
skin.getSkinData()
|
||||||
|
);
|
||||||
|
|
||||||
|
PlayerListPacket playerAddPacket = new PlayerListPacket();
|
||||||
|
playerAddPacket.setAction(PlayerListPacket.Action.ADD);
|
||||||
|
playerAddPacket.getEntries().add(updatedEntry);
|
||||||
|
session.sendUpstreamPacket(playerAddPacket);
|
||||||
|
|
||||||
|
// It's a skull. We don't want them in the player list.
|
||||||
|
PlayerListPacket playerRemovePacket = new PlayerListPacket();
|
||||||
|
playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE);
|
||||||
|
playerRemovePacket.getEntries().add(updatedEntry);
|
||||||
|
session.sendUpstreamPacket(playerRemovePacket);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skinConsumer != null) {
|
||||||
|
skinConsumer.accept(skin);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -51,12 +51,14 @@ import lombok.experimental.UtilityClass;
|
||||||
import org.geysermc.connector.GeyserConnector;
|
import org.geysermc.connector.GeyserConnector;
|
||||||
import org.geysermc.connector.entity.Entity;
|
import org.geysermc.connector.entity.Entity;
|
||||||
import org.geysermc.connector.entity.ItemFrameEntity;
|
import org.geysermc.connector.entity.ItemFrameEntity;
|
||||||
|
import org.geysermc.connector.entity.player.SkullPlayerEntity;
|
||||||
import org.geysermc.connector.network.session.GeyserSession;
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
|
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
|
||||||
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
|
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
|
||||||
import org.geysermc.connector.network.translators.world.block.entity.BedrockOnlyBlockEntity;
|
import org.geysermc.connector.network.translators.world.block.entity.BedrockOnlyBlockEntity;
|
||||||
import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
|
import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
|
||||||
import org.geysermc.connector.network.translators.world.block.entity.RequiresBlockState;
|
import org.geysermc.connector.network.translators.world.block.entity.RequiresBlockState;
|
||||||
|
import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator;
|
||||||
import org.geysermc.connector.network.translators.world.chunk.BlockStorage;
|
import org.geysermc.connector.network.translators.world.chunk.BlockStorage;
|
||||||
import org.geysermc.connector.network.translators.world.chunk.ChunkSection;
|
import org.geysermc.connector.network.translators.world.chunk.ChunkSection;
|
||||||
import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArray;
|
import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArray;
|
||||||
|
@ -68,9 +70,7 @@ import java.util.ArrayList;
|
||||||
import java.util.BitSet;
|
import java.util.BitSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.geysermc.connector.network.translators.world.block.BlockTranslator.JAVA_AIR_ID;
|
import static org.geysermc.connector.network.translators.world.block.BlockTranslator.*;
|
||||||
import static org.geysermc.connector.network.translators.world.block.BlockTranslator.BEDROCK_AIR_ID;
|
|
||||||
import static org.geysermc.connector.network.translators.world.block.BlockTranslator.BEDROCK_WATER_ID;
|
|
||||||
|
|
||||||
@UtilityClass
|
@UtilityClass
|
||||||
public class ChunkUtils {
|
public class ChunkUtils {
|
||||||
|
@ -293,6 +293,11 @@ public class ChunkUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
bedrockBlockEntities[i] = blockEntityTranslator.getBlockEntityTag(tagName, tag, blockState);
|
bedrockBlockEntities[i] = blockEntityTranslator.getBlockEntityTag(tagName, tag, blockState);
|
||||||
|
|
||||||
|
// Check for custom skulls
|
||||||
|
if (SkullBlockEntityTranslator.ALLOW_CUSTOM_SKULLS && tag.contains("SkullOwner")) {
|
||||||
|
SkullBlockEntityTranslator.spawnPlayer(session, tag, blockState);
|
||||||
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -357,6 +362,12 @@ public class ChunkUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SkullPlayerEntity skull = session.getSkullCache().get(position);
|
||||||
|
if (skull != null && blockState != skull.getBlockState()) {
|
||||||
|
// Skull is gone
|
||||||
|
skull.despawnEntity(session, position);
|
||||||
|
}
|
||||||
|
|
||||||
int blockId = BlockTranslator.getBedrockBlockId(blockState);
|
int blockId = BlockTranslator.getBedrockBlockId(blockState);
|
||||||
|
|
||||||
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
|
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
|
||||||
|
|
|
@ -66,6 +66,7 @@ public class DimensionUtils {
|
||||||
|
|
||||||
session.getEntityCache().removeAllEntities();
|
session.getEntityCache().removeAllEntities();
|
||||||
session.getItemFrameCache().clear();
|
session.getItemFrameCache().clear();
|
||||||
|
session.getSkullCache().clear();
|
||||||
if (session.getPendingDimSwitches().getAndIncrement() > 0) {
|
if (session.getPendingDimSwitches().getAndIncrement() > 0) {
|
||||||
ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true);
|
ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,14 +215,27 @@ public class FileUtils {
|
||||||
* @return The byte array of the file
|
* @return The byte array of the file
|
||||||
*/
|
*/
|
||||||
public static byte[] readAllBytes(File file) {
|
public static byte[] readAllBytes(File file) {
|
||||||
int size = (int) file.length();
|
|
||||||
byte[] bytes = new byte[size];
|
|
||||||
try {
|
try {
|
||||||
BufferedInputStream buf = new BufferedInputStream(new FileInputStream(file));
|
return readAllBytes(new FileInputStream(file));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Cannot read " + file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param stream the InputStream to read off of
|
||||||
|
* @return the byte array of an InputStream
|
||||||
|
*/
|
||||||
|
public static byte[] readAllBytes(InputStream stream) {
|
||||||
|
try {
|
||||||
|
int size = stream.available();
|
||||||
|
byte[] bytes = new byte[size];
|
||||||
|
BufferedInputStream buf = new BufferedInputStream(stream);
|
||||||
buf.read(bytes, 0, bytes.length);
|
buf.read(bytes, 0, bytes.length);
|
||||||
buf.close();
|
buf.close();
|
||||||
} catch (IOException ignored) { }
|
return bytes;
|
||||||
|
} catch (IOException e) {
|
||||||
return bytes;
|
throw new RuntimeException("Error while trying to read input stream!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
package org.geysermc.connector.utils;
|
package org.geysermc.connector.utils;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import org.geysermc.connector.GeyserConnector;
|
import org.geysermc.connector.GeyserConnector;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
@ -57,6 +58,18 @@ public class WebUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a web request to the given URL and returns the body as a {@link JsonNode}.
|
||||||
|
*
|
||||||
|
* @param reqURL URL to fetch
|
||||||
|
* @return the response as JSON
|
||||||
|
*/
|
||||||
|
public static JsonNode getJson(String reqURL) throws IOException {
|
||||||
|
HttpURLConnection con = (HttpURLConnection) new URL(reqURL).openConnection();
|
||||||
|
con.setRequestProperty("User-Agent", "Geyser-" + GeyserConnector.getInstance().getPlatformType().toString() + "/" + GeyserConnector.VERSION);
|
||||||
|
return GeyserConnector.JSON_MAPPER.readTree(con.getInputStream());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a file from the given URL and saves it to disk
|
* Downloads a file from the given URL and saves it to disk
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"format_version": "1.10.0",
|
||||||
|
"geometry.humanoid.customskull": {
|
||||||
|
"texturewidth": 64,
|
||||||
|
"textureheight": 64,
|
||||||
|
"visible_bounds_width": 2,
|
||||||
|
"visible_bounds_height": 1,
|
||||||
|
"visible_bounds_offset": [0, 0, 0],
|
||||||
|
"bones": [
|
||||||
|
{
|
||||||
|
"name": "head",
|
||||||
|
"pivot": [0, 24, 0],
|
||||||
|
"cubes": [
|
||||||
|
{"origin": [-4, 0, -4], "size": [8, 8, 8], "uv": [0, 0]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hat",
|
||||||
|
"parent": "head",
|
||||||
|
"pivot": [0, 24, 0],
|
||||||
|
"cubes": [
|
||||||
|
{"origin": [-4, 0, -4], "size": [8, 8, 8], "uv": [32, 0], "inflate": 0.5}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -109,6 +109,9 @@ cache-chunks: true
|
||||||
# A value of 0 is disabled. (Default: 0)
|
# A value of 0 is disabled. (Default: 0)
|
||||||
cache-images: 0
|
cache-images: 0
|
||||||
|
|
||||||
|
# Allows custom skulls to be displayed. Keeping them enabled may cause a performance decrease on older/weaker devices.
|
||||||
|
allow-custom-skulls: true
|
||||||
|
|
||||||
# Bedrock prevents building and displaying blocks above Y127 in the Nether -
|
# Bedrock prevents building and displaying blocks above Y127 in the Nether -
|
||||||
# enabling this config option works around that by changing the Nether dimension ID
|
# enabling this config option works around that by changing the Nether dimension ID
|
||||||
# to the End ID. The main downside to this is that the sky will resemble that of
|
# to the End ID. The main downside to this is that the sky will resemble that of
|
||||||
|
|
Loading…
Reference in a new issue