BLOG Mini-DevBlog команды GM-X

Статус
В этой теме нельзя размещать новые ответы.
Сообщения
2,491
Реакции
2,794
Помог
61 раз(а)
Здраствуйте. Совсем недавно пользователь wopox1337 поднял отличную тему https://dev-cs.ru/threads/3592/. В продолжении темы хотелось бы расказать об опыте и тонкостях разработкы подобной системы. Но сначала краткое описание.

photo_2018-08-29_13-00-29.jpg

GM-X - это система управления игровыми серверами на движках GoldSrc и Source. Данная система с легкостью позволяет делать такие стандартные вещи как выдача привилегий та наказания игрока. Но в отличии от все существующих систем она не базируется на структуре базы AmxBans. Тем самым появляются дополнительные возможности, которые раньше добавлялись костылями. Вот некоторые из них:
  • Группы привилегий. Больше не нужно помнить какие флаги принадлежать випу или админу. Мы просто создаем группу и уже ее присваиваем игроку
  • Игроку/Группе можно присвоить префикс в чате. Больше не нужно создавать конфиги на сервере. Все намного проще.
  • Можно присвоить несколько групп одновременно. Может быть полезно если админку мы выдали сами, а вип - уже нужно покупать
  • Пункт выше дает возможность сделать разные термины на разные привилегии на разных сервера. Это значит что игрок может иметь бесконечную админку, но вип у него будет на 10 дней. Но зато на другом нашем сервере он будет как простой игрок без привилегий
  • Разные контакты. Теперь не нужно добавлять поля ВК или Скайп. Игрок сам может ввести свои контакты. Статус в разработке
  • Теперь не нужно делать несколько таблиц для Бан/Гаг/Мут. Все это объединено в наказания.
Также стоит учесть, что все плагины общаются с базой через API вэб части. Это позволяет сократить количество запросов, а также убирает проблемы с кодировкой.
Особые благодарности Sonyx за название GM-X и SonG за логотип, oxoTHuk за предоставление git репозитория.


Данные мысли гуляли в умах многих. И мной предпринимались попытки несколько раз сделать подобное. Но все безуспешные ввиду утери рано или поздно интереса. Но в последний раз мной было снова начата разработка и позван Sonyx. Вместе мы начали разработку плана действий. Итого мы остановились на выборе микро-фреймворка на PHP Slim. Дальше я расскажу чем обусловлен данный выбор и какие проблемы он принес. Дальше к нам присоединились CrazyHackGUT, wopox1337, Pokemoshka, breakt, BoecSpecOPs, d3m37r4, h1k3, panikajo, SonG, oxoTHuk, Алексеич и AleXr. Работа закипела и начались первые трудности.
Трудность №1. Выбор языка. Так как от идеи в виде сервиса я решил отказаться и вести разработку в ключе self-hosted сервисов, то выбор очевиден - PHP. Он есть почти везде в отличии от других.
Трудность №2. Версия PHP. Так как формат системы CMS, то и учитывать нужно версии которые используются. И тут крайней версией стала 5.4 ниже которой спускаться уже нельзя. Но и это сильно старая версия, которая не поддерживается большинством компонентов из нашей системы. Посему выбор спал на PHP >= 5.6.
Трудность №3. Выбор фреймворка. В мире веб разработчиков давно существует правило не строить велосипеды. Самих фреймворков очень много, но лично для меня существуют только два самых популярных. Это Symfony и Laravel. Но так как это монструозные фреймворки хотелось что нибудь полегче. Из микро-фреймворков выбор падал на Slim, Silexи Lumen. Тут стоит учесть что Silex больше не разрабатывается, а Lumen поддерживает только PHP >= 7.1.3. В добавок тому что Slim поддерживает 5.4 еще понравилась концепция с middleware которая присуща в expressJS на NodeJS и которая мне сильно нравится. Но сам фреймворк состоит только из DI контейнера и роутера. Все остальное подключается через Composer. Посмотрев список дополнений я нашел почти все что нужно было. И выбор стал очевиден.
Трудность №4. Ничто так не разочаровывает как неудачный выбор. В итоге где то на 10% проекта стало известно что почти все те дополнения либо совсем не работают либо работают не так как нужно. Итого нами было переписано очень много (привет велосипедостроению), А именно работа с сессией, проверка CSRF (Сross Site Request Forgery) токена, авторизация и регистрация пользователей. Также были написаны с 0 компоненты форм и много чего еще.
Трудность №5. Авто-инсталлятор. Так повелось что все системы подобного рода имеют инсталлятор. А учитывая тот факт что мы пользуемся пакетным менеджером composer, то потребность в таковом просто необходима. Ведь обычные пользователи не сразу поймут как его установить и запустить, а учитывая что у многих хостинги, так и вовсе не получится. Итого был написан инсталлятор который умеет закачивать композер, устанавливать его, качать библиотеки, запускать миграции и создавать конфиг.
Screen Shot 2018-11-19 at 13.45.44.png

