mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-08-14 23:57:35 +00:00
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.entity.BlockEntityTranslator;
|
||||
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.LanguageUtils;
|
||||
import org.geysermc.connector.utils.LocaleUtils;
|
||||
|
@ -78,7 +79,11 @@ import java.util.concurrent.TimeUnit;
|
|||
@Getter
|
||||
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 VERSION = "DEV"; // A fallback for running in IDEs
|
||||
|
@ -182,6 +187,7 @@ public class GeyserConnector {
|
|||
authType = AuthType.getByName(config.getRemote().getAuthType());
|
||||
|
||||
DimensionUtils.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether
|
||||
SkullBlockEntityTranslator.ALLOW_CUSTOM_SKULLS = config.isAllowCustomSkulls();
|
||||
|
||||
// https://github.com/GeyserMC/Geyser/issues/957
|
||||
RakNetConstants.MAXIMUM_MTU_SIZE = (short) config.getMtu();
|
||||
|
|
|
@ -85,6 +85,8 @@ public interface GeyserConfiguration {
|
|||
|
||||
int getCacheImages();
|
||||
|
||||
boolean isAllowCustomSkulls();
|
||||
|
||||
IMetricsInfo getMetrics();
|
||||
|
||||
interface IBedrockConfiguration {
|
||||
|
|
|
@ -101,6 +101,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
|
|||
@JsonProperty("cache-images")
|
||||
private int cacheImages = 0;
|
||||
|
||||
@JsonProperty("allow-custom-skulls")
|
||||
private boolean allowCustomSkulls = true;
|
||||
|
||||
@JsonProperty("above-bedrock-nether-building")
|
||||
private boolean aboveBedrockNetherBuilding = false;
|
||||
|
||||
|
|
|
@ -65,7 +65,6 @@ public class PlayerEntity extends LivingEntity {
|
|||
private GameProfile profile;
|
||||
private UUID uuid;
|
||||
private String username;
|
||||
private long lastSkinUpdate = -1;
|
||||
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.common.AuthType;
|
||||
import org.geysermc.connector.entity.Entity;
|
||||
import org.geysermc.connector.entity.player.SkullPlayerEntity;
|
||||
import org.geysermc.connector.entity.player.SessionPlayerEntity;
|
||||
import org.geysermc.connector.inventory.PlayerInventory;
|
||||
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.inventory.EnchantmentInventoryTranslator;
|
||||
import org.geysermc.connector.network.translators.item.ItemRegistry;
|
||||
import org.geysermc.connector.skin.SkinManager;
|
||||
import org.geysermc.connector.utils.*;
|
||||
import org.geysermc.floodgate.util.BedrockData;
|
||||
import org.geysermc.floodgate.util.EncryptionUtil;
|
||||
|
@ -90,6 +92,7 @@ import java.security.NoSuchAlgorithmException;
|
|||
import java.security.PublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
|
@ -121,7 +124,7 @@ public class GeyserSession implements CommandSender {
|
|||
*/
|
||||
private final CollisionManager collisionManager;
|
||||
|
||||
@Getter
|
||||
private final Map<Vector3i, SkullPlayerEntity> skullCache = new ConcurrentHashMap<>();
|
||||
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
|
||||
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;
|
||||
|
||||
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.network.session.GeyserSession;
|
||||
import org.geysermc.connector.network.translators.PacketTranslator;
|
||||
import org.geysermc.connector.network.translators.Translator;
|
||||
import org.geysermc.connector.utils.SkinUtils;
|
||||
|
||||
import com.nukkitx.protocol.bedrock.packet.SetLocalPlayerAsInitializedPacket;
|
||||
import org.geysermc.connector.skin.SkinManager;
|
||||
import org.geysermc.connector.skin.SkullSkinManager;
|
||||
|
||||
@Translator(packet = SetLocalPlayerAsInitializedPacket.class)
|
||||
public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslator<SetLocalPlayerAsInitializedPacket> {
|
||||
|
@ -44,10 +45,20 @@ public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslat
|
|||
|
||||
for (PlayerEntity entity : session.getEntityCache().getEntitiesByType(PlayerEntity.class)) {
|
||||
if (!entity.isValid()) {
|
||||
SkinUtils.requestAndHandleSkinAndCape(entity, session, null);
|
||||
SkinManager.requestAndHandleSkinAndCape(entity, session, null);
|
||||
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.translators.PacketTranslator;
|
||||
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.PlayerListEntryAction;
|
||||
|
@ -57,7 +57,8 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator<ServerPlayer
|
|||
if (self) {
|
||||
// Entity is ourself
|
||||
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"));
|
||||
} else {
|
||||
playerEntity = session.getEntityCache().getPlayerEntity(entry.getProfile().getId());
|
||||
|
@ -81,7 +82,7 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator<ServerPlayer
|
|||
playerEntity.setPlayerList(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);
|
||||
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.Translator;
|
||||
import org.geysermc.connector.utils.LanguageUtils;
|
||||
import org.geysermc.connector.utils.SkinUtils;
|
||||
import org.geysermc.connector.skin.SkinManager;
|
||||
|
||||
@Translator(packet = ServerSpawnPlayerPacket.class)
|
||||
public class JavaSpawnPlayerTranslator extends PacketTranslator<ServerSpawnPlayerPacket> {
|
||||
|
@ -62,7 +62,7 @@ public class JavaSpawnPlayerTranslator extends PacketTranslator<ServerSpawnPlaye
|
|||
|
||||
if (session.getUpstream().isInitialized()) {
|
||||
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;
|
||||
|
||||
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.translators.PacketTranslator;
|
||||
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)
|
||||
public class JavaUnloadChunkTranslator extends PacketTranslator<ServerUnloadChunkPacket> {
|
||||
|
@ -37,5 +39,15 @@ public class JavaUnloadChunkTranslator extends PacketTranslator<ServerUnloadChun
|
|||
@Override
|
||||
public void translate(ServerUnloadChunkPacket packet, GeyserSession session) {
|
||||
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.Translator;
|
||||
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.ChunkUtils;
|
||||
|
||||
|
@ -64,6 +65,10 @@ public class JavaUpdateTileEntityTranslator extends PacketTranslator<ServerUpdat
|
|||
// Cache chunks is not enabled; use block entity cache
|
||||
ChunkUtils.CACHED_BLOCK_ENTITIES.removeInt(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 (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 Int2ByteMap SKULL_VARIANTS = 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();
|
||||
|
||||
/**
|
||||
|
@ -110,6 +111,26 @@ public class BlockStateValues {
|
|||
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");
|
||||
if (shulkerDirection != null) {
|
||||
BlockStateValues.SHULKERBOX_DIRECTIONS.put(javaBlockState, (byte) shulkerDirection.intValue());
|
||||
|
@ -222,6 +243,15 @@ public class BlockStateValues {
|
|||
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.
|
||||
|
|
|
@ -25,12 +25,31 @@
|
|||
|
||||
package org.geysermc.connector.network.translators.world.block.entity;
|
||||
|
||||
import com.github.steveice10.mc.auth.data.GameProfile;
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
||||
import com.nukkitx.math.vector.Vector3f;
|
||||
import com.nukkitx.math.vector.Vector3i;
|
||||
import com.nukkitx.nbt.NbtMapBuilder;
|
||||
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
||||
import org.geysermc.connector.entity.player.SkullPlayerEntity;
|
||||
import org.geysermc.connector.network.session.GeyserSession;
|
||||
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
|
||||
import org.geysermc.connector.skin.SkinProvider;
|
||||
import org.geysermc.connector.skin.SkullSkinManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@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;
|
||||
|
@ -47,4 +66,98 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
|
|||
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
|
||||
*/
|
||||
|
||||
package org.geysermc.connector.utils;
|
||||
package org.geysermc.connector.skin;
|
||||
|
||||
import lombok.Getter;
|
||||
|
|
@ -23,10 +23,9 @@
|
|||
* @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.ObjectMapper;
|
||||
import com.github.steveice10.mc.auth.data.GameProfile;
|
||||
import com.nukkitx.protocol.bedrock.data.skin.ImageData;
|
||||
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.network.session.GeyserSession;
|
||||
import org.geysermc.connector.network.session.auth.BedrockClientData;
|
||||
import org.geysermc.connector.utils.LanguageUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
@ -45,12 +45,11 @@ import java.util.Collections;
|
|||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class SkinUtils {
|
||||
public class SkinManager {
|
||||
|
||||
public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, GameProfile profile, long geyserId) {
|
||||
GameProfileData data = GameProfileData.from(profile);
|
||||
public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, PlayerEntity playerEntity) {
|
||||
GameProfileData data = GameProfileData.from(playerEntity.getProfile());
|
||||
SkinProvider.Cape cape = SkinProvider.getCachedCape(data.getCapeUrl());
|
||||
|
||||
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
|
||||
|
||||
SkinProvider.Skin skin = SkinProvider.getCachedSkin(data.getSkinUrl());
|
||||
|
@ -60,25 +59,24 @@ public class SkinUtils {
|
|||
|
||||
return buildEntryManually(
|
||||
session,
|
||||
profile.getId(),
|
||||
profile.getName(),
|
||||
geyserId,
|
||||
playerEntity.getProfile().getId(),
|
||||
playerEntity.getProfile().getName(),
|
||||
playerEntity.getGeyserId(),
|
||||
skin.getTextureUrl(),
|
||||
skin.getSkinData(),
|
||||
cape.getCapeId(),
|
||||
cape.getCapeData(),
|
||||
geometry.getGeometryName(),
|
||||
geometry.getGeometryData()
|
||||
geometry
|
||||
);
|
||||
}
|
||||
|
||||
public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId,
|
||||
String skinId, byte[] skinData,
|
||||
String capeId, byte[] capeData,
|
||||
String geometryName, String geometryData) {
|
||||
SkinProvider.SkinGeometry geometry) {
|
||||
SerializedSkin serializedSkin = SerializedSkin.of(
|
||||
skinId, geometryName, ImageData.of(skinData), Collections.emptyList(),
|
||||
ImageData.of(capeData), geometryData, "", true, false, !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId
|
||||
skinId, geometry.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
|
||||
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
|
||||
|
@ -119,11 +117,11 @@ public class SkinUtils {
|
|||
try {
|
||||
SkinProvider.Skin skin = skinAndCape.getSkin();
|
||||
SkinProvider.Cape cape = skinAndCape.getCape();
|
||||
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
|
||||
|
||||
if (cape.isFailed()) {
|
||||
cape = SkinProvider.getOrDefault(SkinProvider.requestBedrockCape(
|
||||
entity.getUuid(), false
|
||||
), SkinProvider.EMPTY_CAPE, 3);
|
||||
cape = SkinProvider.getOrDefault(SkinProvider.requestBedrockCape(entity.getUuid()),
|
||||
SkinProvider.EMPTY_CAPE, 3);
|
||||
}
|
||||
|
||||
if (cape.isFailed() && SkinProvider.ALLOW_THIRD_PARTY_CAPES) {
|
||||
|
@ -133,9 +131,8 @@ public class SkinUtils {
|
|||
), SkinProvider.EMPTY_CAPE, SkinProvider.CapeProvider.VALUES.length * 3);
|
||||
}
|
||||
|
||||
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
|
||||
geometry = SkinProvider.getOrDefault(SkinProvider.requestBedrockGeometry(
|
||||
geometry, entity.getUuid(), false
|
||||
geometry, entity.getUuid()
|
||||
), geometry, 3);
|
||||
|
||||
// Not a bedrock player check for ears
|
||||
|
@ -165,8 +162,6 @@ public class SkinUtils {
|
|||
}
|
||||
}
|
||||
|
||||
entity.setLastSkinUpdate(skin.getRequestedOn());
|
||||
|
||||
if (session.getUpstream().isInitialized()) {
|
||||
PlayerListPacket.Entry updatedEntry = buildEntryManually(
|
||||
session,
|
||||
|
@ -177,8 +172,7 @@ public class SkinUtils {
|
|||
skin.getSkinData(),
|
||||
cape.getCapeId(),
|
||||
cape.getCapeData(),
|
||||
geometry.getGeometryName(),
|
||||
geometry.getGeometryData()
|
||||
geometry
|
||||
);
|
||||
|
||||
|
||||
|
@ -252,7 +246,7 @@ public class SkinUtils {
|
|||
GameProfile.Property skinProperty = profile.getProperty("textures");
|
||||
|
||||
// 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 skinTexture = textures.get("SKIN");
|
|
@ -23,10 +23,14 @@
|
|||
* @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.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.CacheBuilder;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
@ -34,15 +38,20 @@ import lombok.Getter;
|
|||
import lombok.NoArgsConstructor;
|
||||
import org.geysermc.connector.GeyserConnector;
|
||||
import org.geysermc.connector.network.session.GeyserSession;
|
||||
import org.geysermc.connector.utils.FileUtils;
|
||||
import org.geysermc.connector.utils.WebUtils;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
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.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
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 String EARS_GEOMETRY;
|
||||
public static String EARS_GEOMETRY_SLIM;
|
||||
public static SkinGeometry SKULL_GEOMETRY;
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
static {
|
||||
/* Load in the normal ears geometry */
|
||||
InputStream earsStream = FileUtils.getResource("bedrock/skin/geometry.humanoid.ears.json");
|
||||
|
||||
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();
|
||||
|
||||
EARS_GEOMETRY = new String(FileUtils.readAllBytes(FileUtils.getResource("bedrock/skin/geometry.humanoid.ears.json")), StandardCharsets.UTF_8);
|
||||
|
||||
/* 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();
|
||||
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_SLIM = earsDataBuilder.toString();
|
||||
/* Load in the custom skull geometry */
|
||||
String skullData = new String(FileUtils.readAllBytes(FileUtils.getResource("bedrock/skin/geometry.humanoid.customskull.json")), StandardCharsets.UTF_8);
|
||||
SKULL_GEOMETRY = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.customskull\"}}", skullData, false);
|
||||
|
||||
// Schedule Daily Image Expiry if we are caching them
|
||||
if (GeyserConnector.getInstance().getConfig().getCacheImages() > 0) {
|
||||
|
@ -205,7 +194,6 @@ public class SkinProvider {
|
|||
if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE);
|
||||
if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested
|
||||
|
||||
boolean officialCape = provider == CapeProvider.MINECRAFT;
|
||||
Cape cachedCape = cachedCapes.getIfPresent(capeUrl);
|
||||
if (cachedCape != null) {
|
||||
return CompletableFuture.completedFuture(cachedCape);
|
||||
|
@ -280,7 +268,7 @@ public class SkinProvider {
|
|||
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");
|
||||
if (bedrockCape == null) {
|
||||
bedrockCape = EMPTY_CAPE;
|
||||
|
@ -288,7 +276,7 @@ public class SkinProvider {
|
|||
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);
|
||||
return CompletableFuture.completedFuture(bedrockGeometry);
|
||||
}
|
||||
|
@ -444,6 +432,60 @@ public class SkinProvider {
|
|||
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 {
|
||||
if (provider == CapeProvider.FIVEZIG)
|
||||
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.entity.Entity;
|
||||
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.translators.world.block.BlockStateValues;
|
||||
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.BlockEntityTranslator;
|
||||
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.ChunkSection;
|
||||
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.List;
|
||||
|
||||
import static org.geysermc.connector.network.translators.world.block.BlockTranslator.JAVA_AIR_ID;
|
||||
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;
|
||||
import static org.geysermc.connector.network.translators.world.block.BlockTranslator.*;
|
||||
|
||||
@UtilityClass
|
||||
public class ChunkUtils {
|
||||
|
@ -293,6 +293,11 @@ public class ChunkUtils {
|
|||
}
|
||||
|
||||
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++;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
|
||||
|
|
|
@ -66,6 +66,7 @@ public class DimensionUtils {
|
|||
|
||||
session.getEntityCache().removeAllEntities();
|
||||
session.getItemFrameCache().clear();
|
||||
session.getSkullCache().clear();
|
||||
if (session.getPendingDimSwitches().getAndIncrement() > 0) {
|
||||
ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true);
|
||||
}
|
||||
|
|
|
@ -215,14 +215,27 @@ public class FileUtils {
|
|||
* @return The byte array of the file
|
||||
*/
|
||||
public static byte[] readAllBytes(File file) {
|
||||
int size = (int) file.length();
|
||||
byte[] bytes = new byte[size];
|
||||
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.close();
|
||||
} catch (IOException ignored) { }
|
||||
|
||||
return bytes;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Error while trying to read input stream!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
package org.geysermc.connector.utils;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.geysermc.connector.GeyserConnector;
|
||||
|
||||
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
|
||||
*
|
||||
|
|
|
@ -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)
|
||||
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 -
|
||||
# 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
|
||||
|
|
Loading…
Reference in a new issue