Не могу пофиксить баг с инвентарем

Версия Minecraft
1.7.10
1,159
38
544
Здравствуйте, почти неделю убил на устранение ошибки, но все безрезультатно

У меня есть два инвентаря - StatsInventory и SkillsInventory. Если навести мышку на какой-либо слот в StatsInventory, то SkillsInventory заполнится только теми блоками, которые подходят для выбранного в StatsInventory. Т.е. сделано что-то вроде вкладок.

Вот так это выглядит:

Баг заключается в расчете параметра "Защита", который расчитывается на основе меты предмета FightSkillItem (навык драки). Т.е., когда GUI открывается, мод начинает искать в инвентаре предмет FightSkillItem и применяет алгоритм расчета (не думаю, что алгоритм имеет значение). Если предмет не найдет - тупо пишет в стойкости 2. В штатных ситуациях все работает хорошо и расчет происходит корректно...

Проблемы начинаются тогда, когда пользователь заходит в игру и открывает GUI. И тут мы видим, что Стойкость (зависит от выносливости) рассчиталась верно, а Защита - не расчиталась вообще (тупо написана двойка, мол мета FightSkillItem = 0). Но как мы помним, на момент выхода пользователя из игры его FightSkillItem был != 0. И в тот момент, когда мы наведем мышку на второй слот в StatsInventory, в SkillsInventory появится FightSkillItem (иконка кулака) с той метой, которая была у пользователя на момент выхода. И в тот же момент Защита расчитается верно.

Поначалу я думал, что это проблема синхронизации инвентаря на сервере и на клиенте. Опытным путем установил, что это не так - проблема в стартовой инициализации ExtendedPlayer (по крайней мере, мне так кажется)

код
ExtendedPlayer - мой IEEP:
Java:
package rsstats.data;

import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.world.World;
import net.minecraftforge.common.IExtendedEntityProperties;
import org.apache.commons.logging.Log;
import rsstats.common.RSStats;
import rsstats.inventory.SkillsInventory;
import rsstats.inventory.StatsInventory;
import rsstats.inventory.WearableInventory;
import rsstats.items.SkillItem;
import rsstats.items.StatItem;

import java.util.logging.Logger;

/**
 *
 * @author rares
 */
public class ExtendedPlayer implements IExtendedEntityProperties {
    /** Каждый наследник {@link IExtendedEntityProperties} должен иметь индивидуальное имя */
    private static final String EXTENDED_ENTITY_TAG = RSStats.MODID;
    
    private final EntityPlayer entityPlayer;

    /** Основной параметр игрока - Шаг */
    private int step = 6;
    /** Основной параметр игрока - Защита */
    private int protection;
    /** Основной параметр игрока - Стойкость */
    private int persistence;
    /** Основной параметр игрока - Харизма */
    private int charisma = 0;

    private int exp = 0;
    private int lvl = 0;
    private int tiredness = 0;
    private int tirednessLimit = 25;
    
    /** Инвентарь для статов */
    public final StatsInventory statsInventory;
    /** Инвентарь для скиллов */
    public final SkillsInventory skillsInventory;
    /** Инвентарь для носимых предметов */
    public final WearableInventory wearableInventory;
    
    /*
    Тут в виде полей можно хранить дополнительную информацию о Entity: мана,
    золото, хп, переносимый вес, уровень радиации, репутацию и т.д. Т.е. все то,
    что нельзя хранить в виде блоков
    */

    private ExtendedPlayer(EntityPlayer player) {
        this.entityPlayer = player;
        statsInventory = new StatsInventory(player);
        skillsInventory = new SkillsInventory(player);
        wearableInventory = new WearableInventory();
    }
    
    /**
     * Used to register these extended properties for the entityPlayer during EntityConstructing event
     * This method is for convenience only; it will make your code look nicer
     * @param player
     */
    public static final void register(EntityPlayer player) {
        player.registerExtendedProperties(ExtendedPlayer.EXTENDED_ENTITY_TAG, new ExtendedPlayer(player));
    }
    
    /**
     * Returns ExtendedPlayer properties for entityPlayer
     * This method is for convenience only; it will make your code look nicer
     */
    public static final ExtendedPlayer get(EntityPlayer player) {
       return (ExtendedPlayer) player.getExtendedProperties(EXTENDED_ENTITY_TAG);
    }

    public boolean isServerSide() {
        return this.entityPlayer instanceof EntityPlayerMP;
    }

    @Override
    public void saveNBTData(NBTTagCompound properties) {
        properties.setInteger("exp", exp);
        properties.setInteger("lvl", lvl);
        properties.setInteger("tiredness", tiredness);
        properties.setInteger("tirednessLimit", tirednessLimit);

        this.statsInventory.writeToNBT(properties);
        this.skillsInventory.writeToNBT(properties);
        this.wearableInventory.writeToNBT(properties);

        System.out.println("LOG: saveNBTData(), NBTTagCompound" + properties.toString());
    }

    // TODO: Почему-то когда открывается GUI - Отображается категорий скиллов ловкости
    @Override
    public void loadNBTData(NBTTagCompound properties) {
        this.statsInventory.totalClear();
        this.skillsInventory.totalClear();

        exp = properties.getInteger("exp");
        lvl = properties.getInteger("lvl");
        tiredness = properties.getInteger("tiredness");
        tirednessLimit = properties.getInteger("tirednessLimit");

        System.out.println("LOG: loadNBTData(), NBTTagCompound" + properties.toString());

        this.statsInventory.readFromNBT(properties);
        this.skillsInventory.readFromNBT(properties);
        this.wearableInventory.readFromNBT(properties);
    }

