Содержит приличный объем кода, который лучше видно в исходном блоге.

Знакомство с онлайн-играми

Мы все играли в онлайн-игры в своей жизни раньше, возможно, даже не замечая этого. Будь то MMO, многопользовательский режим или просто игра, требующая подключения к серверу, с конца 90-х они присутствуют везде и в разных формах, а с включением мобильного рынка поверх уже огромная база пользователей ПК, с годами она набирала все больше и больше оборотов.

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

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

Фон

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

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

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

Алмаз в необработанном виде

На протяжении многих лет я вел список онлайн-игр, которые уже встретили свою судьбу, и которые станут потенциальными кандидатами, когда придет подходящее время. Тот, кто всегда был на вершине списка, и я знал, что он будет первым, кто упадет, — это Jewelry Master.

Jewelry Master — это онлайн-игра-головоломка, разработанная Arika (известная в сериях Tetris Grand Master и Street Fighter EX). ) в 2006 году, и он был выпущен в качестве тестового проекта для потенциальной полнофункциональной консольной версии в будущем, что и произошло четыре года спустя с выпуском Jewelry Master Twinkle для Xbox 360. Серверы для игра была закрыта примерно в 2011 году, что сделало игру полностью неиграбельной.

Думаю, к этому моменту вы уже поняли, почему я выбрал именно это название, а не что-то еще. Эта игра не только имеет некоторое сходство с моей любимой игрой TGM3 (вплоть до того, что я сделал для нее эмулятор и патч разрешения), но и потому, что она >не онлайновая многопользовательская игра, а скорее онлайновая одиночная игра, позвольте мне объяснить.

В Jewelry Master есть только две основные онлайн-функции: аутентификация пользователей и управление рейтингами/таблицами лидеров. Но даже в этом случае игра не пройдет дальше экрана входа в систему, если на другой стороне не будет запущен сервер. Это делает эту игру идеальной целью для первого проекта, подобного этому, поскольку мне не нужно иметь дело с несколькими серверами, многопользовательским управлением состоянием и другими забавными концепциями многопользовательской онлайн-игры. И, как вы прочтете позже, в этой игре нет никакой системы безопасности/защиты, так что сразу к делу.

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

Связь с сервером

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

Теперь, когда мы знаем текущую ситуацию, давайте посмотрим на связь с сервером в Wireshark:

Мы можем взять ценную информацию отсюда. Мы видим, что домен сервера — hg.arika.co.jp, и DNS-сервер не может разрешить для него адрес. Из-за этого мы не можем двигаться дальше в текущем состоянии, поэтому нам нужно найти способ обойти это, прежде чем продолжить.

Обман игры

Итак, теперь нам нужно обмануть игру, чтобы найти сервер в другом месте. К счастью, в Windows у нас есть файл hosts, который позволяет нам сопоставлять имя хоста с другим IP-адресом; идеальное временное решение без необходимости исправлять программу или перехватывать сетевые функции. Добавляя новую запись, мы можем перенаправить доменное имя на любой сервер, который нам нужен, в нашем случае localhost:

127.0.0.1 hg.arika.co.jp

С этим небольшим изменением для всех вызовов сервера в этом домене исходное имя хоста сервера будет заменено на localhost, при этом номер порта и остальная часть маршрута останутся нетронутыми. Теперь это позволяет нам внедрить наш собственный сервер в качестве замены на месте, при условии, что мы сохраним структуру оригинала, так что давайте приступим к делу.

Создание сервера

Для разработки реимплементации сервера я буду использовать Node.js, так как работаю с ним почти каждый день, но как только все будет готово, я также собираюсь портировать код сервера на C, используя некоторую сетевую библиотеку, чтобы сделать его более родным и простым в использовании. Кроме того, я буду использовать Express.js, чтобы упростить маршрутизацию и другие операции, поэтому давайте начнем с простого сервера:

const express = require('express');
const app = express();
const port = 8081;

app.get('/', (req, res) => {
  res.statusCode = 200;
  res.send();
});

app.listen();

Он просто возвращает ответ «ОК».

Это должно позволить нам обойти ошибку DNS и, наконец, увидеть, что на самом деле запрашивает игра. Итак, теперь давайте снова проследим за пакетами в Wireshark (используя теперь адаптер петлевого трафика; иначе мы не сможем захватить с локального хоста) и посмотрим, что делает игра:

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

Реализация сервера

Если мы посмотрим на дамп Wireshark, мы увидим несколько запросов к серверу, чьи конечные точки не могут быть найдены, все они по маршруту /JM_test/service/:

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

