Пишем моды для minecraft — статья 6
Ииии, здравствуйте, уважаемые!
Соскучились? Нет? Ну и ладно! А я все равно представлю вам шестую статью по моддингу, в которой мы с вами добавим функционал нашему закрывающемуся сундуку. Функционал позволит сделать следующее:
- Закрыть сундук на ключ. Таким образом, сундук нельзя будет открыть не кликнув по нему определенным ключом
- Каждый ключ будет иметь собственный «шаблон», чтобы один ключ подходил только к определенному замку
- Добавим возможность взаимодействовать с сундуком трубами и воронками, если он открыт, и блокировать эту возможность, если он закрыт
- При срубании закрытого сундука, в нем будет оставаться его содержимое, а если сундук открыт - содержимое будет выброшено в мир
Думаю, этого хватит, чтобы заполнить ваши мозги новой интересной информацией о механиках Forge. Поехали!
Для начала сделаем нашему блоку еще одно состояние: ISLOCKED. Оно будет представлять собой булевое значение и разрешится в true если сундук заперт и false — если сундук отперт (да, я специально использовал это наречие ;).
В классе блока сундука (LockedChest) добавим все необходимое:
В теле определения класса добавим свойство:
...
public static PropertyBool ISLOCKED = PropertyBool.create("islocked");
...
В конструкторе дополним строку с установкой состояния-по-умолчанию:
...
setDefaultState(blockState.getBaseState().withProperty(FACING, EnumFacing.NORTH).withProperty(ISLOCKED, false));
...
В методе создания состояния блока так же не забудем про новое свойство:
...
@Override
protected BlockStateContainer createBlockState() {
return new BlockStateContainer(this, FACING, ISLOCKED);
}
...
И дополним методы приведения состояния в мету и меты в состояние:
...
@Override
public int getMetaFromState(IBlockState state) {
return state.getValue(FACING).ordinal() + (state.getValue(ISLOCKED) ? 8: 0);
}
@Override
public IBlockState getStateFromMeta(int meta) {
return this.getDefaultState().withProperty(FACING, EnumFacing.getHorizontal(meta & 7)).withProperty(ISLOCKED, ((meta & 8) > 0));
}
...
Как видно, я использовал последний оставшийся 4-ый бит в мете для хранения значения свойства ISLOCKED.
Теперь изменим blockstate нашего блока, чтобы визуально замочек на лицевой стороне блока был открытым, если свойство ISLOCKED ложно, и закрытым — если истинно. Для этого сначала создадим еще одну модель для сундука, которая будет отличаться от первой только одной текстурой. Я назову файл модели lockedchest_unlocked.json, а файл текстуры — lockedchest_front_unlocked.png. Кстати, вот и текстура:
Файл модели:
{
"parent": "block/cube",
"textures": {
"particle": "tutorial:blocks/lockedchest_front",
"down": "tutorial:blocks/lockedchest_top",
"up": "tutorial:blocks/lockedchest_top",
"east": "tutorial:blocks/lockedchest_side",
"west": "tutorial:blocks/lockedchest_side",
"north": "tutorial:blocks/lockedchest_front_unlocked",
"south": "tutorial:blocks/lockedchest_side"
}
}Для текстуры партиклов я не стал менять имя файла, так как, в принципе, игроку без разницы, что там за текстура на партиклах. Главное, чтобы не розово-черное нечто.
Файл blockstate-а:
{
"forge_marker": 1,
"defaults": {
"model": "tutorial:lockedchest_unlocked"
},
"variants": {
"normal": [{}],
"inventory": [{
"transform": "forge:default-block"
}],
"facing": {
"north": {},
"south": {"y": 180},
"west": {"y": 270},
"east": {"y": 90}
},
"islocked": {
"true": {
"model": "tutorial:lockedchest"
},
"false": {}
}
}
}Я изменил модель-по-умолчанию, чтобы изначально наш сундук выглядел, как незапертый. И добавил для истинного значения свойства islocked переопределение модели на запертую версию.
Кстати: можно было все содержимое модели вписать в блокстейт, и, нам не пришлось бы создавать еще один файл модели, отличающийся лишь одной строчкой, однако, если вы хотите это сделать — я оставлю вам это удовольствие на самостоятельную доработку.
Теперь нам нужно модифицировать код нашего ключа. Я планирую сделать следующим образом: как только игрок крафтит ключ, на выходе он получает ключ произвольного «шаблона». Чтобы сделать копию ключа, мы добавим еще один рецепт, а шаблон замка сундука будет устанавливаться согласно запирающему сундук ключу.
Можно было, разумеется, создать еще один предмет — замок, и крафтить ключ, используя в качестве ингредиента замок, чтобы задать шаблон для ключа, а потом этот замок навешивать на сундук, но опять же - вам же нужно тренироваться в моддинге, правда? Вот и реализуйте это самостоятельно - все необходимые для этого знания у вас появятся к концу этой статьи.
Итак, ключ. Каким образом задать шаблон? Если для блоков у нас есть TileEntity, чтобы хранить какие-то абстрактные данные, то для предметов — просто NBT тэги! Причем, размер тэга, опять же, ограничен только количеством оперативной памяти, которое будет «отъедать» предмет, находясь в игре.
Чтобы задать предмету NBT-тэг при крафте, нам нужно переопределить один метод в классе предмета:
...
@Override
public void onCreated(ItemStack stack, World worldIn, EntityPlayer playerIn) {
super.onCreated(stack, worldIn, playerIn);
if (!stack.hasTagCompound())
stack.setTagCompound(new NBTTagCompound());
NBTTagCompound nbt = stack.getTagCompound();
if(!nbt.hasKey("pattern"))nbt.setInteger("pattern", worldIn.rand.nextInt(10000));
stack.setTagCompound(nbt);
}
...
Этот метод вызывается при создании предмета. А когда создается предмет? Ну, как минимум, при получении результата крафта! Что мы делаем в методе? После вызова метода из супер-класса (а-то, мало ли что там ванилла еще делает с предметом при создании), мы проверяем, есть ли у предмета тэг? Разумеется, тэга у него нет (но, зная ваниллу, лучше проверить), значит — надо создать тэг. Дальше мы берем этот тэг и проверяем, есть ли в нем ключ «pattern». Если его нет (а его не должно быть, по идее), то задаем этот ключ и устанавливаем ему произвольное значение от нуля до 9999. После этого, присваиваем новый тэг нашему предмету. В результате, когда игрок заберет предмет с верстака, у него в тэге появится ключ «pattern» с произвольным значением. Хорошо? Ну, неплохо. Но хотелось бы еще и визуально видеть, с каким именно «шаблоном» у нас получился ключ, а-то вдруг мы захотим создать несколько ключей и закрыть разные сундуки разными ключами? И будем потом тыркаться в сундук всей связкой ключей, чтобы понять, какой из них правильный... Поэтому, давайте переопределим еще один метод для нашего предмета:
...
@Override
public void addInformation(ItemStack stack, @Nullable World worldIn, List<String> tooltip, ITooltipFlag flagIn) {
if (!stack.hasTagCompound()) return;
NBTTagCompound nbt = stack.getTagCompound();
if(!nbt.hasKey("pattern"))return;
tooltip.add(TextFormatting.GRAY + I18n.format("key.pattern", nbt.getInteger("pattern")));
}
...Этот метод, как понятно из названия, будет добавлять информацию в тултип предмета. Тултип — это надпись, которая появляется под названием предмета, когда игрок наводит на него мышку. Что мы делаем в методе? Проверяем, не потерялся ли у нашего ключа его тэг. Если потерялся — нам нечего делать — просто возвращаемся из метода. Если не потерялся — ищем ключ «pattern». Не нашли — хм, странно, но лучше тоже вернуться. Если же мы дошли до четвертой строчки и все еще не вернулись из метода, значит у нашего ключа все в порядке и мы можем добавить строчку в тултип. В качестве тултипа в майнкрафте используется обычный абстрактный список строк. А каждая строка в этом списке представлена форматированным текстом с локализуемыми фразами. Именно поэтому я использую константу TextFormatting.GRAY, чтобы задать «серый-майнкрафтовский» цвет тултипу, а дальше пользуюсь статическим методом для локализации строк I18n.format, чтобы подсунуть нелокализованную строку.
Обратите внимание: в метод можно вместо нелокализованной строки передать локализованную (просто уберите I18n.format), но хорошей практикой считается возможность перевода текстов в вашем моде на любой язык при помощи lang-файлов.
Обратите внимание 2: метод I18n.format является клиентским. Если вы хотите что-то локализовывать на стороне сервера, используется немного иной подход (если интересно прямо сейчас — погуглите TextComponentTranslation).
Окей. Мы задали ключ локализации для нашего сообщения, а это значит, что нам нужно его задать в lang-файле:
Куда-нибудь в конец файла en_US.lang добавим строчку:
...
key.pattern=Key Pattern %d
...
Что такое «%d»? Это маркер в строке, который будет заменен на объект целочисленного типа, следующий в качестве опционального аргумента при вызове метода I18n.format (да-да, здесь работают те же правила форматирования строк, что и в Си-шной printf. Более подробно про эти маркеры можно почитать, например, здесь).
Теперь можем запустить клиент, и проверить, что у нас создаются разные ключи при крафте и у них есть тултипы, показывающие, какого именно шаблона получился ключ.
У меня все работает, а у вас? Работает? Едем дальше!
Давайте научим наш сундук реагировать на клики ключом. Сначала нам необходимо добавить возможность «настраивать» замок сундука на шаблон ключа, которым по нему кликнули в отпертом состоянии. Где будем хранить эту информацию? Ну, разумеется, в TileEntity сундука. Начинаем править соответствующий класс (LockedChestTE):
В тело класса добавим переменную типа int и назовем ее «pattern»:
...
public int pattern = 0;
...
Дальше, не забудем дописать методы для сохранения этого значения в сейв мира:
...
@Override
public void readFromNBT(NBTTagCompound compound) {
super.readFromNBT(compound);
if(compound.hasKey("inventory"))inventory.deserializeNBT((NBTTagCompound)compound.getTag("inventory"));
if(compound.hasKey("pattern"))pattern = compound.getInteger("pattern");
}
@Override
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
compound = super.writeToNBT(compound);
compound.setTag("inventory", inventory.serializeNBT());
compound.setInteger("pattern", pattern);
return compound;
}
...
Тут, вроде, все должно быть уже понятно.
Теперь нам нужно написать код для изменения состояния свойства ISLOCKED для блока сундука и логику открытия GUI сундука по клику в зависимости от этого состояния.
Перепишем метод onBlockActivated в классе LockedChest.
Чтобы было понятнее, я уберу строчку, которая открывает GUI, и начну писать тело метода вот от такого состояния:
...
@Override
public boolean onBlockActivated(World worldIn, BlockPos pos, IBlockState state, EntityPlayer playerIn, EnumHand hand, EnumFacing facing, float hitX, float hitY, float hitZ) {
if(!worldIn.isRemote)
{
if(!playerIn.isSneaking())
{
...код будем писать здесь...
}
}
return true;
}
...
То-есть, мы в любом случае будем реагировать на правый клик только на стороне сервера и только если игрок не приседает.
Начнем с того, что запросим у мира TileEntity по координатам блока, приведем его тип к нашему и проверим, что мы не получили null:
...
LockedChestTE te = (LockedChestTE)worldIn.getTileEntity(pos);
if(te != null) {
...код будем писать здесь...
}
...
Если все хорошо, и у нас есть наш тайлэнтити, можем работать с блоком дальше. Проверим, что игрок держит в руке. И, если это — ключ, то сделаем ответвление в коде, чтобы описать логику открытия и закрытия сундука:
...
if (playerIn.getHeldItem(hand).getItem() == TutorialItems.key)
{
if(!state.getValue(ISLOCKED))
{
if (playerIn.getHeldItem(hand).hasTagCompound())
{
NBTTagCompound nbt = playerIn.getHeldItem(hand).getTagCompound();
if(!nbt.hasKey("pattern")) return true;
te.pattern = nbt.getInteger("pattern");
te.markDirty();
worldIn.setBlockState(pos, state.withProperty(ISLOCKED, true), 3);
} else
return true;
}
}
...
Сначала мы запрашиваем предмет (getItem()) у ItemStack (getHeldItem()), который находится у игрока (playerIn) в руке (hand), которой игрок кликнул по блоку. Дело в том, что у игрока, начиная с майнкрафта 1.8, две руки, как бы странно это не звучало. И метод вызывается сначала для главной руки, потом для оффхэнда (если он не пустой). Нам нужно проверить оба варианта, поэтому в сам метод onBlockActivated передается, с какой именно рукой мы сейчас работаем. И именно от этой руки мы берем ItemStack.
Проверяем, является ли предмет в ItemStack-е нашим ключом. Делаем это простым сравнением, и, если это все же наш ключ, то проверяем состояние блока. Сейчас я описал только поведение, если наш сундук не заперт (значение свойства ISLOCKED — ложно).
Мы проверяем, есть ли у ItemStack-а тэг. Если нет — наш ключ поломатый и нам надо просто поскорее выйти из метода, чтобы ничего не крашнуть. Дальше — проверяем, есть ли в тэге ключ «pattern». Если нет — бежим скорее. Однако, если все хорошо, то задаем значение pattern для нашей тайлэнтити, вызываем для TE метод markDirty(), чтобы сообщить движку майнкрафта о том, что внутри TE поменялись данные и нужно их обновить, и, наконец, обновляем состояние блока в мире.
Метод setBlockState, как вы можете догадаться, как раз нужен для того, чтобы выставить блоку новое состояние. Как им пользоваться, я объяснял в предыдущих статьях.
А теперь самое интересное. Если мы сейчас попробуем запустить игру, мы сможем поменять состояние блока! Йееей! Но, значение pattern в TE не изменится с нуля, которым мы его задали изначально. Почему так?
Потому что метод setBlockState буквально меняет блок в мире. И ему совершенно фиолетово, что мы просто хотим задать новое значение для свойства и вообще — у нас там тайлэнтити! То-есть, с точки зрения кода, при вызове setBlockState, у нас убирается блок и тайлэнтити, и появляется новый блок и новый экземпляр тайлэнтити! Нам такого не нужно. Хорошо, что есть достаточно несложный способ сохранить тайлэнтити в случае, если у нас только меняется состояние блока, а не сам блок. Для этого вернемся в класс нашей тайлэнтити и переопределим один метод:
...
@Override
public boolean shouldRefresh(World world, BlockPos pos, IBlockState oldState, IBlockState newSate) {
return oldState.getBlock() != newSate.getBlock();
}
...
В метод приходят два важных нам параметра — это старое состояние блока и новое состояние блока. Мы просто сравниваем блоки этих состояний. И, если это один и тот же блок — мы должны вернуть ложь из метода, чтобы наш TE сохранился.
Теперь надо описать поведение блока в случае, если сундук уже заперт. Пишем, конечно же, для условия «else» от предыдущего блока кода:
...
if(!state.getValue(ISLOCKED))
{
...
} else
{
if (playerIn.getHeldItem(hand).hasTagCompound())
{
NBTTagCompound nbt = playerIn.getHeldItem(hand).getTagCompound();
if(!nbt.hasKey("pattern")) return true;
if(nbt.getInteger("pattern") == te.pattern) {
worldIn.setBlockState(pos, state.withProperty(ISLOCKED, false), 3);
}
} else
return true;
}
...
Здесь у нас очень похожая ситуация. Мы проверяем, что чем игрок кликнул по блоку, если это наш ключ — берем у него из тэга ключ «pattern», и сравниваем значение в тэге ItemStack'а со значением в нашей TE. Если эти значения совпадают, меняем состояние блока, устанавливая значение свойства ISLOCKED в ложь, и на этом успокаиваемся.
Теперь давайте напишем логику открытия сундука. Происходить это будет в случае, если у игрока в руке что угодно, но не ключ. То-есть — снова ветка «else» но уже для другого условия:
...
if (playerIn.getHeldItem(hand).getItem() == TutorialItems.key)
{
...
} else
{
if(state.getValue(ISLOCKED))return true;
playerIn.openGui(Tutorial.instance, TutorialGUIs.LOCKEDCHESTGUI, worldIn, pos.getX(), pos.getY(), pos.getZ());
}
...
Если у игрока в руке не ключ, то просто проверим значение свойства ISLOCKED. И, если оно истинно — вернемся, иначе — откроем GUI.
Давайте проверим, что наш код работает. Запустим клиент, скрафтим ключ, и попробуем пооткрывать-позакрывать наши ящики.
Я создал два ключа и потыкал по сундуку. Все работает!
Теперь, предлагаю немного передохнуть, сходить за кофе и, когда информация уляжется в мозге, продолжить. Ведь нам нужно сделать за сегодня еще две важных вещи с нашим сундуком.
Отдохнули? Продолжаем!
Давайте начнем со сложного. Сложное в данном случае — это способности (или возможности) TileEntity, или Capability. На самом деле, ничего сложного тут нет. Способности — это, собственно, то, что умеет делать наш TileEntity по отношению к другим TileEntity из других модов, или из ваниллы. Например, если бы мы с вами сейчас писали какой-нибудь генератор, то у него обязательно была бы способность передавать энергию другим тайлэнтити. Если бы мы писали насос — он мог бы передавать жидкость другим тайлэнтити. Давайте подумаем, какая же способность есть у сундуков? Хммм… Видимо, они должны быть способны принимать предметы из других тайлэнтити (например, труб или воронок) и, возможно, разрешать забирать предметы из своего внутреннего инвентаря.
Резюмируя сказанное — forge представляет три основных способности для тайлэнтити:
— CapabilityEnergy.ENERGY - способность работать с энергией
— CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY — способность работать с жидкостями
— CapabilityItemHandler.ITEM_HANDLER_CAPABILITY — способность работать с предметами
Каким образом рассказать майнкрафту, что наш сундук способен работать с предметами? Делается это перегрузкой двух методов для TileEntity. В нашем случае это будет выглядеть вот так:
...
@Override
public boolean hasCapability(Capability<?> capability, @Nullable EnumFacing facing) {
return this.getCapability(capability, facing) != null;
}
@Nullable
@Override
public <T> T getCapability(Capability<T> capability, @Nullable EnumFacing facing) {
if (capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY) {
return (T) inventory;
} else
return super.getCapability(capability, facing);
}
...
Метод hasCapability возвращает истину или ложь в зависимости от того, обладает ли TE запрошенной способностью. Именно к этому методу обращаются другие TE, когда хотят узнать, может ли сосед принять или отдать жидкость, энергию или предмет. В метод поступают два аргумента: способность и сторона, с которой эта способность запрашивается.
В нашем случае, мы, внутри своего TE, сразу запросим обработчик способности с нужной стороны и, если этот обработчик не null, вернем истину. В противном случае - вернем ложь.
Второй метод (getCapability) возвращает обработчик, который будет заниматься, ну, обработкой! В случае с предметами, очень удобно пользоваться обработчиком ItemStackHandler, который мы и использовали для создания инвентаря нашего сундука. Чтож, проверим в обработчике, какую именно способность у нас запросили и, если это способность CapabilityItemHandler.ITEM_HANDLER_CAPABILITY — вернем наш обработчик, приведя его к шаблонному типу. Мы разрешим взаимодействовать с инвентарем нашего сундука с любой стороны, поэтому просто проигнорируем аргумент facing. Если у нас запросили какую-то иную способность, вместо того, чтобы грубо возвращать null самим, поручим это грядное дело суперклассу. Пусть он отдувается перед коварными TE, которые хотели бы залить нам в инвентарь воду, а потом подать энергию, чтобы все к чертям сгорело...
Окей. Если мы сейчас запустим клиент, то увидим, что наш сундук успешно принимает предметы из воронок и отдает их в воронки!
Но теперь вспомним, что мы хотели бы запретить это взаимодействие, если наш сундук заперт. Чтобы кто-то особо хитрый не подключил свою безразмерную трубу к нашим сокровищам! Как это сделать? Да просто в методе getCapability, если у нас запросили способность работы с инвентарем, проверим состояние блока и вернем null, если сундук заперт:
...
if (capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY) {
if(world.getBlockState(pos).getValue(LockedChest.ISLOCKED))
return null;
else
return (T) inventory;
} else
...
Вот. Теперь все работает как надо. Осталось последнее: сейчас, если мы срубим наш сундук, все его содержимое магическим образом исчезнет, потому что майнкрафт не знает, что делать с содержимым инвентаря нашей TE. Давайте ему расскажем! Сделаем это путем переопределения метода breakBlock в классе блока.
...
@Override
public void breakBlock(World worldIn, BlockPos pos, IBlockState state) {
LockedChestTE te = (LockedChestTE)worldIn.getTileEntity(pos);
if (te != null)
{
for(int slot = 0; slot < te.inventory.getSlots(); slot++)
InventoryHelper.spawnItemStack(worldIn, pos.getX(), pos.getY(), pos.getZ(), te.inventory.getStackInSlot(slot));
}
super.breakBlock(worldIn, pos, state);
}
...
Этот метод вызывается только на стороне сервера, так что нам нет необходимости проверять на сторону вызова. Сам метод вызывается после того, как блок уже уничтожен, но TileEntity еще на месте. Самое время проверить, что тайлэнтити действительно есть, да еще и это наш тайлэнтити, и выбросить каждый ItemStack из инвентаря в мир при помощи метода-хелпера InventoryHelper.spawnItemStack.
Если есть желание, можете проверить, что теперь, при срубании нашего сундука, все содержимое его инвентаря будет выброшено в мир.
Дальше, нам нужно перед тем, как выбрасывать что-то, убедиться, что сундук не заперт. Просто добавим проверку блокстейта на свойство ISLOCKED перед запуском цикла прохода по инвентарю? Хммм... Не получится. Почему? Вспомните, что я писал по поводу метода breakBlock. Он вызывается после того, как блок уже выброшен! То-есть, внутри метода, world.getBlockState нам уже выдаст блок воздуха, который появился на месте нашего сундука. Поэтому, самым правильным решением в данном случае — будет продублировать состояние сундука (заперт/не заперт) в нашей TE. Создадим в ней еще одну переменную типа boolean с именем isLocked, допишем для нее код синхронизации во writeToNBT/readFromNBT и, при взаимодействии с блоком при помощи ключа, добавим изменение этого значения:
LockedChestTE:
...
public boolean isLocked = false;
...
@Override
public void readFromNBT(NBTTagCompound compound) {
...
if(compound.hasKey("isLocked"))isLocked = compound.getBoolean("isLocked");
}
@Override
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
...
compound.setBoolean("isLocked", isLocked);
return compound;
}
...
LockedChest:
...
@Override
public boolean onBlockActivated(World worldIn, BlockPos pos, IBlockState state, EntityPlayer playerIn, EnumHand hand, EnumFacing facing, float hitX, float hitY, float hitZ) {
...
if(!state.getValue(ISLOCKED))
{
if (playerIn.getHeldItem(hand).hasTagCompound())
{
...
te.isLocked = true;
te.markDirty();
...
} else
return true;
} else
{
if (playerIn.getHeldItem(hand).hasTagCompound())
{
...
if(nbt.getInteger("pattern") == te.pattern) {
te.isLocked = false;
te.markDirty();
...
}
} else
return true;
}
...
}
А вот теперь мы сможем определить, заперт ли тайлэнтити сундука в методе breakBlock:
...
@Override
public void breakBlock(World worldIn, BlockPos pos, IBlockState state) {
LockedChestTE te = (LockedChestTE)worldIn.getTileEntity(pos);
if (te != null)
{
if(!te.isLocked)
for(int slot = 0; slot < te.inventory.getSlots(); slot++)
InventoryHelper.spawnItemStack(worldIn, pos.getX(), pos.getY(), pos.getZ(), te.inventory.getStackInSlot(slot));
}
super.breakBlock(worldIn, pos, state);
}
...
Теперь, если мы срубим запертый сундук, его содержимое снова будет пропадать. Лажа? Лажа. Давайте исправлять! Мы же сегодня уже научились приписывать к предметам дополнительную информацию, верно? И, что я говорил про NBT-тэги? А говорил я то, что в них можно хранить сколь угодно много данных. Значит - это отличное место, чтобы записать туда инвентарь нашего сундука. Только вот незадача… У нас в блоке есть метод под названием getItemDropped, который должен вернуть предмет, который выпадает при срубании блока. Он был бы идеальным методом, чтобы дописать к предмету NBT, однако, он вызывается уже после того, как наш TileEntity превратится в null. К сожалению, нам ничего не остается, кроме как переписать код превращения блока в предмет самим. Благо, это несложно сделать. В уже знакомый нам метод breakBlock допишем ветку «else» для случая, если наш сундук заперт:
...
if(!te.isLocked) {
...
} else
{
ItemStack itemBlock = new ItemStack(Item.getItemFromBlock(this));
NBTTagCompound storedTE = new NBTTagCompound();
storedTE = te.writeToNBT(storedTE);
itemBlock.setTagCompound(storedTE);
InventoryHelper.spawnItemStack(worldIn, pos.getX(), pos.getY(), pos.getZ(), itemBlock);
}
...
Что нам нужно сделать? Нужно создать новый ItemStack, в который поместить наш блок сундука в виде предмета. Взять предмет от блока позволит статический метод класса Item под названием getItemFromBlock. Дальше, создадим пустой NBT, в который запишем все данные из TE. Проще всего это сделать просто вызвав writeToNBT, который любезно вернет нам все данные TE в одном тэге. Этот тэг мы присобачим к созданному ItemStack и выбросим получившееся добро в мир уже знакомым методом хелпера.
Если мы сейчас запустим наш клиент, то, при срубании запертого сундука, получим два предмета блока, вместо одного. Почему так? Потому что никто не сказал майнкрафту, что мы сами занимаемся созданием блока и нам не надо помогать. Давайте исправим это недоразумение, перегрузив тот самый getItemDropped:
...
@Override
public Item getItemDropped(IBlockState state, Random rand, int fortune) {
return null;
}
...
Правда теперь мы не получим сам предмет блока, если срубим незапертый сундук. Просто добавим дроп предмета и при незапертом сундуке в соответствующее место (сразу после дропа содержимого сундука):
...
if(!te.isLocked) {
InventoryHelper.spawnItemStack(worldIn, pos.getX(), pos.getY(), pos.getZ(), new ItemStack(Item.getItemFromBlock(this)));
} else
...
Вот. Осталось последнее. Когда мы будем ставить наш блок в мир, нужно обращать внимание на наличие NBT-тэга у предмета. Сделаем эту проверку в методе onBlockPlacedBy после установки состояния блока:
...
@Override
public void onBlockPlacedBy(World worldIn, BlockPos pos, IBlockState state, EntityLivingBase placer, ItemStack stack) {
...
if(stack.hasTagCompound())
{
LockedChestTE te = (LockedChestTE)worldIn.getTileEntity(pos);
if(te != null)
{
te.readFromNBT(stack.getTagCompound());
}
worldIn.setBlockState(pos, state.withProperty(FACING, placer.getHorizontalFacing().getOpposite()).withProperty(ISLOCKED, true), 3);
}
}
...
Если у предмета есть NBT, то возьмем из мира TE, и впишем в него данные из тэга. Единственное, после этого надо еще и обновить состояние блока, так как, если у предмета есть NBT, значит сундук должен быть заперт, так что в конце условия просто еще раз вызовем setBlockState.
Ну вот и все на сегодня. Да, я знаю, что я не рассказал, как сделать копию ключа, но об этом мы с вами поговорим в следующей статье. А я пока что подумаю, о чем вам рассказать дальше. Собственно, если есть какие-то пожелания по этому поводу — готов их выслушать в комментариях. А еще — не забывайте, что есть github-репозиторий с кодом из моих статей. Найти его можно здесь: https://github.com/ZigTheHedge/tutorial1.12.2
Счаааастливо!
