Долгосрочная память: флаги (flags)

На предыдущей странице вы узнали об определениях и об их использовании в роли краткосрочной памяти. На этой странице речь пойдёт о флагах.

Что такое флаги?

Флаги, как и определения, — это одна из форм памяти. Это точки данных, которыми вы свободно управляете и которые можно менять в скрипте, не затрагивая ничего, кроме собственных скриптов.

У флагов есть два ключевых отличия от определений:

  • Во-первых, флаги — это долгосрочная память, а определения — краткосрочная. Как вы узнали на прошлой странице, определение живёт ровно столько же, сколько живёт очередь. Флаги же хранятся гораздо дольше — при необходимости они могут храниться вечно.

  • Во-вторых, вы сами выбираете, к какому объекту привязан флаг. Когда вы разберётесь в системе флагов, станет понятно, что определения — это, по сути, частный случай флагов, привязанных исключительно к очередям.

Флаги тоже используют систему Data Actions, как и определения.

В отличие от команды define, команда flag всегда использует синтаксис data actions. То есть в простейшей форме установки значения используется : вместо пробела, как здесь: - flag <object> my_flag:my_value.

Базовое использование флагов

Прежде чем углубляться в теорию флагов, давайте попрактикуемся — с помощью команды /ex!

Откройте окно Minecraft и введите такую команду: /ex flag server greetable:World

Этой командой вы задали флаг с именем greetable и значением World на объекте server.

А теперь воспользуемся этим флагом — введите: /ex narrate "Hello, <server.flag[greetable]>!"

Если всё сделано правильно, в чате появится примерно следующее:

../../_images/hello_greetable.png

Флаги привязываются к объектам

Флаги всегда привязаны к какому-то объекту. В примере выше мы использовали объект server. Объект server — это специальный вспомогательный объект, существующий в том числе для того, чтобы флаги могли иметь глобальный статический «якорь». Флаги, привязанные к серверу, хранятся вечно и доступны откуда угодно и когда угодно.

Чаще всего флаги привязывают к игроку. Когда флаг привязан к игроку, это значит, что он есть только у этого конкретного игрока — у других он появится, только если для каждого из них снова выполнить команду flag.

Установить флаг на игрока очень просто — почти так же, как глобальный флаг на сервер, только вместо server нужно передать команде конкретного игрока.

Попробуйте ввести: /ex flag <player> status:learning

А затем: /ex narrate "<player.name> is <player.flag[status]> Denizen!"

Вы должны увидеть примерно такое сообщение:

../../_images/learning_denizen_flag.png

Если кто-то из других игроков попробует выполнить ту же команду /ex narrate без предварительного /ex flag, появится сообщение об ошибке — ведь флаг status есть только у вас, а у остальных его нет.

Зачем мне это вообще может понадобиться?

Примеры выше довольно абстрактные, поэтому давайте используем уже изученные части системы флагов, чтобы сделать небольшой скрипт, который больше похож на что-то реально полезное.

ore_counter:
    type: world
    events:
        after player breaks iron_ore:
        - flag player ores_broken:++
        - narrate "You have broken <player.flag[ores_broken]> iron ore so far! Keep it up!"

Этот скрипт будет вести текущий счёт того, сколько блоков железной руды добыл игрок. У каждого игрока — свой собственный счётчик (здесь используется data action «инкремент», которая описана на странице об определениях).

../../_images/ore_counter.gif

С помощью data action «clear», обозначаемой символом !, можно в любой момент удалить флаг. Поработав с примером скрипта выше, попробуйте выполнить /ex flag player ores_broken:!, чтобы удалить свой счётчик железной руды, а затем сломайте ещё один блок железной руды. Если всё прошло как надо, вы увидите, что тег флага подставляет значение 1 (то есть, насколько известно скрипту, вы только что впервые в жизни добыли железную руду).

А есть ли у тебя этот флаг?

Если вы хотите узнать, есть ли у объекта нужный флаг, ответ прост: has_flag! Этот тег выглядит так же, как тег flag, но вместо значения флага возвращает простое булево значение: true или false.

Его можно использовать внутри команды if, например: - if <player.has_flag[ores_broken]>: — тогда команды внутри if будут выполняться только если игрок уже сломал хотя бы один блок железной руды в примере скрипта выше.

Это же, по сути, и базовый способ реализовать значения вида «есть/нет» (в отличие от значений, у которых есть какие-то данные — как, например, у ores_broken с привязанным числом).