GET     /JM_test/service/GameEntry
GET     /JM_test/service/GetMessage
GET     /JM_test/service/GetName
GET     /JM_test/service/GetRanking
GET     /JM_test/service/GetReplay
POST    /JM_test/service/ScoreEntry

Список всех запросов, сделанных программой.

Давайте начнем с того, что избавимся от самого простого из всех, GetMessage, просто вернув строку:

app.get('/JM_test/service/GetMessage', (req, res) => {
  res.statusCode = 200;
  res.send('Hello World!');
});

Чаще всего вызывается GameEntry, который, я полагаю, предназначен для аутентификации пользователя, используемого для входа в систему, получения рейтинга и запуска игры. В настоящее время вы ожидаете, что зашифрованный токен будет безопасно передан на сервер, но здесь у нас есть только параметры запроса id и pass. Также есть параметр game, который всегда имеет значение 0, и параметр ver, обеспечивающий постоянное обновление клиента.

Это тот момент, когда мы должны начать думать о создании базы данных для хранения этих пользовательских данных (а позже и о ранжировании). Для этого я собираюсь использовать MongoDB с Mongoose для объектного моделирования (потому что я бы предпочел умереть, прежде чем использовать SQL), хотя любой другой движок базы данных подойдет. Таким образом, предполагая пользовательскую модель с полями id, pass и rankings (для личных оценок), полная реализация будет выглядеть так:

app.get('/JM_test/service/GameEntry', (req, res) => {
  res.statusCode = 200;
  User.findOne({ id: req.query.id, pass: req.query.pass }, (e, user) => {
    // Check if user exists and the credentials are correct.
    if (user) { res.send(); }
    // Check for users with the same id and create a new one if allowed.
    else if (req.query.id.length > 0 && options.register) {
      User.findOne({ id: req.query.id }, (e, exists) => {
        if (!exists) {
          // Store new user into the database.
          const user = new User({ id: req.query.id, pass: req.query.pass, rankings: [] });
          user.save(); res.send();
        // An user with this id already exists.
        } else { res.send('1'); }
      });
    // Wrong user id or password.
    } else { res.send('1'); }
  });
});

Этот код не выполняет никаких мер безопасности, так как это выходит за рамки данного проекта. В реальном онлайн-сервисе (если кто-то захочет это сделать) это поведение, вероятно, следует изменить или, по крайней мере, не хранить пароли в виде простого текста. Кроме того, я добавил свойство register в объект настроек, которое позволяет пользователям регистрироваться из самого игрового клиента, если id не соответствует ни одному результату в базе данных (это также может быть изменено для реального онлайн-сервера). Кроме того, вы можете заметить возврат 1 в качестве сообщения об ошибке в случае сбоя входа в систему. После некоторых исследований я обнаружил, что это ответ на появление сообщения 'неправильный идентификатор или пароль' в игровом клиенте, в дополнение к коду 10 ошибки подключения к серверу и еще одному коду, который я не смог не найти, что вызовет ошибку несоответствия версии.

Затем у нас есть вызов GetRanking, который, возможно, является последним этапом реверсирования, который нам придется выполнить для остальной части проекта, поскольку ни одна из оставшихся конечных точек не требует данных, специально отформатированных в качестве ответа. Параметры запроса здесь: id (идентификатор пользователя, который может быть необязательным), mode (сложность), который может быть 0 (нормальный), 1 (жесткий) или 2 (смерть), и параметр view для определения между пользователем или глобальным рейтинги. Прежде чем продолжить, давайте посмотрим, как ранжирование структурировано и классифицировано, и какова реакция игры на данный момент.

Рейтинги подразделяются на две основные группы: личные рейтинги и глобальные рейтинги, каждая из которых подразделяется на подклассы в зависимости от сложности, что в сумме дает 6. сильные> рейтинговые списки. Основное различие между ними заключается в том, что личные рейтинги могут хранить до 10 записей очков для каждого режима без сохранения повторов, тогда как глобальные рейтинги могут хранить неопределенное количество очков с хранилищем повторов, но только по одному на пользователя. Рейтинги, как личные, так и глобальные, состоят из следующих полей: id (идентификатор пользователя, владелец записи), mode, score, jewel, level, classпредполагаю, что это для звания игрока) и time (в игре также есть поле date, но, видимо, оно так и не было реализовано). Эти поля также отправляются клиентом для вызова ScoreEntry, поэтому мы можем создать из них модель схемы.

