2020-01-09 03:05:42 +00:00
/ *
2021-01-01 15:10:36 +00:00
* Copyright ( c ) 2019 - 2021 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
* /
2020-12-04 21:55:24 +00:00
package org.geysermc.connector.skin ;
2019-10-09 18:39:38 +00:00
2020-04-20 20:10:30 +00:00
import com.fasterxml.jackson.databind.JsonNode ;
2019-10-09 18:39:38 +00:00
import com.github.steveice10.mc.auth.data.GameProfile ;
2020-06-23 00:11:09 +00:00
import com.nukkitx.protocol.bedrock.data.skin.ImageData ;
import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin ;
2019-10-09 18:39:38 +00:00
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket ;
import lombok.AllArgsConstructor ;
import lombok.Getter ;
2019-11-27 01:52:13 +00:00
import org.geysermc.connector.GeyserConnector ;
2021-02-15 21:36:47 +00:00
import org.geysermc.connector.common.AuthType ;
2020-11-20 19:56:39 +00:00
import org.geysermc.connector.entity.player.PlayerEntity ;
2019-10-09 18:39:38 +00:00
import org.geysermc.connector.network.session.GeyserSession ;
2020-05-06 21:50:01 +00:00
import org.geysermc.connector.network.session.auth.BedrockClientData ;
2020-12-04 21:55:24 +00:00
import org.geysermc.connector.utils.LanguageUtils ;
2019-10-09 18:39:38 +00:00
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
* /
2020-12-04 21:55:24 +00:00
public static PlayerListPacket . Entry buildCachedEntry ( GeyserSession session , PlayerEntity playerEntity ) {
GameProfileData data = GameProfileData . from ( playerEntity . getProfile ( ) ) ;
2019-11-19 20:31:24 +00:00
SkinProvider . Cape cape = SkinProvider . getCachedCape ( data . getCapeUrl ( ) ) ;
2020-05-23 21:06:34 +00:00
SkinProvider . SkinGeometry geometry = SkinProvider . SkinGeometry . getLegacy ( data . isAlex ( ) ) ;
2020-05-06 21:50:01 +00:00
2020-08-07 16:33:21 +00:00
SkinProvider . Skin skin = SkinProvider . getCachedSkin ( data . getSkinUrl ( ) ) ;
2020-08-12 13:23:11 +00:00
if ( skin = = null ) {
skin = SkinProvider . EMPTY_SKIN ;
}
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 ,
2020-12-04 21:55:24 +00:00
playerEntity . getProfile ( ) . getId ( ) ,
playerEntity . getProfile ( ) . getName ( ) ,
playerEntity . getGeyserId ( ) ,
2020-08-07 16:33:21 +00:00
skin . getTextureUrl ( ) ,
skin . getSkinData ( ) ,
2019-11-19 20:31:24 +00:00
cape . getCapeId ( ) ,
cape . getCapeData ( ) ,
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 .
* /
2020-08-07 16:33:21 +00:00
public static PlayerListPacket . Entry buildEntryManually ( GeyserSession session , UUID uuid , String username , long geyserId ,
2020-12-04 21:55:24 +00:00
String skinId , byte [ ] skinData ,
String capeId , byte [ ] capeData ,
SkinProvider . SkinGeometry geometry ) {
2019-11-19 20:31:24 +00:00
SerializedSkin serializedSkin = SerializedSkin . of (
2021-02-17 23:00:53 +00:00
skinId , " " , geometry . getGeometryName ( ) , ImageData . of ( skinData ) , Collections . emptyList ( ) ,
2021-01-22 00:03:46 +00:00
ImageData . of ( capeData ) , geometry . getGeometryData ( ) , " " , true , false ,
! capeId . equals ( SkinProvider . EMPTY_CAPE . getCapeId ( ) ) , capeId , skinId
2019-11-19 20:31:24 +00:00
) ;
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-01-22 00:03:46 +00:00
GeyserSession playerSession = GeyserConnector . getInstance ( ) . getPlayerByUuid ( uuid ) ;
2020-12-17 16:58:49 +00:00
2021-01-22 00:03:46 +00:00
if ( playerSession ! = null ) {
xuid = playerSession . getAuthData ( ) . getXboxUUID ( ) ;
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 ) ) {
entry = new PlayerListPacket . Entry ( session . getAuthData ( ) . getUUID ( ) ) ;
} 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 ;
}
2020-10-15 03:34:24 +00:00
public static void requestAndHandleSkinAndCape ( PlayerEntity entity , GeyserSession session ,
Consumer < SkinProvider . SkinAndCape > skinAndCapeConsumer ) {
GameProfileData data = GameProfileData . from ( entity . getProfile ( ) ) ;
SkinProvider . requestSkinAndCape ( entity . getUuid ( ) , data . getSkinUrl ( ) , data . getCapeUrl ( ) )
. whenCompleteAsync ( ( skinAndCape , throwable ) - > {
try {
SkinProvider . Skin skin = skinAndCape . getSkin ( ) ;
SkinProvider . Cape cape = skinAndCape . getCape ( ) ;
2020-12-04 21:55:24 +00:00
SkinProvider . SkinGeometry geometry = SkinProvider . SkinGeometry . getLegacy ( data . isAlex ( ) ) ;
2020-10-15 03:34:24 +00:00
if ( cape . isFailed ( ) ) {
2020-12-04 21:55:24 +00:00
cape = SkinProvider . getOrDefault ( SkinProvider . requestBedrockCape ( entity . getUuid ( ) ) ,
SkinProvider . EMPTY_CAPE , 3 ) ;
2020-10-15 03:34:24 +00:00
}
if ( cape . isFailed ( ) & & SkinProvider . ALLOW_THIRD_PARTY_CAPES ) {
cape = SkinProvider . getOrDefault ( SkinProvider . requestUnofficialCape (
cape , entity . getUuid ( ) ,
entity . getUsername ( ) , false
) , SkinProvider . EMPTY_CAPE , SkinProvider . CapeProvider . VALUES . length * 3 ) ;
}
geometry = SkinProvider . getOrDefault ( SkinProvider . requestBedrockGeometry (
2020-12-04 21:55:24 +00:00
geometry , entity . getUuid ( )
2020-10-15 03:34:24 +00:00
) , geometry , 3 ) ;
2021-01-22 00:03:46 +00:00
boolean isDeadmau5 = " deadmau5 " . equals ( entity . getUsername ( ) ) ;
2020-10-15 03:34:24 +00:00
// Not a bedrock player check for ears
2021-01-22 00:03:46 +00:00
if ( geometry . isFailed ( ) & & ( SkinProvider . ALLOW_THIRD_PARTY_EARS | | isDeadmau5 ) ) {
2020-10-15 03:34:24 +00:00
boolean isEars ;
// Its deadmau5, gotta support his skin :)
2021-01-22 00:03:46 +00:00
if ( isDeadmau5 ) {
2020-10-15 03:34:24 +00:00
isEars = true ;
} else {
// Get the ears texture for the player
skin = SkinProvider . getOrDefault ( SkinProvider . requestUnofficialEars (
skin , entity . getUuid ( ) , entity . getUsername ( ) , false
) , skin , 3 ) ;
isEars = skin . isEars ( ) ;
}
// Does the skin have an ears texture
if ( isEars ) {
// Get the new geometry
geometry = SkinProvider . SkinGeometry . getEars ( data . isAlex ( ) ) ;
// Store the skin and geometry for the ears
2021-02-15 21:36:47 +00:00
SkinProvider . storeEarSkin ( skin ) ;
2020-10-15 03:34:24 +00:00
SkinProvider . storeEarGeometry ( entity . getUuid ( ) , data . isAlex ( ) ) ;
}
}
if ( session . getUpstream ( ) . isInitialized ( ) ) {
PlayerListPacket . Entry updatedEntry = buildEntryManually (
session ,
entity . getUuid ( ) ,
entity . getUsername ( ) ,
entity . getGeyserId ( ) ,
skin . getTextureUrl ( ) ,
skin . getSkinData ( ) ,
cape . getCapeId ( ) ,
cape . getCapeData ( ) ,
2020-12-04 21:55:24 +00:00
geometry
2020-10-15 03:34:24 +00:00
) ;
PlayerListPacket playerAddPacket = new PlayerListPacket ( ) ;
playerAddPacket . setAction ( PlayerListPacket . Action . ADD ) ;
playerAddPacket . getEntries ( ) . add ( updatedEntry ) ;
session . sendUpstreamPacket ( playerAddPacket ) ;
if ( ! entity . isPlayerList ( ) ) {
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 ( skinAndCapeConsumer ! = null ) {
skinAndCapeConsumer . accept ( skinAndCape ) ;
}
} ) ;
}
public static void handleBedrockSkin ( PlayerEntity playerEntity , BedrockClientData clientData ) {
GeyserConnector . getInstance ( ) . getLogger ( ) . info ( LanguageUtils . getLocaleStringLog ( " geyser.skin.bedrock.register " , playerEntity . getUsername ( ) , playerEntity . getUuid ( ) ) ) ;
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 ) ;
} else {
GeyserConnector . getInstance ( ) . getLogger ( ) . info ( LanguageUtils . getLocaleStringLog ( " geyser.skin.bedrock.fail " , playerEntity . getUsername ( ) ) ) ;
GeyserConnector . getInstance ( ) . getLogger ( ) . debug ( " The size of ' " + playerEntity . getUsername ( ) + " ' skin is: " + clientData . getSkinImageWidth ( ) + " x " + clientData . getSkinImageHeight ( ) ) ;
}
if ( ! clientData . getCapeId ( ) . equals ( " " ) ) {
SkinProvider . storeBedrockCape ( playerEntity . getUuid ( ) , capeBytes ) ;
}
} catch ( Exception e ) {
throw new AssertionError ( " Failed to cache skin for bedrock user ( " + playerEntity . getUsername ( ) + " ): " , e ) ;
}
}
2019-10-09 18:39:38 +00:00
@AllArgsConstructor
@Getter
public static class GameProfileData {
2020-10-15 03:34:24 +00:00
private final String skinUrl ;
private final String capeUrl ;
private final boolean alex ;
2019-10-09 18:39:38 +00:00
2020-04-21 05:28:44 +00:00
/ * *
* Generate the GameProfileData from the given GameProfile
*
* @param profile GameProfile to build the GameProfileData from
* @return The built GameProfileData
* /
2019-10-09 18:39:38 +00:00
public static GameProfileData from ( GameProfile profile ) {
2019-10-10 21:27:30 +00:00
try {
GameProfile . Property skinProperty = profile . getProperty ( " textures " ) ;
2019-10-09 18:39:38 +00:00
2021-01-22 00:03:46 +00:00
if ( skinProperty = = null ) {
// Likely offline mode
return loadBedrockOrOfflineSkin ( profile ) ;
}
2020-12-04 21:55:24 +00:00
JsonNode skinObject = GeyserConnector . JSON_MAPPER . readTree ( new String ( Base64 . getDecoder ( ) . decode ( skinProperty . getValue ( ) ) , StandardCharsets . UTF_8 ) ) ;
2020-04-20 20:10:30 +00:00
JsonNode textures = skinObject . get ( " textures " ) ;
2019-10-09 18:39:38 +00:00
2020-04-20 20:10:30 +00:00
JsonNode skinTexture = textures . get ( " SKIN " ) ;
2020-08-28 18:36:24 +00:00
String skinUrl = skinTexture . get ( " url " ) . asText ( ) . replace ( " http:// " , " https:// " ) ;
2019-10-09 18:39:38 +00:00
2021-01-22 00:03:46 +00:00
boolean isAlex = skinTexture . has ( " metadata " ) ;
2019-10-09 18:39:38 +00:00
2019-10-10 21:27:30 +00:00
String capeUrl = null ;
if ( textures . has ( " CAPE " ) ) {
2020-04-20 20:10:30 +00:00
JsonNode capeTexture = textures . get ( " CAPE " ) ;
2020-08-28 18:36:24 +00:00
capeUrl = capeTexture . get ( " url " ) . asText ( ) . replace ( " http:// " , " https:// " ) ;
2019-10-10 21:27:30 +00:00
}
2019-10-09 18:39:38 +00:00
2019-10-10 21:27:30 +00:00
return new GameProfileData ( skinUrl , capeUrl , isAlex ) ;
} catch ( Exception exception ) {
2021-02-15 21:36:47 +00:00
GeyserConnector . getInstance ( ) . getLogger ( ) . debug ( " Something went wrong while processing skin for " + profile . getName ( ) ) ;
if ( GeyserConnector . getInstance ( ) . getConfig ( ) . isDebugMode ( ) ) {
exception . printStackTrace ( ) ;
}
2021-01-22 00:03:46 +00:00
return loadBedrockOrOfflineSkin ( profile ) ;
}
}
2020-12-17 16:58:49 +00:00
2021-01-22 00:03:46 +00:00
/ * *
* @return default skin with default cape when texture data is invalid , or the Bedrock player ' s skin if this
* is a Bedrock player .
* /
private static GameProfileData loadBedrockOrOfflineSkin ( GameProfile profile ) {
// Fallback to the offline mode of working it out
boolean isAlex = ( Math . abs ( profile . getId ( ) . hashCode ( ) % 2 ) = = 1 ) ;
String skinUrl = isAlex ? SkinProvider . EMPTY_SKIN_ALEX . getTextureUrl ( ) : SkinProvider . EMPTY_SKIN . getTextureUrl ( ) ;
String capeUrl = SkinProvider . EMPTY_CAPE . getTextureUrl ( ) ;
2021-07-28 23:44:09 +00:00
if ( ( " steve " . equals ( skinUrl ) | | " alex " . equals ( skinUrl ) ) & & GeyserConnector . getInstance ( ) . getConfig ( ) . getRemote ( ) . getAuthType ( ) ! = AuthType . ONLINE ) {
2021-01-22 00:03:46 +00:00
GeyserSession session = GeyserConnector . getInstance ( ) . getPlayerByUuid ( profile . getId ( ) ) ;
if ( session ! = null ) {
skinUrl = session . getClientData ( ) . getSkinId ( ) ;
capeUrl = session . getClientData ( ) . getCapeId ( ) ;
2020-10-23 04:01:03 +00:00
}
2019-10-10 21:27:30 +00:00
}
2021-01-22 00:03:46 +00:00
return new GameProfileData ( skinUrl , capeUrl , isAlex ) ;
2019-10-09 18:39:38 +00:00
}
}
}