Cawfee

Подсчет размера строки

В теме 6 сообщений

Введение.

К сожалению, у многих пользователей, работающих с языком pawn, возникают вопросы, связанные с подсчетом размера строки. Вероятно, их просто отпугивает ошеломляющий вид конечной строки (которую мы получим на выходе). Хотя на самом деле здесь нет ничего сложного. Данная тема обретает актуальность, потому что все большее количество разработчиков пользуются данным методом. Ну, что ж, попробуем разобраться.

 

А действительно ли оно того стоит?

Было время, когда я и сам использовал таковой метод. Однако со временем я решил воздержаться от него. Почему? Казалось бы, все удобно и чудесненько. Но меня напрягает в таком случае мой же код: появляется некий мусор, который только мешает мне обрабатывать код. Давайте взглянем на два примера. В одном из них я использую метод, о котором пойдет речь. В другом же я воздерживаюсь от него и делаю это альтернативно (узнаем дальше).

Спойлер
Спойлер


#define MAX_LENGTH_MESSAGE	100 // объявим значение максимального количества символов в сообщении

CMD:send(playerid, params[])
{
	static const USE_COMMAND = !"Используйте команду следующим образом: /send [playerid] [text]"; // объявим массив, представляющий собой строку
	
	if(isnull(params)) // проверим, точно ли в params присутствует какой-либо текст
		return SendClientMessage(playerid, -1, USE_COMMAND) & 0; // если нет, уведомим пользователя и завершим работу функции
	
	new targetid; // объявим переменную для записи ID игрока, которому отправляем сообщение
	if(sscanf(params, "us[ "MAX_LENGTH_MESSAGE" ]", targetid, params)) // извлечем из массива params два аргумента: ID игрока (которому отправим сообщение) и само сообщение
		return SendClientMessage(playerid, -1, USE_COMMAND) & 0; // если не получилось выделить (нет этих аргументов), завершим работу функции с выводом сообщения
	if(targetid == INVALID_PLAYER_ID) // если игрок не подключен к серверу
		return SendClientMessage(playerid, -1, !"Указанный вами игрок отключен от сервера."); // завершим работу функции с выводом сообщения
	if(targetid == playerid) // если игрок пытается отправить сообщение сам себе
		return SendClientMessage(playerid, -1, !"Ну и зачем же отправлять сообщение самому себе?"); // завершим работу функции с выводом сообщения
	
	static const
		fmt_str1[] = "Ваше сообщение успешно отправлено указанному игроку (%s, ID: %d)", // объявим строку для подсчета
		fmt_str2[] = "Новое сообщение от %s: %s"; // объявим строку для подсчета
		
	new
		string1[sizeof fmt_str1+(-2+MAX_PLAYER_NAME)+(-2+3)], // объявим переменную, в которую будет записана наша строка
		str2[sizeof fmt_str2+(-2+MAX_PLAYER_NAME)+(-2+MAX_LENGTH_MESSAGE)]; // объявим переменную, в которую будет записана наша строка
		
	GetPlayerName(targetid, string1, MAX_PLAYER_NAME); // получаем имя игрока
	format(string1, sizeof string1, fmt_str1, string1, targetid); // записываем в первую строку имя игрока и его ID
	SendClientMessage(playerid, -1, string1); // отправляем сообщение
	
	GetPlayerName(playerid, string2, MAX_PLAYER_NAME); // получаем имя игрока
	format(string2, sizeof string2, fmt_str2, string2, params); // записываем в первую строку имя игрока и его ID
	SendClientMessage(targetid, -1, string2); // отправляем сообщение
	return 1;
}

 

Спойлер


#define MAX_LENGTH_MESSAGE	100 // объявим значение максимального количества символов в сообщении