Это заняло у меня пару часов, чтобы понять, но вот разбивка того, как работает форматирование и синтаксический анализ ранжирования: каждый объект ранжирования представляет собой строку длиной в 10 строк, а в группе они разделяются символом точки (.) . Первые две строки соответствуют индексу таблицы и идентификатору рейтинга, а следующие (по порядку) id (пользователь), score, неиспользуемое значение, level, class, time, jewel и, наконец, флаг выделения. Первые два значения полезны только для глобального рейтинга, так как первое используется для установки начальной позиции пользовательского рекорда при загрузке глобального рейтинга (и для получения индекса My Record), а второе является значением идентификатора (задается сервером) для получения файла повтора с помощью GetReplay. Значение class (опять же, если предположить, что это название партитуры) может быть 101, 102, 201, 202, 301, 302 и 303, а флаг выделения делает цвет партитуры желтым, хотя я не знаю, при каких условиях партитура должна быть помечен с ним (еще раз, я предполагаю, что его предполагаемое использование состоит в том, чтобы отметить наивысший балл пользователя в таблице).

Самое смешное, что каждый объект ранжирования должен иметь длину ровно в 10 строк, на одну больше или на одну меньше, иначе клиент вылетит. Так уж получилось, что ответ Express.js по умолчанию также состоит из 10 строк, поэтому отлично работает в качестве действительного объекта ранжирования. Это было ключом к пониманию того, почему программа вылетала у меня всякий раз, когда я менял ответ, и в конечном итоге помогло мне разобраться с форматом. Полная реализация:

app.get('/JM_test/service/GetRanking', (req, res) => {
  res.statusCode = 200;
  // Manage personal rankings.
  if (req.query.id && req.query.view == '0') {
    User.findOne({ id: req.query.id }, (e, u) => {
      let ranks = [];
      // Sort rankings in descending order.
      let rankings = u.rankings.sort((a, b) => b.score - a.score);
      for (let r of rankings) {
        // Build response string.
        let lit = (r == 0) ? 1 : 0;
        let rank = `0\n0\n${r.id}\n${r.score}\n0\n${r.level}\n0\n${r.time}\n${r.jewel}\n${lit}`;
        // Add rankings for the selected mode.
        if (req.query.mode == r.mode) { ranks.push(rank); }
      }
      // Return concatenated strings.
      res.send(ranks.join('.'));
    });
  // Manage global rankings.
  } else {
    // Sort rankings in descending order.
    Ranking.find({ mode: req.query.mode }).sort({ score: -1 }).exec((e, r) => {
      if (r.length > 0) {
        let ranks = [], f = -1;
        // Set rankings table index.
        let index = req.query.view == '-1' ? 0 : req.query.view;
        if (req.query.id) {
          // Get user score position table index.
          f = r.findIndex((v) => v.id == req.query.id);
          if (f != -1) { index = Math.floor(f / 10); }
        }
        // Fill the 10-slots scores table.
        for (let i = (index * 10); i < (index * 10 + 10); i++) {
          if (!r[i]) { break; }
          // Build response string.
          let lit = (i == f) ? 1 : 0;
          ranks.push(`${index}\n${r[i]._id}\n${r[i].id}\n${r[i].score}\n0\n${r[i].level}\n${r[i].class}\n${r[i].time}\n${r[i].jewel}\n${lit}`);
        } res.send(ranks.join('.'));
      } else { res.send(); }
    });
  }
});

Но чтобы иметь возможность видеть рейтинги, мы должны иметь возможность сохранять их в первую очередь. Итак, пришло время добавить конечную точку для вызова ScoreEntry, который, в отличие от остальных, является единственным запросом POST в группе. Несмотря на это, все данные о счете по-прежнему передаются через параметры запроса, поскольку для отправки данных воспроизведения используется фактическое тело. Я не буду вдаваться в код этого, так как он довольно длинный и просто хранит полученные данные, но стоит упомянуть, что сервер обрабатывает ранжирование и воспроизводит соответственно, обновляя и заменяя записи при необходимости. Я также реализовал опцию multiscores, чтобы позволить каждому пользователю получить несколько очков в глобальном рейтинге, а также сохранить несколько повторов. Данные отправляются в виде объекта application/octet-stream, обернутого вокруг запроса multipart/form-data. Хотя Express.js имеет встроенную функцию для отправки данных в виде вложений (загрузки), он не может принимать/сохранять (загружать) запросы такого типа, для чего я буду использовать промежуточное ПО Multer, чтобы выполнить эту работу.

И теперь, когда у нас есть повторы, хранящиеся на сервере, мы можем получить их по запросу клиента. Реализация GetReplay настолько проста, насколько это возможно (помните, что мы управляем значением идентификатора повтора, отправленным с GetRanking):

app.get('/JM_test/service/GetReplay', (req, res) => {
  res.download(path.resolve() + '/rep/' + req.query.id + '.rep');
});