Продолжение следует.
19 Ноя 2018
Продолжение.
По инициативе wopox1337, приведу кусок кода инсталятора composer-а
PHP:
function downloadComposer($dir) {
    return file_put_contents($dir . DS . 'composer.phar', file_get_contents('https://getcomposer.org/composer.phar')) !== false;
}

function extractComposer($dir) {
    $composerPhar = new Phar($dir . DS .  'composer.phar');
    return $composerPhar->extractTo($dir);
}

function rrmdir($dir) {
    if (is_dir($dir)) {
        $objects = scandir($dir);
        foreach ($objects as $object) {
            if ($object != '.' && $object != '..') {
                if (is_dir($dir . DS . $object))
                    rrmdir($dir . DS . $object);
                else
                    unlink($dir . DS . $object);
            }
        }
        rmdir($dir);
    }
}

function composerInstall() {
    $tempDir = sys_get_temp_dir() . DS . uniqid('GameX', true) . DS;

    if (!is_dir($tempDir)) {
        if (!mkdir($tempDir, 0777, true)) {
            throw new Exception('Can\'t create folder ' . $tempDir);
        }
    }

    if (!downloadComposer($tempDir)) {
        throw new Exception('Can\'t download composer to ' . $tempDir);
    }
    if (!extractComposer($tempDir)) {
        throw new Exception('Can\'t download composer to ' . $tempDir);
    }
    require_once($tempDir . 'vendor' . DS . 'autoload.php');

    chdir(BASE_DIR);
//    https://getcomposer.org/doc/03-cli.md#composer-vendor-dir
    putenv('COMPOSER_HOME=' . $tempDir . 'vendor/bin/composer');
    putenv('COMPOSER_VENDOR_DIR=' . BASE_DIR . 'vendor');
    putenv('COMPOSER_BIN_DIR=' . BASE_DIR . 'vendor/bin');

    $input = new \Symfony\Component\Console\Input\ArrayInput(['command' => 'install']);
    $output = new \Symfony\Component\Console\Output\NullOutput();
    $application = new \Composer\Console\Application();
    $application->setAutoExit(false);
    $application->run($input, $output);

    rrmdir($tempDir);
}

Конешно инсталятор еще нужно пилить и пилить, но пока что более мнее работает. И вот тут новые трудности
Трудность №6. Хостингы со своими проблемами. После скачивания самого композера его нужно распаковать. Для этого создается временной каталог. И как оказалось не везде есть доступ к записи туда. Пока данную проблему мы еще не решили, но скорей всего распаковывать будем внутрь себя самих.

Следующим постом будет выбор стурктуры проекта. Как появится свободное время распишу
 
Последнее редактирование:
Сообщения
207
Реакции
420
Помог
10 раз(а)
Всем привет. Я занимаюсь в GameX разработкой серверной части для Source движка. Выполнена она будет в привычном обычному пользователю виде - плагин для SourceMod.
Плагину потребуется расширение REST in Pawn для работы. Как ранее указал fantom, система не предусматривает прямой работы с базой, потому плагин использует встроенное Web API самой панели.

Плагин будет поделён на составные части. Сейчас есть такой набросок деления:
  • Ядро. Предоставляет доступ к основному своему конфигу, к API сайта.
    Всю магию по составлению запросов к сайту оно берёт на себя.
  • Загрузчик игроков. Выполняет загрузку информации об игроке.
  • Модуль банов. Обрабатывает выданные баны игрокам при их заходе.
    Позволяет выдавать баны прямо на сервере.
    Само слово "бан" понимается в рамках модуля шире, чем обычно. Под баном может скрываться как полный запрет захода на сервера, так и обычное отключение чата.
  • Модуль администраторских прав. Загружает админки игроков, занимается их выдачей.
