mirror of
https://github.com/GeyserMC/Geyser.git
synced 2025-12-19 14:59:27 +00:00
Work on proper ResolvableProfile resolving
This commit is contained in:
@@ -33,7 +33,7 @@ import org.geysermc.geyser.session.GeyserSession;
|
|||||||
import org.geysermc.geyser.text.ChatColor;
|
import org.geysermc.geyser.text.ChatColor;
|
||||||
import org.geysermc.geyser.text.MinecraftLocale;
|
import org.geysermc.geyser.text.MinecraftLocale;
|
||||||
import org.geysermc.geyser.translator.item.BedrockItemBuilder;
|
import org.geysermc.geyser.translator.item.BedrockItemBuilder;
|
||||||
import org.geysermc.mcprotocollib.auth.GameProfile;
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
|
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
|
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
|
||||||
|
|
||||||
@@ -49,9 +49,12 @@ public class PlayerHeadItem extends BlockItem {
|
|||||||
// Use the correct color, determined by the rarity of the item
|
// Use the correct color, determined by the rarity of the item
|
||||||
char rarity = Rarity.fromId(components.getOrDefault(DataComponentTypes.RARITY, Rarity.COMMON.ordinal())).getColor();
|
char rarity = Rarity.fromId(components.getOrDefault(DataComponentTypes.RARITY, Rarity.COMMON.ordinal())).getColor();
|
||||||
|
|
||||||
/*GameProfile profile = components.get(DataComponentTypes.PROFILE);
|
// Ideally we'd resolve the profile here and show the resolved name if it's a dynamic profile
|
||||||
|
// but, resolving is done async, which isn't really possible here
|
||||||
|
// TODO FIXME 1.21.9? also see comment in ItemTranslator
|
||||||
|
ResolvableProfile profile = components.get(DataComponentTypes.PROFILE);
|
||||||
if (profile != null) {
|
if (profile != null) {
|
||||||
String name = profile.getName();
|
String name = profile.getProfile().getName();
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
// Add correct name of player skull
|
// Add correct name of player skull
|
||||||
String displayName = ChatColor.RESET + ChatColor.ESCAPE + rarity +
|
String displayName = ChatColor.RESET + ChatColor.ESCAPE + rarity +
|
||||||
@@ -62,6 +65,6 @@ public class PlayerHeadItem extends BlockItem {
|
|||||||
builder.setCustomName(ChatColor.RESET + ChatColor.ESCAPE + rarity +
|
builder.setCustomName(ChatColor.RESET + ChatColor.ESCAPE + rarity +
|
||||||
MinecraftLocale.getLocaleString("block.minecraft.player_head", session.locale()));
|
MinecraftLocale.getLocaleString("block.minecraft.player_head", session.locale()));
|
||||||
}
|
}
|
||||||
}*/ // TODO 1.21.9
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import java.io.IOException;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
@@ -176,15 +177,14 @@ public class CustomSkullRegistryPopulator {
|
|||||||
*/
|
*/
|
||||||
private static @Nullable String getProfileFromUuid(String uuid) {
|
private static @Nullable String getProfileFromUuid(String uuid) {
|
||||||
try {
|
try {
|
||||||
String uuidDigits = uuid.replace("-", "");
|
UUID parsed = UUID.fromString(uuid);
|
||||||
if (uuidDigits.length() != 32) {
|
return SkinProvider.requestTexturesFromUUID(parsed).get();
|
||||||
GeyserImpl.getInstance().getLogger().error("Invalid skull uuid " + uuid + " This skull will not be added as a custom block.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return SkinProvider.requestTexturesFromUUID(uuid).get();
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
GeyserImpl.getInstance().getLogger().error("Unable to request skull textures for " + uuid + " This skull will not be added as a custom block.", e);
|
GeyserImpl.getInstance().getLogger().error("Unable to request skull textures for " + uuid + " This skull will not be added as a custom block.", e);
|
||||||
return null;
|
return null;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
GeyserImpl.getInstance().getLogger().error("Invalid skull uuid " + uuid + " This skull will not be added as a custom block.");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
|
|||||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.HandPreference;
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.HandPreference;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerAction;
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerAction;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.setting.ChatVisibility;
|
import org.geysermc.mcprotocollib.protocol.data.game.setting.ChatVisibility;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.setting.ParticleStatus;
|
import org.geysermc.mcprotocollib.protocol.data.game.setting.ParticleStatus;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.setting.SkinPart;
|
import org.geysermc.mcprotocollib.protocol.data.game.setting.SkinPart;
|
||||||
@@ -398,7 +399,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||||||
* A map of all players (and their heads) that are wearing a player head with a custom texture.
|
* A map of all players (and their heads) that are wearing a player head with a custom texture.
|
||||||
* Our workaround for these players is to give them a custom skin and geometry to emulate wearing a custom skull.
|
* Our workaround for these players is to give them a custom skin and geometry to emulate wearing a custom skull.
|
||||||
*/
|
*/
|
||||||
private final Map<UUID, GameProfile> playerWithCustomHeads = new Object2ObjectOpenHashMap<>();
|
private final Map<UUID, ResolvableProfile> playerWithCustomHeads = new Object2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
private boolean droppingLecternBook;
|
private boolean droppingLecternBook;
|
||||||
|
|||||||
@@ -43,17 +43,15 @@ import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
|||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.skin.SkinManager.GameProfileData;
|
import org.geysermc.geyser.skin.SkinManager.GameProfileData;
|
||||||
import org.geysermc.geyser.text.GeyserLocale;
|
import org.geysermc.geyser.text.GeyserLocale;
|
||||||
import org.geysermc.mcprotocollib.auth.GameProfile;
|
|
||||||
import org.geysermc.mcprotocollib.auth.GameProfile.Texture;
|
import org.geysermc.mcprotocollib.auth.GameProfile.Texture;
|
||||||
import org.geysermc.mcprotocollib.auth.GameProfile.TextureModel;
|
import org.geysermc.mcprotocollib.auth.GameProfile.TextureModel;
|
||||||
import org.geysermc.mcprotocollib.auth.GameProfile.TextureType;
|
import org.geysermc.mcprotocollib.auth.GameProfile.TextureType;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@@ -104,70 +102,46 @@ public class FakeHeadProvider {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
public static void setHead(GeyserSession session, PlayerEntity entity, @Nullable GameProfile profile) {
|
public static void setHead(GeyserSession session, PlayerEntity entity, @Nullable ResolvableProfile profile) {
|
||||||
if (profile == null) {
|
if (profile == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GameProfile current = session.getPlayerWithCustomHeads().get(entity.getUuid());
|
ResolvableProfile current = session.getPlayerWithCustomHeads().get(entity.getUuid());
|
||||||
if (profile.equals(current)) {
|
if (profile.equals(current)) {
|
||||||
// We already did this, no need to re-compute
|
// We already did this, no need to re-compute
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<TextureType, Texture> textures;
|
SkinManager.resolveProfile(profile).whenCompleteAsync((resolved, throwable) -> {
|
||||||
try {
|
|
||||||
textures = profile.getTextures(false);
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
GeyserImpl.getInstance().getLogger().debug("Could not decode player head from profile %s, got: %s".formatted(profile, e.getMessage()));
|
|
||||||
textures = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textures == null || textures.isEmpty()) {
|
|
||||||
loadHeadFromProfile(session, entity, profile);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Texture skinTexture = textures.get(TextureType.SKIN);
|
|
||||||
|
|
||||||
if (skinTexture == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Texture capeTexture = textures.get(TextureType.CAPE);
|
|
||||||
String capeUrl = capeTexture != null ? capeTexture.getURL() : null;
|
|
||||||
|
|
||||||
boolean isAlex = skinTexture.getModel() == TextureModel.SLIM;
|
|
||||||
|
|
||||||
loadHeadFromProfile(session, entity, new GameProfileData(skinTexture.getURL(), capeUrl, isAlex), profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void loadHeadFromProfile(GeyserSession session, PlayerEntity entity, GameProfile profile) {
|
|
||||||
CompletableFuture<String> texturesFuture;
|
|
||||||
if (profile.getId() != null) {
|
|
||||||
texturesFuture = SkinProvider.requestTexturesFromUUID(profile.getId().toString());
|
|
||||||
} else {
|
|
||||||
texturesFuture = SkinProvider.requestTexturesFromUsername(profile.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
texturesFuture.whenCompleteAsync((encodedJson, throwable) -> {
|
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), throwable);
|
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), throwable);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<TextureType, Texture> textures;
|
||||||
try {
|
try {
|
||||||
SkinManager.GameProfileData gameProfileData = SkinManager.GameProfileData.loadFromJson(encodedJson);
|
textures = resolved.getTextures(false);
|
||||||
if (gameProfileData == null) {
|
} catch (IllegalStateException exception) {
|
||||||
return;
|
// TODO translate?
|
||||||
}
|
GeyserImpl.getInstance().getLogger().error("Could not decode player head from profile %s, got: %s".formatted(profile, exception.getMessage()));
|
||||||
loadHeadFromProfile(session, entity, gameProfileData, profile);
|
return;
|
||||||
} catch (IOException e) {
|
|
||||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid(), e.getMessage()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Texture skinTexture = textures.get(TextureType.SKIN);
|
||||||
|
if (skinTexture == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Texture capeTexture = textures.get(TextureType.CAPE);
|
||||||
|
String capeUrl = capeTexture != null ? capeTexture.getURL() : null;
|
||||||
|
|
||||||
|
boolean isAlex = skinTexture.getModel() == TextureModel.SLIM;
|
||||||
|
loadHeadFromProfile(session, entity, new GameProfileData(skinTexture.getURL(), capeUrl, isAlex), profile);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void loadHeadFromProfile(GeyserSession session, PlayerEntity entity, SkinManager.GameProfileData gameProfileData, GameProfile profile) {
|
private static void loadHeadFromProfile(GeyserSession session, PlayerEntity entity, SkinManager.GameProfileData gameProfileData, ResolvableProfile profile) {
|
||||||
String fakeHeadSkinUrl = gameProfileData.skinUrl();
|
String fakeHeadSkinUrl = gameProfileData.skinUrl();
|
||||||
|
|
||||||
session.getPlayerWithCustomHeads().put(entity.getUuid(), profile);
|
session.getPlayerWithCustomHeads().put(entity.getUuid(), profile);
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
package org.geysermc.geyser.skin;
|
package org.geysermc.geyser.skin;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.google.common.cache.Cache;
|
||||||
|
import com.google.common.cache.CacheBuilder;
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
import org.cloudburstmc.nbt.NbtMap;
|
import org.cloudburstmc.nbt.NbtMap;
|
||||||
import org.cloudburstmc.nbt.NbtType;
|
import org.cloudburstmc.nbt.NbtType;
|
||||||
@@ -44,17 +46,27 @@ import org.geysermc.geyser.session.GeyserSession;
|
|||||||
import org.geysermc.geyser.session.auth.BedrockClientData;
|
import org.geysermc.geyser.session.auth.BedrockClientData;
|
||||||
import org.geysermc.geyser.text.GeyserLocale;
|
import org.geysermc.geyser.text.GeyserLocale;
|
||||||
import org.geysermc.geyser.util.FileUtils;
|
import org.geysermc.geyser.util.FileUtils;
|
||||||
|
import org.geysermc.mcprotocollib.auth.GameProfile;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class SkinManager {
|
public class SkinManager {
|
||||||
|
|
||||||
|
private static final Cache<ResolvableProfile, GameProfile> RESOLVED_PROFILES_CACHE = CacheBuilder.newBuilder()
|
||||||
|
.expireAfterAccess(1, TimeUnit.HOURS)
|
||||||
|
.build();
|
||||||
|
private static final UUID EMPTY_UUID = new UUID(0L, 0L);
|
||||||
|
|
||||||
static final String GEOMETRY = new String(FileUtils.readAllBytes("bedrock/geometries/geo.json"), StandardCharsets.UTF_8);
|
static final String GEOMETRY = new String(FileUtils.readAllBytes("bedrock/geometries/geo.json"), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,6 +202,66 @@ public class SkinManager {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<GameProfile> resolveProfile(ResolvableProfile profile) {
|
||||||
|
GameProfile partial = profile.getProfile();
|
||||||
|
if (!profile.isDynamic()) {
|
||||||
|
// This is easy: the server has provided the entire profile for us (or however much it knew),
|
||||||
|
// and is asking us to use this
|
||||||
|
return CompletableFuture.completedFuture(partial);
|
||||||
|
} else if (!partial.getProperties().isEmpty() || (partial.getId() == null && partial.getName() == null)) {
|
||||||
|
// If properties have been provided to us, or no ID and no name have been provided, create a static profile from
|
||||||
|
// what we do know
|
||||||
|
// This replicates vanilla Java client behaviour
|
||||||
|
String name = partial.getName() == null ? "" : partial.getName();
|
||||||
|
UUID uuid = partial.getName() == null ? EMPTY_UUID : createOfflinePlayerUUID(partial.getName());
|
||||||
|
GameProfile completed = new GameProfile(uuid, name);
|
||||||
|
completed.setProperties(partial.getProperties());
|
||||||
|
return CompletableFuture.completedFuture(completed);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameProfile cached = RESOLVED_PROFILES_CACHE.getIfPresent(profile);
|
||||||
|
if (cached != null) {
|
||||||
|
return CompletableFuture.completedFuture(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The resolvable profile is dynamic - server wants the client (us) to retrieve the full GameProfile
|
||||||
|
// from Mojang's API
|
||||||
|
|
||||||
|
// The partial profile *should* always have either a name or a UUID, not both
|
||||||
|
CompletableFuture<GameProfile> completedProfileFuture;
|
||||||
|
if (partial.getName() != null) {
|
||||||
|
completedProfileFuture = SkinProvider.requestUUIDFromUsername(partial.getName())
|
||||||
|
.thenApply(uuid -> new GameProfile(uuid, partial.getName()));
|
||||||
|
} else {
|
||||||
|
completedProfileFuture = SkinProvider.requestUsernameFromUUID(partial.getId())
|
||||||
|
.thenApply(name -> new GameProfile(partial.getId(), name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return completedProfileFuture
|
||||||
|
.thenCompose(nameAndUUID -> {
|
||||||
|
// Fallback to partial if anything goes wrong - should replicate vanilla Java client behaviour
|
||||||
|
if (nameAndUUID.getId() == null) {
|
||||||
|
return CompletableFuture.completedFuture(partial);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SkinProvider.requestTexturesFromUUID(nameAndUUID.getId())
|
||||||
|
.thenApply(encoded -> {
|
||||||
|
if (encoded == null) {
|
||||||
|
return partial;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GameProfile.Property> properties = new ArrayList<>();
|
||||||
|
properties.add(new GameProfile.Property("textures", encoded));
|
||||||
|
nameAndUUID.setProperties(properties);
|
||||||
|
return nameAndUUID;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.thenApply(resolved -> {
|
||||||
|
RESOLVED_PROFILES_CACHE.put(profile, resolved);
|
||||||
|
return resolved;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session,
|
public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session,
|
||||||
Consumer<SkinProvider.SkinAndCape> skinAndCapeConsumer) {
|
Consumer<SkinProvider.SkinAndCape> skinAndCapeConsumer) {
|
||||||
SkinProvider.requestSkinData(entity, session).whenCompleteAsync((skinData, throwable) -> {
|
SkinProvider.requestSkinData(entity, session).whenCompleteAsync((skinData, throwable) -> {
|
||||||
@@ -240,6 +312,10 @@ public class SkinManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static UUID createOfflinePlayerUUID(String username) {
|
||||||
|
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
public record GameProfileData(String skinUrl, String capeUrl, boolean isAlex) {
|
public record GameProfileData(String skinUrl, String capeUrl, boolean isAlex) {
|
||||||
/**
|
/**
|
||||||
* Generate the GameProfileData from the given CompoundTag representing a GameProfile
|
* Generate the GameProfileData from the given CompoundTag representing a GameProfile
|
||||||
|
|||||||
@@ -476,16 +476,84 @@ public class SkinProvider {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static @Nullable String shorthandUUID(@Nullable UUID uuid) {
|
||||||
|
if (uuid == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uuid.toString().replace("-", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @Nullable UUID expandUUID(@Nullable String uuid) {
|
||||||
|
if (uuid == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
long mostSignificant = Long.parseUnsignedLong(uuid.substring(0, 16), 16);
|
||||||
|
long leastSignificant = Long.parseUnsignedLong(uuid.substring(16), 16);
|
||||||
|
return new UUID(mostSignificant, leastSignificant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a player's username from their UUID
|
||||||
|
*
|
||||||
|
* @param uuid the player's UUID
|
||||||
|
* @return a completable username of the player
|
||||||
|
*/
|
||||||
|
public static CompletableFuture<@Nullable String> requestUsernameFromUUID(UUID uuid) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
JsonNode node = WebUtils.getJson("https://api.minecraftservices.com/minecraft/profile/lookup/" + shorthandUUID(uuid));
|
||||||
|
JsonNode name = node.get("name");
|
||||||
|
if (name == null) {
|
||||||
|
GeyserImpl.getInstance().getLogger().debug("No username found in Mojang response for " + uuid);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return name.asText();
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, getExecutorService());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a player's UUID from their username
|
||||||
|
*
|
||||||
|
* @param username the player's username
|
||||||
|
* @return a completable UUID of the player
|
||||||
|
*/
|
||||||
|
public static CompletableFuture<@Nullable UUID> requestUUIDFromUsername(String username) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
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 expandUUID(id.asText());
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, getExecutorService());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request textures from a player's UUID
|
* Request textures from a player's UUID
|
||||||
*
|
*
|
||||||
* @param uuid the player's UUID without any hyphens
|
* @param uuid the player's UUID
|
||||||
* @return a completable GameProfile with textures included
|
* @return a completable GameProfile with textures included
|
||||||
*/
|
*/
|
||||||
public static CompletableFuture<@Nullable String> requestTexturesFromUUID(String uuid) {
|
public static CompletableFuture<@Nullable String> requestTexturesFromUUID(UUID uuid) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
try {
|
try {
|
||||||
JsonNode node = WebUtils.getJson("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid);
|
JsonNode node = WebUtils.getJson("https://sessionserver.mojang.com/session/minecraft/profile/" + shorthandUUID(uuid));
|
||||||
JsonNode properties = node.get("properties");
|
JsonNode properties = node.get("properties");
|
||||||
if (properties == null) {
|
if (properties == null) {
|
||||||
GeyserImpl.getInstance().getLogger().debug("No properties found in Mojang response for " + uuid);
|
GeyserImpl.getInstance().getLogger().debug("No properties found in Mojang response for " + uuid);
|
||||||
@@ -509,28 +577,13 @@ public class SkinProvider {
|
|||||||
* @return a completable GameProfile with textures included
|
* @return a completable GameProfile with textures included
|
||||||
*/
|
*/
|
||||||
public static CompletableFuture<@Nullable String> requestTexturesFromUsername(String username) {
|
public static CompletableFuture<@Nullable String> requestTexturesFromUsername(String username) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return requestUUIDFromUsername(username)
|
||||||
try {
|
.thenCompose(uuid -> {
|
||||||
// Offline skin, or no present UUID
|
if (uuid == null) {
|
||||||
JsonNode node = WebUtils.getJson("https://api.mojang.com/users/profiles/minecraft/" + username);
|
return CompletableFuture.completedFuture(null);
|
||||||
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();
|
return requestTexturesFromUUID(uuid);
|
||||||
} catch (Exception e) {
|
});
|
||||||
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, getExecutorService()).thenCompose(uuid -> {
|
|
||||||
if (uuid == null) {
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
|
||||||
return requestTexturesFromUUID(uuid);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BufferedImage downloadImage(String imageUrl) throws IOException {
|
private static BufferedImage downloadImage(String imageUrl) throws IOException {
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ public abstract class InventoryTranslator<Type extends Inventory> {
|
|||||||
GeyserItemStack javaItem = inventory.getItem(sourceSlot);
|
GeyserItemStack javaItem = inventory.getItem(sourceSlot);
|
||||||
if (javaItem.asItem() == Items.PLAYER_HEAD
|
if (javaItem.asItem() == Items.PLAYER_HEAD
|
||||||
&& javaItem.hasNonBaseComponents()) {
|
&& javaItem.hasNonBaseComponents()) {
|
||||||
//FakeHeadProvider.setHead(session, session.getPlayerEntity(), javaItem.getComponent(DataComponentTypes.PROFILE)); TODO 1.21.9
|
FakeHeadProvider.setHead(session, session.getPlayerEntity(), javaItem.getComponent(DataComponentTypes.PROFILE));
|
||||||
}
|
}
|
||||||
} else if (sourceSlot == 5) {
|
} else if (sourceSlot == 5) {
|
||||||
// we are probably removing the head, so restore the original skin
|
// we are probably removing the head, so restore the original skin
|
||||||
|
|||||||
@@ -153,12 +153,11 @@ public class PlayerInventoryTranslator extends InventoryTranslator<PlayerInvento
|
|||||||
|
|
||||||
if (slot == 5) {
|
if (slot == 5) {
|
||||||
// Check for custom skull
|
// Check for custom skull
|
||||||
/*if (javaItem.asItem() == Items.PLAYER_HEAD
|
if (javaItem.asItem() == Items.PLAYER_HEAD && javaItem.hasNonBaseComponents()) {
|
||||||
&& javaItem.hasNonBaseComponents()) {
|
|
||||||
FakeHeadProvider.setHead(session, session.getPlayerEntity(), javaItem.getComponent(DataComponentTypes.PROFILE));
|
FakeHeadProvider.setHead(session, session.getPlayerEntity(), javaItem.getComponent(DataComponentTypes.PROFILE));
|
||||||
} else {
|
} else {
|
||||||
FakeHeadProvider.restoreOriginalSkin(session, session.getPlayerEntity());
|
FakeHeadProvider.restoreOriginalSkin(session, session.getPlayerEntity());
|
||||||
}*/ // TODO
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slot >= 1 && slot <= 44) {
|
if (slot >= 1 && slot <= 44) {
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import org.cloudburstmc.nbt.NbtMapBuilder;
|
|||||||
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
|
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
|
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
|
||||||
import org.geysermc.geyser.api.block.custom.CustomBlockData;
|
import org.geysermc.geyser.api.block.custom.CustomBlockData;
|
||||||
import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
|
import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
|
||||||
import org.geysermc.geyser.inventory.GeyserItemStack;
|
import org.geysermc.geyser.inventory.GeyserItemStack;
|
||||||
@@ -59,12 +58,10 @@ import org.geysermc.geyser.text.MinecraftLocale;
|
|||||||
import org.geysermc.geyser.translator.text.MessageTranslator;
|
import org.geysermc.geyser.translator.text.MessageTranslator;
|
||||||
import org.geysermc.geyser.util.InventoryUtils;
|
import org.geysermc.geyser.util.InventoryUtils;
|
||||||
import org.geysermc.geyser.util.MinecraftKey;
|
import org.geysermc.geyser.util.MinecraftKey;
|
||||||
import org.geysermc.mcprotocollib.auth.GameProfile;
|
|
||||||
import org.geysermc.mcprotocollib.auth.GameProfile.Texture;
|
|
||||||
import org.geysermc.mcprotocollib.auth.GameProfile.TextureType;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect;
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType;
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.ModifierOperation;
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.ModifierOperation;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
|
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.item.component.AdventureModePredicate;
|
import org.geysermc.mcprotocollib.protocol.data.game.item.component.AdventureModePredicate;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
|
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
|
||||||
@@ -237,7 +234,7 @@ public final class ItemTranslator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (bedrockItem.getJavaItem().equals(Items.PLAYER_HEAD)) {
|
if (bedrockItem.getJavaItem().equals(Items.PLAYER_HEAD)) {
|
||||||
// translatePlayerHead(session, components.get(DataComponentTypes.PROFILE), builder); TODO 1.21.9
|
translatePlayerHead(session, components.get(DataComponentTypes.PROFILE), builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
translateCustomItem(components, builder, bedrockItem);
|
translateCustomItem(components, builder, bedrockItem);
|
||||||
@@ -623,11 +620,14 @@ public final class ItemTranslator {
|
|||||||
builder.blockDefinition(blockDefinition);
|
builder.blockDefinition(blockDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @Nullable CustomSkull getCustomSkull(@Nullable GameProfile profile) {
|
private static @Nullable CustomSkull getCustomSkull(@Nullable ResolvableProfile profile) {
|
||||||
if (profile == null) {
|
if (profile == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO FIXME 1.21.9 - maybe if dynamic first send vanilla player head, then once resolved resend the proper player head??
|
||||||
|
// TODO could also work with the head name (see PlayerHeadItem)
|
||||||
|
/*
|
||||||
Map<TextureType, Texture> textures;
|
Map<TextureType, Texture> textures;
|
||||||
try {
|
try {
|
||||||
textures = profile.getTextures(false);
|
textures = profile.getTextures(false);
|
||||||
@@ -649,9 +649,11 @@ public final class ItemTranslator {
|
|||||||
|
|
||||||
String skinHash = skinTexture.getURL().substring(skinTexture.getURL().lastIndexOf('/') + 1);
|
String skinHash = skinTexture.getURL().substring(skinTexture.getURL().lastIndexOf('/') + 1);
|
||||||
return BlockRegistries.CUSTOM_SKULLS.get(skinHash);
|
return BlockRegistries.CUSTOM_SKULLS.get(skinHash);
|
||||||
|
*/
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void translatePlayerHead(GeyserSession session, GameProfile profile, ItemData.Builder builder) {
|
private static void translatePlayerHead(GeyserSession session, ResolvableProfile profile, ItemData.Builder builder) {
|
||||||
CustomSkull customSkull = getCustomSkull(profile);
|
CustomSkull customSkull = getCustomSkull(profile);
|
||||||
if (customSkull != null) {
|
if (customSkull != null) {
|
||||||
CustomBlockData customBlockData = customSkull.getCustomBlockData();
|
CustomBlockData customBlockData = customSkull.getCustomBlockData();
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import java.util.UUID;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
// TODO 1.21.9
|
||||||
@BlockEntity(type = BlockEntityType.SKULL)
|
@BlockEntity(type = BlockEntityType.SKULL)
|
||||||
public class SkullBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState {
|
public class SkullBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState {
|
||||||
@Override
|
@Override
|
||||||
@@ -81,8 +82,7 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
|
|||||||
List<NbtMap> properties = profile.getList("properties", NbtType.COMPOUND);
|
List<NbtMap> properties = profile.getList("properties", NbtType.COMPOUND);
|
||||||
if (properties.isEmpty()) {
|
if (properties.isEmpty()) {
|
||||||
if (uuid != null && uuid.version() == 4) {
|
if (uuid != null && uuid.version() == 4) {
|
||||||
String uuidString = uuid.toString().replace("-", "");
|
return SkinProvider.requestTexturesFromUUID(uuid);
|
||||||
return SkinProvider.requestTexturesFromUUID(uuidString);
|
|
||||||
} else {
|
} else {
|
||||||
String nameTag = profile.getString("name", null);
|
String nameTag = profile.getString("name", null);
|
||||||
if (nameTag != null) {
|
if (nameTag != null) {
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ import org.geysermc.geyser.session.GeyserSession;
|
|||||||
import org.geysermc.geyser.skin.FakeHeadProvider;
|
import org.geysermc.geyser.skin.FakeHeadProvider;
|
||||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||||
import org.geysermc.geyser.translator.protocol.Translator;
|
import org.geysermc.geyser.translator.protocol.Translator;
|
||||||
import org.geysermc.mcprotocollib.auth.GameProfile;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Equipment;
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Equipment;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
|
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
|
||||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundSetEquipmentPacket;
|
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundSetEquipmentPacket;
|
||||||
|
|
||||||
@@ -61,12 +61,12 @@ public class JavaSetEquipmentTranslator extends PacketTranslator<ClientboundSetE
|
|||||||
GeyserItemStack stack = GeyserItemStack.from(equipment.getItem());
|
GeyserItemStack stack = GeyserItemStack.from(equipment.getItem());
|
||||||
switch (equipment.getSlot()) {
|
switch (equipment.getSlot()) {
|
||||||
case HELMET -> {
|
case HELMET -> {
|
||||||
/*GameProfile profile = stack.getComponent(DataComponentTypes.PROFILE);
|
ResolvableProfile profile = stack.getComponent(DataComponentTypes.PROFILE);
|
||||||
if (livingEntity instanceof PlayerEntity && stack.asItem() == Items.PLAYER_HEAD && profile != null) {
|
if (livingEntity instanceof PlayerEntity && stack.asItem() == Items.PLAYER_HEAD && profile != null) {
|
||||||
FakeHeadProvider.setHead(session, (PlayerEntity) livingEntity, profile);
|
FakeHeadProvider.setHead(session, (PlayerEntity) livingEntity, profile);
|
||||||
} else {
|
} else {
|
||||||
FakeHeadProvider.restoreOriginalSkin(session, livingEntity);
|
FakeHeadProvider.restoreOriginalSkin(session, livingEntity);
|
||||||
}*/ // TODO 1.21.9
|
}
|
||||||
|
|
||||||
livingEntity.setHelmet(stack);
|
livingEntity.setHelmet(stack);
|
||||||
armorUpdated = true;
|
armorUpdated = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user