Обзор JMC 3.7

Прочитать на гитхабе

1 Введение

Несмотря на то, что MUD-ы являются весьма архаичным игровым жанром, игроки в них всё-таки ещё поигрывают, а некоторые серверы иногда даже развиваются. В связи с этим старый привычный многим клиент JMC даже для отнюдь не современных серверов не может обеспечить использование всех доступных возможностей, таких как сжатие (MCCP), дополнительные данные (GMCP, MSDP), шифрование трафика и т.д.

Основной целью доработок в версии 3.7 являлось “осовременивание” JMC и его адаптация к текущей ситуации в мире MUD-ов, насколько это возможно без существенного изменения всего привычного и полюбившегося.

В данной статье рассмотрены нововведения и изменения в JMC версии 3.7. Предполагается, что читатель уже достаточно хорошо знаком с клиентом JMC 3.6, благо статей по нему великое множество (например здесь). Также местами могут затрагиваться различные технические детали (например, протокол Telnet), но все они могут быть пропущены без существенного ущерба для понимания “в целом”.

Примечание: большое распространение в Интернете имеют версии JMC 3.26, 3.27, 3.27rus и т.п. Несмотря на высокое значение подверсии, всё это более старые варианты клиента, чем JMC 3.6. Единственное, что есть в этих версиях, но нет в 3.6/3.7 – это справка в hlp-формате (в 3.6/3.7 используется #help <команда>), дискретный RMA-процессор (в 3.6 отсутствует, а в 3.7 встроен в сам клиент) и русифицированные формы (в 3.6 отсутствуют, в 3.7 язык интерфейса зависит от языка ОС).

Разработка версии 3.7 основывалась на следующих принципах:

  • максимально возможная обратная совместимость;

    В идеале всё, что было настроено в версии 3.6, должно идентично выглядеть и функционировать в версии 3.7 (за исключением явных багов, конечно). Т.е. просто перезаписав новые бинарные (exe, dll) файлы поверх старых, клиент должен запуститься и работать 1-в-1 как было с теми же конфигурационными файлами.

  • сохранение концепции JMC как легковесного клиента с минимумом экранов настроек;

    Для многих одним из главных преимуществ JMC является его компактность, портабельность (в смысле переноса с одной машины на другую), нетребовательность к системным ресурсам и малое количество опций, настраиваемых через экранные формы. Гибкое поведение и функциональность при этом обеспечиваются средствами команд и конфигурационных файлов, что тоже для многих удобно, т.к. позволяет читать и писать настройки при помощи текстовых редакторов, не запуская программу вообще.

  • JMC – текстовый клиент для текстовых игр;

    Это означает, в частности, совершенное отсутствие ориентира на поддержку медийных расширений (иконки, графические карты, звуки и т.п.) и соответствующих протоколов (MXP, MSP). Такая поддержка, кроме прочего, противоречила бы и второму принципу компактности.

  • специфические возможности серверов не обязательно должны поддерживаться на уровне ядра, но доступ к ним при помощи конфигурирования у пользователя должен быть обязательно;

    Например, ряд MUD-ов используют свои нестандартные Telnet-опции (как 102 в Aardwolf или 87 в Адане). Встраивать их все в JMC не представляется целесообразным, тогда как пользователь всё-таки должен иметь к ним доступ (например, через ActiveX).

Ориентиром в разработке являлся популярный клиент TinTin++, следующий, в сущности, тем же принципам и имеющий с JMC общие корни, но, к сожалению, совершенно неудобоваримый на ОС Windows, а также имеющий слишком много различий в командах, т.е. несовместимые файлы конфигурации.

В результате внешний вид JMC практически не изменился, но используя новые функции его можно изменить по своему усмотрению в гораздо более широких пределах, чем прежде, даже без помощи ActiveX.

aardwolf arda bylins adan

Статья состоит из следующих разделов:

  • Краткий обзор: перечисляются новые опции, команды, глобальные переменные, методы ActiveX, а также изменения в старых командах, даются максимально короткие комментарии;
  • Описание нововведений: основной раздел, где каждое нововведение описывается более подробно;
  • Применение нововведений: конкретные примеры использования при игре на конкретных серверах;
  • Детали работы: некоторые технические детали функционирования JMC 3.7, которые могут быть полезны потенциальным желающим доделать/переделать программу или же просто интересны;
  • Заключение.

2 Краткий обзор

2.1 Новые опции (экран настроек)

  • запись в лог “как видит пользователь” либо “как показывает сервер” (без замен, подсветок и т.п.);
  • запись “живых” html-логов, которые “проигрываются” в том же темпе, в каком строки поступали при записи (пример);
  • MUD-эмулятор может читать строки из файла, при этом может обрабатывать RMA-команды (паузы между строками);
  • перенос строк можно отключать, а если он включен, то перенос меняется при изменении размеров окна;
  • возможность выделения текста мышкой в виде прямоугольной области;
  • возможность копирования в буфер ansi-команд;
  • отображение “скрытого” текста (у которого цвет фона и цвет текста совпадают);
  • отображение задержки пинга до сервера в строке статуса;
  • отображение пользовательского ввода на строке приглашения либо на новой строке;
  • кодировка MUD-сервера и кодировка лога;
  • метки времени у каждой строки основного окна.

2.2 Глобальные переменные

  • $CLOCK - время в “тиках” JMC (0.1 секунды);
  • $CLOCKMS - время в миллисекундах;
  • $RANDOM - случайное 32-битное число (от 0 до 4294967295);
  • $EOP - символ ASCII 0x01, JMC рассматривает его как маркер конца строки приглашения;
  • $EOL - символ ASCII \n, новая строка;
  • $ESC - символ ASCII 0x1B, начинает ESC-последовательность (ansi-команду);
  • $PING - задержка пинга до игрового сервера в миллисекундах;
  • $PINGPROXY - задержка пинга до прокси-сервера в миллисекундах;
  • $HOSTNAME - имя сервера, с которым установлено соединение;
  • $HOSTIP - адрес сервера, с которым установлено соединение;
  • $HOSTPORT - порт, с которым установлено соединение;
  • $COMMAND - последняя отправленная на MUD-сервер команда;
  • $FILENAME - имя файла, который в данный момент читается командой #read.

2.3 Новые команды

  • clear, wclear: очистка содержимого окон;
  • strcmp: простое сравнение двух строк (равны либо не равны);
  • loopback: отправка строки клиенту так, будто она получена от сервера (т.е. с проверкой на триггеры и т.п.);
  • broadcast: широковещательная рассылка строки клиентам JMC (в зависимости от настроек, может действовать во всей локальной сети);
  • proxy: настройка подключения через proxy-сервер (SOCKS4 / SOCKS5);
  • telnet: управление telnet-опциями (ECHO, NAWS, MTTS, MSDP, MSSP, MCCP1/2, GMCP, AYT, CHARSET и т.п.);
  • random: получение случайного числа, равномерно распределенного в заданном интервале;
  • srandom: инициализация генератора случайных чисел;
  • sync: внеочередное считывание и обработка содержимого всех входящих данных;
  • wsize: изменение размера окна (в символах с учетом текущего шрифта);
  • replace: замена текущей строки на новую (в обработчиках триггеров, как расширение старой команды #drop);
  • secure: управление настройками защищенного подключения (SSL3 / TLS1 / TLS1.1 / TLS1.2);
  • codepage: управление используемой кодировкой;
  • bar: генерация текстовых “процентных” полосок (типа [###..], (:::::::--) и т.п.);
  • oob: управление обработкой дополнительных (out-of-band) данных (GMCP / MSDP / MSSP);
  • match: сопоставление строки с регулярным выражением;
  • promptend: управление маркером конца строки приглашения;
  • mapper: управление картографом (конструирование и вывод карты).

    Почти на все новые команды есть краткая справочная информация, доступная по команде #help <команда>.

2.4 Изменения в старых командах

  • action: имеет первый необязательный параметр “тип” (по умолчанию текстовый, как старые триггеры, но теперь можно делать и цветные); также в шаблоне регулярного выражения распознает флаги: i, g, m;
  • log: имеет второй необязательный параметр количества последних строк из буфера, которые сразу запишутся при начале логирования; также позволяет управлять кодировкой логов;
  • alias: позволяет создавать алиасы с регулярными выражениями;
  • read: позволяет читать файлы, в которых одна команда JMC разбита на несколько строк (для читабельности);
  • flash: имеет необязательный параметр nopopup, если он указан, то окно JMC только мигает в панели задач, но не разворачивается.

2.5 Скриптинг (ActiveX объект jmc)

  • событие Prompt: возникает всякий раз при получении строки приглашения;
  • событие Telnet: возникает всякий раз при получении telnet-команды;
  • метод DoTelnet(cmd[,opt[,data]]): позволяет отправлять произвольные telnet-команды;
  • методы MSDP2GMCP(msdp), GMCP2MSDP(gmcp), MSSP2GMCP(mssp): облегчают работу с данными MSDP / MSSP, обеспечивая их конверсию в/из GMCP, с которым уже должно быть легко работать в основных скриптовых языках;
  • методы ToText(ansi), ToColored(ansi), FromColored(colored): облегчают работу с ansi-цветами из скрипта;
  • методы wGetWidth(wndNum), wGetHeight(wndNum): позволяют узнать текущий размер окон (в символах с учетом текущего шрифта).

2.6 Прочее

  • вывод текста не построчно, а сразу по мере получения от сервера (без подлаговывания);
  • в пользовательском вводе игнорируется символ ; внутри ansi-команды;
  • jmc.GetVar() работает с глобальными переменными;
  • jmc.DropEvent() в обработчике события Incoming сбрасывает строку;
  • частота перерисовки экрана ограничена 50 Гц, что ускоряет вывод большого количества данных;
  • полностью переделан парсер команд;
  • переход на юникод (внимание: популярный шрифт Fixedsys корректно отображает далеко не все символы);
  • отключение алгоритма Нейгла для убирания искусственного лага при вводе команды;
  • возможность использовать переменные в шаблонах регулярных выражений для триггеров и алиасов (шаблоны “реагируют” на смену значения переменной);
  • расширены возможности по размещению дополнительных окон (можно докать несколько в ряд/столбец);
  • более удобная (чем ESC-последовательности) система управления цветами в командах #showme, #output, #woutput, #status через ключевое слово colorcodes (в качестве цвета) и коды цветов &R (ярко-красный), &g (темно-зеленый) и т.п.;
  • все команды неявно содержатся в таблисте, т.е. можно набрать #co, а затем табуляцией перебирать команды #codepage, #colon, #comment, #connect.

3 Описание нововведений

3.1 Глобальные изменения

Во-первых, теперь внутреннее представление всех текстовых данных в JMC кодировано в Юникоде (хотя строго говоря, это не совсем так). Для управления кодированием-декодированием данных, поступающих с MUD-сервера и отправляемых ему, предусмотрены команда #codepage, опция в настройках, а также telnet-опция CHARSET (поддерживается далеко не всеми MUD-серверами). Потенциально теперь в JMC можно ввести любой символ, но надо помнить, что далеко не любой символ может быть представлен в кодировке, используемой в соединении; а даже если и может, то не факт, что он может быть представлен в кодировке, используемой во внутреннем представлении сервера.

Пример: кодировка соединения CP-1251 (#codepage 1251); можно ввести строку

г Comment ça va?

но на сервер отправится только

г Comment ca va?

(символ ç не имеет представления в CP-1251, потому будет выбран максимально близкий к нему).

Пример: кодировка соединения UTF-8 (#codepage utf-8), но внутреннее представление сервера KOI8-R; можно ввести строку

г Comment ça va?

и на сервер уйдёт именно она, но результатом будет что-то вроде

Вы говорите 'Comment ca va?'

или

Вы говорите 'Comment '

Также существует возможность управлять кодировкой лог-файлов. Она может быть либо фиксированной, либо автоматически устанавливаться в соответствии с кодировкой соединения.

При работе с MUD-эмулятором в старом стиле (при ручном вводе текста), необходимо установить #codepage unicode, поскольку именно такую кодировку будет иметь посылаемый эмулятором текст. При чтении данных из файла необходимо установить такую же кодировку, как в файле.

Во-вторых, добавлена русская локализация графического интерфейса. Язык пользовательского интерфейса определяется языком, выбранным в ОС Windows в качестве основного; это касается всех диалоговых окон, форм и т.п. Подход к локализации сообщений ядра (tintin) остался прежним: в файле language.ini хранятся все выдаваемые пользователю текстовые сообщения, каждый перевод (язык) в своей секции; затем название секции можно ввести для каждого JMC-профиля в отдельности и видеть сообщения на указанном языке. При этом добавлен автоматический выбор английской (English), русской (Russian) или украинской (Ukrainian) секции в зависимости от языка ОС в том случае, если никакая секция пользователем явно не задана (ранее по умолчанию бралась секция English).

В-третьих, полностью переработан парсер команд. В результате логика их выполнения сохранена, но существенно расширены возможности по рекурсивным обращениям к алиасам. Также при чтении команд из файла (командой #read) возможно читабельное форматирование текста.

Пример:

  #var S 0
  #var i 0
  #alias {CalcSum} 
  {
    #math S {$S + $i}; 
    #math i {$i + 1}; 
    #if {$i <= 1000} 
    {
      CalcSum
    }
  }
  CalcSum

Другие примеры можно посмотреть в скриптах самотестирования JMC, а также в разделе 4.

В-четвёртых, полностью изменен механизм вывода текста на экран. Если раньше часть строки не выводилась до тех пор, пока строка не будет сформирована полностью, то теперь пользователь видит все данные сразу по мере их поступления. Если раньше разбивка строк при переносе не менялась при изменении ширины окна, то теперь может меняться. Если раньше собственный вывод JMC (сообщений #message, по команде #showme и т.п.) мог перемешиваться с выводом MUD-сервера в одной строке, то теперь этого не происходит.

В-пятых, шаблон HTML-логов теперь можно изменить, он хранится в отдельном файле (html.log.template). Например, можно добавить в шапку лога информацию о клане/гильдии, свои контактные данные и т.п. Если этот файл удалить, то будет использоваться минимизированный встроенный шаблон.

3.2 Команды

  • #clear, #wclear <номер_окна>

    Очищает содержимое главного или одного из дополнительных окон.

  • #strcmp {<строка1>} {<строка2>} {<если-равны>} [{<если-не-равны>}]

    Сравнивает значения первых двух аргументов как строки. Если строки совпадают, то выполняется скрипт, указанный третьим аргументом, если не совпадают, то четвертым. Сравнение производится с учетом регистра. По сути, является упрощённой версией #match.

    Пример:

 #strcmp {$var1} {$var2} {#showme var1 == var2} {#showme var1 =/= var2}
  • #loopback {<строка>}

    JMC отправляет строку, указанную в качестве аргумента, самому себе. Строка будет обработана при ближайшей проверке наличия входных данных (< 20 мс). Строка будет обработана как если бы она поступила с сервера (т.е. со всеми заменами, проверкой на триггеры и т.п.), за исключением логирования: строка будет помещена в лог только в том случае, если установлен метод логирования “Как видит пользователь”.

    Пример:

 #loopback {Connected to $HOSTNAME}
  • #broadcast

    Показывает текущие настройки механизма широковещания.

    #broadcast enable|disable

    Включает или выключает прослушивание широковещания (по умолчанию отключено).

    #broadcast filterip|filterport on|off

    Включает или выключает фильтрацию входящих широковещательных данных по IP и/или порту. Если фильтрация включена, то проходят только сообщения, отправленные со своего IP и/или с того же самого порта.

    #broadcast port <номер>

    Устанавливает порт, на котором ожидаются широковещательные сообщения и с которого они рассылаются (по умолчанию 8136).

    #broadcast send {<строка>}

    Отправляет строку в широковещательном режиме. Она будет получена всеми копиями приложения JMC (включая выполняющее команду) с включенным широковещанием и настроенными на тот же порт. Если эта строка пройдёт фильтрацию по адресу и порту отправителя, то она будет обработана как после выполнения команды #loopback.

    Пример:

 #broadcast enable
 #broadcast port 12345
 #broadcast send Connected to $HOSTIP:$HOSTPORT
  • #proxy

    Показывает текущие настройки прокси-сервера при подключении. Эти настройки будут применены во время выполнения подключения командой #connect и их изменение на уже установленное соединение никак не влияет.

    #proxy disable

    Отключает использование прокси-сервера при подключении.

    #proxy {socks4|socks5} {<IP>}[:<порт>] [<логин>] [<пароль>]

    Настраивает тип прокси-сервера (SOCK4/SOCKS5) и его адрес, а также, опционально, порт (по умолчанию 1080), имя пользователя и пароль.

    #proxy list {<имя_файла>} [<номер_строки>]

    Берет настройки прокси-сервера из указанного файла. Если указан номер строки, то настройки берутся из неё. Если номер не указан, то при первичном обращении к файлу будет взята первая строка, а при каждом последующем – следующая строка. Строка должна быть в виде аргументов команды proxy (например, socks4 127.0.0.1:9150 tor).

    Пример:

 #proxy socks5 81.33.12.34:4500 admin qwertyuio
  • #telnet

    Отображает список включенных на данных момент telnet-опций.

    #telnet <опция>

    Отображает статус указанной telnet-опции, которая может быть задана своим кодом (от 1 до 254), либо являться одним из известных JMC наименований: EOR, ECHO, NAWS, MTTS, MSDP, MSSP, MCCP1, MCCP2, MCCP (то же, что MCCP2), MSP, MXP, ATCP, GMCP, AYT, CHARSET

    #telnet <опция> on|off

    Включает или выключает использование telnet-опции. Если опция включена, то JMC будет автоматически отвечать DO/WILL на запросы WILL/DO соответственно. Если опция выключена, то JMC будет автоматически отвечать DONT/WONT на запросы WILL/DO соответственно.

    Автоматическая обработка внутри самого клиента реализована для следующих опций: GA, EOR, ECHO, NAWS, AYT, MTTS, MCCP, CHARSET (см. 5.4).

    Также данные, поступающие с опциями MSDP, GMCP, MSSP преобразуются в вид, удобный для дальнейшей обработки пользователем (см. 5.5).

    #telnet debug [on|off]

    Включает или выключает вывод сообщений о полученных/отправленных telnet-сообщениях (начинающихся с IAC).

    Пример:

 #telnet MCCP on
 #telnet MSDP on
 #telnet MXP off
 #telnet CHARSET on
  • #random <имя_переменной> {<минимум>} {<максимум>}

    Устанавливает переменной с указанным именем случайно выбранное значение из диапазона [<минимум>..<максимум>).

    #random {имя_переменной} {<максимум>}

    Устанавливает переменной с указанным именем случайно выбранное значение из диапазона [0..<максимум>).

    #random {имя_переменной}

    Устанавливает переменной с указанным именем случайно выбранное значение из диапазона [0..100).

    Пример:

 #random MyRandVar
 #random Dice1d6 1 7
  • #srandom [<инициализатор>]

    Инициализирует генератор случайных чисел заданным числом либо текущим временем.

  • #sync

    Принудительно (вне очереди) считывает все входящие данные: от MUD-сервера, от других копий JMC (широковещанием), от команды #loopback и от MUD-эмулятора.

  • #wsize <окно> <ширина> <высота>

    Изменят размер указанного дополнительного окна вывода. Ширина и высота указываются в количестве символов текущего шрифта.

    Пример:

 #wsize 0 40 10
  • #replace <строка>

    При выполнении в обработчике триггера, заменяет текущую обрабатываемую строку на указанное значение. При включении опции #multiaction проверенные к моменту выполнения команды триггеры не перепроверяются.

  • #secure

    Отображает текущие настройки защиты (шифрования) соединения. Эти настройки будут применены во время выполнения подключения командой #connect и их изменение на уже установленное соединение никак не влияет.

    #secure disable

    Отключает защиту соединения.

    #secure [ssl3|tls1|tls1.1|tls1.2] [ca clear|<имя_файла>]

    Устанавливает защиту соединения. Можно указать протокол (SSL3, TLS1 / TLS1.1 / TLS1.2) или файл CA (Certificate Authority).

    #secure enable

    То же, что #secure tls1

    Пример:

 #secure tls1.2 ca myca.pem
  • #codepage

    Отображает текущую кодировку, используемую при взаимодействии с MUD-сервером. Эта настройка будет применена во время выполнения подключения командой #connect и её изменение на уже установленное соединение никак не влияет.

    Кодированию и декодированию, помимо основного игрового текста и команд, подвергаются также все данные, пересылаемые в рамках telnet-расширений (MSDP, GMCP и т.д., кроме NAWS).

    Также с использованием выбранной кодовой страницы декодируются данные из MUD-эмулятора.

    #codepage list

    Отображает список всех доступных кодовых страниц, обнаруженных в ОС.

    #codepage <номер>|<название>

    Устанавливает кодировку с указанным номером или названием (название должно в точности соответствовать тому, которое выводится по команде #codepage list).

    #codepage default

    Устанавливает кодировку, выбранную в системе Windows по умолчанию.

    Пример:

 #codepage list
 #codepage 20866
 #codepage utf-8
 #codepage windows-1251
  • #bar <имя_переменной> <ширина> <символ_полный> <символ_пустой> <значение> <максимум>

    Помещает в заданную переменную строку, являющуюся символьным представлением ASCII-полоски (типа [###..], (:::::::--) и т.п.), соответствующей заданному значению.

    Пример:

 #bar hpbar 20 : - 540 789

{hpbar} == {:::::::::::::-------}

  • #oob

Отображает список доступных для настройки OOB (out-of-band) протоколов (или модулей): GMCP, MSDP, MSSP.

#oob <модуль>

Отображает список включенных подмодулей указанного модуля.

Включение подмодуля означает, что при получении сообщений, которые с ним связаны (например, получение Char.Vitals.Hp при включенном Char), будет отображаться системное сообщение об этом, на которое можно навесить триггер. Также включенные подмодули будут запрашиваться у сервера автоматически при установлении соединения, например:

  • для каждого подмодуля GMCP будет выполнено request <подмодуль>;
  • для каждого подмодуля MSDP будет выполнено REPORT <подмодуль>.

    #oob <модуль> disable|enable

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

    Например, для GMCP включаются: core, char, room, comm, group.

    #oob <модуль> add|del <подмодуль>

    Включает/отключает подмодуль указанного модуля.

    #oob <модуль> request <подмодуль>[ <подмодуль>[ ...]]

    Запрашивает указанный(е) подмодуль(и).

    Для GMCP выполняет запросы request <подмодуль>, для MSDP выполняет запросы SEND <подмодуль>, для MSSP отправляет telnet-команду DO MSSP.

    Пример:

 #oob MSSP enable
 #oob GMCP add char comm room
 #oob MSDP request ROOM

Полученные по этим протоколам данные становятся доступны через фиктивные переменные (не сохраняющиеся в файле профиля и не доступные для записи). Их именование проще всего понять на примерах:

  • таблицы (словари, хеши): $GmcpCharBaseVitalsHp, $MsdpRoomName, $MsspPlayers, …
  • списки (массивы): $MsdpReportableVariablesLength, $MsdpReportableVariablesValue1, $MsspPortLength, …
  • таблицы как списки: $GmcpRoomExitsLength, $GmcpRoomExitsKey1 (направление), $GmcpRoomExitsValue1 (номер комнаты в этом направлении), … Можно использовать неполное именование переменных. Так, например, #showme $GmcpCharBase при наличии таких данных отобразит что-то вроде {"Vitals": {"Hp": 123, "Ma": 456}, "Position": "sleep", ...}. Таким же образом через переменные $Gmcp, $Msdp, $Mssp можно увидеть все полученные по этим протоколам и актуальные на данный момент данные.
  • #match {<шаблон>} {<строка>} {<если-соответствует>} [{<если-не-соответствует>}]

    Проверят соответствие строки шаблону (регулярному выражению). В скрипте, выполняемом в случае соответствия, можно использовать переменные %1, %2, …, захваченные скобками регулярного выражения. Если в шаблоне указан флаг g, то скрипт соответствия будет вызван для каждого совпадения.

    Пример:

 #match /ab*cb?d/ qabbbbbcd {#showme matched} {#showme not matched}
 #match {/(\d+)/(\d+)(\w+)/g} {<123/456hp 789/1023ma 111/222mv>} {#showme %2: %0 of %1}
  • #promptend

    Показывает текущие настройки маркера (последовательности символов) конца строки приглашения.

    #promptend disable

    Отключает идентификацию строк приглашения по маркеру.

    #promptend <маркер> [<замена>]

    Включает определение конца строки приглашения по указанному маркеру. Если указана строка, заменяющая маркер, то во всех строках приглашения такая замена будет произведена перед дальнейшей работой (проверкой на триггеры, выводом на экран, добавлением в лог).

    Эта команда является альтернативой стандартной идентификации строки приглашения (telnet-сообщения GA и EOR) и будет полезна для серверов, не поддерживающих эти механизмы, но позволяющих произвольным образом менять строку приглашения или хотя бы просто делать её окончание однозначно идентифицируемым (с учётом ESC-последовательностей, т.е. цветовых команд).

    Пример:

 #promptend {>>}
 #promptend {>secret.password<} {>}
 prompt < %h/%H %m/%M %v/%V >secret.password<
 #promptend {> }
  • #mapper add room <номер>|auto <название> [<описание> [<зона> [<комментарий> [<флаг1>{,<флаг2>} [<имя_переменной>]]]]]

    Добавление новой “комнаты” (клетки) с указанными номером, названием, описанием, названием зоны, пользовательским комментарием и набором флагов. Номером комнаты может являться либо натуральное число, либо пара натуральных чисел, разделённых точкой (например 123 или 123.456).

    Также возможно использование автонумерации комнат указанием auto вместо номера комнаты. В этом режиме JMC пробует автоматически генерировать номера с учетом следующих правил:

    1. номер генерируется как пара ключей: <первичный>.<вторичный>;
    2. первичный ключ – это хеш-код названий комнаты и зоны, а также ее описания;
    3. вторичный ключ – это хеш-код упорядоченного набора всех возможных путей определённой длины, исходящих из комнаты, при этом в записи путей используются только первичные ключи комнат.

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

    #mapper add exit <номер-из> <команда> <номер-в> [bidirectional] [<имя_переменной_из> [<имя_переменной_в>]]

    Добавить переход из одной комнаты в другую при помощи указанной команды. Если это известная команда-направление (например, “север”), то указание флага bidirectional создаст переход в обратном направлении. Если одна или обе задействованных комнаты имеют автоматически сгенерированный номер, то при добавлении перехода он может поменяться; в этом случае можно указать имена переменных, в которых будут сохранены новые значения номеров.

    #mapper add direction <команда>{,<синоним>} <dx> <dy> <dz> [<команда-обратно>{,<синоним>} [<знак>]]

    Добавляет известное направление на карте, доступное по указанной команде или её синониму(ам). Комната в этом направлении будет отрисована на карте с указанными смещениями по X и Y, а комната, содержащая такой выход будет помечена указанным знаком. Если указана команда обратного перемещения, то <dx>, <dy> и <dz> для неё домножатся на -1.

    Синонимы команды могут быть использованы в картографе всюду вместо самой команды.

    #mapper add flag <название> <знак> [<цвет>]

    Добавляет флаг, который могут иметь комнаты. Название флага используется при создании комнат (#mapper add room), знак и цвет используется при отрисовке комнаты (#mapper print).

    #mapper del room <номер>

    Удаляет комнату с указанным номером.

    #mapper del exit <номер> <команда>

    Удаляет выход из комнаты с указанным номером по указанной команде.

    #mapper merge <номер1> <номер2> [<имя_переменной>]

    “Сливает” (объединяет) две комнаты с указанными номерами в одну, при этом объединяя их входы и выходы. Обе комнаты должны обладать автоматически сгенерированным номером. Номер новой комнаты может быть получен в переменной с указанным именем.

    Следует учитывать, что эта операция может вызвать “цепную реакцию”, т.е. если обе комнаты имеют выходы в одном направлении, ведущие к разным “соседям”, то и соседи будут объединены (“слиты”).

    #mapper set flag <номер> <флаг>

    Добавляет флаг для комнаты с заданным номером.

    #mapper set comment <номер> <строка>

    Устанавливает пользовательский комментарий комнате с указанным номером.

    #mapper set avoidance <номер> <значение>

    Устанавливает уровень “избегания” комнаты с указанным номером. Это значение используется при поиске “кратчайших” путей на карте. По умолчанию все комнаты имеют уровень избегания 1.

    Уровень 0 означает полный запрет прохода через комнату.

    #mapper set pass <номер> <команда> <команда-открытия>

    Устанавливает команду, выполнение которой делает доступным выход из комнаты с указанным номером по указанной команде.

    Пример:

    #mapper set pass 123 север {взять ключ сундук;отпереть дверь;открыть дверь}
    #mapper set pass 321 east {say Open, Sesame!}
    

    #mapper set maxdifflen|maxidentlen <значение>

    Установка ключевых параметров алгоритма автонумерации комнат. При формировании вторичного ключа (см. #mapper add room auto) используются пути длиной до maxdifflen, если они содержат комнаты с разными первичными ключами, и длиной до maxidentlen, если они содержат комнаты с одинаковыми первичными ключами.

    #mapper reset flag <номер> <флаг>

    Сбрасывает указанный флаг для комнаты с указанным номером.

    #mapper get name|descr|area|comment|flags|exits|flag<N>|exit<N> <номер> <имя_переменной>

    Помещает в переменную с указанным именем определенной свойство комнаты с указанным номером:

    • name/descr/area/comment – название, описание, название зоны, пользовательский комментарий
    • flags/exits – количество флагов и выходов
    • flag<N> – флаг номер N (первый флаг имеет номер 1)
    • exit<N> – команда выхода в направлении номер N (первый выход имеет номер 1)

    #mapper get flag|exit <значение> <номер> <имя_переменной>

    Помещает в указанную переменную “0” либо “1” в зависимости от того, имеет ли комната с указанным номером указанный флаг или команду выхода.

    #mapper autoid <название> <описание> <зона> <имя_переменной>

    Помещает в указанную переменную хеш-код (первичный ключ) комнаты с указанными параметрами. Если в карте есть комната с таким первичным ключом, то в переменную также добавляется её вторичный ключ (т.е. формируется полный номер).

    #mapper track add <команда> <номер>

    Используется для отслеживания текущего положения на карте в режиме автогенерации номеров. Добавляет в историю перемещений указанный номер комнаты, в которую пришли указанной командой.

    #mapper track clear

    Очищает историю перемещений.

    #mapper track position <имя_переменной>

    Помещает в указанную переменную номер наиболее “подходящей” (вероятной) комнаты, в которой можно находиться с имеющейся на данный момент историей перемещений.

    #mapper search [near <номер_комнаты>] [<номер_результата>] {name|area|description|comment|flag <значение>} <имя_переменной>

    Выполняет поиск комнаты, имеющей указанные название, название зоны, описание, комментарий и/или флаг. Найденные комнаты упорядочиваются по номеру либо по “расстоянию” (с учетом уровней “избегания”) до указанной комнаты (near <номер_комнаты>). Из итогового списка берется первая комната, а если указан <номер_результата>, то комната с указанной позицией. Номер найденной комнаты сохраняется в указанную переменную.

    Пример:

    #mapper search name {secret} SecretRoomID
    #mapper search 2 name {armoury} flag {vendor} flag {plains} area {Sky city} SecondArmouryInSkycityID
    #mapper search near 123 name {cave} flag {danger} NearestDangerCave
    

    #mapper path <номер_из> <номер_в> [<имя_переменной>]

    Ищет кратчайший путь из одной комнаты в другую. Результат помещается в память пути JMC (см. старую команду #path и всё что к ней прилагалось). Если задан последний параметр, то первый шаг (команда, связанная с перемещением в следующую комнату) помещается в переменную с указанным именем.

    #mapper clear

    Очищает все данные картографа (комнаты, переходы, направления, флаги, историю перемещений).

    #mapper write <имя_файла>

    Записывает все данные картографа в файл: направления, флаги, комнаты, переходы.

    Файл представляет собой простой набор команд JMC (#mapper ...), а потому может быть легко просмотрен и, при необходимости, откорректирован вручную. При этом загрузка данных из этого файла может быть осуществлена с использованием старой команды #read <имя_файла> (как правило, с предшествующей командой #mapper clear).

    При сохранении карты в файл, автоматически сгенерирпованные номера комнат записываются “как есть” и при последующей загрузке уже не считаются автоматически сгенерированными.

    #mapper print [tiny|normal|full] [nocolors|html] [crop] [exits] [header] [description] [flags] [comment] [main|w<N>|<имя_файла>] [<ширина>x<высота>] [<номер>]

    Выводит текстовое представление карты в указанное окно: main – основное (по умолчанию), w<N> – N-ое окно вывода, <имя_файла> – вывод в файл. Можно задать размер карты: tiny (каждая комната 1х1 символ), normal (5x3, по умолчанию), full (7х5). Можно отключить ansi-цвета (nocolors) или заменить их html-тегами (html). Можно задать размер карты в символах <ширина>x<высота> (по умолчанию – размеры окна вывода или 80х24 для файла), а также “урезание” пустых строк/столбцов по краям (crop). Можно задать отображение дополнительной информации (header, description, flags, comment).

    Если указан номер комнаты, то комната с этим номером помещается в центр отображаемого фрагмента карты, если номер не указан, то берётся комната с наименьшим номером.

    Пример:

    #mapper clear
    #mapper add direction {восток,в,east,e} 1 0 0 {запад,з,west,w}
    #mapper add direction {север,с,north,n} 0 1 0 {юг,ю,south,s}
    #mapper add direction {вверх,вв,up,u}   0 0 1 {вниз,вн,down,d}
    #mapper add flag {магазин}  {$}  {light green}
    #mapper add flag {кузница}  {\%\}  {brown}
    #mapper add room 1001 {Перекресток} {} {Деревня} {} {}
    #mapper add exit 1001 {север} 1002 {}
    #mapper add exit 1001 {юг} 1003 {}
    #mapper add exit 1001 {восток} 1004 {}
    #mapper add exit 1001 {запад} 1005 {}
    #mapper add room 1002 {Северная улица} {} {Деревня} {} {}
    #mapper add exit 1002 {юг} 1001 {}
    #mapper add exit 1002 {запад} 1006 {}
    #mapper add room 1003 {Южная улица} {} {Деревня} {} {}
    #mapper add exit 1003 {север} 1001 {}
    #mapper add exit 1003 {запад} 1007 {}
    #mapper add room 1004 {Восточная улица} {} {Деревня} {} {}
    #mapper add exit 1004 {север} 1008 {}
    #mapper add exit 1004 {запад} 1001 {}
    #mapper add room 1005 {Западная улица} {} {Деревня} {} {}
    #mapper add exit 1005 {восток} 1001 {}
    #mapper add room 1006 {Хижина} {} {Деревня} {} {}
    #mapper add exit 1006 {восток} 1002 {}
    #mapper add room 1007 {Закусочная} {} {Деревня} {} {магазин}
    #mapper add exit 1007 {восток} 1003 {}
    #mapper add room 1008 {Мастерская} {} {Деревня} {} {кузница,магазин}
    #mapper add exit 1008 {юг} 1004 {}
    #mapper print normal exits header flags 1001
    
  • #action {/<шаблон>/[i|m|g]} <реакция> [<приоритет>] [<группа>]

    Шаблонам триггеров в виде регулярных выражений теперь можно устанавливать следующие флаги (после закрывающего слеша):

    • i – нечувствительность к регистру;
    • m – многострочный шаблон, т.е. на соответствие ему проверяются сразу несколько подряд идущих строк (механизм работы описан в п. 5.1);
    • g – при включении #multiaction on, разрешает несколько срабатываний триггера на одну строку (группу строк для многострочных триггеров).

    #action <тип> {<шаблон>} <реакция> [<приоритет>] [<группа>]

    Может быть указан тип триггера (по умолчанию text), который влияет на обработку проверяемой строки (группы строк) непосредственно перед проверкой на соответствие шаблону. Может иметь следующие значения:

    • text – удаляются все esc-последовательности (цвета), как было раньше;
    • raw – ничего не удаляется, текст проверяется “как есть”;
    • color – все цветовые команды удаляются, но вместо них добавляется специальная разметка, которая указывает как раскрашен текст; применяется следующая разметка:
    • &d/&D - черный/темно-серый
    • &r/&R - темный/яркий красный
    • &g/&G - темный/яркий зелёный
    • &y/&Y - темный/яркий жёлтый
    • &b/&B - темный/яркий синий
    • &p/&P - темный/яркий розовый
    • &c/&C - темный/яркий голубой
    • &w/&W - светло-серый/белый
    • && - символ &

    Механизм разметки таков, что одинаково выглядящие строки всегда будут размечены строго одинаково (не принимая в расчёт цвет фона). Подробное описание приводится в 5.2, но общую суть можно понять, добавив триггер #act color {/(.*)/} {#output %0} и побродив немного по какому-нибудь миру.

    Пример:

    ## вывод мини-карты в окно с сохранением цветов для МПМ "Былины"
    #action RAW {/^:(.*)/} {#woutput 1 {\%0};#drop} {5} {default}
    
    ## реакция на обезоруживание в Арда MUD с условием цвета строки
    #action COLOR {^&G%1 выби%2 оружие у вас из рук!} {onDisarm} {5} {default}
    
  • #log <имя_файла> [all|<количество_строк>] [append|overwrite|html] Второй необязательный параметр позволяет добавить в открываемый лог-файл все или заданное количество строк из буфера. Это может быть полезно, если на начало записи уже произошло что-то важное/интересное.
  • #alias {/<шаблон>/[i]} {<команда>} [<группа>] Теперь в шаблонах алиасов можно использовать регулярные выражения, аналогично триггерам. Из флагов при этом допустим только флаг i – нечувствительности к регистру.

    Пример:

    #alias {/t (\w+) (.*)/} {tell %0 [Vasya] %1}
    

3.3 Интерфейс объекта jmc (скриптинг)

  • Событие Prompt

    Обработчик события вызывается в каждом из нижеперечисленных случаев:

    1. получена telnet-команда Go Ahead (GA);
    2. получена telnet-команда End Of Record (EOR);
    3. обнаружен маркер конца строки приглашения, установленный командой #promptend (если этот механизм не отключен командой #promptend disable);
    4. с сервера получена незавершенная строка и в течение определенного времени не приходит новых данных (старая настройка Uncompleted lines delay/Задержка незавершенных строк);
    5. с сервера получена незавершенная строка и пользователь отправляет команду на сервер.

    jmc.Event содержит весь текст (как правило, многострочный), полученный с момента предыдущей генерации события Prompt либо с момента установления соединения.

    Пример:

    jmc.RegisterHandler('Prompt', 'jmc.ShowMe("Prompt (last) line: " + jmc.Event.substr(jmc.Event.lastIndexOf("\\n") + 1))');
    
  • Событие Telnet

    Обработчик события вызывается при каждом получении от сервера telnet-команды.

    • jmc.Event(0) содержит код (число) команды;
    • jmc.Event(1) содержит код (число) опции, если команда подразумевает использование опции (например, для команды 253 – DO);
    • jmc.Event(2) содержит данные, если команда подразумевает их наличие (например, команда 240 – End Subnegotiation).

    Пример:

    jmc.RegisterHandler('Telnet', 'if (jmc.Event(0) == 249 /* GA */) jmc.ShowMe("GA received")');
    
  • Метод DoTelnet(cmd[,opt[,data]])

    Отправляет серверу telnet-команду cmd. Если указана опция opt, то отправляется и она. Данные data могут быть указаны только если команда cmd равна 250 (Begin Subnegotiation).

    Пример:

    jmc.RegisterHandler('Telnet', 'if (jmc.Event(0) == 251 /* WILL */ && jmc.Event(1) == 246 /* AYT */) jmc.DoTelnet(254 /* DONT */, 246 /* AYT */)');
    
  • Группа методов MSDP2GMCP(msdp), GMCP2MSDP(gmcp), MSSP2GMCP(mssp)

    Предназначена для облегчения работы с данными MSDP/MSSP путем конвертирования их в/из формат GMCP, который уже очень близок к JSON, т.е. с которым должно быть удобно работать из большинства скриптовых сред.

    Пример:

    jmc.ShowMe(jmc.MSDP2GMCP("\x01ROOM\x02\x03\x01VNUM\x026008\x01NAME\x02The forest clearing\x01AREA\x02Haon Dor\x01TERRAIN\x02forest\x01EXITS\x02\x03\x01n\x026011\x01e\x026007\x04\x04"));
    

    ROOM {"VNUM": "6008", "NAME": "The forest clearing", "AREA": "Haon Dor", "TERRAIN": "forest", "EXITS": {"n": "6011", "e": "6007"}}

    jmc.RegisterHandler('Telnet', 'if (jmc.Event(0) == 240 /* SE */ && jmc.Event(1) == 70 /* MSSP */) {MSSP = {}; eval("MSSP = " + jmc.MSSP2GMCP(jmc.Event(2))); jmc.ShowMe("Players: " + MSSP.PLAYERS);}');
    
  • Группа методов ToText(ansi), ToColored(ansi), FromColored(colored)

    Облегчает работу с ansi-цветами из скриптов, а именно:

    • удаляет все ansi-команды (ToText);
    • преобразует ansi-команды в цветовую разметку как в случае “цветных” (colored) триггеров (ToColored);
    • преобразует цветовую разметку в ansi-команды (FromColored);

    Пример:

    jmc.ShowMe(jmc.ToText('\x1B[0;37;40m\x1B[1;31mR\x1B[0;33ma\x1B[1;33mi\x1B[1;32mn\x1B[1;36mb\x1B[1;34mo\x1B[0;34mw'));
    

    Rainbow

    jmc.ShowMe(jmc.ToColored('\x1B[0;37;40m\x1B[1;31mR\x1B[0;33ma\x1B[1;33mi\x1B[1;32mn\x1B[1;36mb\x1B[1;34mo\x1B[0;34mw'));
    

    &RR&ya&Yi&Gn&Cb&Bo&bw

    jmc.ShowMe(jmc.FromColored('&RR&ya&Yi&Gn&Cb&Bo&bw'));
    

    \x1B[0;37;40m\x1B[1;31mR\x1B[0;33ma\x1B[1;33mi\x1B[1;32mn\x1B[1;36mb\x1B[1;34mo\x1B[0;34mw

  • Методы wGetWidth(wndNum), wGetHeight(wndNum)

    Возвращают ширину и высоту окна в символах текущего шрифта соответственно. Могут быть полезны для форматирования вывода информации из скрипта (например, для определения ширины и высоты выводимой таблицы аффектов).

    Пример:

  var h = jmc.wGetHeight(1), 
  w = jmc.wGetWidth(1)-1; 
  for (var i = 0; i < h; i++) {
  var l = ''; 
  for (var j = 0; j < w; j++) 
    if ((i - h/2)*(i - h/2) + (j - w/2)*(j - w/2) < (h/2)*(h/2)) 
      l += '#'; 
    else 
      l += ' '; 
   jmc.wOutput(1, l);
  }

3.4 Настройки

  • Управление отображением пользовательского ввода (User’s input/Ввод пользователя).

    Доступно три варианта:

    • не отображать ввод (Don’t display/Не отображать);
    • отображать в конце строки приглашения (On prompt line/На строке приглашения);
    • отображать на новой строке, как было раньше (On new line/На новой строке, по умолчанию).
  • Управление отображением дополнительной информации (Display settings/Отображение).

    Доступны следующие новые опции:

    • перенос строк, раньше нельзя было отключить (Wrap lines/Перенос строк);
    • отображение “скрытого” текста, т.е. такого, у которого цвета текста и фона совпадают, при этом цвет текста меняется на противоположный (Show hidden text/Подсветка невидимого текста);
    • отображение информации о пинге до сервера, выводится в нижнем правом углу (Show ping/Показывать пинг);
    • отображение момента времени для каждой выведенной в основное окно строки (Line timestamps/Метки времени).
  • Управление выделением текста для копирования (Selection/Выделение текста).

    Доступны следующие новые опции:

    • выделение прямоугольной области, полезно при копировании фрагментов ASCII-графики (Rectangular selection/Прямоугольная область);
    • удаление ESC-последовательностей при копировании, полезно при копировании текста с сохранением цвета на серверах, не удаляющих ESC-последовательности из пользовательского ввода (Remove ESC-sequences/Удалять ESC-команды).
  • Метки времени при логировании в формате HTML (Timestamps/Метки времени).

    Если выбрана эта опция, то в html-лог будут проставлены метки времени. Такой лог может быть “проигран” в браузере с включенным javascript как “фильм”, в том темпе, в котором лог записывался.

  • Стратегия записи в лог (Log contents/Содержимое лога).

    Можно выбрать одну из двух:

    • запись “как показывает сервер”, т.е. без пользовательских замен, подсветок, отображений дополнительной информации (#showme) и т.п.;
    • запись “как видит пользователь”.

    Команды #logadd и #logpass имеют приоритет над этой настройкой.

4 Применение некоторых нововведений

4.1 Пользовательский телнет

В JMC 3.7 пользователю предоставлены возможности практически полного контроля над telnet-соединением из скриптов (Active Scripting). Это позволяет использовать в том числе нестандартные telnet-опции (протоколы), такие как 102 в Aardwolf или 87 в Адане.

Пусть, к примеру, стоит задача работать с некоторой специфичной для Адана информацией из javascript. Тогда надо включить 87-ую опцию:

#telnet 87 on

Теперь JMC будет при подключении автоматически подтверждать серверу возможность обработки 87-ой опции. В скрипте понадобится обработчик события Telnet:

  jmc.RegisterHandler('Telnet', 'OnTelnet()');
  function OnTelnet() {
    if (jmc.Event(0) == 240 /* Subnegotiation End -- new data arrival */ &&
        jmc.Event(1) == 87 /* Adan's custom option */)  {
      var doc = new ActiveXObject("Msxml.DOMDocument");
      doc.async = false;
      doc.resolveExternals = false;
      doc.validateOnParse = false;
      doc.loadXML(jmc.Event(2).substr(1));
      var room = doc.selectSingleNode('CurrentRoomMessage');
      if (room != null) {
        jmc.Output("Room: " + room.getAttribute("RoomId"));
      }
      var mates = doc.selectNodes('GroupStatusMessage/GroupMates/GroupMate');
      if (mates != null) {
        for (var i = 0; i < mates.length; i++)
          jmc.Output("Mate[" + (i + 1) + "]: " + mates[i].getAttribute("Name") + ", " + mates[i].getAttribute("HitsPercent") + "% hp, " + mates[i].getAttribute("MovesPercent") + "% mv");
      }
    }
  }

Здесь скрипт обрабатывает информацию об уникальном номере комнаты (RoomId), а также имени и процентах здоровья/движения всех персонажей в группе игрока.

Для Aardwolf можно записать пример отслеживания тиков средствами опции 102:

#telnet 102 on

И обработчик события:

  jmc.RegisterHandler('Telnet', 'OnTelnet()');
  function OnTelnet() {
    if (jmc.Event(0) == 240 /* Subnegotiation End -- new data arrival */ &&
        jmc.Event(1) == 102 /* Aardwolf's custom option */)  {
      if (jmc.Event(2).charCodeAt(0) == 101 && jmc.Event(2).charCodeAt(1) == 1) {
        jmc.Output("Tick!");
      }
    }
  }

Другой вариант применения события Telnet – работа с Out-Of-Band-данными, например с GMCP. В javascript есть простой способ распарсить корректные GMCP-данные посредством, например, eval() или JSON.parse(). Пусть необходимо обрабатывать состояние персонажа в Aardwolf. Тогда надо включить GMCP и добавить нужный модуль (char):

  #telnet GMCP on
  #oob GMCP add char

Затем отловить событие Telnet и использовать eval():

  jmc.RegisterHandler('Telnet', 'OnTelnet()');
  function OnTelnet() {
    if (jmc.Event(0) == 240 /* Subnegotiation End -- new data arrival */ &&
        jmc.Event(1) == 201 /* GMCP */)  {
      eval(jmc.Event(2).replace(/ /,'='));
      if (char.base.vitals.hp < 100) {
        jmc.Parse("cast word of recall");
      }
    }
  }

Также несложно при помощи скрипта на базе JMC сделать MSSP-бота, который будет опрашивать MUD-серверы из заданного списка и вести какую-либо статистику. Нужно лишь включить MSSP:

  #telnet MSSP on
  #oob MSSP enable

Организовать цикличное подключение к интересующим серверам по списку:

  Index = 0;
  ServerList = [
    'forgottenkingdoms.org:4000',
    'abandonedrealms.com:9000',
    'coimuck.com:1771',
    'arbitraryorder.com:8195',
    'mud.kharkov.org:3000'];
  function CheckNextServer() {
    jmc.Disconnect();
    Index = (Index + 1) % ServerList.length;
    var address = ServerList[Index].split(/:/);
    jmc.Connect(address[0], address[1]);
  }
  jmc.RegisterHandler('Timer', 'CheckNextServer()');
  jmc.SetTimer(1, 100 /* every 10 seconds */);

А потом парсить все доступные данные и куда-нибудь сохранять:

  ServerData = {};
  function OnTelnet() {
    if (jmc.Event(0) == 240 /* Subnegotiation End */ &&
        jmc.Event(1) == 70 /* MSSP */)  {
      eval("MSSP = " + jmc.MSSP2GMCP(jmc.Event(2)));
      ServerData[ServerList[Index]] = {
        "LastChecked": (new Date),
        "Data": MSSP
      };
      jmc.Disconnect();
    }
  }
  jmc.RegisterHandler('Telnet', 'OnTelnet');

4.2 Автокартирование

Группа команд картографа (#mapper) предоставляет базовые инструменты создания и отрисовки карты. Однако в конечном счёте, как правило, требуется максимальная автоматизация этих процессов, интегрированная в игровой процесс, или _авто_картирование.

4.2.1 Постановка задачи

Обычными ожидаемыми функциями автокартографа являются:

  • составление карты при посещении новых мест в игровом мире без существенного отвлечения от игрового процесса;
  • сохранение составленной карты с возможностью её последующей загрузки;
  • автоматическое отслеживание положения игрока на имеющейся карте;
  • поиск определённых комнат на карте, а также путей между определённым образом заданными пунктами;
  • возможность просмотра карты (навигации).

В JMC 3.7 есть средства решения такой задачи без применения Active Scripting. Ниже приводятся примеры её решения в разных ситуациях. При этом во всех случаях используется следующий подход:

  • автокартограф представляет собой файл с JMC-командами, который, будучи однократно прочитан командой #read, добавляет в текущий профиль все необходимые сущности (триггеры, алиасы и т.п.); для удобства все новые сущности добавляются в группу automap;
  • при установке соединения с сервером автокартограф загружает карту из файла, в зависимости от имени сервера, к которому установилось подключение, а при разрыве соединения сохраняет карту в тот же файл;
  • при перемещении по миру автокартограф автоматически создаёт необходимые комнаты и переходы, а также отслеживает положение игрока и отображает в определённом окне карту окрестностей;
  • в трудных случаях автокартограф имеет два режима: режим построения карты (полный) и режим отслеживания положения (когда карта считается уже составленной и не подлежащей изменению).

Поскольку каждый игровой мир имеет свои уникальные особенности, то и автокартограф будет немного отличаться для разных серверов. Кроме того, каждый игрок может иметь определённые собственные предпочтения, в таком случае он может использовать tintin или Active Scripting для практически произвольной кастомизации нижеописанных подходов.

4.2.2 Есть GMCP/MSDP (простейший случай)

Если сервер поддерживает автоматическую передачу информации о комнатах средствами протоколов GMCP или MSDP, то задача автокартографии становится технически тривиальной. Однако на ней очень удобно рассмотреть и отработать все основные функции.

Пусть игровой сервер (для конкретики пусть это будет МПМ “Былины”) поддерживает минимально достаточный для данных целей набор переменных MSDP, а именно:

  • REPORTABLE_VARIABLES содержит ROOM;
  • переменная ROOM представляет собой таблицу с элементами VNUM (число), NAME, AREA, TERRAIN (строки) и EXITS (таблица в формате <направление>: VNUM, например {"N": "123", "U": "321"}).

Тогда, прежде всего, необходимо включить MSDP (ответ DO MSDP на запрос WILL MSDP):

#telnet MSDP on

Затем добавить переменную ROOM в список запрашиваемых по умолчанию (после ответа DO MSDP будет послан запрос LIST REPORTABLE_VARIABLES, и если в ответе будет перечислена ROOM, то будет послан запрос REPORT ROOM):

#oob MSDP add ROOM

… а также включить отображение сообщений о получении новых данных:

#message oob ON

Теперь при получении MSDP-переменных будет появляться строчка #oob MSDP <имя_переменной>, на которую можно (и нужно) навесить триггер, вызывающий алиас onRoom (определённый ниже) и скрывающий строчку, чтобы она не мешалась пользователю:

#action TEXT {/^#oob MSDP ROOM/} {onRoom; #drop} {5} {automap}

Для осмысленного изображения карты необходимо описать команды-направления, используемые в игровом мире, причем в числе синонимов должны быть мнемоники направлений, используемые в MSDP (ROOM.EXITS.<направление>):

#mapper add direction {вверх,вв,up,u} 1 1 0 {вниз,вн,down,d}
#mapper add direction {восток,в,east,e} 1 0 0 {запад,з,west,w}
#mapper add direction {север,с,north,n} 0 1 0 {юг,ю,south,s}

При каждой смене комнаты игровым персонажем, сервер должен отсылать новое значение переменной ROOM, при этом будет вызываться алиас onRoom. Внутри этого алиаса, по меньшей мере, необходимо создать (или перезаписать) комнату с текущими номером и названием, а также добавить все выходы из неё:

#alias onRoom
{
 #mapper add room $MsdpRoomVnum {$MsdpRoomName} {} {$MsdpRoomArea} {} {$MsdpRoomTerrain};
 #if {$MsdpRoomExitsLength > 0}
 {
  #loop {1, $MsdpRoomExitsLength}
  {
  #mapper add exit $MsdpRoomVnum {$MsdpRoomExitsKey%0} {$MsdpRoomExitsValue%0}
  }
 };
 #mapper print normal header flags exits comment w1 $MsdpRoomVnum
}
{automap}

Теперь карта создаётся в памяти JMC, отображается в первом окне вывода, но не сохраняется и не загружается при перезапусках. Проще всего навесить триггеры на системные сообщения JMC об установке/разрыве соединения, при этом по глобальной переменной $HOSTNAME определять файл для сохранения/загрузки карты, чтобы не путать данные разных миров, а заодно создать поддиректорию ./maps, в которой хранить все карты:

#action {/^#Connection established\./} {#var MapperWorldName $HOSTNAME; #read maps/$MapperWorldName.msdp.map} {5} {automap}
#action {/^#Connection lost\./} {#mapper write maps/$MapperWorldName.msdp.map} {5} {automap}
#action {/^#Connection closed by user\./} {#mapper write maps/$MapperWorldName.msdp.map} {5} {automap}

Конечно, чтобы это работало, необходимо создать папку maps в основной директории JMC.

Конкретный образец триггера будет зависеть от выбранного языка (секции в language.ini или другом указанном в настройках файле). Причем, пользователь может изменить вид сообщений произвольным образом (см. п. 5.8). Например, для стандартного русского перевода эти триггеры будут иметь вид:

#action {/^#Соединение установлено/} {#var MapperWorldName $HOSTNAME; #read maps/$MapperWorldName.msdp.map} {5} {automap}
#action {/^#Соединение утеряно/} {#mapper write maps/$MapperWorldName.msdp.map} {5} {automap}
#action {/^#Соединение разорвано пользователем/} {#mapper write maps/$MapperWorldName.msdp.map} {5} {automap}

Далее всюду используются англоязычные варианты сообщений, используемые по умолчанию в случае отсутствия файла language.ini вообще.

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

Прежде всего, необходимо добавить возможность записи на карту скрытых проходов и проходов по триггерам, т.к. они, конечно, не указываются в MSDP-данных. Например, можно сделать это добавлением в конец алиаса onRoom отслеживания “предыдущей комнаты”, а также алиаса:

#alias onRoom
  ...
  #var PrevVNum {$CurrentVNum};
  #var CurrentVNum {$MsdpRoomVnum};
  ...
#alias запвых {#mapper add exit $PrevVNum {$COMMAND} $CurrentVNum {}} {automap}

Теперь пройдя по скрытому/триггерному проходу можно ввести запвых и этот проход будет записан на карте.

Затем надо сделать инструменты расстановки дополнительных флагов комнат, т.к. этих данных тоже нет в MSDP (квестер, постой, кузнец, банкир и т.п.). А заодно и добавления комментариев и специальных действий по открытию проходов (дверей, паролей и т.п.).

#alias здесь {#mapper set flag $CurrentVNum %1} {automap}
#alias здесьне {#mapper reset flag $CurrentVNum %1} {automap}
#alias коммент {#mapper set comment $CurrentVNum {\%\%0}} {automap}
#alias {/^проход (\w+) (.*)/} {#mapper set pass $CurrentVNum {\%\%0} {\%\%1}} {automap}

Теперь крайне желательно иметь возможность навигации по карте без фактического перемещения по ней игрового персонажа. Для этого можно ввести новую переменную – номер комнаты, которая помещается в центр отображаемой карты и которая при перемещении устанавливается равной текущему положению персонажа, но которую затем можно “двигать” отдельно от него.

Конец алиаса onRoom будет выглядеть так:

#alias onRoom
  ...
  #var MapVNum {$CurrentVNum};
  #mapper print normal header flags exits comment w1 $MapVNum
  ...

Добавится новый алиас и какие-нибудь горячие клавиши:

#alias покарте
{
 #mapper get exit %1 $MapVNum NextMapVNum;
 #if {$NextMapVNum != 0}
 {
  #var MapVNum $NextMapVNum;
  #mapper print normal header flags exits comment w1 $MapVNum
 }
}
{automap}
#hot {Alt+UP} {покарте север} {automap}
#hot {Alt+LEFT} {покарте запад} {automap}
#hot {Alt+RIGHT} {покарте восток} {automap}
#hot {Alt+DOWN} {покарте юг} {automap}
#hot {Alt+0} {#var MapVNum $CurrentVNum;#mapper print normal header flags exits comment w1 $MapVNum} {automap}

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

#alias бежсюда 
{
 #mapper path $CurrentVNum $MapVNum;
 #savepath MapperAutoRun;
 MapperAutoRun
} 
{automap}

При этом нужно в режиме навигации по карте выбрать целевую комнату и ввести бежсюда. Главный минус такого подхода такой же, как у всех спидволков: если в пути случится задержка (агромоб, триггер с паузой) или персонаж каким-либо образом самопроизвольно сместится (из-за ветра, течения, призыва) или путь содержит элементы случайных перемещений, то можно оказаться в неприятной ситуации.

Потому лучше пойти более сложным, но более надёжным и правильным путём. Понадобятся две новые переменные – номер целевой комнаты ($TargetVNum) и признак автобега ($AutoRun). Идея в том, чтобы при каждом перемещении в новую комнату проверять этот признак и если автобег включен, то делать ровно один шаг в направлении целевой комнаты.

#alias stepAutoRun
{
 #strcmp $CurrentVNum $TargetVNum {#var AutoRun 0};
 #if {$AutoRun == 1}
 {
  #mapper path $CurrentVNum $TargetVNum AutoRunStep;
  #strcmp {$AutoRunStep} {} {#var AutoRun {0}} {$AutoRunStep}
 }
}
{automap}
#alias бежсюда 
{
 #var TargetVNum $MapVNum;
 #var AutoRun 1;
 stepAutoRun
} 
{automap}
#alias стопбеж {#var AutoRun 0} {automap}

Также в конец алиаса onRoom добавится новая строка:

#alias onRoom
  ...
  #if {$AutoRun == 1} {stepAutoRun}
  ...

Удобно также иметь алиас движения к ближайшей комнате, обладающей каким-либо флагом:

#alias кближнему 
{
 #mapper search near $MapVNum flag %1 TargetVNum;
 #strcmp {$TargetVNum} 0 {} 
 {
  #var AutoRun 1;
  stepAutoRun
 }
}
{automap}

Теперь можно ввести что-то вроде кближнему постой, без необходимости вручную искать нужную комнату по карте.

Применительно к МПМ “Былины” у получившегося картографа остался ещё один существенный недостаток: в него весьма трудоёмко добавить DT (deathtrap) комнаты, не зайдя в них, чего делать, конечно, не хочется. Да и вообще удобно видеть на карте не только все посещённые комнаты, но и все смежные с ними, даже если туда ещё не ступала нога персонажа. Сделать это совершенно несложно, например, введя флаг что-то вроде неисследовано и чуть переписав алиас onRoom:

#mapper add flag {неисследовано} {?} {brown}
#alias onRoom
{
 #mapper add room $MsdpRoomVnum {$MsdpRoomName} {} {$MsdpRoomArea} {} {$MsdpRoomTerrain};
 #mapper reset flag $MsdpRoomVnum неисследовано;
 #if {$MsdpRoomExitsLength > 0}
 {
  #loop {1, $MsdpRoomExitsLength}
  {
   #mapper get id {$MsdpRoomExitsValue%0} ExitVNum;
   #if {$ExitVNum == 0}
{
#mapper add room {$MsdpRoomExitsValue%0} {?} {} {} {} {неисследовано}
};
   #mapper add exit $MsdpRoomVnum {$MsdpRoomExitsKey%0} {$MsdpRoomExitsValue%0}
  }
 };
  ...
}
{automap}

Специально для DT-комнат можно добавить флаг смерть и его особую проверку в алиасе здесь:

#mapper add flag {смерть} {†} {light red}
#alias здесь 
{
 #mapper set flag $MapVNum %1;
 #strcmp %1 смерть {#mapper set avoidance $MapVNum 0};
 ...

Теперь, зная, что в соседней комнате (например, на севере) ждёт верная смерть, можно выполнить покарте север и здесь смерть, что пометит комнату на карте как смертельную и исключит её из поиска путей, хотя она по-прежнему останется неисследованной.

Итоговый скрипт: автокартограф МПМ “Былины”, основанный на MSDP.

Для других MUD-серверов с поддержкой MSDP правки должны быть минимальны или не нужны вовсе. В основном они коснутся флагов комнат, специфичных для конкретного мира и, возможно, наименования направлений передвижения.

Используется скрипт именно так, как предполагалось: находясь в JMC под профилем, в который необходимо “установить” картограф, надо выполнить #read mapper.msdp.bylins.set, после чего подключиться к серверу и играть, расположив окно вывода карты в удобном месте.

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

4.2.3 Есть уникальный ключ

Пусть теперь сервер не поддерживает ни MSDP, ни GMCP, но зато позволяет так или иначе однозначно идентифицировать комнату, в которой находится персонаж. Уникальный номер (vnum) иногда указывается в названии комнаты, может отображаться в строке приглашения (обычно только для бессмертных) или получаться иным путём (например, в Адане через telnet-опцию 87, см. п. 4.1).

Так или иначе, пусть алиас onRoom вызывается в момент получения информации с номером комнаты, который при этом хранится в переменной $RoomVNum, а название в $RoomName. Тогда его можно переписать следующим образом:

#alias onRoom
{
 #mapper add room $RoomVNum {$RoomName} {} {} {} {};
 #mapper add exit $CurrentVNum {$COMMAND} $RoomVNum;
 #var PrevVNum {$CurrentVNum};
 #var CurrentVNum {$RoomVNum};
 #var MapVNum {$CurrentVNum};
 #mapper print normal header flags exits comment w1 $MapVNum
}

У такого подхода есть ряд существенных недостатков:

  • на карту помещаются только комнаты, в которые заходил игровой персонаж (это неустранимо: даже если видны направления всех выходов из комнаты, нет никакой уверенности, что хотя бы один из них не ведёт в эту же самую комнату);
  • переходы между комнатами создаются только при непосредственном перемещении по ним; это значит, что для записи двустороннего перехода между двумя комнатами А <=> Б, нужно пройти по нему дважды: из A в Б и обратно;
  • при составлении карты требуется дожидаться смены комнаты (ответа сервера) после отправки каждой команды, если же отправить подряд сразу несколько команд, то все комнаты по пути будут добавлены на карту в одном направлении (последнем);
  • нехорошо будет в ситуациях, когда персонаж перемещается по миру без участия игрока (ветер, течение, призыв и т.п.): это будет записано на карту как перемещение в направлении последней введенной команды.

    Создание сразу двунаправленных переходов можно реализовать при помощи опции bidirectional команды #mapper add exit, однако это может мешать при составлении карт лабиринтов и других мест, где проходы несимметричны. Потому целесообразно предусмотреть два переключаемых вручную режима:

#var LabyrinthMode 0
#alias лабиринт
{
 #math LabyrinthMode {1 - $LabyrinthMode};
 #if {$LabyrinthMode == 0} {#showme Режим лабиринта отключен.} {#showme Режим лабиринта включен.}
}
{automap}
#alias onRoom
 ...
 #if {$LabyrinthMode == 1}
 {#mapper add exit $CurrentVNum {$COMMAND} $RoomVNum}
 {#mapper add exit $CurrentVNum {$COMMAND} $RoomVNum bidir};
 ...

Сложнее дело обстоит с возможностью иметь асинхронные потоки уходящих на сервер команд передвижения и возвращающихся описаний комнат с номерами. Самым правильным решением будет являться сохранение команд в виде очереди: при отправке команды передвижения она добавляется в конец очереди, а при получении от сервера новой комнаты, команда извлекается из начала очереди. Работу с очередями можно организовать средствами tintin, хотя выглядеть будет и не очень красиво. Например, так:

#alias initQueue 
{
 #unvar Queue%1*;
 #var Queue%1Begin {0};
 #var Queue%1End {0}
}
#alias putQueue 
{
 #var Queue%1Value$Queue%1End {\%\%2};
 #math Queue%1End {$Queue%%1End + 1}
}
#alias getQueue 
{
 #if {$Queue%%1Begin != $Queue%%1End}
 {
  #var %%2 {$Queue%%%1Value$$Queue%%%1Begin};
  #unvar Queue%%1Value$Queue%%1Begin;
  #math Queue%1Begin {$Queue%%1Begin + 1}
 }
 {
  #var %%2 {0}
 }
}

Здесь каждой очереди с именем <name> соответствует группа переменных:

  • Queue<name>Begin - номер элемента, являющегося первым в очереди;
  • Queue<name>End - номер, следующий за элементом, являющимся последним в очереди (если Queue<name>Begin == Queue<name>End, то очередь пуста);
  • Queue<name>Value<index> - значение элемента очереди под номером <index>.

Использование самое обыкновенное:

initQueue TestQ
putQueue TestQ 123
putQueue TestQ 456
getQueue TestQ x; #showme {$x}
putQueue TestQ 789
getQueue TestQ x; #showme {$x}
getQueue TestQ x; #showme {$x}

123 456 789

Тогда можно организовать управление очередью команд CommandsQ и переписать алиас onRoom:

#alias {/^(север|юг|восток|запад|вверх|вниз|войти|выйти)/} {putQueue CommandsQ {\%\%0}; %0}
#action TEXT {/^(Вы не можете идти в этом направлении\.|Во сне\?|Выход .* закрыт\.|Вы слишком устали\.)/} {getQueue CommandsQ LastCommand}
#alias {/^см/} {initQueue CommandsQ;look}
#alias onRoom
{
 #mapper add room $RoomVNum {$RoomName} {} {} {} {};
 getQueue CommandsQ LastCommand;
 #strcmp {$LastCommand} {0} 
 {}
 {
  #if {$LabyrinthMode == 1}
  {#mapper add exit $CurrentVNum {$LastCommand} $NextVNum}
  {#mapper add exit $CurrentVNum {$LastCommand} $NextVNum bidir}
 };
 #var PrevVNum {$CurrentVNum};
 #var CurrentVNum {$NextVNum};
 #var MapVNum {$CurrentVNum};
 #mapper print normal header flags exits comment w1 {$MapVNum}
}
{automap}

Все остальные части картографа (расстановка флагов и комментариев, навигация по карте, поиск путей и т.п.) могут быть полностью позаимствованы из п. 4.2.2.

4.2.4 Нет уникального ключа

Это тяжёлая ситуация, в которой нет простого решения, а иногда решения нет вовсе. С другой стороны, это и наиболее “реалистичный” случай: материальные объекты и, тем более, области пространства, как правило, не нумерованы; вполне естественно, что гипотетический персонаж, составляющий карту, должен испытывать определённые трудности при исследовании обширных и однообразных пейзажей или лабиринтов. Напротив, если посещаемые места чем-то ярко выделяются, сильно отличаясь друг от друга, то проблем быть не должно, как в случае наличия заведомо уникального идентификатора, рассмотренном в предыдущем пункте. Примерно так и будет происходить с встроенными в JMC алгоритмами автонумерации комнат.

Можно привести один известный пример: пусть есть условный MUD с лишь двумя направлениями “север” и “юг”, причём комнаты зациклены, т.е. из последней комнаты можно попасть в первую, а карта представляет собой замкнутое кольцо. Все комнаты выглядят совершенно идентично, и в каждой есть по совершенно одинаковому пустому сундуку. Изначально каждый сундук находится в неизвестном состоянии: крышка открыта либо закрыта. Игроку предлагается построить карту этого мира (по сути, определить количество комнат в кольце), при том, что всё что он может делать – передвигаться и открывать или закрывать сундуки.

Эта задача имеет простое решение. Однако если оставить игроку лишь возможность перемещаться (скажем, все сундуки заржавели и ни открываются, ни закрываются), она станет теоретически неразрешима. Этот пример наглядно иллюстрирует: в общем случае задача составления карты не решается пассивным наблюдением, а решается только при активном воздействии на окружение (здесь можно глубоко залезть в философию отличий экспериментальной физики от наблюдательной – вроде астрономии – но, пожалуй, это будет неуместно). И это даже для простейшего среди MUD-лабиринтов случая симметричности выходов, когда пройдя на север и затем на юг, персонаж оказывается там же, где был.

С другой стороны, теоретическая недостижимость не делает невозможным практически полезное (FAPP) решение. К примеру, если в описанном примере пассивно двигаясь постоянно на север персонаж замечает, что каждый 100-ый сундук открыт, а 99 между ними закрыты, он может предположить, что в мире всего 100 комнат. Он может пройти 100, 200, 1000 сундуков и с каждой сотней его уверенность в этой гипотезе будет расти. Она никогда не достигнет 100% по понятным причинам, но рано или поздно окажется достаточно высокой, чтобы гипотезу можно было принять. При этом даже не нужна симметричность выходов.

Таким образом, при составлении карты без уникального ключа важен некоторый показатель, отображающий степень “воспроизводимости” наблюдаемой картины при “хождении кругами”, достаточную для уверенности в правильности построения карты. Чем он выше, тем больше нужно покружить по определённой местности, прежде чем карта будет уверенно построена; чем ниже, тем больше вероятность ошибиться в какой-нибудь однообразной местности, приняв две похожие комнаты за одну и ту же. Такими параметрами в картографе JMC являются maxdifflen/maxidentlen (#mapper set maxdifflen|maxidentlen <значение>).

Схожим образом необходимо решать задачу определения положения игрока на карте, а именно – принимая во внимание историю перемещений. Трудно понять, где конкретно находится шахматная фигура, если известно только то, что она стоит на белой клетке, а вокруг четыре чёрных; но если известно, что фигура до этого прошла 6 раз в одну и ту же сторону по диагонали, то задача становится элементарной. Для реализации такого подхода к ориентации в картографе JMC предусмотрена группа команд #mapper track.

Итак, для составления карты необходимо, как и в предыдущем пункте, определить момент посещения новой комнаты, а главное – её неизменные “признаки”: как минимум, обычно, название. В отличие от предыдущего пункта этот признак не будет полностью уникальным, и картограф JMC постарается это учесть. Но работать он будет тем лучше, чем более уникальный (при условии постоянства) признак получится сформировать. Например, в некоторых MUD-ах можно к названию комнаты добавить перечисление всех видимых выходов из неё. В других MUD-ах это сделать нельзя, т.к. найденные/откопанные скрытые проходы становятся видимыми и, таким образом, этот признак не является неизменным. В одних случаях игроку может быть неудобно видеть описание всех комнат (слишком много текста), но если это не так, то описание комнаты станет хорошей прибавкой к степени уникальности признака.

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

Примеры триггера для вызова алиаса onRoom с названием комнаты, передающимся первым аргументом в качестве признака:

  • для Арда MUD: ` #action COLOR {/^&W(.?)\n(.?)\n?&?W?Выходы: (.*?).\n/mg} {onRoom {\%\%0} {} {}} {5} {automap} `
  • для Аладона: ` #action COLOR {/^ &C(.?)\n &w(.?)\n&c[Видимые выходы: (.*?)]\n/mg} {onRoom {\%\%0} {} {}} {5} {automap} `
  • для Сферы Миров: ` #action COLOR {/^&C(.?)\n &w(.?)\n&c[ Exits: (.*?)]\n/mg} {onRoom {\%\%0} {} {}} {5} {automap} `

Далее в onRoom вместо известного номера комнаты необходимо использовать автонумерацию:

#mapper add room auto {\%1} {} {} {} {} AutoId

В переменной $AutoId будет храниться автоматически сгенерированный номер комнаты.

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

  • режим составления карты (игрок перемещается осторожно, следя, чтобы персонаж видел названия всех комнат);
  • режим отслеживания положения (карта не изменятся, картограф только пытается отследить положение игрока на карте, и даже если это не получается из-за описанных трудностей, порчи карты не происходит).

Пользователям популярных автомапперов из zMUD / CMUD и MudMapper эти режимы уже знакомы – там они были введены из тех же соображений.

Вся остальная часть автокартографа (флаги, пути, вывод на экран и т.д.) соответствует описаниям предыдущих пунктов. Итоговый пример скрипта автокартографа для MUD Арда. Примерно то же самое должно работать для Аладона и Сферы Миров, с минимальными правками:

  • другой триггер на смену комнаты (см. выше);
  • отсутствие смешанных направлений (северо-запад и т.п.);
  • возможность использовать выходы как часть признака комнаты.

4.4 Краткие рецепты

4.4.1 Полоски жизней, маны и т.д.

Популярный способ отображения числовых индикаторов – в виде “полосок” – может быть реализован при помощи команды #bar, при этом вывод удобно делать в окна статуса (#status).

Пример для МПМ “Былины”

Определение максимальных показателей (при команте счет) и сохранение в переменные $MaxHP, $MaxMV:

  #action COLOR {/^&wВы можете выдержать (\d+)\((\d+)\) единиц.? повреждения, и пройти (\d+)\((\d+)\) верст.? по ровной местности\./} {#var MaxHP %1; #var MaxMV %2;updateHpBar}

Определение текущих показателей (из строки приглашения) и сохранение в переменные $CurrentHP, $CurrentMV:

  #action TEXT {/^(\d+)H (\d+)M (\d+)о Зауч:(\d+)/} {#var CurrentHP %0;#var CurrentMV %1;updateHpBar {$CurrentHP} {$MaxHP};updateMvBar {$CurrentMV} {$MaxMV}}

Вывод полосок в статус:

  #alias updateHpBar {#bar {hpbar} {20} {#} {.} {\%1} {\%2};#status 1 {$hpbar} {light green}}
  #alias updateMvBar {#bar {mvbar} {20} {#} {.} {\%1} {\%2};#status 2 {$mvbar} {white}}
Пример для Адана на основе telnet-опции 87
jmc.RegisterHandler('Telnet', 'OnTelnet()');
function OnTelnet() {
  if (jmc.Event(0) == 240 /* Subnegotiation End -- new data arrival */ &&
      jmc.Event(1) == 87 /* Adan's custom option */) {
    var doc = new ActiveXObject("Msxml.DOMDocument");
    doc.async = false;
    doc.resolveExternals = false;
    doc.validateOnParse = false;
    doc.loadXML(jmc.Event(2).substr(1));
    var mates = doc.selectSingleNode('GroupStatusMessage/GroupMates');
    if (mates != null) {
      mates = mates.selectNodes('GroupMate');
      for (var i = 0; i < 5; i++) {
        if (i < mates.length) {
          jmc.Parse('#bar hpbar 10 {=} { } ' + parseInt(mates[i].getAttribute("HitsPercent")) + ' 100');
          jmc.Parse('#bar mvbar 10 {=} { } ' + parseInt(mates[i].getAttribute("MovesPercent")) + ' 100');
          jmc.SetStatus(i + 1, '&W' + mates[i].getAttribute("Name") + '&w[&G$hpbar&w][&y$mvbar&w]', 'colorcodes');
        } else {
          jmc.SetStatus(i + 1, ' ');
        }
      }
    }
  }
}
Пример для Aardwolf с извлечением данных по GMCP

Включение получения данных о персонаже:

  #telnet GMCP on
  #oob GMCP add char

Обновление полосок при получении новых данных (предполагается режим #multiaction on):

  #action TEXT {/^#oob GMCP char/i} {#bar {hpbar} {15} {*} {-} {$GmcpCharVitalsHp} {$GmcpCharMaxstatsMaxhp};#status 1 {$hpbar} {light green}}
  #action TEXT {/^#oob GMCP char/i} {#bar {mabar} {15} {*} {-} {$GmcpCharVitalsMana} {$GmcpCharMaxstatsMaxmana};#status 2 {$mabar} {light cyan}}

4.4.2 Каналы в дополнительных окнах

Традиционно многим удобно видеть все или некоторые каналы сообщений в отдельном окне. С цветными триггерами это делается весьма безопасным способом.

Пример для Арда MUD
#action COLOR {/^(&(?:Y|R|G)\*&w: .*)/} {#woutput 2 {colorcodes} {&w[$TIME] %0}}
#action COLOR {/^(&C\w+ помечтал.?|&C\w+ спел.?|&C\w+ говорит.?|&C\w+ сказал.? всем|&W\w+ говорит вам|&W\w+ сказал.? \w+|&W\w+ обратились к \w+|&W\w+ шепчет вам на ухо|&C\w+ спросил.?|&G\w+ говорит группе|&C\w+ заорал.?|&C\w+ закричал.?|&y\w+ прогнусил.?|&C\w+ простонал.?|&g\w+ сказал.? расе|&Y\w+ сказал.? клану|&Y\w+ сказал.? ордену|&R\w+ сказал.? атакующим|&C\w+ сказал.? состЯзающимсЯ)( '.*')/} {#woutput 2 {colorcodes} {&w[$TIME] %0%1}}
Пример для МПМ “Былины” для двух каналов, остальные можно добавить по аналогии
#action COLOR {/^(&y\w+ заметил|&C\w+ сказал.? \w+): '(.*)'/} {#woutput 2 {colorcodes} {[$TIME] %0: '%1'}}
Пример для Aardwolf через теги каналов, при этом сами теги удаляются из выводимой строки
#action RAW {/^\\{chan ch=(.+)\\}(.*)} {#woutput 5 {[$TIME] %1};#replace {\%1}}

4.4.3 Подключение через Тор

Поскольку теперь JMC умеет устанавливать соединение через SOCKS-прокси, то можно легко организовать подключение через Тор: достаточно включить в нём локальный SOCKS-proxy и выполнить в JMC #proxy socks4 127.0.0.1:9150 (или иное, в соответствии с настройками Тора). При этом несложно при необходимости перестраивать цепочку (что почти всегда, но не всегда приводит к смене выходной ноды и, соответственно, IP-адреса) прямо из JMC, включив в Торе управление по telnet и воспользовавшись алиасом наподобие:

#alias {TorNewChain} {#zap;#action {/^#Connection established/} {authenticate "foo";signal newnym;quit;#unact {/^#Connection established/}};#connect 127.0.0.1 9151} {default}

(для случая пароля foo)

4.4.4 Отключение отложенного подтверждения TCP

Взаимодействие с MUD-серверами происходит по протоколу TCP, который по изначальному замыслу не предназначен для быстрого обмена небольшими сообщениями. В ОС Windows предусмотрено сразу два механизма искусственного увеличения задержек (latency) для снижения нагрузки на сеть: алгоритм Нейгла и т.н. отложенное подтверждение (delayed acknowledgement).

Алгоритм Нейгла создаёт искусственную задержку при отправке (от JMC к MUD-серверу); JMC 3.7 отключает его, так что команды пользователя отправляются на сервер так быстро, насколько позволяет сеть.

Но средств программного отключения отложенного подтверждения для конкретного соединения в Windows не предусмотрено, при этом по умолчанию оно включено. Это значит, что если программа MUD-сервера посылает данные небольшими “порциями” (обращениями к send()) с малой задержкой, то к JMC они будут приходить с паузой около 200 мс между последней и предпоследней “порцией”. Например, в Арда MUD при включенной опции GA (т.е. строка приглашения должна завершаться telnet-последовательностью IAC GA) приходит сначала строка приглашения, а лишь через 200 мс – GA, что приводит к паузе между фактическим получением строки и её обработкой (срабатыванием триггеров и т.п.). По всей видимости, это является следствием двукратного обращения к функции отправки данных (send(<приглашение>) + send(GA)) вместо однократного (send(приглашение + GA)), что действительно можно увидеть в дизассемблированном коде демо-версии Арды (хотя в SMAUG 1.4 и выше уже реализована нормальная буферизация).

Таким образом, со стороны сервера проблема решается легко, однако ясно, что от многих серверов доработки ожидать не стоит. Со стороны пользователя же решение может быть только одно – глобальное отключение отложенных подтверждений для всех TCP-подключений на конкретной машине. Руководств по осуществлению этой процедуры в сети множество, она сводится просто к созданию ключей в реестре TcpAckDelay := 0 и TcpAckFrequency := 1 с последующей перезагрузкой.

4.4.5 Удобное логирование

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

#alias лог {#log;#log logs/$HOSTNAME.$DATE.$CLOCK.html %0}

Если создан каталог logs в основной директории JMC, то ввод лог 100 начнёт запись нового лога, поместив туда 100 последних строк из буфера, при этом в названии файла лога будет имя MUD-сервера и дата записи, а благодаря $CLOCK имя будет уникальным и такую команду можно будет выполнять сколько угодно раз, получая разложенные по отдельным файлам логи.

4.4.6 Импорт карт

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

Удобно иметь решение этой задачи в виде скрипта javascript, запускаемого из самой JMC. В большинстве случаев такой скрипт весьма лёгок в написании.

Примеры:

В приведённых примерах запуск процесса импортирования требует указания источника (файла или директории) и описан в комментарии в конце каждого скрипта. Загрузка производится в текущее состояние картографа, которое предварительно сбрасывается (#mapper clear). После успешной загрузки карту можно сохранить в любое удобное место (#mapper write <filename>), после чего реализовать автозагрузку этой карты при подключении и отслеживание положения персонажа на карте (см. п. 4.2).

4.4.7 Выполнение команд в разных окнах JMC

Команда #broadcast является базовым инструментом для взаимодействия копий приложения JMC, работающих на одной машине или в одной локальной сети. Её несложно адаптировать для адресного выполнения команд (как, например, в zMUD).

Необходимо иметь одинаковые настройки во всех копиях, которые должны взаимодействовать. Например,

#broadcast filterip on
#broadcast filterport on
#broadcast port 12345
#broadcast enable

Затем именовать все приложения по отдельности какими-то (желательно осмысленными) разными именами:

## (в главном окне)
#var MyName {Основа}
## (во вспомогательном окне 1)
#var MyName {Хил}
## (во вспомогательном окне 2)
#var MyName {Маг}
## (во вспомогательном окне 3)
#var MyName {Танк}

Придумать какой-нибудь общий для всех ключ:

#var ComplexKey {qwerty12345}

Создать алиас отправки сообщений о необходимости выполнить команду:

#alias {/!приказ (\w+) (.*)/i} {#broadcast send {$ComplexKey $MyName => %0 %1}}

И триггер реакции на такие сообщения:

#action {/^$ComplexKey (\w+) => ($MyName|всем) (.*)/i} {\%2}

Далее использование простое (предположим, из какого-то одного, “основного” окна):

!приказ всем #connect 127.0.0.1 4000 !приказ всем loginAlias !приказ хил встать !приказ маг взять все

5 Детали работы

5.1 Многострочные триггеры

Ключевым вопросом проверки многострочных паттернов (с флагом m) являются точки (в потоке входящих данных) их проверки. При этом предполагается, что проверке подвергаются только данные между двумя последующими точками, т.к. после проверки некий внутренний буфер очищается и заполняется до следующей точки. Алгоритм расстановки точек должен удовлетворять следующим условиям:

  • проверки не должны быть слишком редкими (триггер должен “иметь реакцию” лучше, чем игрок);
  • точки не должны разбивать потенциально сопряжённые друг с другом по смыслу строки, т.е. которые хотелось бы отловить одним шаблоном.

Достаточно хорошим выбором точки проверки является конец строки приглашения. Он идентифицируется четырьмя способами:

  • получение последовательности IAC GA;
  • получение последовательности IAC EOR;
  • обнаружение последовательности байт, заданной командой #promptend (если включено);
  • ожидание конца строки (\n) сверх установленного лимита времени (Uncompleted lines delay/Задержка незавершенных строк).

Такой выбор удобен ещё и тем, что проверяемые многострочные данные будут содержать последней строкой пользовательское приглашение, что в ряде задач может быть полезно. Оправданность такого выбора следует из того, что MUD-серверы, как правило, выводят семантически связанную информацию между двумя приглашениями (описание комнаты, список аффектов, содержимое инвентаря и т.п.).

Минусом такого подхода является возможное появление “лишней” точки проверки при наличии лага, однако этого иногда можно избежать увеличением параметра Uncompleted lines delay/Задержка незавершенных строк. Также возможным (по ситуации) минусом может являться поведение при постраничном выводе большого количества информации (длинные списки типа списка игроков он-лайн).

Хуже всего при таком подходе будет поведение JMC при отсутствии у сервера поддержки GA/EOR, отключенном #promptend и непрерывном потоке данных (например, из-за активной игры пользователя), так что Uncompleted lines delay/Задержка незавершенных строк не случается или случается редко. Для отрабатывания этой ситуации было решено расширить список точек проверки ещё одним пунктом:

  • пользователь осуществил ввод команды, отправляемой на сервер.

Как правило, команды вводятся пользователем сразу после получения строки приглашения (возможно, сразу по несколько штук), так что в большинстве случаев добавление этой точки проверки никак не скажется на функционировании. Однако ясно, что иногда (особенно при сочетании медленной связи и активной игры) команды могут разбивать многострочные данные в неподходящих местах.

5.2 Цвета

Основной целью при разработке механизма кодирования цвета в человекочитаемом формате была реализация принципа:

  • строки, которые выглядят одинаково в цветном виде, должны быть идентичными символ-в-символ в кодированном виде

При этом речь идёт только о цвете самого текста, т.е. фон текста игнорируется.

Таким образом, если в тексте встречаются две ansi-команды изменения цвета (без изменения режима яркости), между которыми нет печатаемых (не пробельных) символов, то первая команда может быть проигнорирована. Важно сохранять этот принцип, чтобы было удобно пользоваться цветными триггерами. Такой подход имеет то преимущество над RAW (ANSI) триггерами, что пользователю не нужно каким-то хитрым образом (например, записывая ANSI-лог) узнавать конкретную последовательность байт, присылаемую сервером в конкретной строке, он может описать её кодами цветов просто глядя на экран.

Эта логика касается только преобразования из формата ANSI в формат с кодами цветов. Обратное преобразование сохранит коды (и их команды), которые ни на что не влияют.

5.3 Кодировки в файлах

Для обеспечения обратной совместимости при переходе на юникод, требовалось обеспечить поддержку чтения файлов в старой (win) кодировке. Однако для использования возможностей юникода (например, создания конфиг-файлов одновременно с китайскими иероглифами и арабской вязью), требовалось поддерживать и кодировку utf-8/16. В этой ситуации в JMC был реализован примитивный механизм автоопределения кодировки файла при чтении по следующему механизму:

  • если всё содержимое файла может быть декодировано как utf-8, то используется utf-8;
  • если первые два байта файла равны FF:FE или FE:FF, то используются utf-16LE/BE соответственно;
  • в остальных случаях используется win-кодировка.

Кроме того, чтобы не “портить” файлы (например, при одновременном использовании JMC 3.6 и JMC 3.7) при сохранении ранее прочитанного файла используется кодировка, определённая при чтении, т.е. win-профиль при перезаписи сохранится в win-кодировке (с разрушениями, в случае невозможности преобразования из юникода), а utf-профиль в utf-кодировке.

Кодировка лог-файлов явно задаётся в настройках, при этом в шаблоне html-лога можно использовать макрос %charset%, вместо которого будет подставлено имя кодировки, как она зарегистрирована в ОС Windows (ключи WebCharset/BodyCharset из реестра).

5.4 Телнет

Модуль обработки telnet-команд и опций был существенно доработан, и хотя основной целью было дать пользователю максимальный контроль над соединением (и, таким образом, доступ к сколь угодно сложным функциям современных MUD-серверов), была также заложена определённая автоматическая обработка некоторых опций (конечно, если пользователь включил их командой #telnet <option> on).

  • GA (Go Ahead)/EOR (End Of Record); обработка этих команд уже была в предыдущих версиях: они трактуются как маркер конца строки приглашения;
  • MCCP (MUD Client Compression Protocol); сжатие данных реализовано средствами библиотеки zlib.dll (хотя можно найти линкующиеся статически аналоги, см., например, как сделано в MUSHClient); поддерживаются обе версии протокола сжатия, хотя вряд ли есть серверы, поддерживающие v1 и не поддерживающие v2;
  • ECHO; многие серверы (особенно на базе ROM 2.x) используют эту опцию, чтобы отключить отображение пользовательского ввода на клиенте (как правило, во время запроса пароля от аккаунта/персонажа); логика JMC такова: если сервер включил опцию (WILL ECHO/DONT ECHO), то пользовательский ввод не отображается на экране и не попадает в лог-файл, в остальных случаях поведение зависит от настроек пользователя (User’s input/Ввод пользователя);
  • AYT (Are You There); при получении такой команды от сервера, JMC посылает пустую строку (из одного пробела) серверу;
  • NAWS (Negotiate About Window Size); при определении поддержки сервером этой опции, а также когда впоследствии у JMC меняются размеры основного окна вывода (в том числе из-за скроллинга с Split on scroll/Разбивать при прокрутке), клиент будет посылать серверу ширину и высоту окна вывода в символах (с учётом текущего шрифта); это единственная ситуация, в которой дополнительные данные (subnegotiation) не являются текстом, а потому передаются по telnet без учёта кодировки;
  • CHARSET (Character Set Selection); протокол согласования кодировки реализован достаточно полно, его логика такова: если опция включена, то при установлении соединения посылается команда IAC WILL CHARSET, а при получении от сервера такой команды посылается ответ IAC DO CHARSET с последующей отправкой IAC SB CHARSET REQUEST <список кодировок из реестра Windows> IAC SE, т.е. запросом желаемой кодировки из предоставленного списка; в первом случае сервер сам присылает список поддерживаемых кодировок, из которых JMC выбирает по возможности ту, которую пользователь выбрал командой #charset (или в настройках), а если она не поддерживается сервером, то первую же, найденную в Windows; во втором случае сервер сам выбирает кодировку, а JMC следует этому выбору; на практике поддержка этой опции встречается редко, и, в основном, представлена популярным сниппетом KaVir, в котором реализована в весьма урезанном виде, поддерживающим только UTF-8;
  • MTTS (MUD Terminal Type Standard); при запросе информации о клиенте со стороны сервера JMC посылает следующую информацию: название клиента берётся из ресурсов (Version->ProductName, на данный момент JMC MUD Client); тип терминала ANSI; поддержка стандартов MTTS <ANSI> <UTF-8>.

5.5 Out-of-bound данные

Работа с игровыми данными, не отображаемыми на экране, объединена в одну группу команд #oob. Модуль telnet сам определяет данные известных протоколов – GMCP, MSDP и MSSP – и передаёт их обработку соответствующим модулям. Также, протоколы GMCP и MSDP требуют некоторой “инициализации” при установке соединения (завершении согласования использования этих протоколов):

  • GMCP: клиент должен послать приветственное сообщение, JMC посылает сообщение core.hello {"Client": "<ProductName>", "Version": "<ProductVersion>"} Название и версия программы берутся из соответствующих ресурсов. Также JMC отправляет список включенных GMCP-модулей (командой #oob GMCP add <module>): code.supports.set ["<module1> 1", ..., "<moduleN> 1"] Сверх этой минимальной инициализации, JMC также запрашивает все “включенные” GMCP-модули, чтобы сразу при подключении к игре определить состояние персонажа (жизни, мана, местонахождение), т.е. отправляется серия запросов: request <module>
  • MSDP: прежде всего, JMC запрашивает список переменных, которые сервер может автоматически посылать игроку, т.е. посылает сообщение LIST REPORTABLE_VARIABLES Когда сервер в ответ присылает этот список, JMC ищет среди поддерживаемых сервером переменных те, которые были “включены” пользователем (командой #oob MSDP add <variable>). Каждую такую переменную JMC запрашивает у сервера отдельным сообщением REPORT <variable>

В процессе игры сервер будет периодически присылать новые значения запрошенных переменных. При этом модуль telnet будет вызывать соответствующие парсерсы (GMCP, MSDP, MSSP). Основным форматом для JMC является GMCP (почти JSON), остальные форматы (MSDP, MSSP) предварительно конвертируются в него. Распарсенные данные хранятся в памяти клиента в иерархическом виде (наподобие файловой системы) и доступны пользователю через “виртуальные” переменные. Логика обращения к ним следующая: если в какой-то ситуации (подстановка переменных в tintin-скрипте или jmc.GetVar()) переменная не найдена в списке пользовательских и не является глобальной, то её значение ищется в хранилище данных OOB. Верхний уровень хранилища представляет собой список протоколов: GMCP, MSDP, MSSP и их содержимое доступно через переменные $Gmcp, $Msdp, $Mssp соответственно. Так, при вводе #showme $Gmcp будут отображены все данные, полученные по протоколу GMCP в формате JSON. Чтобы обратиться к следующему уровню иерархии, нужно к имени переменной приписать соответствующее название “категории” и т.д. вплоть до атомарных данных типа чисел или строк. То же самое касается протоколов MSDP и MSSP.

Пример:

#showme $Gmcp

{"Char": {"Base": {"Str": 12, "Int": 13, "Wis": 8, "Con": 10}, "Vitals": {"Hp": 88, "Ma": 55, "Mv": 60}, "Name": "John", "Friends": ["Fred", "Tom", "Harry"]}, "Room": {"VNum": 123, "Name": "Tavern", "Exits": {"N": 124, "U": 125}}}

#showme $GmcpRoom

{"VNum": 123, "Name": "Tavern", "Exits": {"N": 124, "U": 125}}

#showme $GmcpPlayerVitalsHp

88

#showme $GmcpPlayerFriendsLength

3

#showme $GmcpPlayerFriendsValue2

Tom

#showme $GmcpRoomExitsLength

2

#showme $GmcpRoomExitsKey1

N

#showme $GmcpRoomExitsValue1

124

Такой подход к работе со структурированными данными продиктован необходимостью обеспечить обратную совместимость с JMC 3.6, где имя переменной прерывается символами _, ., [, { и т.п. (например, TinTin++ в аналогичной ситуации использует квадратные скобки: $Data[Char][Base][Str]).

Если требуется выполнять какие-то действия автоматически при получении определённых OOB-данных, то это можно сделать средствами триггеров на системные сообщения JMC (см. п. 5.8).

5.6 Картограф

При отрисовке карты картографом используется простой BFS-обход из центра рисуемого фрагмента карты. Никаких “растягиваний” переходов между комнатами или искусственно создаваемых “пустых” комнат для тех же целей не предусмотрено, т.к. пользователь никак не участвует в отрисовке карты, соответственно он не должен (и не может), например, “подправить” мышкой расположение комнаты, как это надо делать в маппере zMUD. Кроме того, в JMC отсутствует жёсткое разбиение всей карты на “зоны”, отрисовывается не вся текущая зона, а фрагмент карты местности, окружающей заданную комнату, который может включать части нескольких зон и не включать целиком ни одну из них. Вследствие этого отображаемый фрагмент карты может довольно сильно отличаться при выборе соседних комнат в качестве “центральных”.

Пример: дорога идёт прямо на восток, потом делает петлю с поворотом на 270 градусов и уходит прямо на север (как в дорожной развязке, без учёта изменения уровня высоты) (в клетке, помеченной звёздочкой (*), находятся две комнаты, как бы одна под другой)

                 ( )
                  |
                  |
                 ( )
                  |
                  |
  ( )--( )--( )--(*)--( )
                  |  / 
                  | /  
                 ( )

этот фрагмент будет отрисован по-разному, в зависимости от выбора центральной комнаты:

( )--( )--( )--( )--(#)
                     / 
                  | /  
                 ( )

или

  ( )
   |
   |
  ( )
   |
   |
  ( ) -( )
   |  / 
   | /  
  (#)

Другим неочевидным аспектом работы картографа является механизм автонумерации. Он, как было описано в п. 4.2.4, основан на идее последовательного создания комнат с нумерацией на основе хеш-функций и последующего “слияния” комнат, выглядящих одинаковыми в том смысле, что все возможные пути из них не более заданной длины “выглядят” одинаково. При этом две части ключа имеют следующие смыслы:

  • первичный ключ отражает “внешний вид” комнаты самой по себе, являясь хеш-функцией её названия, описания и зоны;
  • вторичный ключ отражает “внешний вид” окружения комнаты, являясь хеш-функцией всех известных исходящих путей не более определённой длины.

Механизм определения комнат, подлежащих слиянию (“дубликатов”) достаточно прост: при создании нового выхода из одной комнаты в другую А->Б, происходит цепное изменение вторичных ключей всех комнат, из которых можно попасть в комнату А (В->А, Г->А, Д->Г->A, …); если при этом обнаруживается, что обновлённый номер (первичный ключ + вторичный ключ) уже существует, то происходит попытка слияния двух комнат. Слияние реализовано по следующему алгоритму: 1 пара комнат, подлежащих слиянию, объединяются в одну группу слияния; 2 для каждой группы слияния составляется список выходов как объединения множеств выходов каждой комнаты в группе; 2.1 каждое направление выхода, ведущее в одну и ту же комнату или группу слияния добавляется в список выходов группы в неизменном виде; 2.2 каждое направление выхода, ведущее в разные комнаты/группы слияния, влечёт за собой создание из их совокупности новой группы слияния, в которую ведёт данный выход; 3 если (на шаге 2.2) создана хотя бы одна новая группа слияния, шаг 2 повторяется целиком; 4 для каждой группы слияния все комнаты, содержащиеся в ней, заменяются одной новой комнатой.

Может случиться так, что во время слияния выяснится невозможность его реализации (например, если в результате шага 2.2 в одной группе слияния оказываются комнаты с разными первичными ключами). Это будет означать, что исходные комнаты, помеченные как дубликаты, на самом деле ими не являются, а являются разными комнатами, хотя их различие не может быть обнаружено при текущих настройках (maxdifflen/maxidentlen). В текущей реализации слияние всё равно частично произойдёт.

В остальном алгоритмы, заложенные в картограф, довольно ясны из его поведения.

5.7 Защита соединения SSL / TLS

В JMC 3.7 шифрование данных реализовано с использованием библиотеки WolfSSL, которая является легковесной альтернативой OpenSSL и имеет практически идентичные API и модель использования. Логика работы с сертификатами сделана упрощённо:

  • при первичном подключении к серверу, его сертификат сохраняется как верный;
  • при последующих подключениях, если срок действия сохранённого ранее сертификата не истёк, он сверяется с предоставленным сервером, при несовпадении подключение не устанавливается;
  • если указан CA-файл, то проверяется подпись предоставленного сервером сертификата; самоподписанные сертификаты в этом случае не принимаются;
  • сами сертификаты хранятся в PEM-файлах в директории ./settings/;
  • реализована работа только с Х.509 сертификатами.

На данный момент многие MUD-серверы ещё не имеют поддержки шифрования данных (хотя бы туннелированием типа stunnel), а среди тех, что имеют, преобладает использование SSL / TLS. В единичных случаях встречается использование SSH, при этом разработчики ссылаются на понятные опасения, связанные с отсутствием поддержки SSH в популярных клиентах. В принципе, SSH довольно легко и безболезненно можно встроить с текущую реализацию JMC с использованием той же команды #secure ssh <...> и той же логикой сохранения ssh-ключей при первичном подключении к серверу.

5.8 Триггеры на системные сообщения

Во многих ситуациях бывает полезно выполнять автоматические действия при возникновении определённых “внутренних” событий JMC: установка соединения, получение GMCP-данных и т.п. В родственном клиенте TinTin++ для решения этой задачи была введена новая сущность “событие” (event) и, соответственно, команда #event, по своему формату очень напоминающая триггер. В JMC 3.7, как и в предыдущих версиях, применяется очень близкий подход, только без введения дополнительной сущности. Некоторые системные сообщения JMC проверяются на обычные триггеры так, будто они пришли с сервера.

Примеры:

 #action {/^#Connection established/} {#log $HOSTNAME.$DATE.log;1;Ivan;#daa qwerty}
 #action {/^#10 seconds till TICK/} {sleep}

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

 str1091 (ERROR! Cannot create script engine!)
 str1116 (#Connection closed by user.)
 str1176 (#%d Seconds till tick.)
 str1182 (#You are not connected.)
 str1190 (#Error - connection denied.)
 str1191 (#Error - network is not accesible.)
 str1192 (#Failed to connect.)
 str1193 (#Connection established.)
 str1196 (#Scripts reloaded.)
 str1197 (#TICK)
 str1198 (#10 seconds till TICK)
 str1199 (#Connection lost.)
 str1298 (#oob %ls %ls) -- здесь первым параметром идёт наименование протокола (GMCP/MSDP/MSSP), а вторым имя обновлённой переменной

Можно заменить значения в language.ini на любые другие, если существует угроза их совпадения с чем-то, что может прислать сервер, тем самым вызвав неуместное срабатывание триггера.

6 Заключение

Статья претендует на достаточно полное и корректное описание JMC 3.7, так что если у читателя остались “белые пятна” в понимании новых команд, опций, механизмов и т.п., а также в случае наличия замечаний по содержанию, оформлению, грамматике или просто функционированию новой версии программы, обратная связь может быть реализована через почтовый адрес konelav3 АТ mail.ru .


blog comments powered by Disqus