2020-01-09 03:05:42 +00:00
/ *
2022-01-01 19:03:05 +00:00
* Copyright ( c ) 2019 - 2022 GeyserMC . http : //geysermc.org
2020-01-09 03:05:42 +00:00
*
* 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
* /
2021-11-20 21:34:30 +00:00
package org.geysermc.geyser.skin ;
2019-10-09 18:39:38 +00:00
2020-04-20 20:10:30 +00:00
import com.fasterxml.jackson.databind.JsonNode ;
2021-11-22 19:49:55 +00:00
import com.github.steveice10.opennbt.tag.builtin.CompoundTag ;
import com.github.steveice10.opennbt.tag.builtin.ListTag ;
import com.github.steveice10.opennbt.tag.builtin.StringTag ;
2022-10-30 00:23:21 +00:00
import org.cloudburstmc.protocol.bedrock.data.skin.ImageData ;
import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin ;
import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket ;
2023-02-14 22:09:48 +00:00
import org.cloudburstmc.protocol.bedrock.packet.PlayerSkinPacket ;
2021-11-20 21:34:30 +00:00
import org.geysermc.geyser.GeyserImpl ;
2021-11-20 23:29:46 +00:00
import org.geysermc.geyser.entity.type.player.PlayerEntity ;
2023-01-22 18:23:16 +00:00
import org.geysermc.geyser.entity.type.player.SkullPlayerEntity ;
2021-11-22 19:52:26 +00:00
import org.geysermc.geyser.session.GeyserSession ;
2021-11-20 23:29:46 +00:00
import org.geysermc.geyser.session.auth.BedrockClientData ;
import org.geysermc.geyser.text.GeyserLocale ;
2019-10-09 18:39:38 +00:00
2021-11-22 19:49:55 +00:00
import javax.annotation.Nullable ;
import java.io.IOException ;
2019-12-01 21:16:52 +00:00
import java.nio.charset.StandardCharsets ;
2019-10-09 18:39:38 +00:00
import java.util.Base64 ;
2019-11-16 03:25:43 +00:00
import java.util.Collections ;
2019-10-09 18:39:38 +00:00
import java.util.UUID ;
import java.util.function.Consumer ;
2020-12-04 21:55:24 +00:00
public class SkinManager {
2020-01-04 05:58:58 +00:00
2021-01-22 00:03:46 +00:00
/ * *
* Builds a Bedrock player list entry from our existing , cached Bedrock skin information
* /
2021-11-22 19:52:26 +00:00
public static PlayerListPacket . Entry buildCachedEntry ( GeyserSession session , PlayerEntity playerEntity ) {
2022-12-18 18:18:06 +00:00
// First: see if we have the cached skin texture ID.
2022-03-31 02:30:49 +00:00
GameProfileData data = GameProfileData . from ( playerEntity ) ;
2022-12-18 18:18:06 +00:00
SkinProvider . Skin skin = null ;
SkinProvider . Cape cape = null ;
SkinProvider . SkinGeometry geometry = SkinProvider . SkinGeometry . WIDE ;
if ( data ! = null ) {
// GameProfileData is not null = server provided us with textures data to work with.
skin = SkinProvider . getCachedSkin ( data . skinUrl ( ) ) ;
cape = SkinProvider . getCachedCape ( data . capeUrl ( ) ) ;
geometry = data . isAlex ( ) ? SkinProvider . SkinGeometry . SLIM : SkinProvider . SkinGeometry . WIDE ;
}
2020-05-06 21:50:01 +00:00
2022-12-18 18:18:06 +00:00
if ( skin = = null | | cape = = null ) {
// The server either didn't have a texture to send, or we didn't have the texture ID cached.
// Let's see if this player is a Bedrock player, and if so, let's pull their skin.
// Otherwise, grab the default player skin
2023-01-22 18:23:16 +00:00
SkinProvider . SkinData fallbackSkinData = SkinProvider . determineFallbackSkinData ( playerEntity . getUuid ( ) ) ;
2022-12-18 18:18:06 +00:00
if ( skin = = null ) {
skin = fallbackSkinData . skin ( ) ;
geometry = fallbackSkinData . geometry ( ) ;
}
if ( cape = = null ) {
cape = fallbackSkinData . cape ( ) ;
}
2020-08-12 13:23:11 +00:00
}
2020-08-07 16:33:21 +00:00
2019-10-09 18:39:38 +00:00
return buildEntryManually (
2020-08-07 16:33:21 +00:00
session ,
2022-03-31 02:30:49 +00:00
playerEntity . getUuid ( ) ,
playerEntity . getUsername ( ) ,
2020-12-04 21:55:24 +00:00
playerEntity . getGeyserId ( ) ,
2022-12-17 19:52:20 +00:00
skin ,
cape ,
2020-12-04 21:55:24 +00:00
geometry
2019-10-09 18:39:38 +00:00
) ;
}
2021-01-22 00:03:46 +00:00
/ * *
* With all the information needed , build a Bedrock player entry with translated skin information .
* /
2021-11-22 19:52:26 +00:00
public static PlayerListPacket . Entry buildEntryManually ( GeyserSession session , UUID uuid , String username , long geyserId ,
2022-12-17 19:52:20 +00:00
SkinProvider . Skin skin ,
SkinProvider . Cape cape ,
2021-11-21 18:36:42 +00:00
SkinProvider . SkinGeometry geometry ) {
2022-12-17 19:52:20 +00:00
SerializedSkin serializedSkin = getSkin ( skin . getTextureUrl ( ) , skin , cape , geometry ) ;
2019-10-31 02:14:23 +00:00
2021-01-22 00:03:46 +00:00
// This attempts to find the XUID of the player so profile images show up for Xbox accounts
2020-08-08 22:41:12 +00:00
String xuid = " " ;
2021-11-22 19:52:26 +00:00
GeyserSession playerSession = GeyserImpl . getInstance ( ) . connectionByUuid ( uuid ) ;
2020-12-17 16:58:49 +00:00
2021-01-22 00:03:46 +00:00
if ( playerSession ! = null ) {
2021-11-21 18:36:42 +00:00
xuid = playerSession . getAuthData ( ) . xuid ( ) ;
2020-08-08 22:41:12 +00:00
}
2020-08-07 16:33:21 +00:00
PlayerListPacket . Entry entry ;
// If we are building a PlayerListEntry for our own session we use our AuthData UUID instead of the Java UUID
2021-01-22 00:03:46 +00:00
// as Bedrock expects to get back its own provided UUID
2020-08-07 16:33:21 +00:00
if ( session . getPlayerEntity ( ) . getUuid ( ) . equals ( uuid ) ) {
2021-11-21 18:36:42 +00:00
entry = new PlayerListPacket . Entry ( session . getAuthData ( ) . uuid ( ) ) ;
2020-08-07 16:33:21 +00:00
} else {
entry = new PlayerListPacket . Entry ( uuid ) ;
}
2020-10-15 03:34:24 +00:00
2019-10-09 18:39:38 +00:00
entry . setName ( username ) ;
entry . setEntityId ( geyserId ) ;
2019-10-31 02:14:23 +00:00
entry . setSkin ( serializedSkin ) ;
2020-08-08 22:41:12 +00:00
entry . setXuid ( xuid ) ;
2019-10-09 18:39:38 +00:00
entry . setPlatformChatId ( " " ) ;
2019-11-06 00:55:59 +00:00
entry . setTeacher ( false ) ;
2020-06-29 12:50:16 +00:00
entry . setTrustedSkin ( true ) ;
2019-10-09 18:39:38 +00:00
return entry ;
}
2022-12-17 19:52:20 +00:00
public static void sendSkinPacket ( GeyserSession session , PlayerEntity entity , SkinProvider . SkinData skinData ) {
SkinProvider . Skin skin = skinData . skin ( ) ;
SkinProvider . Cape cape = skinData . cape ( ) ;
SkinProvider . SkinGeometry geometry = skinData . geometry ( ) ;
if ( entity . getUuid ( ) . equals ( session . getPlayerEntity ( ) . getUuid ( ) ) ) {
// TODO is this special behavior needed?
PlayerListPacket . Entry updatedEntry = buildEntryManually (
session ,
entity . getUuid ( ) ,
entity . getUsername ( ) ,
entity . getGeyserId ( ) ,
skin ,
cape ,
geometry
) ;
PlayerListPacket playerAddPacket = new PlayerListPacket ( ) ;
playerAddPacket . setAction ( PlayerListPacket . Action . ADD ) ;
playerAddPacket . getEntries ( ) . add ( updatedEntry ) ;
session . sendUpstreamPacket ( playerAddPacket ) ;
} else {
PlayerSkinPacket packet = new PlayerSkinPacket ( ) ;
packet . setUuid ( entity . getUuid ( ) ) ;
packet . setOldSkinName ( " " ) ;
packet . setNewSkinName ( skin . getTextureUrl ( ) ) ;
packet . setSkin ( getSkin ( skin . getTextureUrl ( ) , skin , cape , geometry ) ) ;
packet . setTrustedSkin ( true ) ;
session . sendUpstreamPacket ( packet ) ;
}
}
private static SerializedSkin getSkin ( String skinId , SkinProvider . Skin skin , SkinProvider . Cape cape , SkinProvider . SkinGeometry geometry ) {
2022-12-18 18:18:06 +00:00
return SerializedSkin . of ( skinId , " " , geometry . geometryName ( ) ,
2022-12-17 19:52:20 +00:00
ImageData . of ( skin . getSkinData ( ) ) , Collections . emptyList ( ) ,
2022-12-18 18:18:06 +00:00
ImageData . of ( cape . capeData ( ) ) , geometry . geometryData ( ) ,
" " , true , false , false , cape . capeId ( ) , skinId ) ;
2022-12-17 19:52:20 +00:00
}
2021-11-22 19:52:26 +00:00
public static void requestAndHandleSkinAndCape ( PlayerEntity entity , GeyserSession session ,
2020-10-15 03:34:24 +00:00
Consumer < SkinProvider . SkinAndCape > skinAndCapeConsumer ) {
2021-11-22 19:49:55 +00:00
SkinProvider . requestSkinData ( entity ) . whenCompleteAsync ( ( skinData , throwable ) - > {
if ( skinData = = null ) {
if ( skinAndCapeConsumer ! = null ) {
skinAndCapeConsumer . accept ( null ) ;
}
2020-10-15 03:34:24 +00:00
2021-11-22 19:49:55 +00:00
return ;
}
if ( skinData . geometry ( ) ! = null ) {
2022-12-17 19:52:20 +00:00
sendSkinPacket ( session , entity , skinData ) ;
2021-11-22 19:49:55 +00:00
}
if ( skinAndCapeConsumer ! = null ) {
skinAndCapeConsumer . accept ( new SkinProvider . SkinAndCape ( skinData . skin ( ) , skinData . cape ( ) ) ) ;
}
} ) ;
2020-10-15 03:34:24 +00:00
}
public static void handleBedrockSkin ( PlayerEntity playerEntity , BedrockClientData clientData ) {
2021-11-20 21:34:30 +00:00
GeyserImpl geyser = GeyserImpl . getInstance ( ) ;
if ( geyser . getConfig ( ) . isDebugMode ( ) ) {
2021-11-20 23:29:46 +00:00
geyser . getLogger ( ) . info ( GeyserLocale . getLocaleStringLog ( " geyser.skin.bedrock.register " , playerEntity . getUsername ( ) , playerEntity . getUuid ( ) ) ) ;
2021-09-28 23:25:34 +00:00
}
2020-10-15 03:34:24 +00:00
try {
byte [ ] skinBytes = Base64 . getDecoder ( ) . decode ( clientData . getSkinData ( ) . getBytes ( StandardCharsets . UTF_8 ) ) ;
byte [ ] capeBytes = clientData . getCapeData ( ) ;
byte [ ] geometryNameBytes = Base64 . getDecoder ( ) . decode ( clientData . getGeometryName ( ) . getBytes ( StandardCharsets . UTF_8 ) ) ;
byte [ ] geometryBytes = Base64 . getDecoder ( ) . decode ( clientData . getGeometryData ( ) . getBytes ( StandardCharsets . UTF_8 ) ) ;
if ( skinBytes . length < = ( 128 * 128 * 4 ) & & ! clientData . isPersonaSkin ( ) ) {
SkinProvider . storeBedrockSkin ( playerEntity . getUuid ( ) , clientData . getSkinId ( ) , skinBytes ) ;
SkinProvider . storeBedrockGeometry ( playerEntity . getUuid ( ) , geometryNameBytes , geometryBytes ) ;
2021-11-20 21:34:30 +00:00
} else if ( geyser . getConfig ( ) . isDebugMode ( ) ) {
2021-11-20 23:29:46 +00:00
geyser . getLogger ( ) . info ( GeyserLocale . getLocaleStringLog ( " geyser.skin.bedrock.fail " , playerEntity . getUsername ( ) ) ) ;
2021-11-20 21:34:30 +00:00
geyser . getLogger ( ) . debug ( " The size of ' " + playerEntity . getUsername ( ) + " ' skin is: " + clientData . getSkinImageWidth ( ) + " x " + clientData . getSkinImageHeight ( ) ) ;
2020-10-15 03:34:24 +00:00
}
if ( ! clientData . getCapeId ( ) . equals ( " " ) ) {
2022-12-18 18:18:06 +00:00
SkinProvider . storeBedrockCape ( clientData . getCapeId ( ) , capeBytes ) ;
2020-10-15 03:34:24 +00:00
}
} catch ( Exception e ) {
throw new AssertionError ( " Failed to cache skin for bedrock user ( " + playerEntity . getUsername ( ) + " ): " , e ) ;
}
}
2021-09-28 23:25:34 +00:00
public record GameProfileData ( String skinUrl , String capeUrl , boolean isAlex ) {
2021-11-22 19:49:55 +00:00
/ * *
* Generate the GameProfileData from the given CompoundTag representing a GameProfile
*
* @param tag tag to build the GameProfileData from
* @return The built GameProfileData , or null if this wasn ' t a valid tag
* /
public static @Nullable GameProfileData from ( CompoundTag tag ) {
if ( ! ( tag . get ( " Properties " ) instanceof CompoundTag propertiesTag ) ) {
return null ;
}
if ( ! ( propertiesTag . get ( " textures " ) instanceof ListTag texturesTag ) | | texturesTag . size ( ) = = 0 ) {
return null ;
}
if ( ! ( texturesTag . get ( 0 ) instanceof CompoundTag texturesData ) ) {
return null ;
}
if ( ! ( texturesData . get ( " Value " ) instanceof StringTag skinDataValue ) ) {
return null ;
}
try {
return loadFromJson ( skinDataValue . getValue ( ) ) ;
} catch ( IOException e ) {
2021-11-23 03:47:58 +00:00
GeyserImpl . getInstance ( ) . getLogger ( ) . debug ( " Something went wrong while processing skin for tag " + tag ) ;
if ( GeyserImpl . getInstance ( ) . getConfig ( ) . isDebugMode ( ) ) {
2021-11-22 19:49:55 +00:00
e . printStackTrace ( ) ;
}
return null ;
}
}
2020-04-21 05:28:44 +00:00
/ * *
2022-03-31 02:30:49 +00:00
* Generate the GameProfileData from the given player entity
2020-04-21 05:28:44 +00:00
*
2022-03-31 02:30:49 +00:00
* @param entity entity to build the GameProfileData from
2020-04-21 05:28:44 +00:00
* @return The built GameProfileData
* /
2022-12-18 18:18:06 +00:00
public static @Nullable GameProfileData from ( PlayerEntity entity ) {
2023-01-22 18:23:16 +00:00
String texturesProperty = entity . getTexturesProperty ( ) ;
if ( texturesProperty = = null ) {
// Likely offline mode
return null ;
}
2019-10-09 18:39:38 +00:00
2023-01-22 18:23:16 +00:00
try {
2022-12-18 18:18:06 +00:00
return loadFromJson ( texturesProperty ) ;
2023-01-22 18:23:16 +00:00
} catch ( Exception exception ) {
if ( entity instanceof SkullPlayerEntity skullEntity ) {
GeyserImpl . getInstance ( ) . getLogger ( ) . debug ( " Something went wrong while processing skin for skull at " + skullEntity . getSkullPosition ( ) + " with Value: " + texturesProperty ) ;
2021-12-04 00:24:22 +00:00
} else {
2023-01-22 18:23:16 +00:00
GeyserImpl . getInstance ( ) . getLogger ( ) . debug ( " Something went wrong while processing skin for " + entity . getUsername ( ) + " with Value: " + texturesProperty ) ;
2021-12-04 00:24:22 +00:00
}
2021-11-20 21:34:30 +00:00
if ( GeyserImpl . getInstance ( ) . getConfig ( ) . isDebugMode ( ) ) {
2021-02-15 21:36:47 +00:00
exception . printStackTrace ( ) ;
}
2021-01-22 00:03:46 +00:00
}
2023-01-22 18:23:16 +00:00
return null ;
2021-01-22 00:03:46 +00:00
}
2020-12-17 16:58:49 +00:00
2023-01-22 18:23:16 +00:00
private static GameProfileData loadFromJson ( String encodedJson ) throws IOException , IllegalArgumentException {
2021-11-23 03:47:58 +00:00
JsonNode skinObject = GeyserImpl . JSON_MAPPER . readTree ( new String ( Base64 . getDecoder ( ) . decode ( encodedJson ) , StandardCharsets . UTF_8 ) ) ;
2021-11-22 19:49:55 +00:00
JsonNode textures = skinObject . get ( " textures " ) ;
2022-10-20 18:17:08 +00:00
if ( textures = = null ) {
return null ;
}
2021-11-22 19:49:55 +00:00
2022-10-20 18:17:08 +00:00
JsonNode skinTexture = textures . get ( " SKIN " ) ;
if ( skinTexture = = null ) {
return null ;
}
2021-11-22 19:49:55 +00:00
2023-01-22 18:23:16 +00:00
String skinUrl ;
JsonNode skinUrlNode = skinTexture . get ( " url " ) ;
if ( skinUrlNode ! = null & & skinUrlNode . isTextual ( ) ) {
skinUrl = skinUrlNode . asText ( ) . replace ( " http:// " , " https:// " ) ;
} else {
return null ;
}
2022-10-20 18:17:08 +00:00
boolean isAlex = skinTexture . has ( " metadata " ) ;
2021-11-22 19:49:55 +00:00
2022-10-20 18:17:08 +00:00
String capeUrl = null ;
JsonNode capeTexture = textures . get ( " CAPE " ) ;
if ( capeTexture ! = null ) {
2023-01-22 18:23:16 +00:00
JsonNode capeUrlNode = capeTexture . get ( " url " ) ;
if ( capeUrlNode ! = null & & capeUrlNode . isTextual ( ) ) {
capeUrl = capeUrlNode . asText ( ) . replace ( " http:// " , " https:// " ) ;
2020-10-23 04:01:03 +00:00
}
2019-10-10 21:27:30 +00:00
}
2022-10-20 18:17:08 +00:00
2021-01-22 00:03:46 +00:00
return new GameProfileData ( skinUrl , capeUrl , isAlex ) ;
2019-10-09 18:39:38 +00:00
}
}
}