toggler_task:
    type: task
    script:
    - if <player.has_flag[my_first_toggle]>:
        - flag player my_first_toggle:!
        - narrate "Disabled your toggle."
    - else:
        - flag player my_first_toggle
        - narrate "Enabled your toggle."

Этот скрипт, запущенный через /ex run toggler_task, переключает флаг my_first_toggle — удаляет его или устанавливает (если значение передавать не нужно, его можно просто не указывать в команде flag), — а тег has_flag используется, чтобы понять, удалять его или устанавливать.

Когда вы освоите другие возможности — например, команды игрока, — с их помощью можно делать простые, но мощные штуки: команду, переключающую флаг при использовании, которая затем привязывается к какому-нибудь world-событию, проверяющему наличие флага, чтобы решить, применять ли особый эффект.

Флаги — это ядро Denizen

Флаги — это одна из ключевых возможностей Denizen… а значит, они встроены в самые разные места Denizen, чтобы работать с флагами было удобнее.

Например, если вы хотите, чтобы событие срабатывало только при наличии у игрока определённого флага, можно воспользоваться переключателем события flagged::

optional_ore_counter:
    type: world
    events:
        after player breaks iron_ore flagged:ore_quest:
        - flag player ores_broken:++
        - narrate "You have broken <player.flag[ores_broken]> iron ore so far! Keep it up!"

Это тот же скрипт ore_counter, что и раньше, но теперь он работает только для игроков, у которых есть переключаемый флаг ore_quest. Так обычный счётчик добытой руды превращается в часть квеста, в котором игроку нужно отправиться и добыть определённое количество железной руды, чтобы его завершить.

Подобных механик в Denizen много: есть переключатель flagged: для игрока, похожие переключатели server_flagged: и location_flagged: и т. д., matchables вида (x)_flagged, разнообразные аргументы команд (например, аргумент to_flagged команды announce), множество удобных тегов вроде <server.online_players_flagged[flag_name]> и так далее. По мере изучения Denizen и знакомства с мета-документацией вы встретитесь со многими из них.

Время жизни

Флаги персистентны. Это значит, что события, которые обычно приводят к исчезновению данных — например, перезапуск сервера, — не стирают данные флагов.

Есть всего четыре случая, в которых флаг пропадает:

  • Явное намеренное удаление в скрипте с помощью data action «clear».

  • Истечение срока жизни (описано в следующем разделе).

  • Привязанный объект исчез (например, флаг был на мобе, а моб умер).

  • И, разумеется: жёсткий крэш сервера или удаление файлов сервера.

Срок жизни флагов

Одно из типичных применений флагов — долгосрочные таймеры задержки, например квест или особый эффект, который можно получить раз в день. Вот как это может выглядеть:

once_a_day:
    type: task
    script:
    - if <player.has_flag[my_script_cooldown]>:
        - narrate "You can only get this reward once per day. You must wait <player.flag_expiration[my_script_cooldown].from_now.formatted>."
        - stop
    - flag player my_script_cooldown expire:1d
    - narrate "Here's your daily reward!"
    - give diamond

Этот скрипт выдаёт игроку алмаз, но не чаще одного раза в сутки, — в качестве кулдауна используется флаг.

В виде task-скрипта это может выглядеть не очень впечатляюще, но дальше в руководстве вы научитесь привязывать его к обработчику клика по NPC — и тогда у вас появится NPC, по клику на которого раз в сутки выдаётся награда. По сути, готовая фича для сервера!

../../_images/once_a_day.gif

Заметьте, что здесь используются две новые части системы флагов:

Во-первых, в команде flag появился аргумент expire:1d. Он делает ровно то, что вы подумали: флаг хранится всего один день… после чего он считается так, как если бы его не было вовсе. Входное значение — это объект DurationTag, который можно записать как 1d (один день), 5h (пять часов) и т. п. На основе этого значения вычисляется дата/время, к которому флаг должен перестать действовать (путём прибавления длительности к текущей дате/времени), и сохраняется вместе с флагом.

Во-вторых, в команде narrate использован тег flag_expiration. Как и следует из названия, он возвращает время, к которому флаг должен перестать действовать, в виде объекта TimeTag. Затем мы применяем тег TimeTag.from_now, который возвращает DurationTag — промежуток времени между «прямо сейчас» и сохранённым TimeTag (то есть в момент установки флага это будет 1d, через час — 23h и так далее), а после него — тег DurationTag.formatted, который превращает «сырой» объект длительности во что-то более-менее читаемое человеком.

Заметьте, что у сроков жизни флагов можно узнать, наступили ли они, — но на них нельзя «подписаться»: события «флаг истёк» не существует. Если вам интересно, почему так, обратитесь к технической документации по системе флагов.

