Частые ошибки
Есть несколько распространённых ошибок и не самых очевидных ожиданий, которые мы регулярно встречаем, когда помогаем новичкам в Denizen. Чтобы вы быстрее освоили Denizen, мы собрали некоторые из этих проблем и что с ними делать.
Отображаемый текст и реальные данные
Одно из важных понятий, которые нужно усвоить, — разница между отображаемым текстом и реальными данными.
Реальные данные — это собственно внутренние данные, отформатированные так, чтобы скриптам было максимально удобно с ними работать.
Отображаемый текст — это сообщение, которое видит пользователь; часто оно генерируется из реальных данных.
Общий принцип: реальные данные можно использовать для генерации отображаемого текста, а вот обратное — нельзя.
Ниже — несколько типичных примеров того, как пользователи путают реальные данные и отображаемый текст.
Lore предмета — это не данные
Одна из распространённых путаниц между реальными данными и отображаемым текстом — lore предмета.
Логика «у предмета в lore написано Bonus Damage: +15, значит, в событии урона мне нужно вытащить это +15 из lore, чтобы применить его!» — в корне неверна. Это попытка превратить отображаемый текст обратно в данные, тогда как разрешено только обратное: данные → отображаемый текст.
Кастомные реальные данные предмета можно хранить в нескольких местах:
Флаги предмета (доступ через
<[item].flag[имя_флага]>)Ключи скрипта предмета (доступ через
<[item].script.data_key[ключ]>)Стандартные механики Minecraft, вроде системы Attributes
После того как данные сохранены в правильном месте, можно отобразить их в lore. А дальше пишите теги и логику, основываясь на реальных данных, а не на lore — lore нужен людям для чтения, а не вашим скриптам!
Игроки — это не их ники
Исторически в Minecraft игроки были уникальны по нику. Это означало, что «Steve» всегда был именно «Steve»: никакого другого «Steve» не существовало, а сам «Steve» не мог сменить имя. Всё изменилось примерно в эпоху Minecraft 1.7 — тогда в качестве уникального идентификатора игрока стали использоваться UUID, и игрокам разрешили менять ники.
Никогда не отслеживайте игроков по нику во внутренней логике. Тег <player.name> следует использовать только для вывода чистого имени в команде narrate или аналогичных командах, предназначенных для чтения игроками. Именно для этого имя и существует — для человеческого чтения. Оно не предназначено для внутреннего отслеживания, оно не уникально и ненадёжно.
* Есть одно исключение: команда execute запускает команды сторонних плагинов, которые обычно на вход ждут либо ник, либо UUID, а не объект Denizen.
Так, игрок — это его UUID?
Игрок — это не просто его UUID. Игрок — это не ник, не UUID, не локация, не что-то ещё. Игрок — это игрок.
Аналогично, NPC — это не просто его ID. NPC — это NPC.
Вообще, ничто не сводится к маленькому кусочку информации, который его уникально идентифицирует.
Система объектов
Важная часть того, как устроен Denizen, — это система объектов.
«Объект» в мире разработки — это представление чего-то конкретного, которое можно найти по lookup-идентификатору, но которое существует как нечто большее, чем просто эти lookup-данные. Звучит путано, поэтому поясним на практике: «объект сущности» — это реальная полноценная сущность, с её ИИ, здоровьем, именем, конкретным положением в мире и всем остальным, что делает её тем, чем она является. Сущность можно быстро найти по UUID, но сама сущность — это гораздо больше, чем просто короткий набор цифр и букв. (Любопытная деталь: в большинстве языков программирования уникальным идентификатором объекта служит его адрес в памяти.)
В Denizen, когда вы смотрите на объект в отладке или через команду narrate (или где угодно в тексте), виден уникальный идентификатор с префиксом, указывающим тип объекта — это называется Object Notation. Это не сам объект, а лишь lookup-идентификатор, по которому вы или система можете понять, на какой объект ссылаются.
Когда вы пишете скрипты, важно работать именно с реальным объектом, а не с каким-то текстом, содержащим lookup-идентификатор.
Несколько примеров, где это важно:
Объект игрока подставлен в строку текста. Допустим,
"Player:<player>"где-то сохранён. Когда вы прочитаете этот текст, вы можете решить, что<[THAT_TEXT].after[:]>вернёт объект игрока — но нет. Вернётся просто текст с уникальным идентификатором игрока. Вам придётся снова превратить его в объект игрока:<player[<[THAT_TEXT].after[:]>]>или<[THAT_TEXT].after[:].as[player]>(хотя в некоторых контекстах Denizen может сделать это за вас автоматически).В ряде случаев чтение напрямую из хранилища данных (YAML, флаги, SQL и т. п.) может вернуть просто текстовое представление идентификатора объекта, который туда был записан. Тогда нужно снова превратить его в реальный объект соответствующим тегом-конвертером.
В общем случае при пользовательском вводе (например, в command-скрипте). Может быть введён уникальный или даже не уникальный идентификатор, и вам придётся делать более сложный поиск реального объекта. Конкретный пример: когда в command-скрипте есть опция с игроком, обычно нельзя ожидать, что пользователь введёт точный правильный object-идентификатор. Тег
server.match_playerпомогает превратить введённый ник в реальный объект игрока.
Не доверяйте игрокам