    /**
     * Used to initialize the extended properties with the entity that this is attached to, as well
     * as the world object.
     * Called automatically if you register with the EntityConstructing event.
     * May be called multiple times if the extended properties is moved over to a new entity.
     *  Such as when a player switches dimension {Minecraft re-creates the player entity}
     * @param entity  The entity that this extended properties is attached to
     * @param world  The world in which the entity exists
     */
    @Override
    public void init(Entity entity, World world) {
        /* Крайне интересный хак. Дело в том, что init() используется для инициализации самого ExtendedPlayer'а,
         * а не Compound'а, который передается в loadNBTData(). TODO: Я все еще не разобрался как связана сущность, создаваемая тут и compound в loadNBTData
         * Я проивожу "инициализацию нового игрока" тут, т.к. если игрок зашел в игру первый раз - loadNBTData никогда не
         * вызовется при логине. Подозреваю это из-за того, что на сервере нет NBT записи об этом игроке.
         * Зато когда она заходит во второй раз - loadNBTData() точно вызовется, но т.к. перед ним вызовется и init(), то
         * в loadNBTData() нужно предварительно очистить инициализацию нового игрока, которая выполнилась тут.
         */
        ExtendedPlayer.get((EntityPlayer) entity).statsInventory.initItems();
        ExtendedPlayer.get((EntityPlayer) entity).skillsInventory.initItems();

        /*if (entity.toString() != null) {
            System.out.print("LOG: init(), Entity" + entity.toString());
        } else {*/
            System.out.println("LOG: init()");
        //}

        // Инициализируем основные параметры
        //loadNBTData(entity.getEntityData());
        try { // КОСТЫЛЬ
            ItemStack itemStack = skillsInventory.getSkill("item.FightingSkillItem");
            if (itemStack.getItem().getDamage(itemStack) == 0) {
                this.protection = 2;
            } else {
                this.protection = 2 + ((SkillItem) itemStack.getItem()).getRollLevel(itemStack) / 2;
            }
            itemStack = statsInventory.getStat("item.EnduranceStatItem");
            this.persistence = 2 + ((StatItem) itemStack.getItem()).getRollLevel(itemStack) / 2;
        } catch (Exception e) {}
    }

    public int getProtection() {
        return protection;
    }

    public int getStep() {
        return step;
    }

    public int getPersistence() {
        return persistence;
    }

    public int getCharisma() {
        return charisma;
    }

    public int getExp() {
        return exp;
    }

    public int getLvl() {
        return lvl;
    }

    public int getTiredness() {
        return tiredness;
    }

    public int getTirednessLimit() {
        return tirednessLimit;
    }

    public void setLvl(int lvl) {
        this.lvl = lvl;
    }

    public void updateParams() {
        try { // КОСТЫЛЬ
            ItemStack itemStack = skillsInventory.getSkill("item.FightingSkillItem");
            if (itemStack.getItem().getDamage(itemStack) == 0) {
                this.protection = 2;
            } else {
                this.protection = 2 + ((SkillItem) itemStack.getItem()).getRollLevel(itemStack) / 2;
            }
        } catch (Exception e) {}
        try {
            ItemStack itemStack = statsInventory.getStat("item.EnduranceStatItem");
            this.persistence = 2 + ((StatItem) itemStack.getItem()).getRollLevel(itemStack) / 2;
        } catch (Exception e) {}
    }
}

StatInventory:
Java:
package rsstats.inventory;

import cpw.mods.fml.common.registry.GameRegistry;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.IInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.StatCollector;
import net.minecraftforge.common.util.Constants;
import rsstats.common.CommonProxy;
import rsstats.common.RSStats;
import rsstats.items.SkillItem;
import rsstats.items.StatItem;

/**
 * Инвентарь для статов игрока (сила, ловкость, выносливость и т.д.)
 * @author RareScrap
 */
public class StatsInventory implements IInventory {
    /** The key used to store and retrieve the inventory from NBT */
    private static final String NBT_TAG = "stats";

    /** Define the inventory size here for easy reference */
    /* This is also the place to define which slot is which if you have different types,
     * for example SLOT_SHIELD = 0, SLOT_AMULET = 1; */
    private static final int INV_SIZE = 9;
    /** Масимальный размер стака для предметов в инвенторе {@link #inventory} */
    private static final int STACK_LIMIT = 1;

    /** Структура, хранящая предметы инвентаря в стаках.
     * Inventory's size must be same as number of slots you add to the Container class. */
    private ItemStack[] inventory = new ItemStack[INV_SIZE];
    /** Игрок, к которому привязан инвентарь */
    private EntityPlayer entityPlayer;

    /**
     * Необходимый публичный контсруктор
     */
    public StatsInventory(EntityPlayer entityPlayer) {
        this.entityPlayer = entityPlayer;
    }
    
    /**
     * Геттер для {@link #inventory}
     * @return Размер массива {@link #inventory}
     */
    @Override
    public int getSizeInventory() {
        return inventory.length;
    }

    /**
     * Геттер для получения элементов из инвентаря {@link #inventory}
     * @param slotIndex Индекс слота в инвенторе, из которого нужно получить предмет
     * @return Стак предметов под индексом slotIndex в инвенторе
     */
    @Override
    public ItemStack getStackInSlot(int slotIndex) {
        return inventory[slotIndex];
    }