Учтите, что для очень коротких и простых кулдаунов (короче нескольких часов) чаще лучше использовать команду ratelimit вместо флагов, например: - ratelimit <player> 1h. Команда ratelimit очень проста и удобна, но, что важно, её состояние не переживает перезапуск сервера, и у неё нет встроенной возможности выводить сообщение об оставшемся времени — как в скрипте выше.

К каким ещё объектам можно привязывать флаги?

На самом деле, флагами можно пометить множество объектов. Вот основные из них:

  • Сам сервер — как глобальный флаг, через ключевое слово server.

  • Любой игрок — через объект PlayerTag.

  • Любой NPC из Citizens — через объект NPCTag.

  • Любая сущность Minecraft — через объект EntityTag.

  • Любой блок в мире — через объект LocationTag.

  • Любой чанк в мире — через объект ChunkTag.

  • Любой предмет — но предметы устроены чуть иначе, это будет рассмотрено на странице о кастомных предметах.

  • Любой мир — через объект WorldTag.

  • Любая заметная область (заметки рассматриваются в продвинутом разделе) через объект CuboidTag, EllipsoidTag или PolygonTag.

  • Любой заметный инвентарь (заметки рассматриваются в продвинутом разделе) через объект InventoryTag.

  • Большинство других типов объектов тоже. Любой тип, который так или иначе однозначно идентифицируется, обычно поддерживает флаги.

Заметьте, что данные флагов, как правило, загружаются вместе с объектом — то есть серверные флаги загружаются при старте сервера, флаги игрока — при его входе на сервер и т. д.

Если игрок не в сети и при этом используются команды или теги flag, его данные будут автоматически загружены в этот момент.

А вот с сущностями всё иначе: если сущность деспаунилась (то есть находится в незагруженном чанке), надёжно загрузить её обратно нельзя, и работа с её флагами не получится.

Для блоков: если чанк, в котором находится блок, не загружен, его нужно сначала загрузить, прежде чем работать с флагами этого блока. Команда flag сделает это автоматически, а вот теги флагов автоматической загрузки чанка не делают — в этом случае стоит заранее выполнить команду chunkload, чтобы чанк точно был доступен.

Подробности о том, как именно разные типы объектов работают с флагами, можно найти в технической документации по системе флагов.

Имена флагов

Многие базовые ASCII-символы (например, ., !, % и т. п., в общем, всё, что не является буквой или цифрой) в именах сущностей (флагов, определений, контейнеров скриптов и т. п.) в Denizen могут иметь особое значение — и, кроме случаев, когда вы намеренно этим пользуетесь, таких символов лучше избегать. Лучше использовать простые текстовые имена, разделяя слова символом подчёркивания _ вместо пробелов.

Продвинутое замечание: карты и подкарты

Опытные пользователи, возможно, уже поняли, что флаги — это одна из форм data-карт (map), то есть системы данных «именованный ключ → значение». Таким пользователям могут быть интересны объекты MapTag или использование флагов для построения динамических вложенных структур (карты внутри карт). Система флагов Denizen автоматически поддерживает подкарты (submapping): в качестве разделителя используется символ ., например: - flag server myroot.mysubmap.mykey:myvalue. При этом флаг с именем myroot.mysubmap получит значение MapTag вида [mykey=myvalue], а флаг myroot — вложенное значение MapTag вида [mysubmap=[mykey=myvalue]]. Тег has_flag вернёт true и для has_flag[myroot], и для has_flag[myroot.mysubmap], и для has_flag[myroot.mysubmap.mykey].

Если всё это звучит для вас слишком мудрёно, не переживайте — использовать или понимать это прямо сейчас необязательно. Подробнее об этой системе, о том, как её применять и для чего она полезна, будет рассказано позже, в продвинутом разделе.

Особое замечание: зарезервированные имена флагов

Стоит знать, что имена флагов, начинающиеся с двух подчёркиваний (например, __name), считаются зарезервированными. Это связано с тем, что некоторые внутренние механизмы Denizen сами создают флаги и используют именно такое соглашение об именовании, чтобы не конфликтовать с пользовательскими скриптами. В частности, так делают команда zap (создаёт __interact_step) и команда cooldown (создаёт __interact_cooldown).

Ещё больше памяти

Опытные пользователи могут решить, что им нужны ещё более кастомные варианты хранения данных. Флагов и определений хватает в подавляющем большинстве случаев, но Denizen предлагает и дополнительные возможности (в основном ради совместимости с уже существующими системами данных), такие как команда YAML и команда SQL.