Когда вы пишете скрипты, в целом можно полагать, что система обработает то, что вы написали, именно так, как вы написали. Если вы поставили флаг через команду flag привязанному игроку, можно вполне уверенно ожидать, что player.flag потом вернёт значение этого флага.
А вот игроки — не машины. Они люди. Люди ошибаются. А ещё иногда они жульничают. Когда вы обрабатываете пользовательский ввод, нужно быть готовым к этому и учитывать.
Покажем разницу между плохим скриптом, который доверяет игрокам, и хорошим, который нет, на примере команды «pay» в экономике.
pay_command:
type: command
name: pay
usage: /pay [player] [amount]
description: Pays the specified player.
script:
- money take quantity:<context.args.get[2]>
- money give players:<server.match_player[<context.args.get[1]>]> quantity:<context.args.get[2]>
- narrate "<blue>You paid <gold><server.match_player[<context.args.get[1]>].name> <green>$<context.args.get[2]>"
Скрипт простой и лаконичный, всего три строки, делает всё, что нужно… если игрок использует команду ровно так, как задумано, без ошибок и без попыток злоупотребления.
Теперь посмотрим, как выглядит скрипт, в котором весь пользовательский ввод тщательно проверяется. Читайте комментарии — там видно, от чего именно защищается каждая проверка.
pay_command:
type: command
name: pay
usage: /pay [player] [amount]
description: Pays the specified player.
# Игроки могут быть в тюрьме или ещё где-то, где у них забрали права,
# поэтому зададим право на использование команды явно.
permission: myscript.pay
script:
# Игрок может просто написать "/pay", забыв про аргументы,
# и в этом случае просто подскажем формат и остановимся.
- if <context.args.size> < 2:
- narrate "<red>/pay [player] [amount]"
- stop
# Fallback на случай, если имя игрока невалидное.
- define target <server.match_player[<context.args.get[1]>].if_null[null]>
# Игрок мог опечататься в нике.
# Если совпадения нет — скажем ему и остановимся.
- if <[target]> == null:
- narrate "<red>Unknown player '<yellow><context.args.get[1]><red>'."
- stop
- define amount <context.args.get[2]>
# Игрок мог ошибиться в числе.
# Если ошибся — сообщим и остановимся.
- if !<[amount].is_decimal>:
- narrate "<red>Invalid amount input (not a number)."
- stop
# Игрок может попытаться сжульничать, заплатив отрицательную сумму,
# чтобы получить деньги, а не потерять их.
# Поэтому проверяем, что число положительное.
# Ноль тоже отсеиваем — платить $0 бессмысленно.
- if <[amount]> <= 0:
- narrate "<red>Amount must be more than zero."
- stop
# Игрок может попытаться заплатить больше, чем у него есть,
# случайно или нарочно. Проверим, хватает ли средств,
# и если нет — остановимся.
- if <player.money> < <[amount]>:
- narrate "<red>You do not have <green>$<[amount]><red>."
- stop
- money take quantity:<[amount]>
- money give players:<[target]> quantity:<[amount]>
- narrate "<blue>You paid <gold><[target].name> <green>$<[amount]>"
Всего-то пришлось добавить кучу проверок! Увы, нормальные скрипты с пользовательским вводом имеют свойство разрастаться из-за валидации. Зато никто не сможет их обойти!
Не сравнивайте сырые объекты
Сравнение сырых объектов поначалу кажется вполне естественным, и ошибочность подхода становится понятна только сильно позже.
«Сравнение сырых объектов» — это когда в if или аналогичной команде сравнивается сырой объект Denizen с чем-то другим (часто с простым текстом object-идентификатора).
Выглядит это, например, так:
- if <player.item_in_hand> == i@diamond_sword:
- narrate "You have a diamond sword!"
На первый взгляд — вполне нормально. Если проверить в игре, скорее всего даже сработает. В чём же проблема?!
Не всегда просто меч

