make heads render when equipped

This commit is contained in:
onebeastchris 2023-05-17 01:38:49 +02:00
parent fbd157ccdf
commit 231095e115
6 changed files with 97 additions and 23 deletions

View file

@ -26,6 +26,8 @@
package org.geysermc.geyser.skin;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
@ -41,7 +43,9 @@ import org.geysermc.geyser.text.GeyserLocale;
import javax.annotation.Nonnull;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@ -91,21 +95,48 @@ public class FakeHeadProvider {
}
});
public static void setHead(GeyserSession session, PlayerEntity entity, CompoundTag profileTag) {
SkinManager.GameProfileData gameProfileData = SkinManager.GameProfileData.from(profileTag);
if (gameProfileData == null) {
public static void setHead(GeyserSession session, PlayerEntity entity, Tag skullOwner) {
if (skullOwner == null) {
return;
}
if (skullOwner instanceof CompoundTag profileTag) {
SkinManager.GameProfileData gameProfileData = SkinManager.GameProfileData.from(profileTag);
if (gameProfileData == null) {
return;
}
loadHead(session, entity, gameProfileData);
} else if (skullOwner instanceof StringTag ownerTag) {
String owner = ownerTag.getValue();
if (owner.isEmpty()) {
return;
}
CompletableFuture<String> completableFuture = SkinProvider.requestTexturesFromUsername(owner);
completableFuture.whenCompleteAsync((encodedJson, throwable) -> {
if (throwable != null) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), throwable);
return;
}
try {
SkinManager.GameProfileData gameProfileData = SkinManager.GameProfileData.loadFromJson(encodedJson);
if (gameProfileData == null) {
return;
}
loadHead(session, entity, gameProfileData);
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid(), e.getMessage()));
}
});
}
}
public static void loadHead(GeyserSession session, PlayerEntity entity, SkinManager.GameProfileData gameProfileData) {
String fakeHeadSkinUrl = gameProfileData.skinUrl();
session.getPlayerWithCustomHeads().add(entity.getUuid());
String texturesProperty = entity.getTexturesProperty();
SkinProvider.EXECUTOR_SERVICE.execute(() -> {
try {
SkinProvider.SkinData mergedSkinData = MERGED_SKINS_LOADING_CACHE.get(new FakeHeadEntry(texturesProperty, fakeHeadSkinUrl, entity));
SkinManager.sendSkinPacket(session, entity, mergedSkinData);
} catch (ExecutionException e) {
GeyserImpl.getInstance().getLogger().error("Couldn't merge skin of " + entity.getUsername() + " with head skin url " + fakeHeadSkinUrl, e);
@ -155,4 +186,4 @@ public class FakeHeadProvider {
}
}
}
}

View file

@ -277,7 +277,7 @@ public class SkinManager {
return null;
}
private static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException {
static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException {
JsonNode skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8));
JsonNode textures = skinObject.get("textures");
@ -312,4 +312,4 @@ public class SkinManager {
return new GameProfileData(skinUrl, capeUrl, isAlex);
}
}
}
}

View file

@ -49,7 +49,9 @@ import org.geysermc.geyser.inventory.click.ClickPlan;
import org.geysermc.geyser.inventory.recipe.GeyserRecipe;
import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe;
import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.skin.FakeHeadProvider;
import org.geysermc.geyser.translator.inventory.chest.DoubleChestInventoryTranslator;
import org.geysermc.geyser.translator.inventory.chest.SingleChestInventoryTranslator;
import org.geysermc.geyser.translator.inventory.furnace.BlastFurnaceInventoryTranslator;
@ -216,6 +218,20 @@ public abstract class InventoryTranslator {
boolean isSourceCursor = isCursor(transferAction.getSource());
boolean isDestCursor = isCursor(transferAction.getDestination());
if ((this) instanceof PlayerInventoryTranslator) {
if (destSlot == 5) {
//only set the head if the destination is the head slot
GeyserItemStack javaItem = inventory.getItem(sourceSlot);
if (javaItem.asItem() == Items.PLAYER_HEAD
&& javaItem.getNbt() != null) {
FakeHeadProvider.setHead(session, session.getPlayerEntity(), javaItem.getNbt().get("SkullOwner"));
}
} else if (sourceSlot == 5) {
//we are probably removing the head, so restore the original skin
FakeHeadProvider.restoreOriginalSkin(session, session.getPlayerEntity());
}
}
if (shouldRejectItemPlace(session, inventory, transferAction.getSource().getContainer(),
isSourceCursor ? -1 : sourceSlot,
transferAction.getDestination().getContainer(), isDestCursor ? -1 : destSlot)) {
@ -916,4 +932,4 @@ public abstract class InventoryTranslator {
TRANSFER,
DONE
}
}
}

View file