CMD:send(playerid, params[])
{
	static const USE_COMMAND = !"Используйте команду следующим образом: /send [playerid] [text]"; // объявим массив, представляющий собой строку
	
	if(isnull(params)) // проверим, точно ли в params присутствует какой-либо текст
		return SendClientMessage(playerid, -1, USE_COMMAND) & 0; // если нет, уведомим пользователя и завершим работу функции
	
	new targetid; // объявим переменную для записи ID игрока, которому отправляем сообщение
	if(sscanf(params, "us[ "MAX_LENGTH_MESSAGE" ]", targetid, params)) // извлечем из массива params два аргумента: ID игрока (которому отправим сообщение) и само сообщение
		return SendClientMessage(playerid, -1, USE_COMMAND) & 0; // если не получилось выделить (нет этих аргументов), завершим работу функции с выводом сообщения
	if(targetid == INVALID_PLAYER_ID) // если игрок не подключен к серверу
		return SendClientMessage(playerid, -1, !"Указанный вами игрок отключен от сервера."); // завершим работу функции с выводом сообщения
	if(targetid == playerid) // если игрок пытается отправить сообщение сам себе
		return SendClientMessage(playerid, -1, !"Ну и зачем же отправлять сообщение самому себе?"); // завершим работу функции с выводом сообщения

	new
		string1[102+(-2+MAX_PLAYER_NAME)+(-2+3)+1], // объявим переменную, в которую будет записана наша строка
		string2[25+(-2+MAX_PLAYER_NAME)+(-2+MAX_LENGTH_MESSAGE)]; // объявим переменную, в которую будет записана наша строка
		
	GetPlayerName(targetid, string1, MAX_PLAYER_NAME); // получаем имя игрока
	format(string1, sizeof string1, "Ваше сообщение успешно отправлено указанному игроку (%s, ID: %d)", string1, targetid); // записываем в первую строку имя игрока и его ID
	SendClientMessage(playerid, -1, string1); // отправляем сообщение
	
	GetPlayerName(playerid, string2, MAX_PLAYER_NAME); // получаем имя игрока
	format(string2, sizeof string2, "Новое сообщение от %s: %s", string2, params); // записываем в первую строку имя игрока и его ID
	SendClientMessage(targetid, -1, string2); // отправляем сообщение
	return 1;
}

 

 

Заметьте: во втором случае мы сразу видим, с какой именно строкой работаем (при форматировании) - это визуальное удобство. Да и сокращается количество строк. Это нисколько не оптимизация, просто намного удобнее, когда в коде нет ничего лишнего. Не так ли? Впрочем, дело ваше. А свою позицию я высказал.

 

В чем принцип работы данного метода?

Идея проста - мы создаем константное выражение. Как известно, обработкой констант занимается сам компилятор, а не сервер. Именно компилятор и рассчитывает размер строки, которую мы будем форматировать. Например, использовав такой код, мы получим размер строки (8, поскольку учитывается еще нуль-символ):

new text[] = "привет";
printf("%d", sizeof text);

Здесь компилятор подсчитывает количество ячеек в массиве "привет!". Именно это количество ячеек (и завершающий нуль-символ) есть размер строки, который выведет функция sizeof.

 

Итак, в моем примере сама строка имеет следующий вид:

static const fmt_str1[] = "Ваше сообщение успешно отправлено указанному игроку (%s, ID: %d)";

Именно ее размер компилятор посчитает автоматически (почему используется static const?).

Далее мы создаем новую переменную, куда будем форматировать нашу строку. Но почему нельзя провернуть такую операцию с имеющейся строкой? Вся проблема в том, что наша строка (fmt_str1) объявлена на количество ячеек, которое занимает весь наш текст (а это количество ячеек - 64+1 - с учетом нуль-символа).

 

Обратите внимание: в нашей исходной строке присутствуют спецификаторы (%s и %d), вместо которых при форматировании будут подставлены наши строковое и цифровое значения соответственно. Именно длины, необходимые для данных аргументов. Например, для аргумента "hello world" нам необходима длина в 11 символов, для записи года нам нужно будет 4 символа, для записи имени игрока - MAX_PLAYER_NAME (24) символов. Динамические (непостоянные) значения использовать не получится. Однако, можно лишь предохраниться и задействовать количество ячеек, в которое вы точно уложитесь.

 