    /**
     * Удаляет предмет из слота инвентаря до определенного количества элементов и возвращает их в новый стак.
     * @param slotIndex Слот в инвенторе, где лежит предмет, стак которого нужно уменьшить
     * @param amount До скольки нужно уменьший стак
     * @return Предмет с уменьшенным стаком
     */
    @Override
    public ItemStack decrStackSize(int slotIndex, int amount) {
        ItemStack stack = getStackInSlot(slotIndex);
        if (stack != null) {
            if (stack.stackSize > amount) {
                stack = stack.splitStack(amount);
                markDirty(); // Аналог onInventoryChanged()
            } else {
                setInventorySlotContents(slotIndex, null);
            }
        }
        return stack;
    }

    /**
     * Clears a slot and returns it's previous content. Аналог removeStackFromSlot() в более новых версиях.
     * @param slotIndex
     * @return
     */
    @Override
    public ItemStack getStackInSlotOnClosing(int slotIndex) {
        ItemStack stack = getStackInSlot(slotIndex);
        setInventorySlotContents(slotIndex, null);
        return stack;
    }

    @Override
    public void setInventorySlotContents(int slotIndex, ItemStack itemStack) {
        this.inventory[slotIndex] = itemStack;
        if (itemStack != null && itemStack.stackSize > this.getInventoryStackLimit()) {
            itemStack.stackSize = this.getInventoryStackLimit();
        }
        markDirty();
    }

    /**
     * Возвращает локализованное имя инвентаря
     * @return Имя инвентаря
     */
    @Override
    public String getInventoryName() {
        return StatCollector.translateToLocal("inventory." + NBT_TAG);
    }
    
    /*
    // Было в туториале но зачем - хз
    @Override
    public boolean isInvNameLocalized() {
        return name.length() > 0;
    }*/

    @Override
    public boolean hasCustomInventoryName() {
        return false;
    }

    /**
     * Возвращает масимальный размер стака для предметов в этом инвенторе
     * @return {@link #STACK_LIMIT}
     */
    @Override
    public int getInventoryStackLimit() {
        return STACK_LIMIT;
    }

    @Override
    public void markDirty() {
        // ТОDO: ХЗ что это мы делаем. Удаляем мусор? Гарантированно очищаем слоты?
        for (int i = 0; i < getSizeInventory(); ++i) {
            if (getStackInSlot(i) != null && getStackInSlot(i).stackSize == 0) {
                inventory[i] = null;
            }
        }

        // TODO: Проверить при помощи NBTEdit как мод ведет себя без этой строки
        writeToNBT(entityPlayer.getEntityData());
    }

    /**
     * Returns true if the given player has access to the inventory. The default implementation just checks
     * the player's distance to the TileEntity and returns true if it is less than 8 blocks. The method uses
     * the player's method getDistanceSq which returns the squared distance to the given point. If this is
     * less than 64, the real distance is less than 8.
     *
     * Инвентарь может использоваться игроком?
     * @param entityPlayer Сущность игрока, взаимодействующая с инвентарем
     * @return false
     */
    @Override
    public boolean isUseableByPlayer(EntityPlayer entityPlayer) {
        // TODO: Без понятия как это работает и зачем нужно
        return entityPlayer.capabilities.isCreativeMode;
        // return true;
    }   

    @Override
    public void openInventory() {}

    @Override
    public void closeInventory() {}

    /**
     * Проверяет, можно ли поместить предмет в данный слот инвентаря {@link #inventory}
     *
     * This method doesn't seem to do what it claims to do, as
     * items can still be left-clicked and placed in the inventory
     * even when this returns false
     * @param slotIndex TODO
     * @param itemStack Предмет, который хочет поместиться в инвентарь
     * @return Итог проверки: возвращает true, если предмет можно поместить в инвентарь.
     * Иначе - false.
     */
    @Override
    public boolean isItemValidForSlot(int slotIndex, ItemStack itemStack) {
        // If you have different kinds of slots, then check them here:
        // if (slot == SLOT_SHIELD && itemstack.getItem() instanceof ItemShield) return true;

        // TODO: Хочу использовать уже реализованную проверку в StatSlot, но не нзнаю как
        return itemStack.getItem() instanceof StatItem && !(itemStack.getItem() instanceof SkillItem);
    }
    
    /**
     * Записывает состояние инвентаря в NBT
     * @param compound TODO
     */
    public void writeToNBT(NBTTagCompound compound) {
        NBTTagList items = new NBTTagList();

        for (int i = 0; i < getSizeInventory(); ++i) {
            if (getStackInSlot(i) != null) {
                NBTTagCompound item = new NBTTagCompound();
                item.setByte("Slot", (byte) i);
                getStackInSlot(i).writeToNBT(item);
                items.appendTag(item);
            }
        }

        // We're storing our items in a custom tag list using our 'NBT_TAG' from above
        // to prevent potential conflicts
        compound.setTag(NBT_TAG, items);
    }

    /**
     * Читает данные из NBT, восстанавливая состояние инвентаря
     * @param compound TODO
     */
    public void readFromNBT(NBTTagCompound compound) {
        NBTTagList items = compound.getTagList(NBT_TAG, Constants.NBT.TAG_COMPOUND);

        /* Если инвентарь статов пустой или не содержвится в пришедшем compound'е (а он скорее всего содержится, см init())
         * - добавляем стандартный набор статов */
        if (items.tagCount() == 0) {
            initItems();
            return;
        }

        // Штатное чтение из NBT
        for (int i = 0; i < items.tagCount(); ++i) {
            NBTTagCompound item = items.getCompoundTagAt(i);
            byte slot = item.getByte("Slot");
            if (slot >= 0 && slot < getSizeInventory()) {
                inventory[slot] = ItemStack.loadItemStackFromNBT(item);
            }
        }
    }