Возможно, что-то ещё будет. Это пока всё ещё на стадии разработки.

Немного о ядре
Как я и сказал ранее, ядро не делает ничего тривиального. Ядро является прослойкой между Web API и плагинами. Всю работу по авторизации в API и отдаче ответа на плагины оно берёт на себя.
Помимо запросов, есть возможность получить некоторые значения из конфига.
AHTUNG! В теме приведено неполное содержимое по некоторым причинам.
AHTUNG 2! Документация, которая есть в нём, пока что хромает, но время на её исправление будет уделено.
C++:
/**
* @section Typedefs
*/
typedef GameXCall = function void(HTTPStatus iStatusCode, JSON hResponse, const char[] szError, any data);

/**
* @section Natives
*/
/**
* Retrieves config value if exists.
*
* @param   szKey       Parameter name (key).
* @param   szBuffer    Buffer for writing value.
* @param   iMaxLength  Buffer size.
* @param   szDefault   Value for writing, if setting can't be found.
* @return              True, if setting exists and written.
* @note                Before writing setting value to plugin buffer,
*                      Core creates a temporary variable for storage
*                      value. This temporary variable have size 1024.
*                      You can't retrieve setting value with size
*                      higher 1024.
*/
native bool GameX_GetConfigValue(const char[] szKey, char[] szBuffer, int iMaxLength, const char[] szDefault = "");

/**
* Processes the request to GameX, if possible.
*
* @param   szCallName  Call name (like "player", "privileges" or "punish")
* @param   hRequest    JSON object with request values.
* @param   pFunction   Pointer to function-callback.
* @param   data        Any data. Data will be passed to callback.
* @throws              Throws error if web client is not ready.
*                      You can check ability for creating request manually
*                      with native GameX_IsReady()
*/
native void GameX_DoRequest(const char[] szCallName, JSONObject hRequest, GameXCall pFunction = INVALID_FUNCTION, any data = 0);

/**
* Checks GameX ability to processing HTTP API requests.
*
* @return              True, if GameX can process requests. False, if not.
*/
native bool GameX_IsReady();
Некоторые параметры из конфига ядро не даёт получить через свою обёртку. Например, ключ от API.

Это пока всё, что я хотел рассказать, и показать. В следующий раз расскажу, и, возможно, продемонстрирую работу модуля банов.
 
Сообщения
2,491
Реакции
2,794
Помог
61 раз(а)
Проблема №7. Структура проекта. Итак сначала мы начали писать в обычной для подобных систем структуре, где у нас есть корень и все внутри него. Но такой подход нам не нравился. Все дело в том что главный конфиг у нас в JSON формате и его спокойно можно будет выкачать через браузер. Также есть файловые кэши и другие критические места, которые нельзя допустить к скачиванию. Конечно, все решается правилами веб сервера. И как всегда проблема из описанного раньше места, Продукт нацелен на обычных пользователей, которые данные правила либо не смогут прописать, либо те которые мы укажем им не подойдут. Решено было вынести главные точки вхождения в каталог public. И заодно туда поместить инсталятор с ресурсами. И тут мы получили +1 проблему на ровном месте.
Проблема №8. Пути. думаю не секрет что пути бывают двух видов: относительные и абсолютные. Мы хотели добиться максимальной гибкости и используем только относительные в URL (для доступа к файлам везде только абсолютные во избежание конфликтов). Так как у нас появилось дополнительное звено public, то наш детект работает не нормально. Дополнительно получились проблемы в правилах rewrite для nginx. Если со вторым уже найдено решение, то с первым кое где еще возникают проблемы, которые мы потихоньку исправляем.
19 Ноя 2018
Итак с структурой мы определились, с фреймворком также определились. Но с отложеними задачами нет.
Проблема №9. Отложеные задачи. У нас присутсвует подтверждение регистрации. А значит нужна отправка email (о проблемах с ней мы также поговорим). Сразу стоит огворится. Я приверженец систем очередей и подобных решений. Главная проблема что если при самом запросе мы начнем отправку почти, то таким образом заставим клиента ждать пока та самая отправка закончится. А это не хорошо. Решение лежит на поверхности. Мы создаем задачу, в которой указываем действие и аргументы и завершаем регистрацию. Потом менеджер очередей отправит почту в фоне. И тут нам препятсвуют хостингы. У них как правило нет очередей. Обычное решение это крон. Так и мы поступили. Правда вокруг всего этого мы создали целую систему в которой есть разные задачи и разные обработчик, статусы и повторные попитки при неудачах. Также задача может быть циклическая с повторением в N минут (cron нельзя запускать чаще чем раз в минуту).
 
