2020-01-09 03:05:42 +00:00
/ *
* 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
* /
2019-10-09 18:39:38 +00:00
package org.geysermc.connector.utils ;
2020-04-20 20:10:30 +00:00
import com.fasterxml.jackson.databind.JsonNode ;
import com.fasterxml.jackson.databind.ObjectMapper ;
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 ;
2020-06-27 22:27:00 +00:00
import org.geysermc.connector.common.AuthType ;
2019-11-27 01:52:13 +00:00
import org.geysermc.connector.GeyserConnector ;
2019-10-09 18:39:38 +00:00
import org.geysermc.connector.entity.PlayerEntity ;
import org.geysermc.connector.network.session.GeyserSession ;
2020-05-06 21:50:01 +00:00
import org.geysermc.connector.network.session.auth.BedrockClientData ;
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 ;
public class SkinUtils {
2020-01-04 05:58:58 +00:00
2019-10-09 18:39:38 +00:00
public static PlayerListPacket . Entry buildCachedEntry ( GameProfile profile , long geyserId ) {
GameProfileData data = GameProfileData . from ( profile ) ;
2019-11-19 20:31:24 +00:00
SkinProvider . Cape cape = SkinProvider . getCachedCape ( data . getCapeUrl ( ) ) ;
2019-10-09 18:39:38 +00:00
2020-05-23 21:06:34 +00:00
SkinProvider . SkinGeometry geometry = SkinProvider . SkinGeometry . getLegacy ( data . isAlex ( ) ) ;
2020-05-06 21:50:01 +00:00
2019-10-09 18:39:38 +00:00
return buildEntryManually (
profile . getId ( ) ,
profile . getName ( ) ,
geyserId ,
profile . getIdAsString ( ) ,
SkinProvider . getCachedSkin ( profile . getId ( ) ) . getSkinData ( ) ,
2019-11-19 20:31:24 +00:00
cape . getCapeId ( ) ,
cape . getCapeData ( ) ,
2020-05-06 21:50:01 +00:00
geometry . getGeometryName ( ) ,
geometry . getGeometryData ( )
2019-10-09 18:39:38 +00:00
) ;
}
public static PlayerListPacket . Entry buildDefaultEntry ( GameProfile profile , long geyserId ) {
return buildEntryManually (
profile . getId ( ) ,
profile . getName ( ) ,
geyserId ,
profile . getIdAsString ( ) ,
SkinProvider . STEVE_SKIN ,
2019-11-19 20:31:24 +00:00
SkinProvider . EMPTY_CAPE . getCapeId ( ) ,
2019-10-09 18:39:38 +00:00
SkinProvider . EMPTY_CAPE . getCapeData ( ) ,
2020-05-06 21:50:01 +00:00
SkinProvider . EMPTY_GEOMETRY . getGeometryName ( ) ,
SkinProvider . EMPTY_GEOMETRY . getGeometryData ( )
2019-10-09 18:39:38 +00:00
) ;
}
public static PlayerListPacket . Entry buildEntryManually ( UUID uuid , String username , long geyserId ,
2019-11-19 20:31:24 +00:00
String skinId , byte [ ] skinData ,
String capeId , byte [ ] capeData ,
2019-10-09 18:39:38 +00:00
String geometryName , String geometryData ) {
2019-11-19 20:31:24 +00:00
SerializedSkin serializedSkin = SerializedSkin . of (
skinId , geometryName , ImageData . of ( skinData ) , Collections . emptyList ( ) ,
2020-05-23 21:06:34 +00:00
ImageData . of ( capeData ) , geometryData , " " , true , false , ! capeId . equals ( SkinProvider . EMPTY_CAPE . getCapeId ( ) ) , capeId , uuid . toString ( )
2019-11-19 20:31:24 +00:00
) ;
2019-10-31 02:14:23 +00:00
2019-10-09 18:39:38 +00:00
PlayerListPacket . Entry entry = new PlayerListPacket . Entry ( uuid ) ;
entry . setName ( username ) ;
entry . setEntityId ( geyserId ) ;
2019-10-31 02:14:23 +00:00
entry . setSkin ( serializedSkin ) ;
2019-10-09 18:39:38 +00:00
entry . setXuid ( " " ) ;
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 ;
}
@AllArgsConstructor
@Getter
public static class GameProfileData {
private String skinUrl ;
private String capeUrl ;
private boolean alex ;
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 ) {
2020-05-23 21:06:34 +00:00
// Fallback to the offline mode of working it out
boolean isAlex = ( ( profile . getId ( ) . hashCode ( ) % 2 ) = = 1 ) ;
2019-10-10 21:27:30 +00:00
try {
GameProfile . Property skinProperty = profile . getProperty ( " textures " ) ;
2019-10-09 18:39:38 +00:00
2020-04-20 20:10:30 +00:00
JsonNode skinObject = new ObjectMapper ( ) . readTree ( new String ( Base64 . getDecoder ( ) . decode ( skinProperty . getValue ( ) ) , StandardCharsets . UTF_8 ) ) ;
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 " ) ;
String skinUrl = skinTexture . get ( " url " ) . asText ( ) ;
2019-10-09 18:39:38 +00:00
2020-05-23 21:06:34 +00:00
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 " ) ;
capeUrl = capeTexture . get ( " url " ) . asText ( ) ;
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 ) {
2020-01-04 05:58:58 +00:00
if ( GeyserConnector . getInstance ( ) . getAuthType ( ) ! = AuthType . OFFLINE ) {
2019-12-21 17:35:48 +00:00
GeyserConnector . getInstance ( ) . getLogger ( ) . debug ( " Got invalid texture data for " + profile . getName ( ) + " " + exception . getMessage ( ) ) ;
2019-11-27 01:52:13 +00:00
}
2019-10-10 21:27:30 +00:00
// return default skin with default cape when texture data is invalid
2020-05-23 21:06:34 +00:00
return new GameProfileData ( ( isAlex ? SkinProvider . EMPTY_SKIN_ALEX . getTextureUrl ( ) : SkinProvider . EMPTY_SKIN . getTextureUrl ( ) ) , SkinProvider . EMPTY_CAPE . getTextureUrl ( ) , isAlex ) ;
2019-10-10 21:27:30 +00:00
}
2019-10-09 18:39:38 +00:00
}
}
public static void requestAndHandleSkinAndCape ( PlayerEntity entity , GeyserSession session ,
Consumer < SkinProvider . SkinAndCape > skinAndCapeConsumer ) {
2019-12-21 17:35:48 +00:00
GeyserConnector . getInstance ( ) . getGeneralThreadPool ( ) . execute ( ( ) - > {
2019-11-19 20:31:24 +00:00
GameProfileData data = GameProfileData . from ( entity . getProfile ( ) ) ;
2019-10-09 18:39:38 +00:00
SkinProvider . requestSkinAndCape ( entity . getUuid ( ) , data . getSkinUrl ( ) , data . getCapeUrl ( ) )
. whenCompleteAsync ( ( skinAndCape , throwable ) - > {
try {
SkinProvider . Skin skin = skinAndCape . getSkin ( ) ;
SkinProvider . Cape cape = skinAndCape . getCape ( ) ;
2020-05-06 21:50:01 +00:00
if ( cape . isFailed ( ) ) {
cape = SkinProvider . getOrDefault ( SkinProvider . requestBedrockCape (
entity . getUuid ( ) , false
) , SkinProvider . EMPTY_CAPE , 3 ) ;
}
2019-10-09 18:39:38 +00:00
if ( cape . isFailed ( ) & & SkinProvider . ALLOW_THIRD_PARTY_CAPES ) {
cape = SkinProvider . getOrDefault ( SkinProvider . requestUnofficialCape (
cape , entity . getUuid ( ) ,
entity . getUsername ( ) , false
2020-04-29 16:04:45 +00:00
) , SkinProvider . EMPTY_CAPE , SkinProvider . CapeProvider . VALUES . length * 3 ) ;
2019-10-09 18:39:38 +00:00
}
2020-05-23 21:06:34 +00:00
SkinProvider . SkinGeometry geometry = SkinProvider . SkinGeometry . getLegacy ( data . isAlex ( ) ) ;
2020-05-06 21:50:01 +00:00
geometry = SkinProvider . getOrDefault ( SkinProvider . requestBedrockGeometry (
geometry , entity . getUuid ( ) , false
) , geometry , 3 ) ;
2020-05-23 21:06:34 +00:00
// Not a bedrock player check for ears
if ( geometry . isFailed ( ) & & SkinProvider . ALLOW_THIRD_PARTY_EARS ) {
boolean isEars = false ;
// Its deadmau5, gotta support his skin :)
if ( entity . getUuid ( ) . toString ( ) . equals ( " 1e18d5ff-643d-45c8-b509-43b8461d8614 " ) ) {
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
SkinProvider . storeEarSkin ( entity . getUuid ( ) , skin ) ;
SkinProvider . storeEarGeometry ( entity . getUuid ( ) , data . isAlex ( ) ) ;
}
}
2019-10-09 18:39:38 +00:00
if ( entity . getLastSkinUpdate ( ) < skin . getRequestedOn ( ) ) {
entity . setLastSkinUpdate ( skin . getRequestedOn ( ) ) ;
if ( session . getUpstream ( ) . isInitialized ( ) ) {
2019-11-19 20:31:24 +00:00
PlayerListPacket . Entry updatedEntry = buildEntryManually (
2019-10-09 18:39:38 +00:00
entity . getUuid ( ) ,
entity . getUsername ( ) ,
entity . getGeyserId ( ) ,
entity . getUuid ( ) . toString ( ) ,
skin . getSkinData ( ) ,
2019-11-19 20:31:24 +00:00
cape . getCapeId ( ) ,
2019-10-09 18:39:38 +00:00
cape . getCapeData ( ) ,
2020-05-06 21:50:01 +00:00
geometry . getGeometryName ( ) ,
geometry . getGeometryData ( )
2019-10-09 18:39:38 +00:00
) ;
2020-05-26 03:39:57 +00:00
// If it is our skin we replace the UUID with the authdata UUID
if ( session . getPlayerEntity ( ) = = entity ) {
// Copy the entry with our identity instead.
PlayerListPacket . Entry copy = new PlayerListPacket . Entry ( session . getAuthData ( ) . getUUID ( ) ) ;
copy . setName ( updatedEntry . getName ( ) ) ;
copy . setEntityId ( updatedEntry . getEntityId ( ) ) ;
copy . setSkin ( updatedEntry . getSkin ( ) ) ;
copy . setXuid ( updatedEntry . getXuid ( ) ) ;
copy . setPlatformChatId ( updatedEntry . getPlatformChatId ( ) ) ;
copy . setTeacher ( updatedEntry . isTeacher ( ) ) ;
updatedEntry = copy ;
}
2019-10-09 18:39:38 +00:00
PlayerListPacket playerRemovePacket = new PlayerListPacket ( ) ;
2020-02-06 00:55:34 +00:00
playerRemovePacket . setAction ( PlayerListPacket . Action . REMOVE ) ;
2019-10-09 18:39:38 +00:00
playerRemovePacket . getEntries ( ) . add ( updatedEntry ) ;
2020-05-05 15:51:43 +00:00
session . sendUpstreamPacket ( playerRemovePacket ) ;
2019-10-09 18:39:38 +00:00
PlayerListPacket playerAddPacket = new PlayerListPacket ( ) ;
2020-02-06 00:55:34 +00:00
playerAddPacket . setAction ( PlayerListPacket . Action . ADD ) ;
2019-10-09 18:39:38 +00:00
playerAddPacket . getEntries ( ) . add ( updatedEntry ) ;
2020-05-05 15:51:43 +00:00
session . sendUpstreamPacket ( playerAddPacket ) ;
2020-05-23 21:06:34 +00:00
if ( entity . getUuid ( ) . equals ( session . getPlayerEntity ( ) . getUuid ( ) ) ) {
session . fetchOurSkin ( updatedEntry ) ;
}
2019-10-09 18:39:38 +00:00
}
}
} catch ( Exception e ) {
2020-07-05 23:35:51 +00:00
GeyserConnector . getInstance ( ) . getLogger ( ) . error ( LanguageUtils . getLocaleStringLog ( " geyser.skin.fail " , entity . getUuid ( ) ) , e ) ;
2019-10-09 18:39:38 +00:00
}
if ( skinAndCapeConsumer ! = null ) skinAndCapeConsumer . accept ( skinAndCape ) ;
} ) ;
} ) ;
}
2019-11-06 00:55:59 +00:00
2020-05-06 21:50:01 +00:00
public static void handleBedrockSkin ( PlayerEntity playerEntity , BedrockClientData clientData ) {
GameProfileData data = GameProfileData . from ( playerEntity . getProfile ( ) ) ;
2020-07-05 23:35:51 +00:00
GeyserConnector . getInstance ( ) . getLogger ( ) . info ( LanguageUtils . getLocaleStringLog ( " geyser.skin.bedrock.register " , playerEntity . getUsername ( ) , playerEntity . getUuid ( ) ) ) ;
2020-05-06 21:50:01 +00:00
try {
byte [ ] skinBytes = com . github . steveice10 . mc . auth . util . Base64 . decode ( clientData . getSkinData ( ) . getBytes ( " UTF-8 " ) ) ;
byte [ ] capeBytes = clientData . getCapeData ( ) ;
2020-05-23 21:06:34 +00:00
byte [ ] geometryNameBytes = Base64 . getDecoder ( ) . decode ( clientData . getGeometryName ( ) . getBytes ( " UTF-8 " ) ) ;
byte [ ] geometryBytes = Base64 . getDecoder ( ) . decode ( clientData . getGeometryData ( ) . getBytes ( " UTF-8 " ) ) ;
2020-05-06 21:50:01 +00:00
2020-05-12 04:45:16 +00:00
if ( skinBytes . length < = ( 128 * 128 * 4 ) & & ! clientData . isPersonaSkin ( ) ) {
2020-05-06 21:50:01 +00:00
SkinProvider . storeBedrockSkin ( playerEntity . getUuid ( ) , data . getSkinUrl ( ) , skinBytes ) ;
2020-05-12 04:45:16 +00:00
SkinProvider . storeBedrockGeometry ( playerEntity . getUuid ( ) , geometryNameBytes , geometryBytes ) ;
2020-05-06 21:50:01 +00:00
} else {
2020-07-05 23:35:51 +00:00
GeyserConnector . getInstance ( ) . getLogger ( ) . info ( LanguageUtils . getLocaleStringLog ( " geyser.skin.bedrock.fail " , playerEntity . getUsername ( ) ) ) ;
2020-05-06 21:50:01 +00:00
GeyserConnector . getInstance ( ) . getLogger ( ) . debug ( " The size of ' " + playerEntity . getUsername ( ) + " ' skin is: " + clientData . getSkinImageWidth ( ) + " x " + clientData . getSkinImageHeight ( ) ) ;
}
2020-05-12 04:45:16 +00:00
2020-05-06 21:50:01 +00:00
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
}