Первая проблема в том, что не-уникальные объекты в Denizen (идентифицируемые по своим характеристикам, как предмет, в отличие от объектов вроде сущностей, которые идентифицируются по ID) в ряде случаев обрастают дополнительными характеристиками, даже если при первом тестировании их не было.
Этот if из примера перестанет работать, как только игрок хоть немного попользуется мечом — изменится значение прочности, и предмет станет i@diamond_sword[durability=1]. То же произойдёт, если он зачарует меч, переименует его на наковальне и так далее…
Стиль идентификаторов в Denizen меняется
Вторая проблема в том, что то, что валидно сейчас, может перестать быть валидным в будущем. Denizen часто обновляется, и формат идентификаторов объектов между версиями меняется. Например, раньше <player> возвращал p@name, а теперь возвращает p@uuid. За эти годы было ещё много подобных изменений формата.
Меч, который сегодня выглядит как i@diamond_sword, когда-нибудь может стать i@diamond_sword[durability=0], или item@diamond_sword, или i@item[material=diamond_sword], или diamond_sword[future_minecraft_shininess_statistic=0], или принять ещё какой-нибудь вид.
Так что мне делать?
Вариант первый: где возможно, используйте специальный матчер. У типа ItemTag есть матчер по имени материала, так что можно использовать оператор matches:
- if <player.item_in_hand> matches diamond_sword:
- narrate "You have a diamond sword!"
Вариант второй: сравнивайте объекты по тегам, которые возвращают значения в гарантированно стабильном формате. То есть по тегам, возвращающим простой текст в чётко определённом формате, а не сам объект.
Тег .material.name гарантированно возвращает только имя материала, так что следующее сравнение безопасно с точки зрения любых будущих изменений деталей предметов или самого Denizen.
- if <player.item_in_hand.material.name> == diamond_sword:
- narrate "You have a diamond sword!"
В обоих случаях единственный риск — что имя материала изменится в будущей версии Minecraft (это уже сложнее обойти — к счастью, такое случается очень редко и обычно заранее известно. Если хочется подстраховаться, можно писать == <item[diamond_sword].material.name>, чтобы положиться на автоконвертацию, которую Denizen добавит при переименовании, — но в общем-то это не обязательно).
Для других типов объектов ищите подходящую точку сравнения. Для материалов, миров, скриптов, плагинов, … подойдёт .name.
Для других значимых типов (локации, кубоиды, эллипсоиды, …) прямого сравнения вообще стоит избегать. Вместо этого запомните объект и проверяйте .note_name — или используйте более специфичные конструкции, вроде переключателя in:<область> в событиях.
Заметка про сравнение идентичных объектов
Когда вам нужно сравнить именно «один и тот же объект» (как правило, для уникальных объектов, вроде сущностей, или в работе со списками и т. п.) и у вас есть теги для обоих (а не один из них в виде простого текста), прямое сравнение относительно безопасно.
Например, можно добавить - if <[target]> == <player>: в пример с командой /pay, чтобы запретить игрокам платить самим себе. Это допустимо: оба значения теоретически указывают на один и тот же объект, никаких разных деталей у них быть не должно, и любые изменения формата идентификаторов автоматически применятся к обоим, а не к одному.
Однако учтите, что сохранённая копия объекта (например, объект игрока, сохранённый во флаге) потенциально может со временем сохранить устаревший формат, что ломает сравнения. Прямое сравнение объектов безопасно, только если оба объекта получены из актуальных тегов-источников.
Не злоупотребляйте fallback'ами
Fallback'и — крайне удобный инструмент в Denizen. Это один из основных способов обрабатывать неопределённые ситуации и пограничные случаи в скриптах. Но, как и многое другое, лучше всего их использовать в меру. Чрезмерное увлечение fallback'ами приносит больше вреда, чем пользы.
Ошибки — это страшно