Сообщения
957
Реакции
1,185
Помог
52 раз(а)
По поводу фронтенда:

Было несколько векторов развития:
  1. VueJS + vuikit
  2. Статика + BootstrapCSS
  3. Статика + UIkitCSS
Было огромное желание сделать это все на Вью, собрать по человечески вебпаком, стили закинуть в less, но исходя из ориентира на конечного пользователя в первой версии решено сверстать статику, применяя шаблонизатор Twig.
Из CSS - фреймворков был выбран UIkit, т.к. он намного легковеснее бутстрапа, более современный и легко расширяем. Да и достал уже этот бутстрап капитально, если честно))) Все кастомизации стилей пока что будут вынесены в отдельный style.css, в дальнейшем сборка стилей будет производиться из LESS в единый монолит.

Почему вес стилей критичен? Потому что CSS в отличие от бекенда юзер будет загружать с сервера, особенно в этом случае выступает слабым звеном мобильный интернет.

Так же вся CMS будет полностью адаптивной - никаких горизонтальных скроллов и нечитаемых масштабов)
Дизайн админ-среды и паблик-страниц будет разделен, можно будет настраивать отдельно, существует возможность применения тем оформления.
 
Последнее редактирование модератором:
Сообщения
2,491
Реакции
2,794
Помог
61 раз(а)
С задачами вопрос решен. Но вот после добавления поддержки разных игр возник вопрос конфликтов STEAM_ID.
Проблема №10. Уникальный STEAM_ID. Проблема в том что разные игрокы на разных серверах или в разных играх могут иметь один и тот же STEAM_ID. То что кажется должно быть уникальным на самом деле таковим не являеться. Особенно если сюда еще добавить Non-Steam. Даную проблему мы решили с помощь дополнительного идентификатора, а именно префикса эмулятора, который в goldsrc и source играх разнится (даже стим). Тем самым мы снизили риск колизий.
Проблема №11. Привязка игрового акаунта к акаунту на сайте. Вопрос на первый взгляд тривиален. И как правило его решают с помощью простого input-а в настройках профиля. Но так как у одного игрока может быть несколько игр, и на проекте несколько серверов с разными играми, то и будут разные STEAM_ID у одного и того же игрока. Значит связь должны быть один ко многим. Также мы пошли путем облегчения ввода того самого ID. Ведь игрок не должен ничего знать о каких либо префиксах, команде status и прочего. Для этого игрок при добавлении нового игрового акаунта к себе в профиль генерирует себе некий токен. Потом он этот токен вводит в консоли игрового клиента, тем самым подтверждая что этот акаунт его.
Проблема №12. Мультыязычность. Почти все системы имеют одну жестко приязаную локаль. Мы же решили делать так называемую интернационализацию. Пока что мы храним переводы в JSON файлах. Но в планах вынести их в админскую часть с последующим кэшированием.
Проблема №13. Таймзоны. Исходя из пробелмы №12 нам также нужно решить вопрос разных часовых зон. На даный момент у нас все даты храняться/считаются и выводятся в UTC таймзоне. Это позволяет отойти от вопроса хранения разных таймзон. В планах добавить JS обработчик, который будет проходится по странице и заменять все времена на текущею таймзону пользователя. Тем самым бан в 19:00 будет реально в 19:00 по месному времени админа который просматривает бан.
19 Ноя 2018
Наверно хватит рсписивать все тонкости веб части. И остановимся на API, а точнее его токене. Сначала был выбран JWT (JSON Web Token) как токен для идентификации клиента. Стоит уточнить сразу что под клиентом мы указываем сам сервер на котором происходят действия. Но ввиду слишком длинного токена, решено отказаться в пользу рандомно сгенерированого токена через библиотеку openSSL.
Работу AmxModX части наверно стоит лучше расказать wopox1337. Но если кратко, то тут все печально ввиду полного отсутсвия модуля для REST API запросов. НО об этом попозже.
20 Ноя 2018
Наверно стоит описать основные проблемы на AmxModX части.
Проблема №14. Архитектура. Так как реено было отказатся от прямых запрсов к базе в пользу HTTP запросов к API, то нужно использовать модуль CURL или ему подобных. И вот тут наша работа существенно затормозилась. На выбор есть два модуля. Один вроде как работает, но имеет проблемы с памятью, второй же не имеет до конца прокинутого API для указания заголовков, и не возвращает полный ответ в следсвии чего нужно склеивать части ответа самому. И Это было бы еще не плохо, если бы не одно большое НО. В результате исполнения запроса мы получаем строку. А как известно строки в амхх сильно ограничены в длине (можно увеличить через #pragma), но нам хотелось получить полноценный REST фреймвок на вход которого мы подали бы JSON обьект, и который после запроса сам бы распарсил JSON и отдал нам готовый обьект с который можна проводить дальнейшие действия. Так как Си-программисты у нас не числятся, были ничтожные попитки сделать самим. Все безуспешны. Если есть желающие отпишитесь.
Проблема №15.
Плагины. Мы полностью изменили структуру базы данных. В следсвии чего все плагины которые существуют на данный момент работать не будут. А это значит что нужно с 0 разработать такие системы как админ лоадер, бан система и все что вокруг них. Сейчас есть только полуготовое ядро, которое может делать запросы.

На даный момент ентузиазм в команды немного приубавился, и проблемы с запросами стоят под большым вопросом. Если есть еще какие то вопросы задавайте не стейсняйтесь
 
Последнее редактирование:
Сообщения
3,572
Реакции
1,562
Помог
134 раз(а)
Так как Си-программисты у нас не числятся, были ничтожные попитки сделать самим. Все безуспешны. Если есть желающие отпишитесь.
у меня есть пару знакомых, попробую пригласить.
 
Сообщения
207
Реакции
420
Помог
10 раз(а)
В этот раз я решил поднять очень важную тему, как "работа с вебом".

Постом выше fantom показал, что именно используется в серверной части для Source движка. Изначально планировалась поддержка нескольких вариантов библиотек работы с API веб-панели:
  • REST in Pawn. Должен был быть самым приоритетным.
  • SteamWorks + SMJansson.
  • cURL + SMJansson.
В планах ещё была реализация (когда-нибудь) работы через Socket расширение. Но в итоге, пришлось выкинуть последние два (три, учитывая ещё Socket) варианта, и плагин работает только с REST in Pawn. Почему?
REST in Pawn
сам, ещё на уровне обработки ответа от сервера, разбирает JSON, и отдаёт уже готовый к работе указатель (handle) для работы со структурой. Чего не скажешь о других расширениях, где нам надо сначала прочитать ответ от библиотеки, и потом его отдать на другую библиотеку (SMJansson). Но тут есть подводный камень.
У SourceMod есть лимит на длину строки. Он составляет 4096 байт. Не забываем про нулевой символ, которым обязана завершаться любая строка.
В итоге получаем "полезную нагрузку" в 4095 байт. Если размер ответа превысит те самые "заветные" 4096 байт (что вполне может случиться), то часть ответа не будет передана, и в итоге JSON не удастся разобрать.
20 Ноя 2018
sbelov020, да, E-Mail + пароль.
 
Сообщения
2,491
Реакции
2,794
Помог
61 раз(а)
Регистрация будет через email?
К той библиотеке есть расширение для OAuth. Это позволит подключить регистрацию через такие сервисы как Facebook, VK, Steam, Twitter, Google. Правда мы еще не подключали, и надеимся, что все заработает без пробелем, но всяко бывает.

Дополнительно скажу, что привилегии (а именно так они у нас называются ибо нету понятия админок) выдаються только по стим ид + пароль или же ник + пароль. Хотя изначально в планах было только Стим ИД + пароль. Клан тэгы не поддерживаются. Для них в планах отдельный модуль. Привилегии разбиты на групы каждая из которых имеет свой набор флагов, и префикс в чате.
 
Сообщения
3,572
Реакции
1,562
Помог
134 раз(а)
fantom, я к тому, что может стоит подумать над каким-либо другим способом регистрации? Более индивидуальном, так скажем. Уникальном. Ибо на email и всем остальном легко зарегать много акков левых. Это может использоваться для различных злоупотреблений.
С другой стороны, способ должен быть не сложным...
 

ssx

Сообщения
288
Реакции
73
несколько таблиц для Бан/Гаг/Мут
Само слово "бан" понимается в рамках модуля шире, чем обычно. Под баном может скрываться как полный запрет захода на сервера, так и обычное отключение чата.
Было бы хорошо чтобы можно было пускать на сервер но не пускать за команду (оставлять в спектаторах) и чтобы то что пишут в чат отображалось бы в отдельный канал.
Или хотя бы чтобы выдавало указанный в конфиге флаг, а там плагинами уже можно всё что кому что нужно..
 
Сообщения
207
Реакции
420
Помог
10 раз(а)
ssx, при такой системе, как "одна таблица на всё" - вполне возможно сделать.
Идея интересная.
 
Сообщения
2,491
Реакции
2,794
Помог
61 раз(а)
Это может использоваться для различных злоупотреблений
Есть много способов идентификации пользователя. И ни один из них не может гарантировать, что не будет мультыакаунтов.
20 Ноя 2018
ssx, у нас единственная таблица для всех наказаний. Мы не различаем бан от мута или гага в базе. Для нас это наказание. Все что различает это тип наказания, некий идентификатор. А что будет реализовано под етим идентификатором зависит только от фантазии. Хоть полный запрет покупки оружия или возможность прыгать. Но стоит отметить, что типы должны быть синхронихированы как в клииенте (плагинах) так и в сервере (веб части).
20 Ноя 2018
Наверно стоит расписать чуть подробней. У нас нетипичное решение. Как писал выше, в базе данных мы не разделяем привилегии по типам демо випка или админка. Также мы не различаем наказания на баны, муты и гагы. Это все две сущности: привилегии и наказания. Разница лишь в типах. Тем самым мы решили конфликт с сотнями таблиц которые дублируют друг друга
 
Последнее редактирование:
Сообщения
957
Реакции
1,185
Помог
52 раз(а)
Есть много способов идентификации пользователя. И ни один из них не может гарантировать, что не будет мультыакаунтов.
в принципе мы можем применить накопленный опыт в отлове мультиаккаунтов, чтобы сделать не 100% защиту, но довольно хорошую (отпечатки)
20 Ноя 2018
мы не разделяем привилегии по типам демо випка или админка
Все же более удачным будет название роли ИМХО
 
Сообщения
2,491
Реакции
2,794
Помог
61 раз(а)
Sonyx, спасиобо что напомнил. Мы же не расписали об раграничении прав в нашей веб части. Может ты опишешь
 
Сообщения
957
Реакции
1,185
Помог
52 раз(а)
Итак, про разграничение прав на веб-части.

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

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

Каждая роль настраивается по принципу CRUD (Create Read Update Delete), то есть на каждое определенное действие юзера можно настроить одну из 16 вариаций прав. Это дает возможность каждой роли стать уникальной и расширяемой.

Локальное разграничение прав дает возможность определить административный ресурс для каждого сервера, подключенного к системе, не открывая доступ к соседнему для "шаловливых ручек")))
 
Сообщения
2,491
Реакции
2,794
Помог
61 раз(а)
sbelov020, уже Inline пишет модуль для АМХХ. Так что все ок.
26 Ноя 2018
Наверно немножко сброшу скринов веб части. Сразу уточню часть переводов еще не доделаная. А часть шаблонов еще старая ибо новые ждут слияния.
 

Download all Attachments

Сообщения
29
Реакции
9
Помог
1 раз(а)
Участие в тестировании принять возможно? Имеется всё для этого 3 игровых сервера и вебхост свой (ispmanager 5) ну и пользователи для теста соответственно)) от меня подробный багрепорт стабильно
 
Статус
В этой теме нельзя размещать новые ответы.

Пользователи, просматривающие эту тему

Сейчас на форуме нет ни одного пользователя.
Сверху Снизу