diff --git a/bungee/base/src/main/java/org/geysermc/floodgate/bungee/listener/BungeeListener.java b/bungee/base/src/main/java/org/geysermc/floodgate/bungee/listener/BungeeListener.java index b9cb202c..55e33de1 100644 --- a/bungee/base/src/main/java/org/geysermc/floodgate/bungee/listener/BungeeListener.java +++ b/bungee/base/src/main/java/org/geysermc/floodgate/bungee/listener/BungeeListener.java @@ -38,6 +38,7 @@ import net.md_5.bungee.api.event.PostLoginEvent; import net.md_5.bungee.api.event.PreLoginEvent; import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.connection.InitialHandler; import net.md_5.bungee.event.EventHandler; import net.md_5.bungee.event.EventPriority; @@ -49,6 +50,7 @@ import org.geysermc.floodgate.core.skin.SkinApplier; import org.geysermc.floodgate.core.skin.SkinDataImpl; import org.geysermc.floodgate.core.util.LanguageManager; +import org.geysermc.floodgate.core.util.MojangUtils; import org.geysermc.floodgate.core.util.ReflectionUtils; @Singleton @@ -60,11 +62,13 @@ public final class BungeeListener implements Listener, McListener { requireNonNull(PLAYER_NAME, "Initial name field cannot be null"); } + @Inject Plugin plugin; @Inject BungeeConnectionManager connectionManager; @Inject ProxyFloodgateConfig config; @Inject SimpleFloodgateApi api; @Inject LanguageManager languageManager; @Inject SkinApplier skinApplier; + @Inject MojangUtils mojangUtils; @Inject @Named("kickMessageAttribute") @@ -112,13 +116,26 @@ public void onLogin(LoginEvent event) { @EventHandler(priority = EventPriority.LOWEST) public void onPostLogin(PostLoginEvent event) { - // To fix the February 2 2022 Mojang authentication changes - if (!config.sendFloodgateData()) { - Connection connection = api.connectionByPlatformIdentifier(event.getPlayer()); - if (connection != null && !connection.isLinked()) { - skinApplier.applySkin(connection, new SkinDataImpl("", "")); - } + Connection connection = api.connectionByPlatformIdentifier(event.getPlayer()); + if (connection == null) { + return; + } + + // Skin look up (on Spigot and friends) would result in it failing, so apply a default skin + if (!connection.isLinked()) { + skinApplier.applySkin(connection, SkinDataImpl.DEFAULT_SKIN); + return; } + + // Floodgate players are seen as offline mode players, meaning we have to look up + // the linked player's textures ourselves + + event.registerIntent(plugin); + + mojangUtils.skinFor(connection.javaUuid()).thenAccept(skin -> { + skinApplier.applySkin(connection, skin); + event.completeIntent(plugin); + }); } @EventHandler(priority = EventPriority.HIGHEST) diff --git a/bungee/base/src/main/java/org/geysermc/floodgate/bungee/pluginmessage/BungeeSkinApplier.java b/bungee/base/src/main/java/org/geysermc/floodgate/bungee/pluginmessage/BungeeSkinApplier.java index 714547e7..19d22bcc 100644 --- a/bungee/base/src/main/java/org/geysermc/floodgate/bungee/pluginmessage/BungeeSkinApplier.java +++ b/bungee/base/src/main/java/org/geysermc/floodgate/bungee/pluginmessage/BungeeSkinApplier.java @@ -92,8 +92,6 @@ public void applySkin(@NonNull Connection connection, @NonNull SkinData skinData SkinData currentSkin = currentSkin(properties); SkinApplyEvent event = new SkinApplyEventImpl(connection, currentSkin, skinData); - event.cancelled(connection.isLinked()); - eventBus.fire(event); if (event.cancelled()) { diff --git a/core/src/main/java/org/geysermc/floodgate/core/http/minecraft/MinecraftClient.java b/core/src/main/java/org/geysermc/floodgate/core/http/minecraft/MinecraftClient.java index 10688e5d..92ee6379 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/http/minecraft/MinecraftClient.java +++ b/core/src/main/java/org/geysermc/floodgate/core/http/minecraft/MinecraftClient.java @@ -1,6 +1,5 @@ package org.geysermc.floodgate.core.http.minecraft; -import static io.micronaut.http.HttpHeaders.ACCEPT; import static io.micronaut.http.HttpHeaders.USER_AGENT; import io.micronaut.http.annotation.Get; @@ -13,7 +12,6 @@ @Client("https://api.minecraftservices.com/minecraft") @Header(name = USER_AGENT, value = "${http.userAgent}") -@Header(name = ACCEPT, value = "application/json") public interface MinecraftClient { @Get("/profile/lookup/name/{name}") CompletableFuture<@Nullable ProfileResult> profileByName(@NonNull String name); diff --git a/core/src/main/java/org/geysermc/floodgate/core/http/mojang/ProfileProperty.java b/core/src/main/java/org/geysermc/floodgate/core/http/mojang/ProfileProperty.java new file mode 100644 index 00000000..5881297b --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/http/mojang/ProfileProperty.java @@ -0,0 +1,9 @@ +package org.geysermc.floodgate.core.http.mojang; + +import io.micronaut.serde.annotation.Serdeable; +import jakarta.validation.constraints.NotNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +@Serdeable +public record ProfileProperty(@NotNull String name, @NotNull String value, @Nullable String signature) { +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/http/mojang/ProfileWithProperties.java b/core/src/main/java/org/geysermc/floodgate/core/http/mojang/ProfileWithProperties.java new file mode 100644 index 00000000..588678bc --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/http/mojang/ProfileWithProperties.java @@ -0,0 +1,17 @@ +package org.geysermc.floodgate.core.http.mojang; + +import io.micronaut.serde.annotation.Serdeable; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +@Serdeable +public record ProfileWithProperties(String id, String name, List properties) { + public @Nullable ProfileProperty texture() { + for (ProfileProperty property : properties) { + if (property.name().equals("texture")) { + return property; + } + } + return null; + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/http/mojang/SessionServerClient.java b/core/src/main/java/org/geysermc/floodgate/core/http/mojang/SessionServerClient.java new file mode 100644 index 00000000..623f963d --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/http/mojang/SessionServerClient.java @@ -0,0 +1,17 @@ +package org.geysermc.floodgate.core.http.mojang; + +import static io.micronaut.http.HttpHeaders.USER_AGENT; + +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Header; +import io.micronaut.http.client.annotation.Client; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@Client("https://sessionserver.mojang.com/session/minecraft") +@Header(name = USER_AGENT, value = "${http.userAgent}") +public interface SessionServerClient { + @Get("/profile/{uuid}?unsigned=false") + CompletableFuture profileWithProperties(@NotNull UUID uuid); +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/skin/SkinDataImpl.java b/core/src/main/java/org/geysermc/floodgate/core/skin/SkinDataImpl.java index fa0ef631..3b861053 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/skin/SkinDataImpl.java +++ b/core/src/main/java/org/geysermc/floodgate/core/skin/SkinDataImpl.java @@ -30,8 +30,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.floodgate.api.event.skin.SkinApplyEvent.SkinData; +import org.geysermc.floodgate.core.util.Constants; public record SkinDataImpl(String value, String signature) implements SkinData { + public static final SkinData DEFAULT_SKIN = new SkinDataImpl( + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE, + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE + ); + public SkinDataImpl(@NonNull String value, @MonotonicNonNull String signature) { this.value = Objects.requireNonNull(value); this.signature = Objects.requireNonNull(signature); diff --git a/core/src/main/java/org/geysermc/floodgate/core/util/Constants.java b/core/src/main/java/org/geysermc/floodgate/core/util/Constants.java index 85232804..aa78d2b9 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/util/Constants.java +++ b/core/src/main/java/org/geysermc/floodgate/core/util/Constants.java @@ -34,26 +34,18 @@ public final class Constants { public static final int MAX_DEBUG_PACKET_COUNT = 20; public static final boolean DEBUG_MODE = false; - public static final boolean PRINT_ALL_PACKETS = false; - public static final String USER_AGENT = "GeyserMC/Floodgate"; private static final String API_BASE_URL = "s://api.geysermc.org"; - public static final String HEALTH_URL = "http" + API_BASE_URL + "/health"; public static final String LINK_INFO_URL = "https://link.geysermc.org/"; public static final String LATEST_DOWNLOAD_URL = "https://geysermc.org/download"; - public static final String INTERNAL_ERROR_MESSAGE = - "An internal error happened while handling Floodgate data." + - " Try logging in again or contact a server administrator if the issue persists."; public static final String UNSUPPORTED_DATA_VERSION = "Received an unsupported Floodgate data version." + " This Floodgate version is made for data version %s, received %s." + " Make sure that Floodgate is up-to-date."; - - public static final int HANDSHAKE_PACKET_ID = 0; - public static final int LOGIN_SUCCESS_PACKET_ID = 2; - public static final int SET_COMPRESSION_PACKET_ID = 3; + public static final String DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE = "ewogICJ0aW1lc3RhbXAiIDogMTcxNTcxNzM1NTI2MywKICAicHJvZmlsZUlkIiA6ICIyMWUzNjdkNzI1Y2Y0ZTNiYjI2OTJjNGEzMDBhNGRlYiIsCiAgInByb2ZpbGVOYW1lIiA6ICJHZXlzZXJNQyIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS8zMWY0NzdlYjFhN2JlZWU2MzFjMmNhNjRkMDZmOGY2OGZhOTNhMzM4NmQwNDQ1MmFiMjdmNDNhY2RmMWI2MGNiIgogICAgfQogIH0KfQ"; + public static final String DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE = "dFKIZ5d6vNqCSe1IFGiVLjt3cnW8qh4qNP2umg9zqkX9bvAQawuR1iuO1kCD/+ye8A6GQFv2wRCdxdrjp5+Vrr0SsWqMnsYDN8cEg6CD18mAnaKI1TYDuGbdJaqLyGqN5wqSMdHxchs9iovFkde5ir4aYdvHkA11vOTi11L4kUzETGzJ4iKVuZOv4dq+B7wFAWqp4n8QZfhixyvemFazQHlLmxnuhU+jhpZMvYY9MAaRAJonfy/wJe9LymbTe0EJ8N+NwZQDrEUzgfBFo4OIGDqRZwvydInCqkjhPMtHCSL25VOKwcFocYpRYbk4eIKM4CLjYlBiQGki+XKsPaljwjVhnT0jUupSf7yraGb3T0CsVBjhDbIIIp9nytlbO0GvxHu0TzYjkr4Iji0do5jlCKQ/OasXcL21wd6ozw0t1QZnnzxi9ewSuyYVY9ErmWdkww1OtCIgJilceEBwNAB8+mhJ062WFaYPgJQAmOREM8InW33dbbeENMFhQi4LIO5P7p9ye3B4Lrwm20xtd9wJk3lewzcs8ezh0LUF6jPSDQDivgSKU49mLCTmOi+WZh8zKjjxfVEtNZON2W+3nct0LiWBVsQ55HzlvF0FFxuRVm6pxi6MQK2ernv3DQl0hUqyQ1+RV9nfZXTQOAUzwLjKx3t2zKqyZIiNEKLE+iAXrsE="; } diff --git a/core/src/main/java/org/geysermc/floodgate/core/util/MojangUtils.java b/core/src/main/java/org/geysermc/floodgate/core/util/MojangUtils.java new file mode 100644 index 00000000..ba3b8bee --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/util/MojangUtils.java @@ -0,0 +1,30 @@ +package org.geysermc.floodgate.core.util; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.geysermc.floodgate.api.event.skin.SkinApplyEvent.SkinData; +import org.geysermc.floodgate.core.http.mojang.SessionServerClient; +import org.geysermc.floodgate.core.logger.FloodgateLogger; +import org.geysermc.floodgate.core.skin.SkinDataImpl; + +@Singleton +public final class MojangUtils { + @Inject SessionServerClient sessionClient; + @Inject FloodgateLogger logger; + + public CompletableFuture skinFor(UUID uuid) { + return sessionClient.profileWithProperties(uuid) + .thenApply(skin -> { + var texture = skin.texture(); + if (texture == null) { + return SkinDataImpl.DEFAULT_SKIN; + } + return new SkinDataImpl(texture.value(), texture.signature()); + }).exceptionally(exception -> { + logger.debug("Unexpected skin fetch error for " + uuid, exception); + return SkinDataImpl.DEFAULT_SKIN; + }); + } +} diff --git a/spigot/base/src/main/java/org/geysermc/floodgate/spigot/addon/data/SpigotDataHandler.java b/spigot/base/src/main/java/org/geysermc/floodgate/spigot/addon/data/SpigotDataHandler.java index 8ac6ca9c..b8e9c807 100644 --- a/spigot/base/src/main/java/org/geysermc/floodgate/spigot/addon/data/SpigotDataHandler.java +++ b/spigot/base/src/main/java/org/geysermc/floodgate/spigot/addon/data/SpigotDataHandler.java @@ -29,6 +29,7 @@ import static org.geysermc.floodgate.core.util.ReflectionUtils.setValue; import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; import io.netty.channel.Channel; import io.netty.util.AttributeKey; import java.lang.reflect.InvocationTargetException; @@ -41,10 +42,17 @@ import org.geysermc.floodgate.core.connection.FloodgateDataHandler; import org.geysermc.floodgate.core.connection.FloodgateDataHandler.HandleResult; import org.geysermc.floodgate.core.logger.FloodgateLogger; +import org.geysermc.floodgate.core.util.Constants; import org.geysermc.floodgate.spigot.util.ClassNames; import org.geysermc.floodgate.spigot.util.ProxyUtils; public final class SpigotDataHandler extends CommonNettyDataHandler { + private static final Property DEFAULT_TEXTURE_PROPERTY = new Property( + "textures", + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE, + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE + ); + private Object networkManager; private Connection connection; private boolean proxyData; @@ -182,6 +190,14 @@ private boolean checkAndHandleLogin(Object packet) throws Exception { } GameProfile gameProfile = new GameProfile(connection.javaUuid(), connection.javaUsername()); + + if (!connection.isLinked()) { + // Otherwise game server will try to fetch the skin from Mojang. + // No need to worry that this overrides proxy data, because those won't reach this + // method / are already removed (in the case of username validation) + gameProfile.getProperties().put("textures", DEFAULT_TEXTURE_PROPERTY); + } + setValue(packetListener, ClassNames.LOGIN_PROFILE, gameProfile); // we have to fake the offline player (login) cycle diff --git a/spigot/base/src/main/java/org/geysermc/floodgate/spigot/listener/PaperProfileListener.java b/spigot/base/src/main/java/org/geysermc/floodgate/spigot/listener/PaperProfileListener.java index b7741d52..e0314770 100644 --- a/spigot/base/src/main/java/org/geysermc/floodgate/spigot/listener/PaperProfileListener.java +++ b/spigot/base/src/main/java/org/geysermc/floodgate/spigot/listener/PaperProfileListener.java @@ -26,7 +26,6 @@ package org.geysermc.floodgate.spigot.listener; import com.destroystokyo.paper.event.profile.PreFillProfileEvent; -import com.destroystokyo.paper.profile.PlayerProfile; import com.destroystokyo.paper.profile.ProfileProperty; import io.micronaut.context.annotation.Requires; import jakarta.inject.Inject; @@ -34,17 +33,22 @@ import java.util.HashSet; import java.util.Set; import java.util.UUID; -import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; import org.geysermc.api.connection.Connection; import org.geysermc.floodgate.core.api.SimpleFloodgateApi; import org.geysermc.floodgate.core.listener.McListener; +import org.geysermc.floodgate.core.util.Constants; @Requires(classes = PreFillProfileEvent.class) @Singleton public final class PaperProfileListener implements Listener, McListener { + private static final ProfileProperty DEFAULT_TEXTURE_PROPERTY = new ProfileProperty( + "textures", + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE, + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE + ); + @Inject SimpleFloodgateApi api; @EventHandler @@ -67,26 +71,8 @@ public void onFill(PreFillProfileEvent event) { } Set properties = new HashSet<>(event.getPlayerProfile().getProperties()); - properties.add(new ProfileProperty("textures", "", "")); - event.setProperties(properties); - } + properties.add(DEFAULT_TEXTURE_PROPERTY); - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - Player player = event.getPlayer(); - Connection connection = api.connectionByPlatformIdentifier(player); - if (connection == null || connection.isLinked()) { - return; - } - - PlayerProfile profile = player.getPlayerProfile(); - if (profile.getProperties().stream().noneMatch( - prop -> "textures".equals(prop.getName()) && prop.getValue().isEmpty() - && prop.getSignature() != null && prop.getSignature().isEmpty())) { - return; - } - - profile.removeProperty("textures"); - player.setPlayerProfile(profile); + event.setProperties(properties); } } diff --git a/velocity/base/src/main/java/org/geysermc/floodgate/velocity/addon/data/VelocityServerDataHandler.java b/velocity/base/src/main/java/org/geysermc/floodgate/velocity/addon/data/VelocityServerDataHandler.java index 5707f62e..73f38dfb 100644 --- a/velocity/base/src/main/java/org/geysermc/floodgate/velocity/addon/data/VelocityServerDataHandler.java +++ b/velocity/base/src/main/java/org/geysermc/floodgate/velocity/addon/data/VelocityServerDataHandler.java @@ -27,6 +27,7 @@ import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.protocol.packet.HandshakePacket; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; @@ -62,7 +63,8 @@ public void write(ChannelHandlerContext ctx, Object packet, ChannelPromise promi // get the FloodgatePlayer from the ConnectedPlayer MinecraftConnection minecraftConnection = (MinecraftConnection) ctx.pipeline().get("handler"); - Player velocityPlayer = (Player) minecraftConnection.getAssociation(); + VelocityServerConnection serverConnection = (VelocityServerConnection) minecraftConnection.getAssociation(); + Player velocityPlayer = serverConnection.getPlayer(); Connection connection = api.connectionByPlatformIdentifier(velocityPlayer); if (connection != null) { diff --git a/velocity/base/src/main/java/org/geysermc/floodgate/velocity/listener/VelocityListener.java b/velocity/base/src/main/java/org/geysermc/floodgate/velocity/listener/VelocityListener.java index aa3bef57..de41cb41 100644 --- a/velocity/base/src/main/java/org/geysermc/floodgate/velocity/listener/VelocityListener.java +++ b/velocity/base/src/main/java/org/geysermc/floodgate/velocity/listener/VelocityListener.java @@ -25,11 +25,13 @@ package org.geysermc.floodgate.velocity.listener; +import com.velocitypowered.api.event.Continuation; import com.velocitypowered.api.event.PostOrder; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.DisconnectEvent; import com.velocitypowered.api.event.connection.PostLoginEvent; import com.velocitypowered.api.event.connection.PreLoginEvent; +import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult; import com.velocitypowered.api.event.player.GameProfileRequestEvent; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.GameProfile.Property; @@ -38,23 +40,32 @@ import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.inject.Singleton; -import java.util.Collections; import net.kyori.adventure.text.Component; import org.geysermc.api.connection.Connection; import org.geysermc.floodgate.core.api.SimpleFloodgateApi; import org.geysermc.floodgate.core.config.ProxyFloodgateConfig; import org.geysermc.floodgate.core.listener.McListener; import org.geysermc.floodgate.core.logger.FloodgateLogger; +import org.geysermc.floodgate.core.util.Constants; import org.geysermc.floodgate.core.util.LanguageManager; +import org.geysermc.floodgate.core.util.MojangUtils; import org.geysermc.floodgate.velocity.player.VelocityConnectionManager; +import java.util.List; + @Singleton public final class VelocityListener implements McListener { + private static final Property DEFAULT_TEXTURE_PROPERTY = new Property( + "textures", + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE, + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE); + @Inject VelocityConnectionManager connectionManager; @Inject ProxyFloodgateConfig config; @Inject SimpleFloodgateApi api; @Inject LanguageManager languageManager; @Inject FloodgateLogger logger; + @Inject MojangUtils mojangUtils; @Inject @Named("connectionAttribute") @@ -78,34 +89,47 @@ public void onPreLogin(PreLoginEvent event) { } if (kickMessage != null) { - event.setResult( - PreLoginEvent.PreLoginComponentResult.denied(Component.text(kickMessage)) - ); + event.setResult(PreLoginComponentResult.denied(Component.text(kickMessage))); return; } if (player != null) { - event.setResult(PreLoginEvent.PreLoginComponentResult.forceOfflineMode()); + event.setResult(PreLoginComponentResult.forceOfflineMode()); } } @Subscribe(order = PostOrder.EARLY) - public void onGameProfileRequest(GameProfileRequestEvent event) { + public void onGameProfileRequest(GameProfileRequestEvent event, Continuation continuation) { Connection connection = connectionManager.connectionByPlatformIdentifier(event.getConnection()); if (connection == null) { + continuation.resume(); return; } - GameProfile profile = new GameProfile( - connection.javaUuid(), - connection.javaUsername(), - Collections.emptyList() - ); - // The texture properties addition is to fix the February 2 2022 Mojang authentication changes - if (!config.sendFloodgateData() && !connection.isLinked()) { - profile = profile.addProperty(new Property("textures", "", "")); + + // Skin look up (on Spigot and friends) would result in it failing, so apply a default skin + if (!connection.isLinked()) { + event.setGameProfile(new GameProfile( + connection.javaUuid(), + connection.javaUsername(), + List.of(DEFAULT_TEXTURE_PROPERTY) + )); + continuation.resume(); + return; } - event.setGameProfile(profile); + + // Floodgate players are seen as offline mode players, meaning we have to look up + // the linked player's textures ourselves + + mojangUtils.skinFor(connection.javaUuid()) + .thenAccept(skin -> { + event.setGameProfile(new GameProfile( + connection.javaUuid(), + connection.javaUsername(), + List.of(new Property("textures", skin.value(), skin.signature())) + )); + continuation.resume(); + }); } @Subscribe(order = PostOrder.FIRST)