Повторы хранятся в папке rep с расширением .rep.

Наконец, последний и абсолютно наименее важный вызов GetName, которому я не нашел применения. Он просто сидит там.

Портирование сервера

Теперь, когда сервер Node.js запущен и работает со всеми реализованными функциями, пришло время перейти на темную сторону, место, которое я не пожелаю даже своему злейшему врагу, создав веб-сервер в Язык программирования C™. В этой мазохистской задаче я буду помогать себе Mongoose Web Server (не путать с предыдущим Mongoose для моделирования баз данных), библиотекой C, предназначенной для создания небольших серверов на встроенных устройствах, и LMDB очень легкий и производительный механизм базы данных ключ-значение (в отличие от документного Mongo). . Другие библиотеки, которые я буду использовать, включают mjson (от разработчиков Mongoose) для анализа объектов базы данных и запросов JSON от клиента, а также ini (отличное имя мой чувак) за разбор конфигурационного файла для сервера. Я не буду подробно объяснять реализацию C (если только вы не хотите услышать, как я разглагольствую о глупых ошибках выделения памяти и о том, как хреново работать с объектами и строками, извините, структуры и массивы символов с завершающим нулем в C), поскольку концептуально это в основном то же самое, что и мы уже видел с Node.js, и код примерно в 4 раза больше, вот и все. Вы можете пойти и проверить это на GitHub, если хотите.

Однако я расскажу о новом дополнении к этой версии, а именно о внедрении DLL и перехвате сетевых функций. До сих пор мы редактировали файл hosts Windows, чтобы перенаправлять все клиентские вызовы на наш локальный сервер, но не лучше ли этого не делать? Кроме того, было бы здорово, чтобы сервер запускался и работал в фоновом режиме, пока игра выполняется, и автоматически останавливался, когда игра закрывается, так что в основном это будет весь пакет, максимально прозрачный. В то же время нам нужна некоторая гибкость, поэтому мы все еще можем использовать этот хук для подключения к другим серверам, как C, так и Node.js, локально или через Интернет, так что давайте приступим к этому.

Введение крючков

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

Из всех библиотек нам нужна WinINet (wininet.dll), системный сетевой API, который обрабатывает все операции связи через HTTP или FTP. Поскольку все, что мы хотим здесь сделать, это перенаправить вызовы сервера, единственные функции, которые нам нужно перехватить, — это InternetOpenUrlA для всех GET запросов и InternetConnectA, которая необходима для ScoreEntry, чтобы открыть соединение перед запросом URL-адреса. Чтобы реализовать хуки, мы инициализируем MinHook в DllMain и указываем обходные функции, которые изменят имя хоста, чтобы оно указывало на адрес, который мы ему даем, и, наконец, возвращают поток выполнения к исходному вызову функции с измененным параметром.

// Define pointers to the original functions.
typedef int (WINAPI *INTERNETCONNECTA)(HINTERNET, LPCSTR, INTERNET_PORT, LPCSTR, LPCSTR, DWORD, DWORD, DWORD_PTR);
INTERNETCONNECTA fpInternetConnectA = NULL;
typedef int (WINAPI *INTERNETOPENURLA)(HINTERNET, LPCSTR, LPCSTR, DWORD, DWORD, DWORD_PTR);
INTERNETOPENURLA fpInternetOpenUrlA = NULL;

// Define functions to overwrite the originals.
int WINAPI dInternetConnectA(HINTERNET hInternet, LPCSTR lpszServerName, INTERNET_PORT nServerPort, LPCSTR lpszUserName, LPCSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD_PTR dwContext) {
  // Change the original host with the configured IP address.
  return fpInternetConnectA(hInternet, (LPCSTR)HOSTNAME, nServerPort, lpszUserName, lpszPassword, dwService, dwFlags, dwContext);
}
int WINAPI dInternetOpenUrlA(HINTERNET hInternet, LPCSTR lpszUrl, LPCSTR lpszHeaders, DWORD dwHeadersLength, DWORD dwFlags, DWORD_PTR dwContext) {
  // Change the original host with the configured IP address.
  char buf[200]; int ofs = 21; snprintf(buf, 200, "http://%s%s", HOSTNAME, lpszUrl + ofs);
  return fpInternetOpenUrlA(hInternet, buf, lpszHeaders, dwHeadersLength, dwFlags, dwContext);
}

