mirror of
https://github.com/GeyserMC/Geyser.git
synced 2025-12-19 14:59:27 +00:00
Fix skins not showing on 1.21.60
This commit is contained in:
@@ -35,7 +35,6 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
|||||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
|
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
@@ -94,6 +93,7 @@ import org.cloudburstmc.protocol.bedrock.packet.ItemComponentPacket;
|
|||||||
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEvent2Packet;
|
import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEvent2Packet;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.packet.SetCommandsEnabledPacket;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.SetTimePacket;
|
import org.cloudburstmc.protocol.bedrock.packet.SetTimePacket;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.StartGamePacket;
|
import org.cloudburstmc.protocol.bedrock.packet.StartGamePacket;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.SyncEntityPropertyPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.SyncEntityPropertyPacket;
|
||||||
@@ -331,10 +331,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||||||
private final Map<Vector3i, ItemFrameEntity> itemFrameCache = new Object2ObjectOpenHashMap<>();
|
private final Map<Vector3i, ItemFrameEntity> itemFrameCache = new Object2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all players that have a player head on 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 Set<UUID> playerWithCustomHeads = new ObjectOpenHashSet<>();
|
private final Map<UUID, GameProfile> playerWithCustomHeads = new Object2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
private boolean droppingLecternBook;
|
private boolean droppingLecternBook;
|
||||||
@@ -787,6 +787,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||||||
playStatusPacket.setStatus(PlayStatusPacket.Status.PLAYER_SPAWN);
|
playStatusPacket.setStatus(PlayStatusPacket.Status.PLAYER_SPAWN);
|
||||||
upstream.sendPacket(playStatusPacket);
|
upstream.sendPacket(playStatusPacket);
|
||||||
|
|
||||||
|
SetCommandsEnabledPacket setCommandsEnabledPacket = new SetCommandsEnabledPacket();
|
||||||
|
setCommandsEnabledPacket.setCommandsEnabled(!geyser.getConfig().isXboxAchievementsEnabled());
|
||||||
|
upstream.sendPacket(setCommandsEnabledPacket);
|
||||||
|
|
||||||
UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
|
UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
|
||||||
attributesPacket.setRuntimeEntityId(getPlayerEntity().getGeyserId());
|
attributesPacket.setRuntimeEntityId(getPlayerEntity().getGeyserId());
|
||||||
// Default move speed
|
// Default move speed
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ public class FakeHeadProvider {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GameProfile current = session.getPlayerWithCustomHeads().get(entity.getUuid());
|
||||||
|
if (profile.equals(current)) {
|
||||||
|
// We already did this, no need to re-compute
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Map<TextureType, Texture> textures;
|
Map<TextureType, Texture> textures;
|
||||||
try {
|
try {
|
||||||
textures = profile.getTextures(false);
|
textures = profile.getTextures(false);
|
||||||
@@ -118,7 +124,7 @@ public class FakeHeadProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (textures == null || textures.isEmpty()) {
|
if (textures == null || textures.isEmpty()) {
|
||||||
loadHead(session, entity, profile.getName());
|
loadHeadFromProfile(session, entity, profile);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,16 +139,18 @@ public class FakeHeadProvider {
|
|||||||
|
|
||||||
boolean isAlex = skinTexture.getModel() == TextureModel.SLIM;
|
boolean isAlex = skinTexture.getModel() == TextureModel.SLIM;
|
||||||
|
|
||||||
loadHead(session, entity, new GameProfileData(skinTexture.getURL(), capeUrl, isAlex));
|
loadHeadFromProfile(session, entity, new GameProfileData(skinTexture.getURL(), capeUrl, isAlex), profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void loadHead(GeyserSession session, PlayerEntity entity, String owner) {
|
public static void loadHeadFromProfile(GeyserSession session, PlayerEntity entity, GameProfile profile) {
|
||||||
if (owner == null || owner.isEmpty()) {
|
CompletableFuture<String> texturesFuture;
|
||||||
return;
|
if (profile.getId() != null) {
|
||||||
|
texturesFuture = SkinProvider.requestTexturesFromUUID(profile.getId().toString());
|
||||||
|
} else {
|
||||||
|
texturesFuture = SkinProvider.requestTexturesFromUsername(profile.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
CompletableFuture<String> completableFuture = SkinProvider.requestTexturesFromUsername(owner);
|
texturesFuture.whenCompleteAsync((encodedJson, throwable) -> {
|
||||||
completableFuture.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;
|
||||||
@@ -152,17 +160,17 @@ public class FakeHeadProvider {
|
|||||||
if (gameProfileData == null) {
|
if (gameProfileData == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loadHead(session, entity, gameProfileData);
|
loadHeadFromProfile(session, entity, gameProfileData, profile);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid(), e.getMessage()));
|
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid(), e.getMessage()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void loadHead(GeyserSession session, PlayerEntity entity, SkinManager.GameProfileData gameProfileData) {
|
public static void loadHeadFromProfile(GeyserSession session, PlayerEntity entity, SkinManager.GameProfileData gameProfileData, GameProfile profile) {
|
||||||
String fakeHeadSkinUrl = gameProfileData.skinUrl();
|
String fakeHeadSkinUrl = gameProfileData.skinUrl();
|
||||||
|
|
||||||
session.getPlayerWithCustomHeads().add(entity.getUuid());
|
session.getPlayerWithCustomHeads().put(entity.getUuid(), profile);
|
||||||
String texturesProperty = entity.getTexturesProperty();
|
String texturesProperty = entity.getTexturesProperty();
|
||||||
SkinProvider.getExecutorService().execute(() -> {
|
SkinProvider.getExecutorService().execute(() -> {
|
||||||
try {
|
try {
|
||||||
@@ -179,7 +187,7 @@ public class FakeHeadProvider {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session.getPlayerWithCustomHeads().remove(entity.getUuid())) {
|
if (session.getPlayerWithCustomHeads().remove(entity.getUuid()) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ import org.geysermc.geyser.text.GeyserLocale;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@@ -102,7 +101,7 @@ public class SkinManager {
|
|||||||
Skin skin,
|
Skin skin,
|
||||||
Cape cape,
|
Cape cape,
|
||||||
SkinGeometry geometry) {
|
SkinGeometry geometry) {
|
||||||
SerializedSkin serializedSkin = getSkin(skin.textureUrl(), skin, cape, geometry);
|
SerializedSkin serializedSkin = getSkin(session, skin.textureUrl(), skin, cape, geometry);
|
||||||
|
|
||||||
// This attempts to find the XUID of the player so profile images show up for Xbox accounts
|
// This attempts to find the XUID of the player so profile images show up for Xbox accounts
|
||||||
String xuid = "";
|
String xuid = "";
|
||||||
@@ -138,7 +137,6 @@ public class SkinManager {
|
|||||||
SkinGeometry geometry = skinData.geometry();
|
SkinGeometry geometry = skinData.geometry();
|
||||||
|
|
||||||
if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) {
|
if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) {
|
||||||
// TODO is this special behavior needed?
|
|
||||||
PlayerListPacket.Entry updatedEntry = buildEntryManually(
|
PlayerListPacket.Entry updatedEntry = buildEntryManually(
|
||||||
session,
|
session,
|
||||||
entity.getUuid(),
|
entity.getUuid(),
|
||||||
@@ -158,17 +156,24 @@ public class SkinManager {
|
|||||||
packet.setUuid(entity.getUuid());
|
packet.setUuid(entity.getUuid());
|
||||||
packet.setOldSkinName("");
|
packet.setOldSkinName("");
|
||||||
packet.setNewSkinName(skin.textureUrl());
|
packet.setNewSkinName(skin.textureUrl());
|
||||||
packet.setSkin(getSkin(skin.textureUrl(), skin, cape, geometry));
|
packet.setSkin(getSkin(session, skin.textureUrl(), skin, cape, geometry));
|
||||||
packet.setTrustedSkin(true);
|
packet.setTrustedSkin(true);
|
||||||
session.sendUpstreamPacket(packet);
|
session.sendUpstreamPacket(packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SerializedSkin getSkin(String skinId, Skin skin, Cape cape, SkinGeometry geometry) {
|
private static SerializedSkin getSkin(GeyserSession session, String skinId, Skin skin, Cape cape, SkinGeometry geometry) {
|
||||||
return SerializedSkin.of(skinId, "", geometry.geometryName(),
|
return SerializedSkin.builder()
|
||||||
ImageData.of(skin.skinData()), Collections.emptyList(),
|
.skinId(skinId)
|
||||||
ImageData.of(cape.capeData()), geometry.geometryData(),
|
.skinResourcePatch(geometry.geometryName())
|
||||||
"", true, false, false, cape.capeId(), skinId);
|
.skinData(ImageData.of(skin.skinData()))
|
||||||
|
.capeData(ImageData.of(cape.capeData()))
|
||||||
|
.geometryData(geometry.geometryData())
|
||||||
|
.premium(true)
|
||||||
|
.capeId(cape.capeId())
|
||||||
|
.fullSkinId(skinId)
|
||||||
|
.geometryDataEngineVersion(session.getClientData().getGameVersion())
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session,
|
public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session,
|
||||||
@@ -334,4 +339,4 @@ public class SkinManager {
|
|||||||
|
|
||||||
private static final String DEFAULT_FLOODGATE_STEVE = "https://textures.minecraft.net/texture/31f477eb1a7beee631c2ca64d06f8f68fa93a3386d04452ab27f43acdf1b60cb";
|
private static final String DEFAULT_FLOODGATE_STEVE = "https://textures.minecraft.net/texture/31f477eb1a7beee631c2ca64d06f8f68fa93a3386d04452ab27f43acdf1b60cb";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,20 +35,24 @@ 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.function.BiConsumer;
|
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 {
|
||||||
|
|
||||||
public static SerializedSkin buildSkullEntryManually(String skinId, byte[] skinData) {
|
public static SerializedSkin buildSkullEntryManually(GeyserSession session, String skinId, byte[] skinData) {
|
||||||
// Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png
|
|
||||||
skinId = skinId + "_skull";
|
skinId = skinId + "_skull";
|
||||||
return SerializedSkin.of(
|
return SerializedSkin.builder()
|
||||||
skinId, "", SkinProvider.SKULL_GEOMETRY.geometryName(), ImageData.of(skinData), Collections.emptyList(),
|
.skinId(skinId)
|
||||||
ImageData.of(SkinProvider.EMPTY_CAPE.capeData()), SkinProvider.SKULL_GEOMETRY.geometryData(),
|
.skinResourcePatch(SkinProvider.SKULL_GEOMETRY.geometryName())
|
||||||
"", true, false, false, SkinProvider.EMPTY_CAPE.capeId(), skinId
|
.skinData(ImageData.of(skinData))
|
||||||
);
|
.capeData(ImageData.of(SkinProvider.EMPTY_CAPE.capeData()))
|
||||||
|
.geometryData(SkinProvider.SKULL_GEOMETRY.geometryData())
|
||||||
|
.premium(true)
|
||||||
|
.capeId(SkinProvider.EMPTY_CAPE.capeId())
|
||||||
|
.fullSkinId(skinId)
|
||||||
|
.geometryDataEngineVersion(session.getClientData().getGameVersion())
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void requestAndHandleSkin(SkullPlayerEntity entity, GeyserSession session,
|
public static void requestAndHandleSkin(SkullPlayerEntity entity, GeyserSession session,
|
||||||
@@ -59,7 +63,7 @@ public class SkullSkinManager extends SkinManager {
|
|||||||
packet.setUuid(entity.getUuid());
|
packet.setUuid(entity.getUuid());
|
||||||
packet.setOldSkinName("");
|
packet.setOldSkinName("");
|
||||||
packet.setNewSkinName(skin.textureUrl());
|
packet.setNewSkinName(skin.textureUrl());
|
||||||
packet.setSkin(buildSkullEntryManually(skin.textureUrl(), skin.skinData()));
|
packet.setSkin(buildSkullEntryManually(session, skin.textureUrl(), skin.skinData()));
|
||||||
packet.setTrustedSkin(true);
|
packet.setTrustedSkin(true);
|
||||||
session.sendUpstreamPacket(packet);
|
session.sendUpstreamPacket(packet);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ import org.geysermc.geyser.util.InventoryUtils;
|
|||||||
import org.geysermc.geyser.util.ItemUtils;
|
import org.geysermc.geyser.util.ItemUtils;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType;
|
import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType;
|
||||||
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.DataComponentTypes;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.EmptySlotDisplay;
|
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.EmptySlotDisplay;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.SlotDisplay;
|
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.SlotDisplay;
|
||||||
|
|
||||||
@@ -258,14 +257,14 @@ public abstract class InventoryTranslator {
|
|||||||
|
|
||||||
if (this instanceof PlayerInventoryTranslator) {
|
if (this instanceof PlayerInventoryTranslator) {
|
||||||
if (destSlot == 5) {
|
if (destSlot == 5) {
|
||||||
//only set the head if the destination is the head slot
|
// only set the head if the destination is the head slot
|
||||||
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));
|
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
|
||||||
FakeHeadProvider.restoreOriginalSkin(session, session.getPlayerEntity());
|
FakeHeadProvider.restoreOriginalSkin(session, session.getPlayerEntity());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,9 +279,22 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
|
|||||||
return bundleResponse;
|
return bundleResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int sourceSlot = bedrockSlotToJava(transferAction.getSource());
|
||||||
|
int destSlot = bedrockSlotToJava(transferAction.getDestination());
|
||||||
|
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.hasNonBaseComponents()) {
|
||||||
|
FakeHeadProvider.setHead(session, session.getPlayerEntity(), javaItem.getComponent(DataComponentTypes.PROFILE));
|
||||||
|
}
|
||||||
|
} else if (sourceSlot == 5) {
|
||||||
|
// we are probably removing the head, so restore the original skin
|
||||||
|
FakeHeadProvider.restoreOriginalSkin(session, session.getPlayerEntity());
|
||||||
|
}
|
||||||
|
|
||||||
int transferAmount = transferAction.getCount();
|
int transferAmount = transferAction.getCount();
|
||||||
if (isCursor(transferAction.getDestination())) {
|
if (isCursor(transferAction.getDestination())) {
|
||||||
int sourceSlot = bedrockSlotToJava(transferAction.getSource());
|
|
||||||
GeyserItemStack sourceItem = inventory.getItem(sourceSlot);
|
GeyserItemStack sourceItem = inventory.getItem(sourceSlot);
|
||||||
if (playerInv.getCursor().isEmpty()) {
|
if (playerInv.getCursor().isEmpty()) {
|
||||||
playerInv.setCursor(sourceItem.copy(0), session);
|
playerInv.setCursor(sourceItem.copy(0), session);
|
||||||
@@ -294,7 +307,6 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
|
|||||||
|
|
||||||
affectedSlots.add(sourceSlot);
|
affectedSlots.add(sourceSlot);
|
||||||
} else if (isCursor(transferAction.getSource())) {
|
} else if (isCursor(transferAction.getSource())) {
|
||||||
int destSlot = bedrockSlotToJava(transferAction.getDestination());
|
|
||||||
GeyserItemStack sourceItem = playerInv.getCursor();
|
GeyserItemStack sourceItem = playerInv.getCursor();
|
||||||
if (inventory.getItem(destSlot).isEmpty()) {
|
if (inventory.getItem(destSlot).isEmpty()) {
|
||||||
inventory.setItem(destSlot, sourceItem.copy(0), session);
|
inventory.setItem(destSlot, sourceItem.copy(0), session);
|
||||||
@@ -307,8 +319,6 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
|
|||||||
|
|
||||||
affectedSlots.add(destSlot);
|
affectedSlots.add(destSlot);
|
||||||
} else {
|
} else {
|
||||||
int sourceSlot = bedrockSlotToJava(transferAction.getSource());
|
|
||||||
int destSlot = bedrockSlotToJava(transferAction.getDestination());
|
|
||||||
GeyserItemStack sourceItem = inventory.getItem(sourceSlot);
|
GeyserItemStack sourceItem = inventory.getItem(sourceSlot);
|
||||||
if (inventory.getItem(destSlot).isEmpty()) {
|
if (inventory.getItem(destSlot).isEmpty()) {
|
||||||
inventory.setItem(destSlot, sourceItem.copy(0), session);
|
inventory.setItem(destSlot, sourceItem.copy(0), session);
|
||||||
|
|||||||
Reference in New Issue
Block a user