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

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

Cawfee
Великий Гуру

Введение.

К сожалению, у многих пользователей, работающих с языком 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 станет всё читать..

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


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

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

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


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

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

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


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

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

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

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

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


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

Войти

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


Войти

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

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

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

    • Dekmveka
      От Dekmveka
      Здравствуйте, уважаемые пользователи форума PAWNO-RUS.
       
      В этой теме хочу поделиться видеоуроками, ориентированных на новичков, которые сейчас начинают только изучать pawn. 
      Хочу сказать сразу, я не профессиональный блогер с крутым оборудованием, поэтому если что за какие то погрешности в съемке заранее извиняюсь.
      Хотелось бы попросить не писать, что типа pawn и samp уже умер, неактуален, уроков и так полно и так далее и тому подобное. Я лишь просто занимаюсь своим увлечением и решил делиться знаниями с новичками.
      Далее на канале после плейлиста с основами планируется выложить плейлист по MySQL и максимально подробно всё рассказать о MySQL, будет старая добрая рубрика "Мод с нуля" (но на основе плагинов, стандартов и др. актуальных в 2025-2026 году). 
       
      Также хочу сказать, что есть также телеграмм канал. 
      На данный момент там есть ветка для общения, ветка "Инструменты" (там лежат архивы со всем нужным + есть навигационный пост, я просто поделил всё на категории и просто вставил ссылки на архивы которые туда же выложил. Можете зайти посмотреть если интересно). Также в телеграмм канале потом будут исходные файлы с плейлиста "Мод с нуля" и других плейлистах, пока что говорить подробно не буду что буду ещё выкладывать. 
       
      В общем, я даю ссылки, кому интересно, можете смотреть.
      [Часть 1]: Архитектура ядра SAMP || Основы программирования PAWN/PAWNO  -  *кликабельно*
      [Часть 2]: База мода, переменные, format() || Основы программирования PAWN/PAWNO  -  *кликабельно*
      Telegram канал  -  *кликабельно*
       
      Желаю всем приятного просмотра, а также успехов в обучении 
    • Antoxa39
      От Antoxa39
      Здарова бандиты. 
      Вообщем, в этом уроке я расскажу и покажу как подключить Базу Данных к MySQL.
      Не регистрацию,и не в готовом моде где просто пользователя пароль хост и т.д меняете.
      А подключение с нуля.
       
      1.Нам понадобится MySQL.  Я использую R39-6. Скачать 
      2.Приступаем к подготовительному этапу.
      Заходим в Pawn и подключаем MySQL чуть ниже инклюда a_samp. #include <a_mysql>
      Должно быть так.
      После этого отчищаем мод от хлама. Должно получится вот так: жмЫкс
      3.Подключение
      Дальше нам требуется создать переменную,эта переменная будет хранить ИД подключения. в MySQL R39 создаем простую переменную.
      new dbHandle;  В MySQL R41 создаем так:
       new MySQL:dbHandle;  
      теперь создаем Константы
      #define Host "127.0.0.1" #define User "root" // это если вы используете Denwer. Если хостинг указываете другого пользователя. #define DataBase "urok" #define Password_SQL ""//Если вы на хостинге ставите пароль,для Denwer пароль не требуется, чуть позже скажу как его установить Все, теперь переходим в public OnGameModeInit()
      public OnGameModeInit() {     dbHandle = mysql_connect(Host,User,DataBase,Password_SQL); // приравниваем нашу переменную с ИД подключением к коннекту к базе данных. Извиняюсь если коряво обьяснил. Это для R39     return 1; } Для R41
      public OnGameModeInit() {     dbHandle = mysql_connect(Host,User,Password_SQL,DataBase);// В R41 поменялись местами Пароль и База     return 1; } Если сделаем чтобы при подключении к базе выводилось сообщение о том,подключилось или нет. Я сделаю это без switch. Чтобы сильно не загружать смотрящих этот урок.
      Делаем все там же в public OnGameModeInit()
      public OnGameModeInit() {     dbHandle = mysql_connect(Host,User,DataBase,Password_SQL);     if(mysql_errno())     {         printf("Подключение к базе данных %s не удалось.",DataBase); // %s означает строку. То есть будет выводить название базы данных которое указано в #define DataBase     }     else     {         printf("Подключение к базе данных %s успешно",DataBase);     }     return 1; } Все, с этим закончили. Остался последний этап, это отключение базы данных когда мод выключается.
      Идем в паблик OnGameModeExit()
      public OnGameModeExit() {     mysql_close(dbHandle);     return 1; } Все. База данных подключена.
      Чтобы скачать Denwer жмите сюды: плямп
      Для установки можете перейти по этой ссылке: УСТАНОВКА
    • sinvays
      От sinvays
      Продаётся проект Criminal RolePlay с 8-летней историей.

      Проект заморожен и не используется длительное время.
      Потратил на него около 250.000 рублей, продаю за приемлимую цену.

      В комплекте:
      Полноценный игровой мод Мобильный лаунчер с Figma-дизайном Кэш клиентской части Все версии оформления проекта Все группы проекта Criminal RolePlay Шаблоны всех версий сайта, включая ранее не выпущенный шаблон Модуль автодоната YooMoney Помощь с установкой игрового мода на хостинг Цена:
      49 900₽ — за весь комплект.
      Возможен торг при адекватных предложениях.

      Важное:
      Проект продаётся в одни руки. Если в течение месяца не найдётся покупатель, будет рассмотрена продажа по частям.
      Связаться: https://vk.com/rosetta
    • sinvays
      От sinvays
      Продаётся проект Criminal RolePlay с 8-летней историей.

      Проект заморожен и не используется длительное время.
      Потратил на него около 250.000 рублей, продаю за приемлимую цену.

      В комплекте:
      Полноценный игровой мод Мобильный лаунчер с Figma-дизайном Кэш клиентской части Все версии оформления проекта Все группы проекта Criminal RolePlay Шаблоны всех версий сайта, включая ранее не выпущенный шаблон Модуль автодоната YooMoney Помощь с установкой игрового мода на хостинг Цена:
      49 900₽ — за весь комплект.
      Возможен торг при адекватных предложениях.

      Важное:
      Проект продаётся в одни руки. Если в течение месяца не найдётся покупатель, будет рассмотрена продажа по частям.
      Связаться: https://vk.com/rosetta
    • Elvis
      От Elvis
      Настоящим информируем о начале набора сотрудников в нашу студию "AlinSA Studios". В рамках текущего проекта "VanguardRP", основанного на мотивах CRMP, мы ищем профессионалов и энтузиастов для выполнения следующих должностей:
       
      1. Технические специалисты
       
      Требования:
       
      | Базовые знания в области информационных технологий
      | Понимание структур кода и принципов работы компиляторов
      | Готовность к обучению и развитию профессиональных навыков
      | Иметь базовое представлении ЯП "Pawn" или "Kotlin" и "Java".
       
      Обязанности:
       
      | Поддержка технической инфраструктуры проекта
      | Участие в разработке и оптимизации программных решений
      | Взаимодействие с командой разработки для внедрения новых функций
       
      2. Пиар-ассистенты
       
      Требования:
       
      | Опыт работы в сфере SMM, маркетинга или PR
      | Навыки взаимодействия с аудиторией и ведения социальных сетей
       
      Обязанности:
       
      | Продвижение проекта в социальных сетях
       
      3. Дизайнеры
       
      Требования:
       
      | Наличие портфолио с примерами выполненных работ (не обязательно, желательно)
      | Владение графическими редакторами ! (Photoshop и Figma)
      | Креативность, внимательность к деталям и способность работать в сжатые сроки
       
      Обязанности:
       
      | создание графического контента для соцсетей, сайта и внутриигровых материалов
      | Разработка дизайна интерфейсов, баннеров, логотипов и иллюстраций
      | Участие в визуальном оформлении проекта
       
      4. Геймдизайнеры
       
      Обязанности:
       
      | Разработка игровых механик, правил взаимодействия и систем баланса
      | Проработка сюжета, персонажей и сценариев для создания эмоциональной связи с игроками
      | Проектирование уровней, задач и сценариев для обеспечения разнообразия игрового процесса
      | Взаимодействие с командой разработчиков, художников и звукорежиссёров для достижения согласованности элементов игры
      | Участие в тестировании прототипов, выявление проблем и балансировка игрового процесса
       
      5. Рупоры
       
      Цель роли: Обеспечить эффективное взаимодействие с целевой аудиторией, формировать положительный имидж проекта, своевременно информировать участников о новостях, обновлениях и
      мероприятиях, а также модерировать сообщества для поддержания комфортной и безопасной среды.
       
      -————————-
      Условия работы:
       
      Обучение и постоянная поддержка со стороны команды
      Гибкий график работы / удалённый формат (обсуждается индивидуально)
       
       
      Если вы заинтересованы в присоединении к нашей команде или хотите получить дополнительную информацию, обращайтесь по следующему контакту:
      Telegram: @AntonLegost
       
       
      Благодарим за внимание! Надеемся на плодотворное сотрудничество.
       
      p.s Новички приветствуются. Для пополнение портфолио и наработки опыта.
       
      С уважением,
      Команда "AlinSA Studios"