format - функция, которая осуществляет форматирование строки. Ее суть проста - мы передаем в функцию ту строку, в которую записываем (а если быть точнее, то ее адрес), передаем адрес строки, которую записываем, передаем аргументы. Далее обычный цикл перебирает все ячейки нашей строки (которую записываем) и ищет спецификаторы (%d, %s, %b, %c, %f, %x, %i). Эти спецификаторы также могут иметь и другие нюансы (измененное количество символов, например, см. дальше). Как только цикл находит спецификатор, то буквально стирает его и записывает наш аргумент первый. Находит второй спецификатор - записывает второй аргумент. Если же количество спецификаторов не совпадает с количеством аргументов, тогда записываются до тех пор, пока существуют "свободные" аргументы и спецификаторы. Не хватает аргументов - спецификаторы просто удаляются. Не хватает спецификаторов - аргументы просто игнорируются. Если тип спецификатора не соответствует типу аргумента, то запись производится неправильно (ибо для избежания этого не реализованы какие-либо проверки).

 

Итак, как ранее было сказано, оператор sizeof вычисляет (причем еще на этапе компиляции!) длину строки, которая указана как аргумент. Наша переменная, в которую будем форматировать строку, должна иметь достаточное количество ячеек как для самой строки, так и для ее аргументов. Поэтому начинаем расписывать ее размер (самая пугающая для большинства часть). Кстати, если же рассмотреть со стороны математики, где A - размер переменной, B - размер строки, а C, D, E... (и так далее) - размеры аргументов, то мы получаем следующее множество: A=B+C+D+E..+Z.

 

Создадим строку, на основе которой будем проводить наши расчеты:

static const fmt_str[] = "Сегодня %02d/%02d/%d, время - %02d/%02d. Вас зовут %s. Ваше здоровье - %.2f";
new string[], year, month, day, hour, minute, Float:health;
getdate(year, month, day);
gettime(hour, minute);
GetPlayerName(playerid, string, MAX_PLAYER_NAME);
format(string, sizeof string, fmt_str, day, month, year, hour, minute, string, health);
SendClientMessage(playerid, -1, string);

Длина нашей строки - 75+1 символов (не забываем про нуль-символ). Любой нормальный редактор кода показывает количество символов в строке.

 

Наша строка хранит в себе информацию о сегодняшней дате (дд/мм/гггг - это 2(дд)+2(мм)+4(гггг)+2(/) =  10 символов), о времени (чч/мм - это 2(чч)+2(мм)  = 4 символа), об имени игрока (MAX_PLAYER_NAME - это константа, которая имеет значение 24), а также о его здоровье. Вещественное (дробное) значение после запятой (плавающей точки) может хранить как 1, так и 6, 7, 8 символов. Потому я обрезал выводимое количество символов после запятой лишь до двух, о чем свидетельствует сама форма записи: "%.2f". Соответственно, если бы она имела вид "%.5f", вы бы увидели после запятой пять символов (при наличии лишь одного выведется "1.800000"). А если вам вообще не нужны цифры после запятой, делаем как-то вот так: "%.0f". Аналогичный метод работает и в целочисленных выражениях. Если мы записываем в форматировании спецификатор вида "%03d" - это значит, что выражение обязательно должно состоять из трех символов (если их больше, остальные просто не отображаются, а если меньше - дописываются нули в начале).

 

Ранее было сказано, что наша форматируемая переменная в свои размеры обязательно включает размер строки с текстом (fmt_str), потому в квадратных скобках мы обязательно указываем "sizeof fmt_str" - это стандартная процедура, которая должна производиться всегда при использовании данного метода.

