Merge remote-tracking branch 'upstream/master' into feature/blocky

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>
This commit is contained in:
Joshua Castle 2023-01-23 22:22:11 -08:00
commit d71d9c142b
No known key found for this signature in database
GPG key ID: F674F38216C35D5D
6 changed files with 125 additions and 77 deletions

View file

@ -26,17 +26,20 @@
package org.geysermc.geyser.entity.type.player; package org.geysermc.geyser.entity.type.player;
import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.GameType; import com.nukkitx.protocol.bedrock.data.GameType;
import com.nukkitx.protocol.bedrock.data.PlayerPermission; import com.nukkitx.protocol.bedrock.data.PlayerPermission;
import com.nukkitx.protocol.bedrock.data.command.CommandPermission; import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import com.nukkitx.protocol.bedrock.packet.AddPlayerPacket; import com.nukkitx.protocol.bedrock.packet.AddPlayerPacket;
import lombok.Getter;
import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.SkullCache; import org.geysermc.geyser.session.cache.SkullCache;
import org.geysermc.geyser.skin.SkullSkinManager; import org.geysermc.geyser.skin.SkullSkinManager;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -46,6 +49,12 @@ import java.util.concurrent.TimeUnit;
*/ */
public class SkullPlayerEntity extends PlayerEntity { public class SkullPlayerEntity extends PlayerEntity {
@Getter
private UUID skullUUID;
@Getter
private Vector3i skullPosition;
public SkullPlayerEntity(GeyserSession session, long geyserId) { public SkullPlayerEntity(GeyserSession session, long geyserId) {
super(session, 0, geyserId, UUID.randomUUID(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "", null); super(session, 0, geyserId, UUID.randomUUID(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "", null);
} }
@ -102,11 +111,14 @@ public class SkullPlayerEntity extends PlayerEntity {
} }
public void updateSkull(SkullCache.Skull skull) { public void updateSkull(SkullCache.Skull skull) {
if (!skull.getTexturesProperty().equals(getTexturesProperty())) { skullPosition = skull.getPosition();
if (!Objects.equals(skull.getTexturesProperty(), getTexturesProperty()) || !Objects.equals(skullUUID, skull.getUuid())) {
// Make skull invisible as we change skins // Make skull invisible as we change skins
setFlag(EntityFlag.INVISIBLE, true); setFlag(EntityFlag.INVISIBLE, true);
updateBedrockMetadata(); updateBedrockMetadata();
skullUUID = skull.getUuid();
setTexturesProperty(skull.getTexturesProperty()); setTexturesProperty(skull.getTexturesProperty());
SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> { SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> {

View file

@ -78,8 +78,9 @@ public class SkullCache {
this.skullRenderDistanceSquared = distance * distance; this.skullRenderDistanceSquared = distance * distance;
} }
public Skull putSkull(Vector3i position, String texturesProperty, int blockState) { public Skull putSkull(Vector3i position, UUID uuid, String texturesProperty, int blockState) {
Skull skull = skulls.computeIfAbsent(position, Skull::new); Skull skull = skulls.computeIfAbsent(position, Skull::new);
skull.uuid = uuid;
if (!texturesProperty.equals(skull.texturesProperty)) { if (!texturesProperty.equals(skull.texturesProperty)) {
skull.texturesProperty = texturesProperty; skull.texturesProperty = texturesProperty;
skull.skinHash = null; skull.skinHash = null;
@ -147,7 +148,7 @@ public class SkullCache {
public Skull updateSkull(Vector3i position, int blockState) { public Skull updateSkull(Vector3i position, int blockState) {
Skull skull = skulls.get(position); Skull skull = skulls.get(position);
if (skull != null) { if (skull != null) {
putSkull(position, skull.texturesProperty, blockState); putSkull(position, skull.uuid, skull.texturesProperty, blockState);
} }
return skull; return skull;
} }
@ -266,6 +267,7 @@ public class SkullCache {
@RequiredArgsConstructor @RequiredArgsConstructor
@Data @Data
public static class Skull { public static class Skull {
private UUID uuid;
private String texturesProperty; private String texturesProperty;
private String skinHash; private String skinHash;

View file

@ -35,6 +35,7 @@ import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket; import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.entity.type.player.SkullPlayerEntity;
import org.geysermc.geyser.session.GeyserSession; 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;
@ -69,7 +70,7 @@ public class SkinManager {
// The server either didn't have a texture to send, or we didn't have the texture ID cached. // The server either didn't have a texture to send, or we didn't have the texture ID cached.
// Let's see if this player is a Bedrock player, and if so, let's pull their skin. // Let's see if this player is a Bedrock player, and if so, let's pull their skin.
// Otherwise, grab the default player skin // Otherwise, grab the default player skin
SkinProvider.SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity); SkinProvider.SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity.getUuid());
if (skin == null) { if (skin == null) {
skin = fallbackSkinData.skin(); skin = fallbackSkinData.skin();
geometry = fallbackSkinData.geometry(); geometry = fallbackSkinData.geometry();
@ -255,24 +256,28 @@ public class SkinManager {
* @return The built GameProfileData * @return The built GameProfileData
*/ */
public static @Nullable GameProfileData from(PlayerEntity entity) { public static @Nullable GameProfileData from(PlayerEntity entity) {
try { String texturesProperty = entity.getTexturesProperty();
String texturesProperty = entity.getTexturesProperty(); if (texturesProperty == null) {
// Likely offline mode
return null;
}
if (texturesProperty == null) { try {
// Likely offline mode
return null;
}
return loadFromJson(texturesProperty); return loadFromJson(texturesProperty);
} catch (IOException exception) { } catch (Exception exception) {
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername()); if (entity instanceof SkullPlayerEntity skullEntity) {
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for skull at " + skullEntity.getSkullPosition() + " with Value: " + texturesProperty);
} else {
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername() + " with Value: " + texturesProperty);
}
if (GeyserImpl.getInstance().getConfig().isDebugMode()) { if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
exception.printStackTrace(); exception.printStackTrace();
} }
return null;
} }
return null;
} }
public static GameProfileData loadFromJson(String encodedJson) throws IOException { public static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException {
JsonNode skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8)); JsonNode skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8));
JsonNode textures = skinObject.get("textures"); JsonNode textures = skinObject.get("textures");
@ -285,14 +290,23 @@ public class SkinManager {
return null; return null;
} }
String skinUrl = skinTexture.get("url").asText().replace("http://", "https://"); String skinUrl;
JsonNode skinUrlNode = skinTexture.get("url");
if (skinUrlNode != null && skinUrlNode.isTextual()) {
skinUrl = skinUrlNode.asText().replace("http://", "https://");
} else {
return null;
}
boolean isAlex = skinTexture.has("metadata"); boolean isAlex = skinTexture.has("metadata");
String capeUrl = null; String capeUrl = null;
JsonNode capeTexture = textures.get("CAPE"); JsonNode capeTexture = textures.get("CAPE");
if (capeTexture != null) { if (capeTexture != null) {
capeUrl = capeTexture.get("url").asText().replace("http://", "https://"); JsonNode capeUrlNode = capeTexture.get("url");
if (capeUrlNode != null && capeUrlNode.isTextual()) {
capeUrl = capeUrlNode.asText().replace("http://", "https://");
}
} }
return new GameProfileData(skinUrl, capeUrl, isAlex); return new GameProfileData(skinUrl, capeUrl, isAlex);

View file

@ -26,9 +26,6 @@
package org.geysermc.geyser.skin; package org.geysermc.geyser.skin;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.IntArrayTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilder;
import it.unimi.dsi.fastutil.bytes.ByteArrays; import it.unimi.dsi.fastutil.bytes.ByteArrays;
@ -172,14 +169,13 @@ public class SkinProvider {
/** /**
* If skin data fails to apply, or there is no skin data to apply, determine what skin we should give as a fallback. * If skin data fails to apply, or there is no skin data to apply, determine what skin we should give as a fallback.
*/ */
static SkinData determineFallbackSkinData(PlayerEntity entity) { static SkinData determineFallbackSkinData(UUID uuid) {
Skin skin = null; Skin skin = null;
Cape cape = null; Cape cape = null;
SkinGeometry geometry = SkinGeometry.WIDE; SkinGeometry geometry = SkinGeometry.WIDE;
if (GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) { 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. // Let's see if this player is a Bedrock player, and if so, let's pull their skin.
UUID uuid = entity.getUuid();
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid); GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
if (session != null) { if (session != null) {
String skinId = session.getClientData().getSkinId(); String skinId = session.getClientData().getSkinId();
@ -192,7 +188,7 @@ public class SkinProvider {
if (skin == null) { if (skin == null) {
// We don't have a skin for the player right now. Fall back to a default. // We don't have a skin for the player right now. Fall back to a default.
ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(entity.getUuid()); ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(uuid);
skin = providedSkin.getData(); skin = providedSkin.getData();
geometry = providedSkin.isSlim() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE; geometry = providedSkin.isSlim() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE;
} }
@ -232,7 +228,7 @@ public class SkinProvider {
SkinManager.GameProfileData data = SkinManager.GameProfileData.from(entity); SkinManager.GameProfileData data = SkinManager.GameProfileData.from(entity);
if (data == null) { if (data == null) {
// This player likely does not have a textures property // This player likely does not have a textures property
return CompletableFuture.completedFuture(determineFallbackSkinData(entity)); return CompletableFuture.completedFuture(determineFallbackSkinData(entity.getUuid()));
} }
return requestSkinAndCape(entity.getUuid(), data.skinUrl(), data.capeUrl()) return requestSkinAndCape(entity.getUuid(), data.skinUrl(), data.capeUrl())

View file

@ -29,11 +29,12 @@ import com.nukkitx.protocol.bedrock.data.skin.ImageData;
import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin; import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket; import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.entity.type.player.SkullPlayerEntity;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.GeyserLocale;
import java.util.Collections; import java.util.Collections;
import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
public class SkullSkinManager extends SkinManager { public class SkullSkinManager extends SkinManager {
@ -48,28 +49,37 @@ public class SkullSkinManager extends SkinManager {
); );
} }
public static void requestAndHandleSkin(PlayerEntity entity, GeyserSession session, public static void requestAndHandleSkin(SkullPlayerEntity entity, GeyserSession session,
Consumer<SkinProvider.Skin> skinConsumer) { Consumer<SkinProvider.Skin> skinConsumer) {
BiConsumer<SkinProvider.Skin, Throwable> applySkin = (skin, throwable) -> {
try {
PlayerSkinPacket packet = new PlayerSkinPacket();
packet.setUuid(entity.getUuid());
packet.setOldSkinName("");
packet.setNewSkinName(skin.getTextureUrl());
packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData()));
packet.setTrustedSkin(true);
session.sendUpstreamPacket(packet);
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
}
if (skinConsumer != null) {
skinConsumer.accept(skin);
}
};
GameProfileData data = GameProfileData.from(entity); GameProfileData data = GameProfileData.from(entity);
if (data == null) {
SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true) GeyserImpl.getInstance().getLogger().debug("Using fallback skin for skull at " + entity.getSkullPosition() +
.whenCompleteAsync((skin, throwable) -> { " with texture value: " + entity.getTexturesProperty() + " and UUID: " + entity.getSkullUUID());
try { // No texture available, fallback using the UUID
PlayerSkinPacket packet = new PlayerSkinPacket(); SkinProvider.SkinData fallback = SkinProvider.determineFallbackSkinData(entity.getSkullUUID());
packet.setUuid(entity.getUuid()); applySkin.accept(fallback.skin(), null);
packet.setOldSkinName(""); } else {
packet.setNewSkinName(skin.getTextureUrl()); SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true)
packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData())); .whenCompleteAsync(applySkin);
packet.setTrustedSkin(true); }
session.sendUpstreamPacket(packet);
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
}
if (skinConsumer != null) {
skinConsumer.accept(skin);
}
});
} }
} }

View file

@ -39,7 +39,9 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.SkullCache; import org.geysermc.geyser.session.cache.SkullCache;
import org.geysermc.geyser.skin.SkinProvider; import org.geysermc.geyser.skin.SkinProvider;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -59,41 +61,52 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
builder.put("SkullType", skullVariant); builder.put("SkullType", skullVariant);
} }
private static CompletableFuture<String> getTextures(CompoundTag tag) { private static UUID getUUID(CompoundTag owner) {
CompoundTag owner = tag.get("SkullOwner"); if (owner.get("Id") instanceof IntArrayTag uuidTag && uuidTag.length() == 4) {
if (owner != null) { int[] uuidAsArray = uuidTag.getValue();
CompoundTag properties = owner.get("Properties"); // thank u viaversion
if (properties == null) { return new UUID((long) uuidAsArray[0] << 32 | ((long) uuidAsArray[1] & 0xFFFFFFFFL),
if (owner.get("Id") instanceof IntArrayTag uuidTag) { (long) uuidAsArray[2] << 32 | ((long) uuidAsArray[3] & 0xFFFFFFFFL));
int[] uuidAsArray = uuidTag.getValue();
// thank u viaversion
UUID uuid = new UUID((long) uuidAsArray[0] << 32 | ((long) uuidAsArray[1] & 0xFFFFFFFFL),
(long) uuidAsArray[2] << 32 | ((long) uuidAsArray[3] & 0xFFFFFFFFL));
if (uuid.version() == 4) {
String uuidString = uuid.toString().replace("-", "");
return SkinProvider.requestTexturesFromUUID(uuidString);
}
}
if (owner.get("Name") instanceof StringTag nameTag) {
// Fall back to username if UUID was missing or was an offline mode UUID
return SkinProvider.requestTexturesFromUsername(nameTag.getValue());
}
return CompletableFuture.completedFuture(null);
}
ListTag textures = properties.get("textures");
LinkedHashMap<?,?> tag1 = (LinkedHashMap<?,?>) textures.get(0).getValue();
StringTag texture = (StringTag) tag1.get("Value");
return CompletableFuture.completedFuture(texture.getValue());
} }
return CompletableFuture.completedFuture(null); // Convert username to an offline UUID
String username = null;
if (owner.get("Name") instanceof StringTag nameTag) {
username = nameTag.getValue().toLowerCase(Locale.ROOT);
}
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8));
}
private static CompletableFuture<String> getTextures(CompoundTag owner, UUID uuid) {
CompoundTag properties = owner.get("Properties");
if (properties == null) {
if (uuid != null && uuid.version() == 4) {
String uuidString = uuid.toString().replace("-", "");
return SkinProvider.requestTexturesFromUUID(uuidString);
} else if (owner.get("Name") instanceof StringTag nameTag) {
// Fall back to username if UUID was missing or was an offline mode UUID
return SkinProvider.requestTexturesFromUsername(nameTag.getValue());
}
return CompletableFuture.completedFuture(null);
}
ListTag textures = properties.get("textures");
LinkedHashMap<?,?> tag1 = (LinkedHashMap<?,?>) textures.get(0).getValue();
StringTag texture = (StringTag) tag1.get("Value");
return CompletableFuture.completedFuture(texture.getValue());
} }
public static int translateSkull(GeyserSession session, CompoundTag tag, Vector3i blockPosition, int blockState) { public static int translateSkull(GeyserSession session, CompoundTag tag, Vector3i blockPosition, int blockState) {
CompletableFuture<String> texturesFuture = getTextures(tag); CompoundTag owner = tag.get("SkullOwner");
if (owner == null) {
session.getSkullCache().removeSkull(blockPosition);
return -1;
}
UUID uuid = getUUID(owner);
CompletableFuture<String> texturesFuture = getTextures(owner, uuid);
if (texturesFuture.isDone()) { if (texturesFuture.isDone()) {
try { try {
SkullCache.Skull skull = session.getSkullCache().putSkull(blockPosition, texturesFuture.get(), blockState); SkullCache.Skull skull = session.getSkullCache().putSkull(blockPosition, uuid, texturesFuture.get(), blockState);
return skull.getCustomRuntimeId(); return skull.getCustomRuntimeId();
} catch (InterruptedException | ExecutionException e) { } catch (InterruptedException | ExecutionException e) {
session.getGeyser().getLogger().debug("Failed to acquire textures for custom skull: " + blockPosition + " " + tag); session.getGeyser().getLogger().debug("Failed to acquire textures for custom skull: " + blockPosition + " " + tag);
@ -111,17 +124,18 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
return; return;
} }
if (session.getEventLoop().inEventLoop()) { if (session.getEventLoop().inEventLoop()) {
putSkull(session, blockPosition, texturesProperty, blockState); putSkull(session, blockPosition, uuid, texturesProperty, blockState);
} else { } else {
session.executeInEventLoop(() -> putSkull(session, blockPosition, texturesProperty, blockState)); session.executeInEventLoop(() -> putSkull(session, blockPosition, uuid, texturesProperty, blockState));
} }
}); });
// We don't have the textures yet, so we can't determine if a custom block was defined for this skull // We don't have the textures yet, so we can't determine if a custom block was defined for this skull
return -1; return -1;
} }
private static void putSkull(GeyserSession session, Vector3i blockPosition, String texturesProperty, int blockState) { private static void putSkull(GeyserSession session, Vector3i blockPosition, UUID uuid, String texturesProperty, int blockState) {
SkullCache.Skull skull = session.getSkullCache().putSkull(blockPosition, texturesProperty, blockState); SkullCache.Skull skull = session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState);
if (skull.getCustomRuntimeId() != -1) { if (skull.getCustomRuntimeId() != -1) {
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
updateBlockPacket.setDataLayer(0); updateBlockPacket.setDataLayer(0);