@ -29,7 +29,6 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundSetCreativeModeSlotPacket;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import it.unimi.dsi.fastutil.ints.IntIterator;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
@ -94,7 +93,13 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
armorContentPacket.setContainerId(ContainerId.ARMOR);
contents = new ItemData[4];
for (int i = 5; i < 9; i++) {
contents[i - 5] = inventory.getItem(i).getItemData(session);
GeyserItemStack item = inventory.getItem(i);
contents[i - 5] = item.getItemData(session);
if (i == 5 &&
item.asItem() == Items.PLAYER_HEAD &&
item.getNbt() != null) {
FakeHeadProvider.setHead(session, session.getPlayerEntity(), item.getNbt().get("SkullOwner"));
}
}
armorContentPacket.setContents(Arrays.asList(contents));
session.sendUpstreamPacket(armorContentPacket);
@ -136,9 +141,8 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
if (slot == 5) {
// Check for custom skull
if (javaItem.asItem() == Items.PLAYER_HEAD
&& javaItem.getNbt() != null
&& javaItem.getNbt().get("SkullOwner") instanceof CompoundTag profile) {
FakeHeadProvider.setHead(session, session.getPlayerEntity(), profile);
&& javaItem.getNbt() != null) {
FakeHeadProvider.setHead(session, session.getPlayerEntity(), javaItem.getNbt().get("SkullOwner"));
} else {
FakeHeadProvider.restoreOriginalSkin(session, session.getPlayerEntity());
}

View file

@ -44,6 +44,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.InventoryActionData;
import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.InventorySource;
import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.LegacySetItemSlotData;
import org.cloudburstmc.protocol.bedrock.packet.ContainerOpenPacket;
import org.cloudburstmc.protocol.bedrock.packet.InventoryTransactionPacket;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
@ -63,8 +64,8 @@ import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.item.type.SpawnEggItem;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.ItemMappings;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.skin.FakeHeadProvider;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
@ -73,7 +74,9 @@ import org.geysermc.geyser.util.BlockUtils;
import org.geysermc.geyser.util.CooldownUtils;
import org.geysermc.geyser.util.EntityUtils;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.InventoryUtils;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
@ -93,8 +96,6 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
// Send book updates before opening inventories
session.getBookEditCache().checkForSend();
ItemMappings mappings = session.getItemMappings();
switch (packet.getTransactionType()) {
case NORMAL:
if (packet.getActions().size() == 2) {
@ -350,6 +351,30 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
ServerboundUseItemPacket useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getWorldCache().nextPredictionSequence());
session.sendDownstreamPacket(useItemPacket);
List<LegacySetItemSlotData> legacySlots = packet.getLegacySlots();
if (packet.getActions().size() == 1 && legacySlots.size() > 0) {
InventoryActionData actionData = packet.getActions().get(0);
LegacySetItemSlotData slotData = legacySlots.get(0);
if (slotData.getContainerId() == 6 && !actionData.getFromItem().isNull()) {
// The player is trying to swap out an armor piece that already has an item in it
// 1.19.4 brings this natively, but we need this specific case for custom head rendering to work
int bedrockHotbarSlot = packet.getHotbarSlot();
Click click = InventoryUtils.getClickForHotbarSwap(bedrockHotbarSlot);
if (click != null && slotData.getSlots().length != 0) {
Inventory playerInventory = session.getPlayerInventory();
// Bedrock sends us the index of the slot in the armor container; armor in Java
// Edition is offset by 5 in the player inventory
int armorSlot = slotData.getSlots()[0] + 5;
if (armorSlot == 5) {
GeyserItemStack armorSlotItem = playerInventory.getItem(armorSlot);
if (armorSlotItem.asItem() == Items.PLAYER_HEAD) {
FakeHeadProvider.restoreOriginalSkin(session, session.getPlayerEntity());
}
}
}
}
}
}
case 2 -> {
int blockState = session.getGameMode() == GameMode.CREATIVE ?
@ -618,4 +643,4 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
}, 150, TimeUnit.MILLISECONDS));
}
}
}
}

View file

@ -28,7 +28,6 @@ package org.geysermc.geyser.translator.protocol.java.entity;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Equipment;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.ClientboundSetEquipmentPacket;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.LivingEntity;
@ -66,9 +65,8 @@ public class JavaSetEquipmentTranslator extends PacketTranslator<ClientboundSetE
if (livingEntity instanceof PlayerEntity
&& javaItem != null
&& javaItem.getId() == Items.PLAYER_HEAD.javaId()
&& javaItem.getNbt() != null
&& javaItem.getNbt().get("SkullOwner") instanceof CompoundTag profile) {
FakeHeadProvider.setHead(session, (PlayerEntity) livingEntity, profile);
&& javaItem.getNbt() != null) {
FakeHeadProvider.setHead(session, (PlayerEntity) livingEntity, javaItem.getNbt().get("SkullOwner"));
} else {
FakeHeadProvider.restoreOriginalSkin(session, livingEntity);
}