BOOL WINAPI DllMain(HMODULE hModule, DWORD fdwReason, LPVOID lpReserved) {
  switch (fdwReason) {
    case DLL_PROCESS_ATTACH:
      // Initialize MinHook.
      MH_Initialize();
      // Hook InternetConnect() and InternetOpenUrl() to redirect server calls to localhost.
      MH_CreateHookApiEx(L"wininet", "InternetConnectA", &dInternetConnectA, (LPVOID *)&fpInternetConnectA, NULL);
      MH_CreateHookApiEx(L"wininet", "InternetOpenUrlA", &dInternetOpenUrlA, (LPVOID *)&fpInternetOpenUrlA, NULL);
      MH_EnableHook(&InternetConnectA);
      MH_EnableHook(&InternetOpenUrlA); break;
    case DLL_THREAD_ATTACH: break;
    case DLL_THREAD_DETACH: break;
    case DLL_PROCESS_DETACH:
      // Disable hooks and close MinHook.
      MH_DisableHook(&InternetConnectA);
      MH_DisableHook(&InternetOpenUrlA);
      MH_Uninitialize(); break;
  } return TRUE;
}

Обратите внимание на глобальную переменную HOSTNAME, в которой будет храниться адрес сервера из файла конфигурации server.ini. Не показывать варианты синтаксического анализа для краткости.

Чтобы закрыть этот сегмент, я хотел бы упомянуть, что я также хотел подключить библиотеку Direct3D 9 и добавить опцию принудительного перехода в полноэкранный режим, но столкнулся с несколькими проблемами. Оказывается, как вы могли видеть из импорта, d3d9.dll не видно. Это связано с тем, что программа делегирует всю функциональность игры (видео, аудио, ввод, распаковку данных и т. д.) библиотеке Skeleton.dll, которая загружается с помощью LoadLibraryA после первоначального выполнения программы. Из-за того, как работает MinHook (и любая другая библиотека перехватчиков), ему нужно, чтобы DLL уже была загружена в память, чтобы получить его адрес и сделать перехватчик. Это не было бы проблемой, если бы мы просто создали хук для LoadLibraryA, а после вызова мы создали новый хук внутри него с уже загруженной DLL, верно? Хотя это должно работать именно так (я имею в виду, я думаю), MinHook возвращает ошибку инициализации при попытке включить хук, несмотря на то, что он может создать его совершенно нормально. Я предполагаю, что это специфическая ошибка MinHook, но на данный момент я слишком ленив (и у меня мало времени), чтобы сообщить о проблеме или внести изменения в библиотеку, и ни одна из найденных мной альтернатив не была такой простой в использовании и минимальной по размеру. . Думаю, это будет в другой раз.

Конфигурация сервера и использование программы

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

  • Режим 0 предназначен для локальной одиночной игры, когда сервер работает в фоновом режиме во время выполнения игрового клиента.
  • Режим 1 предназначен для сетевой игры, отключения инициализации сервера и базы данных и подключения к адресу, указанному в файле конфигурации.
  • Режимы 2 и 3 предназначены для размещения сервера по адресу, указанному в конфигурационном файле. Один открывает игровой клиент, другой консоль с информацией о сервере.

Я также добавил возможность отключить перехват DLL, поскольку он может запускать антивирусы (для этого потребуется вручную добавить запись в файле hosts). Помимо конфигурации подключения, есть также ранее упомянутые пользовательские параметры: зарегистрироваться, множество баллов и вариант без баллов, а почему бы и нет. Кроме того, просто поместите файлы в папку с игрой, и все готово. Кстати говоря, так как официальный клиент игры уже нельзя скачать, я взял на себя смелость выложить его на archive.org. Это клиент версии 1.32, единственный, сохраненный с помощью Wayback Machine, но последняя известная версия — 1.40. Если я когда-нибудь доберусь до этого, я обновлю архив, и если не сильно изменится, сервер тоже должен работать для него, учитывая, что у нас нет никаких жестко закодированных патчей. .

Ошибка сервера: отключение…

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

Я также очень удивлен количеством вещей, которые только что сработали благодаря чистой догадке и пробным вещам, таким как первоначальное создание сервера и формат ранжирования, больше, чем когда-либо прежде. Сначала я думал, что потребуется больше недели, чтобы просто перевернуть клиент и понять, как он работает в какой-то степени, но на самом деле это заняло всего около 3-4 дней, включая полную разработку сервера Node.js. На самом деле, я провел большую часть своего времени, борясь с реализацией C, почти 2 недели. Я думаю, это понятно, учитывая, что до этого проекта я никогда не делал программы на C с нуля (даже Hello World), исключительно сам. В один день нужно было понять, как построить сервер, в другой — разобраться с распределением памяти, указателями, компиляцией, ну вы поняли (каламбур, абсолютно задуманный).

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