    /**
     * Инициализирует начальные статы
     */
    public void initItems() {
        for (int i = 0; i < CommonProxy.Stats.values().length; i++) {
            inventory[i] = new ItemStack(GameRegistry.findItem(RSStats.MODID, CommonProxy.Stats.values()[i].toString()));
        }

        markDirty();
    }

    /**
     * Получаем стак из {@link #inventory} по указанному UnlocalizedName
     * @param unlocalizedSkillName UnlocalizedName нужного стака скилла
     * @return Стак {@link StatItem}'а
     */
    public ItemStack getStat(String unlocalizedSkillName) {
        for (ItemStack stat : inventory) {
            if (stat.getUnlocalizedName().equals(unlocalizedSkillName)) {
                return stat;
            }
        }
        return null;
    }

    /**
     * Очищает все имеющиеся хранилища {@link ItemStack}'ов
     */
    public void totalClear() {
        for (int i = 0; i < getSizeInventory(); ++i) {
            inventory[i] = null;
        }
    }
}

SkillsInventory:
Java:
package rsstats.inventory;

import cpw.mods.fml.common.registry.GameRegistry;
import net.minecraft.client.Minecraft;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.StatCollector;
import net.minecraftforge.common.util.Constants;
import rsstats.client.gui.MainMenuGUI;
import rsstats.common.CommonProxy;
import rsstats.common.RSStats;
import rsstats.common.network.PacketUpdateParams;
import rsstats.items.SkillItem;

import java.util.ArrayList;

public class SkillsInventory extends StatsInventory {
    /** The key used to store and retrieve the inventory from NBT */
    private static final String NBT_TAG = "skills";

    /** Define the inventory size here for easy reference */
    /* This is also the place to define which slot is which if you have different types,
     * for example SLOT_SHIELD = 0, SLOT_AMULET = 1; */
    private static final int INV_SIZE = 27;

    /** Структура, хранящая только те предметы, которые будут отображены в инвентаре пользователя.
     * Inventory's size must be same as number of slots you add to the Container class. */
    private ItemStack[] inventory = new ItemStack[INV_SIZE];

    /** Структура, хранящая все предметы инвентаря в стаках */
    private ArrayList<ItemStack> skills = new ArrayList<ItemStack>();

    private EntityPlayer entityPlayer;

    /**
     * Необходимый публичный контсруктор
     */
    public SkillsInventory(EntityPlayer entityPlayer) {
        super(entityPlayer);
        this.entityPlayer = entityPlayer;
    }

    /**
     * Геттер для размера {@link #inventory}
     * @return Размер массива {@link #inventory}
     */
    @Override
    public int getSizeInventory() {
        return inventory.length;
    }

    /**
     * Геттер для получения элементов из инвентаря {@link #inventory}
     * @param slotIndex Индекс слота в инвенторе, из которого нужно получить предмет
     * @return Стак предметов под индексом slotIndex в инвенторе
     */
    @Override
    public ItemStack getStackInSlot(int slotIndex) {
        return inventory[slotIndex];
    }

    @Override
    public void setInventorySlotContents(int slotIndex, ItemStack itemStack) {
        //super.setInventorySlotContents(slotIndex, itemStack);

        if (itemStack == null) { // Очистить слот
            if (inventory[slotIndex] != null) { // Был ли слот очищенным до этого?
                // Если нет - удаляем то, что есть сейчас в слоте из хранилища скиллов
                removeSkill(inventory[slotIndex].getUnlocalizedName());
            }

            // Обновляем сам слот
            this.inventory[slotIndex] = itemStack;
        } else { // Добавить новый стак в слот
            if (/*inventory[slotIndex]  *!* == null &&*/ containSkill(itemStack.getUnlocalizedName())) {
                removeSkill(itemStack.getUnlocalizedName());
            } // TODO: !!!!!!!!!!!!!!!!!!!!!!!!
            this.inventory[slotIndex] = itemStack;
            skills.add(itemStack);
        }

        //CommonProxy.INSTANCE.sendTo(new PacketUpdateParams(skills), (EntityPlayerMP) entityPlayer);

        if (itemStack != null && itemStack.stackSize > this.getInventoryStackLimit()) {
            itemStack.stackSize = this.getInventoryStackLimit();
        }
        // TODO: ВАЖНО this.onInventoryChanged();
    }

    /**
     * Проверяет, можно ли поместить предмет в данный слот инвентаря {@link #inventory}
     * <p>
     * This method doesn't seem to do what it claims to do, as
     * items can still be left-clicked and placed in the inventory
     * even when this returns false
     *
     * @param slotIndex TODO
     * @param itemStack Предмет, который хочет поместиться в инвентарь
     * @return Итог проверки: возвращает true, если предмет можно поместить в инвентарь.
     * Иначе - false.
     */
    @Override
    public boolean isItemValidForSlot(int slotIndex, ItemStack itemStack) {
        return itemStack.getItem() instanceof SkillItem;
    }

    /**
     * Возвращает локализованное имя инвентаря
     * @return Имя инвентаря
     */
    @Override
    public String getInventoryName() {
        return StatCollector.translateToLocal("inventory." + NBT_TAG);
    }