Образ мысли, который обычно приводит к злоупотреблению fallback'ами — это «ошибки — это страшно». Ошибка — это проблема, значит, её нужно во что бы то ни стало убрать!
На деле ошибки — это ещё один инструмент, который предоставляет Denizen. Само сообщение об ошибке — не проблема, ошибка просто говорит вам, что где-то в скрипте есть проблема. Если понаставить fallback'ов на каждый тег, вы в итоге спрячете ошибки, но не почините настоящую проблему. В консоли не будет страшного красного текста, но и скрипт не будет делать то, что от него требуется!
Когда стоит использовать fallback'и
Fallback стоит ставить на тег только тогда, когда вы ожидаете, что тег может упасть.
Рассмотрите, например, тег server.match_player, который превращает введённые имена в объект игрока. Вполне резонно ожидать, что иногда игрок введёт что-то, что не является валидным ником, и тег упадёт. Это как раз тот случай, когда нужно добавить fallback вроде .if_null[null] или старого ||null.
Когда вы добавляете fallback, часто нужно ещё и проверить fallback-значение. Например, для определения <server.match_player[<input>].if_null[null]> можно сделать - if <[target]> == null: и внутри этого блока обработать случай невалидного ввода. В некоторых случаях, когда нужно только проверить наличие, а само значение после не требуется, можно просто использовать .exists, вроде - if <player.item_in_hand.script.exists>: (чтобы проверить, является ли предмет в руке игрока скриптовым).
В других случаях fallback может быть просто разумным значением по умолчанию, например <player.flag[coins].if_null[0]>, — и тогда никаких дополнительных if делать не нужно.
Когда не стоит их использовать
Fallback'и не нужны, когда у тега нет веской причины падать.
Например, на тег <player.name> в большинстве скриптов fallback ставить не нужно (если только это не переиспользуемый скрипт, который не требует наличия игрока).
Если скрипт, использующий <player.name>, запускается, а игрока нет — в консоли появится сообщение об ошибке. И это хорошо! Это значит, что что-то пошло не так, и скрипт запустился без игрока. Можно разобраться, почему игрока не было, и устранить причину.
«Кавычки оборачивают весь аргумент»
Многие пользователи путают, куда ставить кавычки в командах Denizen.
Синтаксис Denizen устроен так: строка, начинающаяся с -, означает command-строку. Она состоит из имени команды и её аргументов. Сначала идёт имя команды, затем — аргументы, разделённые пробелами (и ничем иным). Например, - commandname arg1 arg2 arg3 — это строка с командой и тремя аргументами.
Когда в аргументе нужен пробел, нужно обернуть весь аргумент в кавычки, чтобы показать, что это один аргумент. Например, - narrate "this is one big argument" — это команда narrate с одним аргументом.
Ещё пример: - flag player "my_flag:my value". Это команда flag с двумя аргументами: player и "my_flag:my value". Обратите внимание: my value — это не отдельный аргумент от my_flag. Двоеточие (:) не разделяет аргументы, а лишь указывает префикс внутри аргумента (и остаётся частью этого аргумента!).
Ставить кавычки внутри аргумента — это ВСЕГДА ошибка. - flag player my_flag:"my value" — совершенно невалидная запись, и она считается ошибкой.
Также не нужно оборачивать в кавычки аргумент, в котором нет пробела. Например, - flag "player" "flag:value" — здесь кавычки лишние и бессмысленные.
Также никогда не нужно оборачивать в кавычки имя команды или ключ скрипта. В следующем примере каждая кавычка лишняя и должна быть убрана.
"This is wrong":
"type": "task"
"script":
- "flag" player flag:value
Вот как должно быть:
This is right:
type: task
script:
- flag player flag:value
Следите за отладочной консолью

