2020-01-09 03:05:42 +00:00
/ *
2024-03-17 22:30:00 +00:00
* Copyright ( c ) 2019 - 2024 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-09-25 21:52:28 +00:00
2020-04-29 16:04:45 +00:00
import com.fasterxml.jackson.databind.JsonNode ;
2020-08-07 16:33:21 +00:00
import com.google.common.cache.Cache ;
import com.google.common.cache.CacheBuilder ;
2022-12-18 18:18:06 +00:00
import it.unimi.dsi.fastutil.bytes.ByteArrays ;
2019-09-25 21:52:28 +00:00
import lombok.AllArgsConstructor ;
import lombok.Getter ;
2020-04-29 16:04:45 +00:00
import lombok.NoArgsConstructor ;
2023-12-05 23:54:42 +00:00
import org.checkerframework.checker.nullness.qual.NonNull ;
import org.checkerframework.checker.nullness.qual.Nullable ;
2021-11-20 21:34:30 +00:00
import org.geysermc.geyser.GeyserImpl ;
2024-03-19 20:27:30 +00:00
import org.geysermc.geyser.api.event.bedrock.SessionSkinApplyEvent ;
2022-12-18 18:18:06 +00:00
import org.geysermc.geyser.api.network.AuthType ;
2024-03-17 17:50:39 +00:00
import org.geysermc.geyser.api.skin.Cape ;
import org.geysermc.geyser.api.skin.Skin ;
import org.geysermc.geyser.api.skin.SkinData ;
import org.geysermc.geyser.api.skin.SkinGeometry ;
2021-11-23 03:47:58 +00:00
import org.geysermc.geyser.entity.type.player.PlayerEntity ;
2021-11-22 19:52:26 +00:00
import org.geysermc.geyser.session.GeyserSession ;
2021-11-23 03:47:58 +00:00
import org.geysermc.geyser.text.GeyserLocale ;
2021-11-20 23:29:46 +00:00
import org.geysermc.geyser.util.FileUtils ;
import org.geysermc.geyser.util.WebUtils ;
2019-09-25 21:52:28 +00:00
import javax.imageio.ImageIO ;
2019-10-02 20:45:29 +00:00
import java.awt.* ;
2019-09-25 21:52:28 +00:00
import java.awt.image.BufferedImage ;
2020-12-04 21:55:24 +00:00
import java.io.ByteArrayOutputStream ;
import java.io.File ;
import java.io.IOException ;
2020-07-11 17:22:02 +00:00
import java.net.HttpURLConnection ;
2019-09-25 21:52:28 +00:00
import java.net.URL ;
2020-05-23 21:06:34 +00:00
import java.nio.charset.StandardCharsets ;
2020-10-23 04:01:03 +00:00
import java.util.* ;
2019-09-25 21:52:28 +00:00
import java.util.concurrent.* ;
2021-12-29 05:01:38 +00:00
import java.util.function.Predicate ;
2019-09-25 21:52:28 +00:00
public class SkinProvider {
2023-12-06 21:22:21 +00:00
private static ExecutorService EXECUTOR_SERVICE ;
2019-09-25 21:52:28 +00:00
2022-12-18 18:18:06 +00:00
static final Skin EMPTY_SKIN ;
2024-03-17 17:50:39 +00:00
static final Cape EMPTY_CAPE = new Cape ( " " , " no-cape " , ByteArrays . EMPTY_ARRAY , true ) ;
2022-12-18 18:18:06 +00:00
private static final Cache < String , Cape > CACHED_JAVA_CAPES = CacheBuilder . newBuilder ( )
. expireAfterAccess ( 1 , TimeUnit . HOURS )
. build ( ) ;
private static final Cache < String , Skin > CACHED_JAVA_SKINS = CacheBuilder . newBuilder ( )
2020-08-07 16:33:21 +00:00
. expireAfterAccess ( 1 , TimeUnit . HOURS )
. build ( ) ;
2022-12-18 18:18:06 +00:00
private static final Cache < String , Cape > CACHED_BEDROCK_CAPES = CacheBuilder . newBuilder ( )
. expireAfterAccess ( 1 , TimeUnit . HOURS )
. build ( ) ;
private static final Cache < String , Skin > CACHED_BEDROCK_SKINS = CacheBuilder . newBuilder ( )
2020-08-07 16:33:21 +00:00
. expireAfterAccess ( 1 , TimeUnit . HOURS )
. build ( ) ;
2022-12-18 18:18:06 +00:00
2020-08-07 16:33:21 +00:00
private static final Map < String , CompletableFuture < Cape > > requestedCapes = new ConcurrentHashMap < > ( ) ;
2022-12-18 18:18:06 +00:00
private static final Map < String , CompletableFuture < Skin > > requestedSkins = new ConcurrentHashMap < > ( ) ;
2019-09-25 21:52:28 +00:00
2020-08-07 16:33:21 +00:00
private static final Map < UUID , SkinGeometry > cachedGeometry = new ConcurrentHashMap < > ( ) ;
2020-05-06 21:50:01 +00:00
2021-12-29 05:01:38 +00:00
/ * *
* Citizens NPCs use UUID version 2 , while legitimate Minecraft players use version 4 , and
* offline mode players use version 3 .
* /
2022-12-18 18:18:06 +00:00
private static final Predicate < UUID > IS_NPC = uuid - > uuid . version ( ) = = 2 ;
2021-12-29 05:01:38 +00:00
2022-12-18 18:18:06 +00:00
static final SkinGeometry SKULL_GEOMETRY ;
static final SkinGeometry WEARING_CUSTOM_SKULL ;
static final SkinGeometry WEARING_CUSTOM_SKULL_SLIM ;
2019-09-25 21:52:28 +00:00
2020-05-23 21:06:34 +00:00
static {
2022-12-18 18:18:06 +00:00
// Generate the empty texture to use as an emergency fallback
final int pink = - 524040 ;
final int black = - 16777216 ;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream ( 64 * 4 + 64 * 4 ) ;
for ( int y = 0 ; y < 64 ; y + + ) {
for ( int x = 0 ; x < 64 ; x + + ) {
int rgba ;
if ( y > 32 ) {
rgba = x > = 32 ? pink : black ;
} else {
rgba = x > = 32 ? black : pink ;
}
outputStream . write ( ( rgba > > 16 ) & 0xFF ) ; // Red
outputStream . write ( ( rgba > > 8 ) & 0xFF ) ; // Green
outputStream . write ( rgba & 0xFF ) ; // Blue
outputStream . write ( ( rgba > > 24 ) & 0xFF ) ; // Alpha
}
}
2024-03-17 21:20:46 +00:00
EMPTY_SKIN = new Skin ( " geysermc:empty " , outputStream . toByteArray ( ) , true ) ;
2022-12-18 18:18:06 +00:00
2020-12-04 21:55:24 +00:00
/* Load in the custom skull geometry */
2021-12-03 16:01:06 +00:00
String skullData = new String ( FileUtils . readAllBytes ( " bedrock/skin/geometry.humanoid.customskull.json " ) , StandardCharsets . UTF_8 ) ;
2024-03-17 17:50:39 +00:00
SKULL_GEOMETRY = new SkinGeometry ( " { \" geometry \" :{ \" default \" : \" geometry.humanoid.customskull \" }} " , skullData ) ;
2020-05-23 21:06:34 +00:00
2021-11-22 19:49:55 +00:00
/* Load in the player head skull geometry */
2021-12-03 16:01:06 +00:00
String wearingCustomSkull = new String ( FileUtils . readAllBytes ( " bedrock/skin/geometry.humanoid.wearingCustomSkull.json " ) , StandardCharsets . UTF_8 ) ;
2024-03-17 17:50:39 +00:00
WEARING_CUSTOM_SKULL = new SkinGeometry ( " { \" geometry \" :{ \" default \" : \" geometry.humanoid.wearingCustomSkull \" }} " , wearingCustomSkull ) ;
2021-12-03 16:01:06 +00:00
String wearingCustomSkullSlim = new String ( FileUtils . readAllBytes ( " bedrock/skin/geometry.humanoid.wearingCustomSkullSlim.json " ) , StandardCharsets . UTF_8 ) ;
2024-03-17 17:50:39 +00:00
WEARING_CUSTOM_SKULL_SLIM = new SkinGeometry ( " { \" geometry \" :{ \" default \" : \" geometry.humanoid.wearingCustomSkullSlim \" }} " , wearingCustomSkullSlim ) ;
2024-03-19 20:34:09 +00:00
GeyserImpl geyser = GeyserImpl . getInstance ( ) ;
2024-04-25 15:22:39 +00:00
if ( geyser . getConfig ( ) . isAllowThirdPartyEars ( ) | | geyser . getConfig ( ) . isAllowThirdPartyCapes ( ) ) {
geyser . getLogger ( ) . warning ( " Third-party ears/capes have been removed from Geyser, if you still wish to have this functionality please use the extension: https://github.com/GeyserMC/ThirdPartyCosmetics " ) ;
2024-03-19 20:34:09 +00:00
}
2022-01-31 14:57:43 +00:00
}
2021-11-22 19:49:55 +00:00
2023-12-06 21:22:21 +00:00
public static ExecutorService getExecutorService ( ) {
2023-10-01 22:15:44 +00:00
if ( EXECUTOR_SERVICE = = null ) {
2024-03-18 09:00:28 +00:00
EXECUTOR_SERVICE = Executors . newFixedThreadPool ( 14 ) ;
2023-10-01 22:15:44 +00:00
}
return EXECUTOR_SERVICE ;
}
public static void shutdown ( ) {
if ( EXECUTOR_SERVICE ! = null ) {
EXECUTOR_SERVICE . shutdown ( ) ;
EXECUTOR_SERVICE = null ;
}
}
2022-01-31 14:57:43 +00:00
public static void registerCacheImageTask ( GeyserImpl geyser ) {
2020-08-07 16:33:21 +00:00
// Schedule Daily Image Expiry if we are caching them
2022-01-31 14:57:43 +00:00
if ( geyser . getConfig ( ) . getCacheImages ( ) > 0 ) {
geyser . getScheduledThread ( ) . scheduleAtFixedRate ( ( ) - > {
2021-11-20 21:34:30 +00:00
File cacheFolder = GeyserImpl . getInstance ( ) . getBootstrap ( ) . getConfigFolder ( ) . resolve ( " cache " ) . resolve ( " images " ) . toFile ( ) ;
2020-08-07 16:33:21 +00:00
if ( ! cacheFolder . exists ( ) ) {
return ;
}
int count = 0 ;
2021-11-20 21:34:30 +00:00
final long expireTime = ( ( long ) GeyserImpl . getInstance ( ) . getConfig ( ) . getCacheImages ( ) ) * ( ( long ) 1000 * 60 * 60 * 24 ) ;
2020-08-07 16:33:21 +00:00
for ( File imageFile : Objects . requireNonNull ( cacheFolder . listFiles ( ) ) ) {
if ( imageFile . lastModified ( ) < System . currentTimeMillis ( ) - expireTime ) {
//noinspection ResultOfMethodCallIgnored
imageFile . delete ( ) ;
count + + ;
}
}
if ( count > 0 ) {
2021-11-20 21:34:30 +00:00
GeyserImpl . getInstance ( ) . getLogger ( ) . debug ( String . format ( " Removed %d cached image files as they have expired " , count ) ) ;
2020-08-07 16:33:21 +00:00
}
} , 10 , 1440 , TimeUnit . MINUTES ) ;
}
2019-09-25 21:52:28 +00:00
}
2022-12-18 18:18:06 +00:00
/ * *
* Search our cached database for an already existing , translated skin of this Java URL .
* /
static Skin getCachedSkin ( String skinUrl ) {
return CACHED_JAVA_SKINS . getIfPresent ( skinUrl ) ;
2019-09-25 21:52:28 +00:00
}
2022-12-18 18:18:06 +00:00
/ * *
* If skin data fails to apply , or there is no skin data to apply , determine what skin we should give as a fallback .
* /
2023-01-22 18:23:16 +00:00
static SkinData determineFallbackSkinData ( UUID uuid ) {
2022-12-18 18:18:06 +00:00
Skin skin = null ;
Cape cape = null ;
SkinGeometry geometry = SkinGeometry . WIDE ;
if ( GeyserImpl . getInstance ( ) . getConfig ( ) . getRemote ( ) . authType ( ) ! = AuthType . ONLINE ) {
// Let's see if this player is a Bedrock player, and if so, let's pull their skin.
GeyserSession session = GeyserImpl . getInstance ( ) . connectionByUuid ( uuid ) ;
if ( session ! = null ) {
String skinId = session . getClientData ( ) . getSkinId ( ) ;
skin = CACHED_BEDROCK_SKINS . getIfPresent ( skinId ) ;
String capeId = session . getClientData ( ) . getCapeId ( ) ;
cape = CACHED_BEDROCK_CAPES . getIfPresent ( capeId ) ;
geometry = cachedGeometry . getOrDefault ( uuid , geometry ) ;
}
}
if ( skin = = null ) {
// We don't have a skin for the player right now. Fall back to a default.
2023-01-22 18:23:16 +00:00
ProvidedSkins . ProvidedSkin providedSkin = ProvidedSkins . getDefaultPlayerSkin ( uuid ) ;
2022-12-18 18:18:06 +00:00
skin = providedSkin . getData ( ) ;
2024-03-17 17:50:39 +00:00
geometry = providedSkin . isSlim ( ) ? SkinGeometry . SLIM : SkinGeometry . WIDE ;
2022-12-18 18:18:06 +00:00
}
if ( cape = = null ) {
cape = EMPTY_CAPE ;
}
return new SkinData ( skin , cape , geometry ) ;
2019-09-25 21:52:28 +00:00
}
2022-12-18 18:18:06 +00:00
/ * *
* Used as a fallback if an official Java cape doesn ' t exist for this user .
* /
2023-12-05 23:54:42 +00:00
@NonNull
2022-12-18 18:18:06 +00:00
private static Cape getCachedBedrockCape ( UUID uuid ) {
GeyserSession session = GeyserImpl . getInstance ( ) . connectionByUuid ( uuid ) ;
if ( session ! = null ) {
String capeId = session . getClientData ( ) . getCapeId ( ) ;
Cape bedrockCape = CACHED_BEDROCK_CAPES . getIfPresent ( capeId ) ;
if ( bedrockCape ! = null ) {
return bedrockCape ;
}
}
return EMPTY_CAPE ;
}
@Nullable
static Cape getCachedCape ( String capeUrl ) {
if ( capeUrl = = null ) {
return null ;
}
return CACHED_JAVA_CAPES . getIfPresent ( capeUrl ) ;
2019-09-25 21:52:28 +00:00
}
2024-03-19 20:27:30 +00:00
static CompletableFuture < SkinData > requestSkinData ( PlayerEntity entity , GeyserSession session ) {
2022-03-31 02:30:49 +00:00
SkinManager . GameProfileData data = SkinManager . GameProfileData . from ( entity ) ;
2022-12-18 18:18:06 +00:00
if ( data = = null ) {
// This player likely does not have a textures property
2023-01-22 18:23:16 +00:00
return CompletableFuture . completedFuture ( determineFallbackSkinData ( entity . getUuid ( ) ) ) ;
2022-12-18 18:18:06 +00:00
}
2021-11-22 19:49:55 +00:00
return requestSkinAndCape ( entity . getUuid ( ) , data . skinUrl ( ) , data . capeUrl ( ) )
. thenApplyAsync ( skinAndCape - > {
try {
2022-12-18 18:18:06 +00:00
Skin skin = skinAndCape . skin ( ) ;
Cape cape = skinAndCape . cape ( ) ;
SkinGeometry geometry = data . isAlex ( ) ? SkinGeometry . SLIM : SkinGeometry . WIDE ;
// Whether we should see if this player has a Bedrock skin we should check for on failure of
// any skin property
boolean checkForBedrock = entity . getUuid ( ) . version ( ) ! = 4 ;
2021-11-22 19:49:55 +00:00
2022-12-18 18:18:06 +00:00
if ( cape . failed ( ) & & checkForBedrock ) {
cape = getCachedBedrockCape ( entity . getUuid ( ) ) ;
2021-11-22 19:49:55 +00:00
}
2024-03-17 22:30:00 +00:00
// Call event to allow extensions to modify the skin, cape and geo
boolean isBedrock = GeyserImpl . getInstance ( ) . connectionByUuid ( entity . getUuid ( ) ) ! = null ;
2024-04-26 23:00:41 +00:00
SkinData skinData = new SkinData ( skin , cape , geometry ) ;
final EventSkinData eventSkinData = new EventSkinData ( skinData ) ;
GeyserImpl . getInstance ( ) . eventBus ( ) . fire ( new SessionSkinApplyEvent ( session , entity . getUsername ( ) , entity . getUuid ( ) , data . isAlex ( ) , isBedrock , skinData ) {
2024-04-26 23:07:49 +00:00
@Override
public SkinData skinData ( ) {
return eventSkinData . skinData ( ) ;
}
2024-03-17 17:50:39 +00:00
@Override
public void skin ( @NonNull Skin newSkin ) {
2024-04-26 23:00:41 +00:00
eventSkinData . skinData ( new SkinData ( newSkin , skinData . cape ( ) , skinData . geometry ( ) ) ) ;
2024-03-17 17:50:39 +00:00
}
@Override
public void cape ( @NonNull Cape newCape ) {
2024-04-26 23:00:41 +00:00
eventSkinData . skinData ( new SkinData ( skinData . skin ( ) , newCape , skinData . geometry ( ) ) ) ;
2024-03-17 17:50:39 +00:00
}
@Override
public void geometry ( @NonNull SkinGeometry newGeometry ) {
2024-04-26 23:00:41 +00:00
eventSkinData . skinData ( new SkinData ( skinData . skin ( ) , skinData . cape ( ) , newGeometry ) ) ;
2024-03-17 17:50:39 +00:00
}
} ) ;
2024-04-26 23:00:41 +00:00
return eventSkinData . skinData ( ) ;
2021-11-22 19:49:55 +00:00
} catch ( Exception e ) {
2021-11-23 03:47:58 +00:00
GeyserImpl . getInstance ( ) . getLogger ( ) . error ( GeyserLocale . getLocaleStringLog ( " geyser.skin.fail " , entity . getUuid ( ) ) , e ) ;
2021-11-22 19:49:55 +00:00
}
2022-12-18 18:18:06 +00:00
return new SkinData ( skinAndCape . skin ( ) , skinAndCape . cape ( ) , null ) ;
2021-11-22 19:49:55 +00:00
} ) ;
}
2022-12-18 18:18:06 +00:00
private static CompletableFuture < SkinAndCape > requestSkinAndCape ( UUID playerId , String skinUrl , String capeUrl ) {
2019-09-25 21:52:28 +00:00
return CompletableFuture . supplyAsync ( ( ) - > {
long time = System . currentTimeMillis ( ) ;
SkinAndCape skinAndCape = new SkinAndCape (
2022-12-18 18:18:06 +00:00
getOrDefault ( requestSkin ( playerId , skinUrl , false ) , EMPTY_SKIN , 5 ) ,
2024-04-26 21:29:01 +00:00
getOrDefault ( requestCape ( capeUrl , false ) , EMPTY_CAPE , 5 )
2019-09-25 21:52:28 +00:00
) ;
2021-11-20 21:34:30 +00:00
GeyserImpl . getInstance ( ) . getLogger ( ) . debug ( " Took " + ( System . currentTimeMillis ( ) - time ) + " ms for " + playerId ) ;
2019-09-25 21:52:28 +00:00
return skinAndCape ;
2023-10-01 22:15:44 +00:00
} , getExecutorService ( ) ) ;
2019-09-25 21:52:28 +00:00
}
2022-12-18 18:18:06 +00:00
static CompletableFuture < Skin > requestSkin ( UUID playerId , String textureUrl , boolean newThread ) {
2019-09-25 21:52:28 +00:00
if ( textureUrl = = null | | textureUrl . isEmpty ( ) ) return CompletableFuture . completedFuture ( EMPTY_SKIN ) ;
2021-09-10 20:32:09 +00:00
CompletableFuture < Skin > requestedSkin = requestedSkins . get ( textureUrl ) ;
if ( requestedSkin ! = null ) {
// already requested
return requestedSkin ;
}
2019-09-25 21:52:28 +00:00
2022-12-18 18:18:06 +00:00
Skin cachedSkin = CACHED_JAVA_SKINS . getIfPresent ( textureUrl ) ;
2020-08-07 16:33:21 +00:00
if ( cachedSkin ! = null ) {
return CompletableFuture . completedFuture ( cachedSkin ) ;
2019-09-25 21:52:28 +00:00
}
CompletableFuture < Skin > future ;
if ( newThread ) {
2023-10-01 22:15:44 +00:00
future = CompletableFuture . supplyAsync ( ( ) - > supplySkin ( playerId , textureUrl ) , getExecutorService ( ) )
2019-09-25 21:52:28 +00:00
. whenCompleteAsync ( ( skin , throwable ) - > {
2022-12-18 18:18:06 +00:00
CACHED_JAVA_SKINS . put ( textureUrl , skin ) ;
2020-08-07 16:33:21 +00:00
requestedSkins . remove ( textureUrl ) ;
2019-09-25 21:52:28 +00:00
} ) ;
2020-08-07 16:33:21 +00:00
requestedSkins . put ( textureUrl , future ) ;
2019-09-25 21:52:28 +00:00
} else {
Skin skin = supplySkin ( playerId , textureUrl ) ;
future = CompletableFuture . completedFuture ( skin ) ;
2022-12-18 18:18:06 +00:00
CACHED_JAVA_SKINS . put ( textureUrl , skin ) ;
2019-09-25 21:52:28 +00:00
}
return future ;
}
2024-04-26 21:29:01 +00:00
private static CompletableFuture < Cape > requestCape ( String capeUrl , boolean newThread ) {
2019-09-25 21:52:28 +00:00
if ( capeUrl = = null | | capeUrl . isEmpty ( ) ) return CompletableFuture . completedFuture ( EMPTY_CAPE ) ;
2022-12-18 18:18:06 +00:00
CompletableFuture < Cape > requestedCape = requestedCapes . get ( capeUrl ) ;
if ( requestedCape ! = null ) {
return requestedCape ;
}
2019-09-25 21:52:28 +00:00
2022-12-18 18:18:06 +00:00
Cape cachedCape = CACHED_JAVA_CAPES . getIfPresent ( capeUrl ) ;
2020-08-07 16:33:21 +00:00
if ( cachedCape ! = null ) {
return CompletableFuture . completedFuture ( cachedCape ) ;
2019-09-25 21:52:28 +00:00
}
CompletableFuture < Cape > future ;
if ( newThread ) {
2024-04-26 21:29:01 +00:00
future = CompletableFuture . supplyAsync ( ( ) - > supplyCape ( capeUrl ) , getExecutorService ( ) )
2019-09-25 21:52:28 +00:00
. whenCompleteAsync ( ( cape , throwable ) - > {
2022-12-18 18:18:06 +00:00
CACHED_JAVA_CAPES . put ( capeUrl , cape ) ;
2019-09-25 21:52:28 +00:00
requestedCapes . remove ( capeUrl ) ;
} ) ;
requestedCapes . put ( capeUrl , future ) ;
} else {
2024-04-26 21:29:01 +00:00
Cape cape = supplyCape ( capeUrl ) ; // blocking
2019-09-25 21:52:28 +00:00
future = CompletableFuture . completedFuture ( cape ) ;
2022-12-18 18:18:06 +00:00
CACHED_JAVA_CAPES . put ( capeUrl , cape ) ;
2019-09-25 21:52:28 +00:00
}
return future ;
}
2022-12-18 18:18:06 +00:00
static void storeBedrockSkin ( UUID playerID , String skinId , byte [ ] skinData ) {
2024-03-17 17:50:39 +00:00
Skin skin = new Skin ( skinId , skinData ) ;
CACHED_BEDROCK_SKINS . put ( skin . textureUrl ( ) , skin ) ;
2020-05-06 21:50:01 +00:00
}
2022-12-18 18:18:06 +00:00
static void storeBedrockCape ( String capeId , byte [ ] capeData ) {
2024-03-17 21:20:46 +00:00
Cape cape = new Cape ( capeId , capeId , capeData ) ;
2022-12-18 18:18:06 +00:00
CACHED_BEDROCK_CAPES . put ( capeId , cape ) ;
2020-05-06 21:50:01 +00:00
}
2022-12-18 18:18:06 +00:00
static void storeBedrockGeometry ( UUID playerID , byte [ ] geometryName , byte [ ] geometryData ) {
2024-03-17 17:50:39 +00:00
SkinGeometry geometry = new SkinGeometry ( new String ( geometryName ) , new String ( geometryData ) ) ;
2020-05-06 21:50:01 +00:00
cachedGeometry . put ( playerID , geometry ) ;
}
2019-09-25 21:52:28 +00:00
private static Skin supplySkin ( UUID uuid , String textureUrl ) {
try {
2024-04-26 21:29:01 +00:00
byte [ ] skin = requestImageData ( textureUrl , false ) ;
2024-03-17 17:50:39 +00:00
return new Skin ( textureUrl , skin ) ;
2019-09-25 21:52:28 +00:00
} catch ( Exception ignored ) { } // just ignore I guess
2020-08-07 16:33:21 +00:00
2024-03-17 21:20:46 +00:00
return new Skin ( " empty " , EMPTY_SKIN . skinData ( ) , true ) ;
2019-09-25 21:52:28 +00:00
}
2024-04-26 21:29:01 +00:00
private static Cape supplyCape ( String capeUrl ) {
2022-12-18 18:18:06 +00:00
byte [ ] cape = EMPTY_CAPE . capeData ( ) ;
2019-09-25 21:52:28 +00:00
try {
2024-04-26 21:29:01 +00:00
cape = requestImageData ( capeUrl , true ) ;
2021-11-22 19:49:55 +00:00
} catch ( Exception ignored ) {
} // just ignore I guess
2019-10-02 20:45:29 +00:00
2019-11-19 20:31:24 +00:00
String [ ] urlSection = capeUrl . split ( " / " ) ; // A real url is expected at this stage
2019-10-02 20:45:29 +00:00
return new Cape (
capeUrl ,
2019-11-19 20:31:24 +00:00
urlSection [ urlSection . length - 1 ] , // get the texture id and use it as cape id
2021-02-15 21:36:47 +00:00
cape ,
2019-10-02 20:45:29 +00:00
cape . length = = 0
) ;
2019-09-25 21:52:28 +00:00
}
2020-08-07 16:33:21 +00:00
@SuppressWarnings ( " ResultOfMethodCallIgnored " )
2024-04-26 21:29:01 +00:00
public static BufferedImage requestImage ( String imageUrl , boolean isCape ) throws IOException {
2020-08-07 16:33:21 +00:00
BufferedImage image = null ;
// First see if we have a cached file. We also update the modification stamp so we know when the file was last used
2023-12-05 23:54:42 +00:00
File imageFile = GeyserImpl . getInstance ( ) . getBootstrap ( ) . getConfigFolder ( ) . resolve ( " cache " ) . resolve ( " images " ) . resolve ( UUID . nameUUIDFromBytes ( imageUrl . getBytes ( ) ) + " .png " ) . toFile ( ) ;
2020-08-07 16:33:21 +00:00
if ( imageFile . exists ( ) ) {
try {
2021-11-20 21:34:30 +00:00
GeyserImpl . getInstance ( ) . getLogger ( ) . debug ( " Reading cached image from file " + imageFile . getPath ( ) + " for " + imageUrl ) ;
2020-08-07 16:33:21 +00:00
imageFile . setLastModified ( System . currentTimeMillis ( ) ) ;
image = ImageIO . read ( imageFile ) ;
} catch ( IOException ignored ) { }
}
// If no image we download it
if ( image = = null ) {
2024-04-26 21:29:01 +00:00
image = downloadImage ( imageUrl ) ;
2021-11-20 21:34:30 +00:00
GeyserImpl . getInstance ( ) . getLogger ( ) . debug ( " Downloaded " + imageUrl ) ;
2020-08-07 16:33:21 +00:00
// Write to cache if we are allowed
2021-11-20 21:34:30 +00:00
if ( GeyserImpl . getInstance ( ) . getConfig ( ) . getCacheImages ( ) > 0 ) {
2020-08-07 16:33:21 +00:00
imageFile . getParentFile ( ) . mkdirs ( ) ;
try {
ImageIO . write ( image , " png " , imageFile ) ;
2021-11-20 21:34:30 +00:00
GeyserImpl . getInstance ( ) . getLogger ( ) . debug ( " Writing cached skin to file " + imageFile . getPath ( ) + " for " + imageUrl ) ;
2020-08-07 16:33:21 +00:00
} catch ( IOException e ) {
2021-11-20 21:34:30 +00:00
GeyserImpl . getInstance ( ) . getLogger ( ) . error ( " Failed to write cached skin to file " + imageFile . getPath ( ) + " for " + imageUrl ) ;
2020-08-07 16:33:21 +00:00
}
}
}
2019-09-25 21:52:28 +00:00
2020-08-07 16:33:21 +00:00
// if the requested image is a cape
2024-04-26 21:29:01 +00:00
if ( isCape ) {
2021-03-04 16:32:56 +00:00
if ( image . getWidth ( ) > 64 | | image . getHeight ( ) > 32 ) {
2021-03-03 23:00:29 +00:00
// Prevent weirdly-scaled capes from being cut off
BufferedImage newImage = new BufferedImage ( 128 , 64 , BufferedImage . TYPE_INT_ARGB ) ;
Graphics g = newImage . createGraphics ( ) ;
g . drawImage ( image , 0 , 0 , image . getWidth ( ) , image . getHeight ( ) , null ) ;
g . dispose ( ) ;
image . flush ( ) ;
image = scale ( newImage , 64 , 32 ) ;
2021-03-04 16:32:56 +00:00
} else if ( image . getWidth ( ) < 64 | | image . getHeight ( ) < 32 ) {
// Bedrock doesn't like smaller-sized capes, either.
BufferedImage newImage = new BufferedImage ( 64 , 32 , BufferedImage . TYPE_INT_ARGB ) ;
Graphics g = newImage . createGraphics ( ) ;
g . drawImage ( image , 0 , 0 , image . getWidth ( ) , image . getHeight ( ) , null ) ;
g . dispose ( ) ;
image . flush ( ) ;
image = newImage ;
2021-02-15 21:36:47 +00:00
}
} else {
// Very rarely, skins can be larger than Minecraft's default.
// Bedrock will not render anything above a width of 128.
if ( image . getWidth ( ) > 128 ) {
2021-03-03 23:00:29 +00:00
// On Height: Scale by the amount we divided width by, or simply cut down to 128
image = scale ( image , 128 , image . getHeight ( ) > = 256 ? ( image . getHeight ( ) / ( image . getWidth ( ) / 128 ) ) : 128 ) ;
2020-05-12 04:45:16 +00:00
}
2021-03-03 23:00:29 +00:00
// TODO remove alpha channel
2019-12-15 02:12:12 +00:00
}
2019-10-02 20:45:29 +00:00
2023-08-21 23:04:08 +00:00
return image ;
}
2024-04-26 21:29:01 +00:00
private static byte [ ] requestImageData ( String imageUrl , boolean isCape ) throws Exception {
BufferedImage image = requestImage ( imageUrl , isCape ) ;
2020-05-23 21:06:34 +00:00
byte [ ] data = bufferedImageToImageData ( image ) ;
image . flush ( ) ;
return data ;
2019-09-25 21:52:28 +00:00
}
2020-12-04 21:55:24 +00:00
/ * *
2023-01-22 18:23:16 +00:00
* Request textures from a player ' s UUID
2021-11-22 19:49:55 +00:00
*
2023-01-22 18:23:16 +00:00
* @param uuid the player ' s UUID without any hyphens
2020-12-04 21:55:24 +00:00
* @return a completable GameProfile with textures included
* /
2023-12-05 23:54:42 +00:00
public static CompletableFuture < @Nullable String > requestTexturesFromUUID ( String uuid ) {
2020-12-04 21:55:24 +00:00
return CompletableFuture . supplyAsync ( ( ) - > {
try {
2023-01-22 18:23:16 +00:00
JsonNode node = WebUtils . getJson ( " https://sessionserver.mojang.com/session/minecraft/profile/ " + uuid ) ;
2020-12-04 21:55:24 +00:00
JsonNode properties = node . get ( " properties " ) ;
if ( properties = = null ) {
2023-01-22 18:23:16 +00:00
GeyserImpl . getInstance ( ) . getLogger ( ) . debug ( " No properties found in Mojang response for " + uuid ) ;
2020-12-04 21:55:24 +00:00
return null ;
}
2022-03-31 02:30:49 +00:00
return node . get ( " properties " ) . get ( 0 ) . get ( " value " ) . asText ( ) ;
2020-12-04 21:55:24 +00:00
} catch ( Exception e ) {
2023-01-22 18:23:16 +00:00
GeyserImpl . getInstance ( ) . getLogger ( ) . debug ( " Unable to request textures for " + uuid ) ;
2021-11-20 21:34:30 +00:00
if ( GeyserImpl . getInstance ( ) . getConfig ( ) . isDebugMode ( ) ) {
2020-12-04 21:55:24 +00:00
e . printStackTrace ( ) ;
}
return null ;
}
2023-10-01 22:15:44 +00:00
} , getExecutorService ( ) ) ;
2020-12-04 21:55:24 +00:00
}
2023-01-22 18:23:16 +00:00
/ * *
* Request textures from a player ' s username
*
* @param username the player ' s username
* @return a completable GameProfile with textures included
* /
2023-12-05 23:54:42 +00:00
public static CompletableFuture < @Nullable String > requestTexturesFromUsername ( String username ) {
2023-01-22 18:23:16 +00:00
return CompletableFuture . supplyAsync ( ( ) - > {
try {
// Offline skin, or no present UUID
JsonNode node = WebUtils . getJson ( " https://api.mojang.com/users/profiles/minecraft/ " + username ) ;
JsonNode id = node . get ( " id " ) ;
if ( id = = null ) {
GeyserImpl . getInstance ( ) . getLogger ( ) . debug ( " No UUID found in Mojang response for " + username ) ;
return null ;
}
return id . asText ( ) ;
} catch ( Exception e ) {
if ( GeyserImpl . getInstance ( ) . getConfig ( ) . isDebugMode ( ) ) {
e . printStackTrace ( ) ;
}
return null ;
}
2023-10-01 22:15:44 +00:00
} , getExecutorService ( ) ) . thenCompose ( uuid - > {
2023-01-22 18:23:16 +00:00
if ( uuid = = null ) {
return CompletableFuture . completedFuture ( null ) ;
}
return requestTexturesFromUUID ( uuid ) ;
} ) ;
}
2024-04-26 21:29:01 +00:00
private static BufferedImage downloadImage ( String imageUrl ) throws IOException {
2024-03-17 16:27:42 +00:00
HttpURLConnection con = ( HttpURLConnection ) new URL ( imageUrl ) . openConnection ( ) ;
2024-04-25 15:20:47 +00:00
con . setRequestProperty ( " User-Agent " , WebUtils . getUserAgent ( ) ) ;
2024-03-17 16:27:42 +00:00
con . setConnectTimeout ( 10000 ) ;
con . setReadTimeout ( 10000 ) ;
2020-07-11 17:22:02 +00:00
2024-03-17 16:27:42 +00:00
BufferedImage image = ImageIO . read ( con . getInputStream ( ) ) ;
2020-07-11 17:22:02 +00:00
2023-12-05 23:54:42 +00:00
if ( image = = null ) {
2024-04-26 21:29:01 +00:00
throw new IllegalArgumentException ( " Failed to read image from: %s " . formatted ( imageUrl ) ) ;
2023-12-05 23:54:42 +00:00
}
2020-04-29 16:04:45 +00:00
return image ;
}
2021-02-25 01:28:48 +00:00
public static BufferedImage scale ( BufferedImage bufferedImage , int newWidth , int newHeight ) {
2021-02-15 21:36:47 +00:00
BufferedImage resized = new BufferedImage ( newWidth , newHeight , BufferedImage . TYPE_INT_ARGB ) ;
2019-12-15 02:12:12 +00:00
Graphics2D g2 = resized . createGraphics ( ) ;
g2 . setRenderingHint ( RenderingHints . KEY_INTERPOLATION , RenderingHints . VALUE_INTERPOLATION_BILINEAR ) ;
2021-02-15 21:36:47 +00:00
g2 . drawImage ( bufferedImage , 0 , 0 , newWidth , newHeight , null ) ;
2019-12-15 02:12:12 +00:00
g2 . dispose ( ) ;
2021-02-15 21:36:47 +00:00
bufferedImage . flush ( ) ;
2019-12-15 02:12:12 +00:00
return resized ;
}
2020-05-23 21:06:34 +00:00
/ * *
* Get the RGBA int for a given index in some image data
*
* @param index Index to get
* @param data Image data to find in
* @return An int representing RGBA
* /
private static int getRGBA ( int index , byte [ ] data ) {
return ( data [ index ] & 0xFF ) < < 16 | ( data [ index + 1 ] & 0xFF ) < < 8 |
data [ index + 2 ] & 0xFF | ( data [ index + 3 ] & 0xFF ) < < 24 ;
}
/ * *
* Convert a byte [ ] to a BufferedImage
*
* @param imageData The byte [ ] to convert
* @param imageWidth The width of the target image
* @param imageHeight The height of the target image
* @return The converted BufferedImage
* /
public static BufferedImage imageDataToBufferedImage ( byte [ ] imageData , int imageWidth , int imageHeight ) {
BufferedImage image = new BufferedImage ( imageWidth , imageHeight , BufferedImage . TYPE_INT_ARGB ) ;
int index = 0 ;
for ( int y = 0 ; y < imageHeight ; y + + ) {
for ( int x = 0 ; x < imageWidth ; x + + ) {
image . setRGB ( x , y , getRGBA ( index , imageData ) ) ;
index + = 4 ;
}
}
return image ;
}
/ * *
* Convert a BufferedImage to a byte [ ]
*
* @param image The BufferedImage to convert
* @return The converted byte [ ]
* /
public static byte [ ] bufferedImageToImageData ( BufferedImage image ) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream ( image . getWidth ( ) * 4 + image . getHeight ( ) * 4 ) ;
for ( int y = 0 ; y < image . getHeight ( ) ; y + + ) {
for ( int x = 0 ; x < image . getWidth ( ) ; x + + ) {
int rgba = image . getRGB ( x , y ) ;
outputStream . write ( ( rgba > > 16 ) & 0xFF ) ;
outputStream . write ( ( rgba > > 8 ) & 0xFF ) ;
outputStream . write ( rgba & 0xFF ) ;
outputStream . write ( ( rgba > > 24 ) & 0xFF ) ;
}
}
return outputStream . toByteArray ( ) ;
}
2019-10-02 20:45:29 +00:00
public static < T > T getOrDefault ( CompletableFuture < T > future , T defaultValue , int timeoutInSeconds ) {
try {
return future . get ( timeoutInSeconds , TimeUnit . SECONDS ) ;
} catch ( Exception ignored ) { }
return defaultValue ;
}
2022-12-18 18:18:06 +00:00
public record SkinAndCape ( Skin skin , Cape cape ) {
2019-09-25 21:52:28 +00:00
}
2024-04-26 23:00:41 +00:00
public static class EventSkinData {
private SkinData skinData ;
public EventSkinData ( SkinData skinData ) {
this . skinData = skinData ;
}
public SkinData skinData ( ) {
return skinData ;
}
public void skinData ( SkinData skinData ) {
this . skinData = skinData ;
}
}
2019-09-25 21:52:28 +00:00
}