    /**
     * Записывает состояние инвентаря в NBT
     * @param compound TODO
     */
    @Override
    public void writeToNBT(NBTTagCompound compound) {
        NBTTagList items = new NBTTagList();

        for (ItemStack skill : skills) {
            if (skill != null) {
                NBTTagCompound item = new NBTTagCompound();
                //item.setByte("Slot", (byte) i);
                skill.writeToNBT(item);
                items.appendTag(item);
            }
        }

        // We're storing our items in a custom tag list using our 'NBT_TAG' from above
        // to prevent potential conflicts
        compound.setTag(NBT_TAG, items);
    }

    /**
     * Читает данные из NBT, восстанавливая состояние инвентаря
     *
     * @param compound TODO
     */
    @Override
    public void readFromNBT(NBTTagCompound compound) {
        NBTTagList items = compound.getTagList(NBT_TAG, Constants.NBT.TAG_COMPOUND);

        /* Сравнивать compound'ы через строки понадобвится того, когда потребуется чтобы статы не
         * добавлялись когда их нет, но в compound есть тег инвентаря статов */
        //String a = compound.toString();
        //String b = a.replaceFirst(NBT_TAG, "");

        if (items.tagCount() == 0) {
            initItems();
            return;
        }

        // Штатное чтение из NBT
        byte slot = 0;
        String asd = ((SkillItem) ItemStack.loadItemStackFromNBT(items.getCompoundTagAt(0)).getItem()).parentStat.getUnlocalizedName();
        for (int i = 0; i < items.tagCount(); ++i) {
            NBTTagCompound NBTItem = items.getCompoundTagAt(i);
            //byte slot = NBTItem.getByte("Slot");

            SkillItem item = (SkillItem) ItemStack.loadItemStackFromNBT(NBTItem).getItem();

            if (!containSkill(item.getUnlocalizedName()))
                skills.add(ItemStack.loadItemStackFromNBT(NBTItem));

            if (slot >= 0 && slot < getSizeInventory() && item.parentStat.getUnlocalizedName().equals(asd)) {
                ItemStack itemstack = skills.get(skills.size()-1);
                inventory[slot++] = itemstack;
            }
        }
    }

    /**
     * Инициализирует начальные скиллы
     */
    @Override
    public void initItems() {
        // Заполняем общее хранилище
        for (CommonProxy.Skills skill : CommonProxy.Skills.values()) {
            skills.add(new ItemStack(GameRegistry.findItem(RSStats.MODID, skill.toString())));
        }

        // Заполняем хранилище отображения
        for (int i = 0, slot = 0; i < skills.size() && slot < inventory.length; i++) {
            SkillItem item = (SkillItem) skills.get(i).getItem();
            if (item.parentStat.getUnlocalizedName().equals("item.StrengthStatItem")) {
                inventory[slot++] = skills.get(i);
            }
        }

        markDirty();
    }

    /**
     * Очищает {@link #inventory}, выставляя все его элементы null
     */
    private void clearInventory() {
        for (int i = 0; i < getSizeInventory(); i++) {
            inventory[i] = null;
        }

    }