Когда вы пишете скрипты, у вас всегда должна быть открыта серверная отладочная консоль. Когда вы запускаете скрипт, держите консоль в углу зрения и заглядывайте в неё по ходу. Если в консоли появляется сообщение об ошибке, оно и скажет вам, что нужно исправить, и подскажет что именно — куда быстрее, чем перечитывать скрипт в попытке понять, что вы там намудрили.
Когда пользователи приходят в Discord за помощью, мы обычно просим показать запись отладки. Слишком часто оказывается, что у них в логах ярко-красным светится сообщение об ошибке, прямо объясняющее, что пошло не так. Если бы пользователь просто смотрел в консоль, он бы всё починил сам, не обращаясь за помощью.
В отладочном выводе также много не-ошибочной информации, которая очень полезна при работе над скриптом. Скрипт дропает слишком много предметов — почему? В логах вы увидите, что цикл крутится слишком долго, что значение quantity оказалось не тем, что вы ожидали, или ещё что-нибудь подобное. Не знаете, какое значение enum'а у context события — логи подскажут!
Аккуратно с настройками отладки
Прежде всего: НИКОГДА не отключайте глобальный отладочный вывод. Отладочная информация крайне важна. Глобальное отключение скроет вообще всё, даже сообщения об ошибках! Вместо этого просто ставьте debug: false на скриптах, для которых хотите убрать отладочный вывод.
Вот так ставится debug: false на скрипт:
No debug script:
type: task
debug: false
script:
- define a b
- (commands here)
Он всегда должен идти сразу после строки type:, на том же уровне отступа.
Когда выставлен debug: false, из этого скрипта в лог пойдут только сообщения об ошибках. Если не выставлен — вся отладочная информация.
Простое правило:
Когда вы правите скрипт / работаете над ним — отладка ни в коем случае не должна быть выключена.
Когда скрипт полностью готов, протестирован и вас всё устраивает — тогда можно отключить.
Боевой сервер — не тестовый
Когда мы помогаем людям в Discord, иногда слышим что-то вроде «сейчас не могу перезапустить сервер, на нём игроки» или «ой, а игрок случайно вызвал событие».
Если у вас БОЕВОЙ сервер с РЕАЛЬНЫМИ ИГРОКАМИ онлайн, на нём НЕЛЬЗЯ писать скрипты. Всегда заводите локальный тестовый сервер для написания скриптов, и переносите скрипты на боевой сервер только после того, как они полностью заработают.
Слишком легко наделать больших проблем многим людям, когда вы правите скрипты на боевом сервере. Не делайте так.
Если «истина равна истине»… и прочая философия
Команда if в Denizen работает так: она обрабатывает аргументы логическим сравнением, и если результат — true, выполняет код внутри, иначе — не выполняет.
Так что если в скрипте стоит - if <sometag> == true:, вы по сути пишете if ( true == true ) == true: — согласитесь, это как-то глупо?
НИКОГДА не пишите == true в if (или while и подобных). Это всегда лишнее.
Также не пишите == false. Чтобы инвертировать проверку, используйте !. Например: - if !<some tag>: или - if <some tag> != somevalue:.
Текст — это не тег
Новички в Denizen часто читают документацию вроде <#> в синтаксисе команды и решают, что <3> — это валидный ввод.
Это не так: как объясняется в документации по синтаксису команд, в этом контексте <> означает «подставьте сюда значение». Эти <> не нужно писать буквально.
Так что если синтаксис команды — - heal [<#>], правильный ввод — - heal 3.
Одна из причин путаницы в том, что <> в скриптах Denizen действительно используются для формирования тегов. Нужно помнить, что теги — никогда не литералы: это инструкция движку скриптов найти какое-то другое значение. Например, тег <player.money> — это не «вставить в команду буквальный текст player.money», а «найти, сколько денег у игрока, и подставить это значение». Так что - heal <player.money> в скрипте обработает значение тега, и в итоге выполнится как - heal 3 (если у игрока в этот момент $3).
Если вам нужно вставить литеральный текст — просто вставьте литеральный текст, и всё. - heal 3 или - narrate mytexthere — совершенно валидные способы задать аргументы, никакие специальные символы не нужны (кроме случаев, когда кавычки требуются, чтобы сохранить пробел внутри аргумента).
Дополнительно: если нужен литеральный текст в форме тега (например, чтобы использовать на нём element-подтег), можно использовать базовый тег element, вроде: <element[3].div[5]> (берёт простое значение «3», превращает в ElementTag, и через ElementTag.div делит на 5).
Команда adjust — не для предметов / материалов и т. п.
Многие пользователи, впервые пытаясь изменить механизм предмета, материала или подобного, пытаются использовать для этого команду adjust, вроде - adjust <[inventory].slot[5]> "lore:My new lore!" или - adjust <[location].material> lit:true.
Хотя на первый взгляд это кажется логичным, на практике не работает — из-за важного различия между типами объектов: уникальные vs. общие объекты. Рекомендуется прочитать и понять ту страницу, чтобы действительно разобраться, почему adjust на item не работает. Короткая суть: ItemTag выглядит как stick, что является описанием предмета, а не конкретным уникальным предметом. В результате у системы нет способа понять, какую именно палку вы хотите изменить.
Если вы всё-таки примените adjust к предмету, он применит изменение к описанию предмета и сохранит изменённое описание в save-запись. С MaterialTag — похожая история. Иногда это бывает полезно, но обычно бесполезно, когда нужно изменить конкретный предмет в мире.
Так как же мне изменить конкретный предмет?
Способ зависит от того, где этот предмет находится. Если предмет лежит в инвентаре, лучше всего использовать команду inventory с аргументами adjust и slot:<#> (например, - inventory adjust slot:5 "lore:My new lore!"). В других случаях пригодится тег ItemTag.with[...]. Он возвращает копию предмета с применённым механизмом. Например, для сущности dropped_item можно задать механизм item через тег with: - adjust <[entity]> "item:<[entity].item.with[lore=my new lore!]>". Чтобы поменять предмет в событии, иногда можно использовать determine с тегом ItemTag.with.
Чтобы изменить MaterialTag, есть тег MaterialTag.with[...], аналогичный версии для ItemTag. Скорее всего, впрочем, вам нужно менять материал блока, и тогда нужна команда adjustblock. Она принимает локацию блока и применяет к нему механизмы MaterialTag (например, - adjustblock <[location]> lit:true).
То же касается и флагов на предметах — не используйте flag, используйте inventory flag (например, - inventory flag slot:5 myflag:value) или тег with_flag.
Не пишите сырые локации в скриптах
В Denizen-Discord нам часто задают вопросы вроде «как задать координаты локации» или «как заставить NPC пройти в точку x,y,z 1,5,7». Иногда это звучит даже как «как передать сырые координаты вместо LocationTag».
Короткий ответ: никак.
В принципе, ради чистоты и правильности скриптования, никогда нет смысла вписывать мировые координаты напрямую в скрипт вместо использования тега для получения локации.
А если для нужной мне локации нет тега?
Тогда сделайте его! Теги Denizen — не неподвижные валуны. Это инструменты, которые работают на вас, а не против.
Например, у вас есть красивый обсидиановый столб в центре арены, и скриптам нужна локация этого столба — просто встаньте на его верхушку и наберите /ex note <player.location.below> as:arena1_pillar. Готово: любой скрипт, которому нужна эта локация, может просто написать arena1_pillar. Нужно, чтобы NPC посмотрел на столб? - look arena1_pillar. Нужно получить точное значение Y этого столба? <location[arena1_pillar].y>. Чисто, понятно, удобно!
Если, например, NPC должен пройти по конкретным точкам пути, можно использовать якоря. Выберите NPC, встаньте в каждую точку пути и наберите /npc anchor --save point1 (затем point2, 3 и т. д.). Тогда скрипт может использовать - ~walk <npc.anchor[point1]> (и затем point2, 3 и т. д.).
Если нужна локация, которая меняется со временем, или выбирается из списка, или привязана к игроку, а не к NPC, и так далее — в этом случае можно сохранить локацию во флаге.
Не печатайте сырую object-нотацию
Denizen использует object-нотацию внутри себя для отслеживания типов объектов. Например, l@ означает локацию, p@ — игрока, и так далее.
Это предназначено исключительно для внутреннего отслеживания сгенерированных значений. Скрипт НИКОГДА не должен содержать эти значения, набранные вручную.
Вместо того чтобы печатать нотацию, используйте один из трёх вариантов:
Просто не указывайте тип. Часто можно передать значение вообще без указания типа, и всё сработает (см. также «Не злоупотребляйте конструктор-тегами»).
Используйте тег, возвращающий нужное значение, вместо попыток задать сырое значение (см. «Не пишите сырые локации в скриптах»).
При необходимости используйте конструктор-тег (см. «Не злоупотребляйте конструктор-тегами»).
«Object hacking» — плохая идея
У Denizen есть стандартные форматы для большинства типов объектов. Например, ItemTag выглядит как i@stick[lore=Fancy stick].
Эти форматы используются для внутреннего отслеживания, и так и должны оставаться. Это сгенерированные внутренние значения, а не то, что стоит править в скриптах.
Иногда пользователи пишут вроде <player.item_in_hand>[lore=Fancy stick]. Если у игрока в руке палка, этот пример создаст новый предмет — палку с заданным lore, как и хотел пользователь. Однако если на предмете уже есть какие-то дополнительные данные, получится что-то вроде i@stick[display_name=Best stick][lore=Fancy stick], что уже не соответствует стандартному формату и работать не будет. (Правильный, «не-хакерский» способ получить копию предмета с дополнительными механизмами: <player.item_in_hand.with[lore=Fancy stick]>).
Стоит учитывать и то, что стандартные форматы меняются по мере того, как объекту нужно хранить разные формы внутренних данных. Например, MaterialTag изначально выглядел как m@chest или m@chest,2, а теперь — скорее как m@chest[direction=north]. Старые данные обычно ещё какое-то время парсятся корректно (по крайней мере, какое-то время после изменения), но любые скрипты, заточенные на конкретный формат, мгновенно ломаются.
Самый свежий пример object hacking'а, на котором люди обжигаются — хакинг ListTag. Изначальный формат ListTag был li@one|two|three, и он по-прежнему прекрасно работает на вход, но больше не является стандартным выходным форматом. У некоторых пользователей были строки вроде <some_list>|additional|value|here, которые работали со старым форматом. Однако формат изменили, чтобы поддерживать вложенные списки и пустые элементы, и его стали отличать дополнительным | на конце. Из-за этого изменения экранирование в сформированных хаком списках перестало парситься, и данные портились. (Правильный, «не-хакерский» способ добавить элементы в список: <some_list.include[additional|value|here]>).
Мораль: никогда не рассчитывайте на конкретный полный формат объекта. Для чтения или изменения любых данных внутри объекта всегда есть тег, который сделает это правильнее.
Инвентари в Creative обрабатываются на клиенте
Многие админы сидят в Creative даже во время работы и тестирования, в том числе когда пишут скрипты для игроков в выживании. Обычно это работает нормально, но есть случаи, когда различия между режимами могут укусить. Часть из них очевидна (например, нельзя протестировать скрипт, наносящий урон, если вы не получаете урон), а часть — нет. Неочевидный случай, который чаще всего сбивает с толку скриптеров, — это инвентари.
Обычно, в режиме выживания (или приключенческом) инвентари обрабатываются на сервере. Это значит, что сервер имеет финальное слово о том, какие предметы где лежат, а значит, любые серверные скрипты или плагины могут менять инвентари и взаимодействия с ними, и быть уверены, что всё сработает.
Однако в режиме Creative инвентари обрабатываются на клиенте. Это значит, что клиент (код, работающий на компьютере игрока, будь то ванильный клиент или модифицированный) решает, как выглядит инвентарь. Сервер тоже может менять инвентари и взаимодействия, но клиент может отменить эти изменения. Это приводит к таким вещам, как дублирование предметов при попытке отменить клик (сервер сказал: «нет, A: ты не подбираешь предмет, и B: предмет остаётся в исходном слоте» — а клиент решает: «A — отвергаю, я подобрал предмет, он мой, но с B согласен, предмет остаётся в исходном слоте» — то есть у нас теперь две копии одного предмета).
Стоит также подумать вот о чём: игроки в Creative могут спавнить любые предметы. В ванильном клиенте Minecraft это значит, что они могут в любой момент взять A: любой стандартный предмет или стак из креатив-списка, или B: сделать идеальную копию любого предмета через средний клик (это можно использовать, чтобы вытащить особый скриптовый предмет из GUI в свой инвентарь). А вот модифицированный клиент потенциально может спавнить любые предметы, в том числе с кастомными NBT-данными. Это важно учитывать каждый раз, когда вы привязываете какие-то возможности или системы к данным предмета. Рассмотрите такой скрипт:
dangerous_powertools:
type: world
events:
after player right clicks block with:powertool_item:
- execute as_server <player.item_in_hand.flag[powertool_command]>
powertool_item:
type: item
material: stick
flags:
powertool_command: broadcast It Works!
На первый взгляд, скрипт создаёт простые «powertool»-предметы, выполняющие кастомные команды… но из-за того, что as_server используется с данными конкретного предмета, игрок в Creative может сгенерировать предмет с любой командой и обойти любые ограничения по правам (злонамеренный игрок в Creative мог бы, например, выдать себе op, или забанить владельца сервера, или…).
Команда /ex — только для тестирования
Некоторые пользователи пытаются использовать /ex как универсальный скриптовый инструмент — но это не совсем правильно. Она предназначена только для тестирования и отладки.
Другими словами: /ex должна появляться только в вашем игровом чате. Её нельзя класть в скрипт, в командный блок, в команду другого плагина и т. п.
Один из самых частых случаев неправильного использования /ex — её вставка в конфиг другого плагина в качестве триггера, например в плагин магазина, где при покупке предмета настроено выполнение /ex run mytaskname player:%player%. Это плохой подход — каждый раз при срабатывании опции поднимается полный парсер и готовится отладочный вывод. Что делать вместо этого? Просто: сделайте command-скрипт! Пусть он делает всё, что нужно, а имя команды пропишите во внешнем плагине как команду-триггер. (Или, разумеется, можно вообще заменить внешний плагин скриптом Denizen, если чувствуете в себе силы!)