18.10.2018

Minecraft 1.7.10: Ньюансы синхронизации контейнеров

Клиентский и серверный Container синхронизируются довольно простым способом. Container#detectAndSendChanges() запоминает содержимое каждого слота, который отсылается на клиент, а затем сравнивает их с действительным содержимым инвентаря. Если оно отличается - на клиент отсылается S2FPacketSetSlot

/**
 * Looks for changes made in the container, sends them to every listener.
 */
public void detectAndSendChanges()
{
    for (int i = 0; i < this.inventorySlots.size(); ++i)
    {
        ItemStack itemstack = ((Slot)this.inventorySlots.get(i)).getStack();
        ItemStack itemstack1 = (ItemStack)this.inventoryItemStacks.get(i);

        if (!ItemStack.areItemStacksEqual(itemstack1, itemstack))
        {
            itemstack1 = itemstack == null ? null : itemstack.copy();
            this.inventoryItemStacks.set(i, itemstack1);

            for (int j = 0; j < this.crafters.size(); ++j)
            {
                ((ICrafting)this.crafters.get(j)).sendSlotContents(this, i, itemstack1);
            }
        }
    }
}

В целом все довольно понятно. Но оссобенно инетересен вызов ((ICrafting)this.crafters.get(j)).sendSlotContents(this, i, itemstack1);. Этот метод переслает данные слота на свой клиентский объект, чтобы успешно его отобразить. Чаще всего интерфейс ICrafting реализует EntityPlayerMP, т.к. основной "потребитель" gui - это игрок:

/**
 * Sends the contents of an inventory slot to the client-side Container. This doesn't have to match the actual
 * contents of that slot. Args: Container, slot number, slot contents
 */
public void sendSlotContents(Container p_71111_1_, int p_71111_2_, ItemStack p_71111_3_)
{
    if (!(p_71111_1_.getSlot(p_71111_2_) instanceof SlotCrafting))
    {
        if (!this.isChangingQuantityOnly)
        {
            this.playerNetServerHandler.sendPacket(new S2FPacketSetSlot(p_71111_1_.windowId, p_71111_2_, p_71111_3_));
        }
    }
}

Как видите, на клиент не будет выслан пакет, если EntityPlayerMP#isChangingQuantityOnly = true. Но что это означает? Чтобы это выяснить, обратимся к методу Contaiiner#slotClick(...). Он вызывается сначала на клиенте, а потом на сервере когда тот получит C0EPacketClickWindow. Когда писался функционал метода Contaiiner#slotClick(...), ребята из Mojang думали, что результат клика по слотам может быть одинакого вычислен и на клиенте, и на сервере. Поэтому когда запускается Container#detectAndSendChanges() с EntityPlayerMP#isChangingQuantityOnly = true, на клиенте уже был отображен верный результат, который получится из Contaiiner#slotClick(...). И слать пакет на клиенте было совершенно не нужно.

Так что если вы делаете какую-нибудь сложную механику для вашего контейнера, которую не реализует ванильный код (скажем, клик по мечу чинит его, удаляя 1 алмаз), то EntityPlayerMP#isChangingQuantityOnly может очень сильно попортить вам жизнь. Как вариант решения этой проблемы, вы можете оверрайдить Container#detectAndSendChanges() вашего контейнера и выставить всем игрокам EntityPlayerMP#isChangingQuantityOnly = false:

@Override
public void detectAndSendChanges() {
    for (int j = 0; j < crafters.size(); ++j)
    {
        ICrafting crafter = (ICrafting) crafters.get(j);
        if (crafter instanceof EntityPlayerMP) {
            ((EntityPlayerMP) crafter).isChangingQuantityOnly = false;
        }
    }
    super.detectAndSendChanges(); // Не боясь запускаем ванильную синхронизацию
}

Комментариев нет:

Отправить комментарий