    /**
     * Проверяет, содержится ли в {@link #skills} стак с предметом по имение skillName
     * @param skillName UnlocalizedName поискового скилла
     * @return True, если элемент есть в {@link #skills}, иначе - false.
     */
    private boolean containSkill(String skillName) {
        for (ItemStack skill : skills) {
            if (skill.getUnlocalizedName().equals(skillName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Находит и удаляет стак из {@link #skills}
     * @param unlocalizedSkillName UnlocalizedName, по которому будет произведен поиск.
     *                             Если элемент найтен - он удалится из {@link #skills}
     */
    private void removeSkill(String unlocalizedSkillName) {
        for (ItemStack skill : skills) {
            if (skill.getUnlocalizedName().equals(unlocalizedSkillName)) {
                skills.remove(skill);
                return;
            }
        }
    }

    /**
     * Получаем стак из {@link #skills} по указанному UnlocalizedName
     * @param unlocalizedSkillName UnlocalizedName нужного стака скилла
     * @return Стак {@link SkillItem}
     */
    public ItemStack getSkill(String unlocalizedSkillName) {
        for (ItemStack skill : skills) {
            if (skill.getUnlocalizedName().equals(unlocalizedSkillName)) {
                return skill;
            }
        }
        return null;
    }

    /**
     * Заполняет {@link #inventory} подходящими элементами из {@link #skills}
     * @param parentStatName {@link #inventory} будет заполнен только теми элентами, которые
     *                        имеют данный parentStat.UnlocalizedName
     */
    public void setSkillsFor(String parentStatName) {
        clearInventory();
        int slot = 0;
        for (ItemStack skill : skills) {
            SkillItem item = (SkillItem) skill.getItem();
            if (parentStatName.equals(item.parentStat.getUnlocalizedName()))
                this.inventory[slot++] = skill;
                //setInventorySlotContents(slot++, skill);
        }
    }

    /**
     * Очищает все имеющиеся хранилища {@link ItemStack}'ов
     */
    @Override
    public void totalClear() {
        // TODO: Почему нельзя вызвать супер?
        skills.clear();
        clearInventory();
    }

    public void setNewSkills(ArrayList<ItemStack> list) {
        this.skills = list;
        Minecraft.getMinecraft().currentScreen.updateScreen();
    }
}

Мой GUI:
Java:
package rsstats.client.gui;

import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.renderer.InventoryEffectRenderer;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.RenderHelper;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ResourceLocation;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import rsstats.common.CommonProxy;
import rsstats.common.RSStats;
import rsstats.common.network.PacketShowSkillsByStat;
import rsstats.data.ExtendedPlayer;
import rsstats.inventory.container.MainContainer;
import rsstats.items.SkillItem;

import java.util.Timer;
import java.util.TimerTask;

/**
 * GUI для основного окна мода, содержащее информацию о персонаже (имя, уровень, здоровье, защита, харизма,
 * стойкость), панель предметов и панели статов, навыков и перков.
 * @author RareScrap
 */
public class MainMenuGUI extends InventoryEffectRenderer {
    /** Расположение фона GUI */
    private static final ResourceLocation background =
            new ResourceLocation(RSStats.MODID,"textures/gui/StatsAndInvTab_FIT.png");

    public ExtendedPlayer player;
    /** UnlocalozedName текущей выбранно статы */
    private String currentStat = "";

    /** Инвентарь для статов */
    // Could use IInventory type to be more generic, but this way will save an import...
    // Нужно для запроса кастомного имени инвентаря для отрисоки названия инвентаря
    //private final StatsInventory statsInventory;

    private Timer timer;


    public MainMenuGUI(ExtendedPlayer player, MainContainer mainContainer) {
        super(mainContainer);
        this.allowUserInput = true;
        this.player = player;

        // Высталяем размеры контейнера. Соответствует размерам GUI на текстуре.
        this.xSize = 340;
        this.ySize = 211;
        // Выставляем края контейнера (верхний и левый)
        this.guiLeft = this.width/2 - xSize/2;
        this.guiTop = this.height/2 - ySize/2;
    }

    /**
     * Свой аналог {@link #drawTexturedModalRect(int, int, int, int, int, int)}, способный работать с текстурами
     * разрешением более чем 256x256.
     * @param x Координата начала отрисовки относительно левого-верхнего угла экрана игрока
     * @param y Координата начала отрисовки относительно левого-верхнего угла экрана игрока
     * @param u Координата начала текстуры по оси X относительно левого-верхнего угла текстуры
     * @param v Координата начала текстуры по оси Y относительно левого-верхнего угла текстуры
     * @param width Ширина текстуры, которую нужно отрисовать
     * @param height Высота текстуры, которую нужно отрисовать
     * @param textureWidth Общая ширина текстуры (кол-во пикселей в файле)
     * @param textureHeight Общая высота текстуры (кол-во пикселей в файле)
     */
    // Взято отсюда: http://www.minecraftforge.net/forum/topic/20177-172-gui-cant-more-than-256256/
    private void drawTexturedRect(int x, int y, int u, int v, int width, int height, int textureWidth, int textureHeight) {
        float f = 1F / (float)textureWidth;
        float f1 = 1F / (float)textureHeight;
        Tessellator tessellator = Tessellator.instance;
        tessellator.startDrawingQuads();
        tessellator.addVertexWithUV((double)(x), (double)(y + height), 0, (double)((float)(u) * f), (double)((float)(v + height) * f1));
        tessellator.addVertexWithUV((double)(x + width), (double)(y + height), 0, (double)((float)(u + width) * f), (double)((float)(v + height) * f1));
        tessellator.addVertexWithUV((double)(x + width), (double)(y), 0, (double)((float)(u + width) * f), (double)((float)(v) * f1));
        tessellator.addVertexWithUV((double)(x), (double)(y), 0, (double)((float)(u) * f), (double)((float)(v) * f1));
        tessellator.draw();
    }

    /**
     * Draw the background layer for the GuiContainer (everything behind the items)
     * @param partialTicks
     * @param mouseX
     * @param mouseY
     */
    @Override
    protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) {
        GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
        //GL11.glScalef(2.0F, 2.0F, 1.0F);
        this.mc.getTextureManager().bindTexture(background);

        // Отрисовываем текстуру GUI
        drawTexturedRect(this.guiLeft, this.guiTop, 0, 0, xSize, ySize, xSize, ySize);
        // Орисовываем превью игрока
        drawPlayerModel(this.guiLeft+30, this.guiTop+90, /*17*/ 40, (float)(this.guiLeft + 51) - mouseX, (float)(this.guiTop + 75 - 50) - mouseY, this.mc.thePlayer);

        // Это было в туторах, но я хз на что это влияет. Слоты и рендер предметов работают и без этого
        /*for (int i1 = 0; i1 < this.inventorySlots.inventorySlots.size(); ++i1)
        {
            Slot slot = (Slot)this.inventorySlots.inventorySlots.get(i1);
            //if (slot.getHasStack() && slot.getSlotStackLimit()==1)
            //{
                this.drawTexturedModalRect(k+slot.xDisplayPosition, l+slot.yDisplayPosition, 200, 0, 16, 16);
            //}
        }*/
    }

    /**
     * Draw the foreground layer for the GuiContainer (everything in front of the items)
     *
     * @param p_146979_1_
     * @param p_146979_2_
     */
    @Override
    protected void drawGuiContainerForegroundLayer(int p_146979_1_, int p_146979_2_) {
        int textY = 123;
        mc.fontRenderer.drawString("Шаг: " + player.getStep(), 8, textY, 0x444444, false);
        mc.fontRenderer.drawString("Уровень: " + player.getLvl(), 60, textY, 0x444444, false);
        mc.fontRenderer.drawString("Защита: " + player.getProtection(), 8, textY+=10, 0x444444, false);
        mc.fontRenderer.drawString("Очки опыта: " + player.getExp(), 60, textY, 0x444444, false);
        mc.fontRenderer.drawString("Стойкость: " + player.getPersistence(), 8, textY+=10, 0x444444, false);
        mc.fontRenderer.drawString("Усталость: " + player.getTiredness(), 60, textY, 0x444444, false);
        mc.fontRenderer.drawString("Харизма: " + player.getCharisma(), 8, textY+=10, 0x444444, false);

        super.drawGuiContainerForegroundLayer(p_146979_1_, p_146979_2_);
    }

    /**
     * Отрисовывает превью игрока
     * @param x TODO
     * @param y TODO
     * @param scale Маштаб модели
     * @param yaw TODO
     * @param pitch TODO
     * @param playerdrawn TODO
     */
    private static void drawPlayerModel(int x, int y, int scale, float yaw, float pitch, EntityLivingBase playerdrawn) {
        GL11.glEnable(GL11.GL_COLOR_MATERIAL);
        GL11.glPushMatrix();
        GL11.glTranslatef((float)x, (float)y, 50.0F);
        GL11.glScalef((float)(-scale), (float)scale, (float)scale);
        GL11.glRotatef(180.0F, 0.0F, 0.0F, 1.0F);
        float f2 = playerdrawn.renderYawOffset;
        float f3 = playerdrawn.rotationYaw;
        float f4 = playerdrawn.rotationPitch;
        float f5 = playerdrawn.prevRotationYawHead;
        float f6 = playerdrawn.rotationYawHead;
        GL11.glRotatef(135.0F, 0.0F, 1.0F, 0.0F);
        RenderHelper.enableStandardItemLighting();
        GL11.glRotatef(-135.0F, 0.0F, 1.0F, 0.0F);
        GL11.glRotatef(-((float)Math.atan((double)(pitch / 40.0F))) * 20.0F, 1.0F, 0.0F, 0.0F);
        playerdrawn.renderYawOffset = (float)Math.atan((double)(yaw / 40.0F)) * 20.0F;
        playerdrawn.rotationYaw = (float)Math.atan((double)(yaw / 40.0F)) * 40.0F;
        playerdrawn.rotationPitch = -((float)Math.atan((double)(pitch / 40.0F))) * 20.0F;
        playerdrawn.rotationYawHead = playerdrawn.rotationYaw;
        playerdrawn.prevRotationYawHead = playerdrawn.rotationYaw;
        GL11.glTranslatef(0.0F, playerdrawn.yOffset, 0.0F);
        RenderManager.instance.playerViewY = 180.0F;
        RenderManager.instance.renderEntityWithPosYaw(playerdrawn, 0.0D, 0.0D, 0.0D, 0.0F, 1.0F);
        playerdrawn.renderYawOffset = f2;
        playerdrawn.rotationYaw = f3;
        playerdrawn.rotationPitch = f4;
        playerdrawn.prevRotationYawHead = f5;
        playerdrawn.rotationYawHead = f6;
        GL11.glPopMatrix();
        RenderHelper.disableStandardItemLighting();
        GL11.glDisable(GL12.GL_RESCALE_NORMAL);
        OpenGlHelper.setActiveTexture(OpenGlHelper.lightmapTexUnit);
        GL11.glDisable(GL11.GL_TEXTURE_2D);
        OpenGlHelper.setActiveTexture(OpenGlHelper.defaultTexUnit);
    }

    @Override
    protected void renderToolTip(ItemStack itemStack, int p_146285_2_, int p_146285_3_) {
        Item item = itemStack.getItem();
        if (!item.getUnlocalizedName().equals(currentStat) && !(item instanceof SkillItem)) {
            PacketShowSkillsByStat packet = new PacketShowSkillsByStat(itemStack.getItem().getUnlocalizedName());
            CommonProxy.INSTANCE.sendToServer(packet);
            currentStat = itemStack.getItem().getUnlocalizedName();
        }

        super.renderToolTip(itemStack, p_146285_2_, p_146285_3_);
    }

    /**
     * Fired when a key is typed. This is the equivalent of KeyListener.keyTyped(KeyEvent e).
     *
     * @param p_73869_1_
     * @param p_73869_2_
     */
    @Override
    protected void keyTyped(char p_73869_1_, int p_73869_2_) {
        // TODO: Добавь отображение скиллов по нажатой цифре
        super.keyTyped(p_73869_1_, p_73869_2_);
    }

    /**
     * Called from the main game loop to update the screen.
     */
    @Override
    public void updateScreen() {
        super.updateScreen();
    }

    /**
     * Called when the screen is unloaded. Used to disable keyboard repeat events
     */
    @Override
    public void onGuiClosed() {
        super.onGuiClosed();
        timer.cancel();
        timer.purge();
    }

    /**
     * Adds the buttons (and other controls) to the screen in question.
     */
    @Override
    public void initGui() {
        super.initGui();
        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                //System.out.println("timer");
                player.updateParams();
                updateScreen();
            }
        }, 0, 100);
    }
}

контейнер
Java:
package rsstats.inventory.container;

import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.InventoryPlayer;
import net.minecraft.inventory.Container;
import net.minecraft.inventory.Slot;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import rsstats.common.CommonProxy;
import rsstats.common.network.PacketUpdateParams;
import rsstats.inventory.SkillsInventory;
import rsstats.inventory.StatsInventory;
import rsstats.inventory.WearableInventory;
import rsstats.inventory.slots.SkillSlot;
import rsstats.inventory.slots.StatSlot;
import rsstats.items.SkillItem;
import rsstats.items.StatItem;

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author rares
 */
public class MainContainer extends Container {
    private final EntityPlayer player;
    private final InventoryPlayer inventoryPlayer;
    private final StatsInventory statsInventory;
    private final WearableInventory wearableInventory;

    private final SkillsInventory skillsInventory;

    public MainContainer(EntityPlayer player, InventoryPlayer inventoryPlayer, StatsInventory statsInventory, SkillsInventory skillsInventory, WearableInventory wearableInventory) {
        this.player = player;
        this.inventoryPlayer = inventoryPlayer;
        this.statsInventory = statsInventory;
        this.skillsInventory = skillsInventory;
        this.wearableInventory = wearableInventory;
        addSlots();
    }


    public MainContainer() {
        this.player = null;
        this.inventoryPlayer = null;
        this.statsInventory = null;
        this.skillsInventory = null;
        this.wearableInventory = null;
    }
    
    private void addSlots() {
        /*if (inventoryPlayer != null)
            for (int y = 0; y < 3; ++y) {
                for (int x = 0; x < 9; ++x) {
                    this.addSlotToContainer(new Slot(inventoryPlayer, x + y * 9 + 9, 8 + x * 18, 84 + y * 18));
                }
            }*/

        // Расставляем слоты на панели руки
        for (int i = 0; i < 9; i++) {
            this.addSlotToContainer(new Slot(inventoryPlayer, i, (i*18 -3) +8, 188));
        }

        // Расставляем слоты на панели статов
        for (int i = 0, slotIndex = 0; i < statsInventory.getSizeInventory(); ++i, slotIndex++) {
            this.addSlotToContainer(new StatSlot(statsInventory, i, (i*18 +167) +8, /*-24*/8));
            //this.addSlotToContainer(new StatSlot(statsInventory, slotIndex, i*9, 0));
        }

        // Расставляем слоты на панели скиллов
        for (int y = 0; y < 3; ++y) {
            for (int x = 0; x < 9; ++x) {
                this.addSlotToContainer(new SkillSlot(skillsInventory, x + y * 9 /*+ 9*/, (x*18 +167) +8, (y * 18) + 26));
            }
        }

        // Расставляем слоты на панели носимых вещей
        for (int y = 0; y < 5; ++y) {
            for (int x = 0; x < 4; ++x) {
                this.addSlotToContainer(new Slot(wearableInventory, x + y * 4 /*+ 9*/, (x*18 + 51) +8, (y * 18) + 8));
            }
        }
    }

    /**
     * This should always return true, since custom inventory can be accessed from anywhere
     * @param player TODO
     * @return TODO
     */
    @Override
    public boolean canInteractWith(EntityPlayer player) {
        return true;
    }
    
    /**
     * Called when a entityPlayer shift-clicks on a slot. You must override this or you will crash when someone does that.
     * Basically the same as every other container I make, since I define the same constant indices for all of them
     * @param player TODO
     * @param par2 TODO
     * @return TODO
     */
    @Override
    public ItemStack transferStackInSlot(EntityPlayer player, int par2) {
        ItemStack itemstack = null;
        Slot slot = (Slot) this.inventorySlots.get(par2);
        return itemstack;
    }

    @Override
    public ItemStack slotClick(int slotId, int clickedButton, int mode, EntityPlayer playerIn) {
        Slot slot;
        try {
            slot = getSlot(slotId);
        } catch(Exception e) {
            return super.slotClick(slotId, clickedButton, mode, playerIn);
            //return null; // костыль
        }
        Item itemInSlot;
        if (slot.getStack() != null && slot.getStack().getItem() != null) {
            itemInSlot = slot.getStack().getItem();
        } else {
            return super.slotClick(slotId, clickedButton, mode, playerIn);
            //return null;
        }

        // Прокачка навыков
        List subitems = new ArrayList();
        itemInSlot.getSubItems(itemInSlot, CreativeTabs.tabMaterials, subitems);
        if (clickedButton == 1) { // ПКМ
            int damage = itemInSlot.getDamage(slot.getStack());
            itemInSlot.setDamage(slot.getStack(), damage < subitems.size()-1 ? damage+1 : subitems.size()-1);
            return null;
        }
        if (clickedButton == 2) { // СКМ
            int damage = itemInSlot.getDamage(slot.getStack());
            itemInSlot.setDamage(slot.getStack(), damage > 0 ? damage-1 : 0);

            return null;
        }

        if ((slot.inventory == statsInventory || slot.inventory == skillsInventory) && (itemInSlot instanceof SkillItem || itemInSlot instanceof StatItem)) {
            ItemStack itemStack = getSlot(slotId).getStack();

            // Защита от дублирующихся сообщений в чате
            if (!playerIn.worldObj.isRemote) {
                ( (StatItem) itemStack.getItem() ).roll(itemStack, playerIn);
            }
            return null;
        }
        return super.slotClick(slotId, clickedButton, mode, playerIn);
    }

    public SkillsInventory getSkillsInventory() {
        return skillsInventory;
    }
}

Что я делаю не так и почему этот баг возникает? Заранее спасибо и простите за костыли и говнокод ибо вообще без понятия как этот баг победить.
 
1,159
38
544
1,159
38
544
Проблема была в том, что я не реализовал синхронизацию клиента и сервера. Решил проблему после чтения вот этого куска документации:
Extended Entity Properties - Forge Documentation
 
Сверху