Теперь мы как бы начинаем приплюсовывать длины наших аргументов. Словно читаем, слева направо имеем:

  1. Вывод дня, месяца, года. Отнимаем длины спецификаторов и заместо них добавляем длины, которые будут записаны вместо этих спецификаторов: (-4+2) - это мы вычисли длину спецификатора "%02d" и добавили длину аргумента (дня), который содержит не больше двух символов. Аналогично имеем для месяца (-4+2), а для года получаем выражение следующего вида: -2+4, ведь здесь спецификатор состоит лишь из двух символов ("%d"), а аргумент (год) состоит из четырех символов (2019). Итого по данному пункту наша строка имеет вид: "sizeof fmt_str+(-4+2)+(-4+2)+(-2+4)".
  2. Вывод времени. Здесь повторяем аналогичную операцию: вычитаем длину спецификаторов, прибавляем длину (максимальную) часов (2) и минут (2). Теперь наша строка должна выглядеть так: "sizeof fmt_str+(-4+2)+(-4+2)+(-2+4)+(-2+2)+(-2+2)".
  3. Вывод имени игрока. Как известно, максимальная длина имени игрока - MAX_PLAYER_NAME (константное выражение, равное 24). Хотя на самом деле максимальная длина ника составляет 22 символа, но все-таки принято использовать константы (ведь неизвестно, какие изменения произойдут с рабочими папками в последующих обновлениях SAMP/CRMP). Вычитаем длину спецификатора, обозначающего запись строки (%s), и записываем максимальную длину имени игрока (константу): (-2+MAX_PLAYER_NAME). Новый вид всей нашей строки: "sizeof fmt_str+(-4+2)+(-4+2)+(-2+4)+(-2+2)+(-2+2)+(-2+MAX_PLAYER_NAME)".
  4. Вывод количества здоровья игрока. Здесь мы сталкиваемся с вещественным числом. Известно, что здоровье игрока не может превышать 100. Следовательно, не может быть длиннее трех символов. А вот количество символов после запятой не ограничено системно (разве что максимальной вместимостью ячеек). Потому я их ограничил вручную, обрезав всего лишь до двух символов. Получаем нашего спецификатора "%.2f": 4. Вычитаем из длины форматируемой строки 4 символа, а заместо них добавляем максимальную длину здоровья игрока (3 символа до запятой и 2 символа после запятой): "-4+5". Теперь вся наша строка должна получить такой вид: "sizeof fmt_str+(-4+2)+(-4+2)+(-2+4)+(-2+2)+(-2+2)+(-2+MAX_PLAYER_NAME)+(-4+5)".

 

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

 

Автор статьи: @odosenok.

 

P.s. многовато получилось текста. Но я старался пояснить все, что только можно. Да и знания лишними не бывают. Не поленитесь и читайте статью полностью :$.

 

Еще один примерчик, для самостоятельного разбора. Ничего в нем особенного нет. Впрочем, можете оставлять в теме свои строки, разберемся, с чем есть строки ваши.

 

Спойлер

const
    MAX_LENGTH_PLAYER_LEVEL = 3,
	MAX_LENGTH_PLAYER_INTERIOR = 5; // 4 символа, но виртуальный мир может быть отрицательный. Под знак минуса тоже требуется ячейка

static const fmt_str[] = "Ваше уровень: %d, уровень розыска: %d, интерьер: %d";
new string[sizeof fmt_str+(-2+MAX_LENGTH_PLAYER_LEVEL)+(-2+1)+(-2+MAX_LENGTH_PLAYER_INTERIOR)];

 

 

Отредактировано пользователем odosenok

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Довольно не плохо, видно что постарался , держи плюсик
Но можно было бы сделать статью не много по меньше, не каждый User станет всё читать..

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

@odosenok Тут полностью согласен!

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты
4*(-4+2)+(-2+4)+(-2+MAX_PLAYER_NAME)+(-4+6)

И откуда взялся +1?

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

@m1n1vv, не учел, что sizeof возвращает размер с учетом нуль-символа. В итоге он прибавлялся дважды в моем коде. Поправил везде. Спасибо.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Для публикации сообщений создайте учётную запись или авторизуйтесь

Вы должны быть пользователем, чтобы оставить комментарий

Создать учетную запись

Зарегистрируйте новую учётную запись в нашем сообществе. Это очень просто!


Регистрация нового пользователя

Войти

Уже есть аккаунт? Войти в систему.


Войти

  • Последние посетители   0 пользователей онлайн

    Ни одного зарегистрированного пользователя не просматривает данную страницу

  • Похожий контент

    • Sleash
      От Sleash
      Всем доброго времени суток. Сегодня мне бы хотелось вам рассказать от таких функциях в PAWN-языке, как SetTimer и SetTimerEx.
      Сразу сделаю такое примечание: Для SetTimer и SetTimerEx можно использовать ТОЛЬКО public!!!
      SetTimer
      Итак, для начала приступим к лёгкому и пойдём по нарастающей:
      Первое, это SetTimer. Функция включает в себя 3 параметра:
      native SetTimer(funcname[], interval, repeating); funcname[] - Это название функции, по сути - название вашего pablic в кавычках.
      interval - Интервалы от вызова таймера до срабатывания pablic
                     Так же если стоит повторение таймера, то будет работать как интервал между таймерами.
                     Измеряется в миллисекундах. 1000 миллисекунд равно 1 секунде
      repating - Тут всё просто: будет ли повторяться ваша функция. Может иметь значения: true - будет выполняться повторение ИЛИ false - вызывается 1 раз.
      Давайте разберём пример для наглядности:
      forward Info(); public Info() {     SendClientMessageToAll(0xFFFFFFFF, "Наш сайт: pawno-rus.ru"); } // И теперь давайте вызовем данный таймер при запуске мода public OngameModeInit() {     SetTimer("Info", 1000*60*20, true);     return true } Давайте разберёмся по интервалу: 1000*60*20. Так как я писал ранее, что 1000 млСек = 1 сек, то: 1 сек * 60 = 1 минута; 1 минута * 20 = 20 минут.
      Таким образом данный таймер будет выводить каждые 20 минут для всех игроков на сервере в чат: "pawno-rus.ru".
       
      SetTimerEx
       
      По сути с SetTimer - всё, теперь перейдём к следующей функции: SetTimerEx:
      native SetTimerEx(funcname[], interval, repeating, const format[], {Float,_}:...); Тут добавляются ещё 2 параметра:
      const format[] - формат данных для ввода в функцию. (Такие как: s, d, f);
      {Float,_}:... - переменные или значение для данных из const fromat[]
      Из данных объяснения мало что можно понять незнающему или начинающему скриптеру, поэтому давайте разберём на примере кода, который при входу даёт на авторизацию две минуты игроку:
      forward autorez(playerid); public autorez(playerid) {     if(/*проверка на то залогинился ли игрок*/)     {         SendClientMessage(playerid, 0xFFFFFFFF, "Вы были кикнуты по причине: Время на авторизацию истекло.");         Kick(playerid);         return true;     }     return true; } //Дальше уже добавлям сам таймер public OnPlayerConnect(playerid) {     SetTimerEx("autorez", 1000*60*2, false, "d", playerid);     return true; } Если первые три пункта мы уже разбирали. то начнём сразу с 4-го:
      4. Тут в кавычках надо вставлять тип данных. Наподобие функции format(output[], len, "%s %d %f", string, int, float), только без знака процента (%)
      5. Тут уже указывают сами данные, так же разберём как и предыдущий: format(output[], len, "%s %d %f ", string, int, float)
       
      Я надеюсь что кому-то помог, а если что непонятно, то спрашивайте, всегда помогу.
    • NoVate
      От NoVate
      Доброго времени суток 
       
      Многие новички задавались вопросом: "Как создать свой RP сервер?", "С чего начать разработку?", "Это сложно и я не справлюсь".
      После таких вопросов многие уходили с этой темы и оставляли её на последней полке. Некоторые брались и сталкивались с большими проблемами. И только часть из этих людей доходили до какого-то результата.
       
      Моя цель: сделать создание сервера в SA:MP проще. Чтобы каждый из тех, кто интересовался созданием сервера мог спокойно взять готовую, а что самое главное - базовую сборку и под неё уже создавать различные свои задумки. SA:MP на данный момент не особо актуален, а тем более самый обычный клиент, НО, как мне кажется, если сделать что-то невероятное, то аудитория заметит это и ей будет достаточно интересно "пощупать" данное новшество.
       
      Для тех, кто начнёт писать: "Ты видел свой код?", "Ты сделал не правильно в 777-ой строке" и так далее. Сразу отвечу, цитируя:
       
      Кому стало интересно и кто хотел бы ознакомиться со сборком - прошу в мой Github (не реклама).
      Буду делать коммиты по мере свободного времени и написании кода.
      Если есть какие-то идеи, то пишите в мой Discord сервера (не реклама), который можно будет найти на той же странице.
       
      Спасибо всем за прочтение и хорошего настроения 
    • xiemoniyaz
      От xiemoniyaz
      C:\Users\111\OneDrive\Ðàáî÷èé ñòîë\rrrr\gamemodes\arizona.pwn(78772) : error 017: undefined symbol "GEO_MAX_COUNTRY_NAME_LENGTH" C:\Users\111\OneDrive\Ðàáî÷èé ñòîë\rrrr\gamemodes\arizona.pwn(78772) : error 009: invalid array size (negative, zero or out of bounds) C:\Users\111\OneDrive\Ðàáî÷èé ñòîë\rrrr\gamemodes\arizona.pwn(78772) : error 036: empty statement C:\Users\111\OneDrive\Ðàáî÷èé ñòîë\rrrr\gamemodes\arizona.pwn(78772) : fatal error 107: too many error messages on one line Compilation aborted. Pawn compiler 3.2.3664          Copyright (c) 1997-2016, ITB CompuPhase 4 Errors.  
       
      cmd:get(playerid, params[]) {     extract params -> new player; else return SendClientMessage(playerid, -1, !"/get [playerid]");     new string[GEO_MAX_COUNTRY_NAME_LENGTH+GEO_MAX_REGION_NAME_LENGTH+GEO_MAX_CITY_NAME_LENGTH+GEO_MAX_PROVIDER_NAME_LENGTH+GEO_MAX_MOBILE_STATUS_LENGTH+GEO_MAX_PROXY_LENGTH-6+118];     format(string, sizeof(string),     "Страна: %s\n\     Регион: %s\n\     Город: %s\n\     Провайдер: %s\n\     Использует ли моб.сеть: %s\n\     Использует ли прокси: %s",     GetPlayerIpCountry(player),     GetPlayerIpRegion(player),     GetPlayerIpCity(player),     GetPlayerIpProvider(player),     GetPlayerIpMobileStatus(player),     GetPlayerIpProxyStatus(player));     ShowPlayerDialog(playerid, 0, DIALOG_STYLE_MSGBOX, !" ", string, !"Закрыть", !"");     return true; }  
    • DEST
      От DEST
      Командный процессор: zcmd / dc_cmd + sscanf.
       
    • Daniil Korochansky
      От Daniil Korochansky
      Здравствуйте. Я заметил что нет нормальных полноценных IDE для Pawn, большинство IDE какие-то багованнные, выскакивают разнообразные ошибки, в "Extreme Studio" к примеру. А в Pawno работать не удобно и в нём довольно скудный функционал. Ну вот.
       
      Вот, сейчас я разрабатываю IDE которое называется "Spawn"(Samp pawn), которое потом будет много раз тестироваться, чтобы минимизировать самые разные ошибки если они будут. Когда будет закончена основа IDE, программа будет разрабатывать по предпочтениям пользователей/скриптеров, чтобы они хотели увидеть в программе, а там я уже буду решать реально ли требуется это программе или нет.
      Главная цель программы по максимуму упростить разработку серверов SAMP.
      Также программа будет как для Windows так и для Linux.
      Распространяться будет по лицензии GPL с закрытым исходным кодом. 
       
      Я немного опишу что можно будет делать в в программе, что уже реализовано и что хочу сделать:
       - Уже закончена работа над графической частью с которой взаимодействует пользователь(Меню, панель инструментов, диалог настроек, редактор и т. д.)
      - Реализована подсветка синтаксиса Pawn
      - Файлы будут открываться в вкладках редактора
      - Реализована "Карта документа (Document map)" в правой стороне редактора с которой можно взаимодействовать(прямо как Sublime Text)(В настройках можно будет скрыть)
      - Автоматическое закрытие скобок -> {}() ""
      - Подсветка скобок которые взаимосвязаны между собой, на которые указал пользователь курсором.(Настраивается)
      - Будет реализованы подсказки вызова(Call Tips) и авто-дополнение кода.
      - Реализована настройка подсветки языка и настройка шрифта
      - Live templates(Живые шаблоны)
      - Поддержка компиляторов от SAMP Team, Zeex и модификацией Zeex(которая на удивление очень быстро компилирует исходники) и конечно же переключение между ними
      И многое другое. 
       
      Есть сообщество Spawn, там можно найти так называемую статью "Roadmap"(или Карта разработки) которая регулярно обновляется, там так же отмечается что уже закончено.
      Сообщество: https://vk.com/spawnsoftware