Скачать Казаки | Профиль Казака | Регистрация нового Казакёра | Выход тут | Вход тутСуббота, 23.11.2024, 17:38


Казаки поиск.


 



Файлы игры КазакиСкачать Казаки Гость | Группа Злого Нуба "Гости"КазакёрГость | RSS
Казаки онлайн

Скачать Казаки

Смотреть Российское Телевидение!

Найду всё

Хочу больше прав


Фильмы по жанрам
Скачать Казаки: Битва за Европу [0]
Скачать Казаки Снова Война 1.35 [2]
Скачать Казаки Эхо Войны 1.38 [1]
Как создать игру Казаки [4]
Как быстро строить город [1]
Настройки игры Казаки [1]
Новые карты к игре Казаки [0]
Казаки модострой [3]
Игра Казаки как строить город [0]
Скачать Казаки 3 [1]
Скачать Казаки 3 бесплатно

Казаки все версии

История Игры

Казаки торент

Скачать Казаки » Скачать игру Казаки бесплатно » Игра Казаки » Казаки модострой

Инжиниринг «Казаков»
Инжиниринг «Казаков», Инжиниринг «Казаков»

Автор статьи: Эреб

 

Начало статьи...

 

После нескольких месяцев работы над исходным кодом игры «Казаки: Снова война» я наконец-то могу умыть руки и представить результат своих трудов. В этой статье мне хотелось бы поделиться с вами опытом рефакторинга этого незаурядного проекта, в частности кодовыми курьёзами. Всем любителям некро-программирования посвящается…

Начало

Первое и одно из самых неприятных препятствий было не в коде, а в самих проектах. Всего их четыре:

статическая библиотека CommCore.lib (сетевой протокол GSCp на базе UDP)
динамическая библиотека IntExplorer.dll (игровое лобби на сервере)
динамическая библиотека IChat.dll (чат в игровом лобби)
исполняемый файл dmcr.exe

И эти ребята повидали некоторого кода! Из-за круговой зависимости между проектами IChat.dll и dmcr.exe для линковки требуется хотя бы один lib-файл из предыдущей сборки. Сами файлы проектов неверно конвертируются в Visual Studio 2015: они содержат ссылки к библиотекам, которые не отображаются в свойствах проекта, абсолютные пути к файлам в системе одного из разработчиков и прочие сюрпризы. В конце концов мне надоели танцы с бубнами, и я создал все проекты заново, попутно узнав, что в архиве присутствуют лишние файлы с устаревшим исходным кодом, который при включении в проект приводит к конфликтам. Ну и куда уж тут без особенностей линковки, для которой обязательно нужно исключить libc.lib.

Стартуем

Так, с проектами разобрались, теперь можно браться за дело. Компилятор рад за нас и приветствует множеством ошибок C2065: необъявленный идентификатор. Смотрим код и видим повсюду такую картину:

for (int i = 0; i < max1; i++) { /* Цикл №1 */ }
for (i = 0; i < max2; i++) { /* C2065 */ }
//Или такую:
for (int i = 0; i < max && arr[i] != value; i++); /* Пустой цикл для поиска индекса */
if (i < max) { /* Есть совпадение в массиве. И C2065 тоже есть */ }

Конечно, можно было бы выставить /Zc:forScope- и забыть об этом, но мы же не кочегары и не плотники. Правим ручками больше сотни таких отрезков кода, продолжаем.

Следующее препятствие заключалось в графическом элементе, точнее в DirectDraw 7. Он активно использует механизм замены системной палитры. И если ранее это было повсеместной практикой, то начиная с Windows Vista такие фокусы больше не проходят. Дело в том, что DWM вместе с Windows Aero вплотную работают с палитрой и не терпят конкуренции. В итоге множество старых игр страдают от искажения цветов.

Не являясь экспертом по DirectX, я стал искать готовое решение и нашёл его в версии «Казаков», опубликованной на Steam в 2010 году. Помимо самой библиотеки ddraw.dll в папке с игрой присутствует дополнительная библиотека mdraw.dll, которая экспортирует функцию инициализации DirectDrawCreate(). Скажу честно – я не знаю, что именно ребята из GSC написали в их библиотеке DDemu DirectDraw Emulator в 2008 году, но она прекрасно справляется со своей задачей. Недолго думая я добавил соответствующую обёртку в Ddini.cpp и забыл об этой проблеме.

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

Небольшое отступление
Весёлая арифметика

Одной из моих целей было добавление настроек для многопользовательских игр, например, возможность отключать дипломатический центр и рынок или ограничивать доступные корабли в верфи. Расширив интерфейс игровой комнаты и добавив нужные ветки в коде, я увеличил массив PlayerInfo.UserParam[], в котором хранятся эти настройки, с семи до десяти элементов. Вот только протестировать новые опции никак не получалось — при старте игры ИИ начинал распоряжаться моими крестьянами вместо своих и играть за меня, при этом его крестьяне стояли неподвижно. Весело, но так не пойдёт.

Причина такого поведения ИИ крылась в следующем финте ушами при копировании настроек от хоста игры в буфер обмена:

//PlayerInfo PINFO[8];
//byte* BUFB = (byte*) ( BUF + 10 + 8 + 32 - 10 );
memcpy( BUFB, PINFO[HostID].MapName + 44 + 16, 16 );

А вот так декларирована структура PlayerInfo:

#pragma pack(1)
struct PlayerInfo {
  char name[32];
  DPID1 PlayerID;
  byte NationID;
  byte ColorID;
  byte GroupID;
  char MapName[36 + 8];
  int ProfileID;
  DWORD Game_GUID;
  byte UserParam[7];
  byte Rank;
  word COMPINFO[8];
  //… (ещё 12 элементов)
}

Как видим, по смещению MapName + 60 находится COMPINFO[8]. Соответственно, при увеличении массива UserParam[7] вызов memcpy() промахивается, и в буфер попадают неверные данные о том, за каких игроков должен играть ИИ. Проблема решается заменой офсетной математики на прямое обращение по адресу PINFO[HostID].COMPINFO.

В итоге я всё же принял решение не трогать UserParam[], а добавить массив UserParam2[3] в конце структуры, так как в одном из последних элементов хранится версия клиента и любое изменение структуры до него чревато неверным определением версий в игровом лобби. А так игроки с версией 1.35 будут видеть, что у других обновлённая версия игры.

Какие уроки можно почерпнуть для себя из этого?

В структуре, передающейся по сети, первым элементом должна быть версия клиента.
Никогда не исходить из того, что расположение структуры в памяти будет неизменным.
Писать функции сериализации, а не полагаться на #pragma pack(1) и побайтовое копирование.

Невидимый Джо Дефайн

Разбираясь с механикой отображения внутриигровых текстовых сообщений с целью увеличения времени отображения и максимального количества сообщений на экране, я наткнулся на занимательную константу:

#define SIGNBYTE ''
void ShowCharUNICODE( int x, int y, byte* strptr, lpRLCFont lpr ) {
  if (strptr[0] == SIGNBYTE) { /* юникод */ }
  else { /* ascii */ }
}

«Ну и что в этом такого? — спросите вы — Всего лишь пробел в качестве константы». Ну, во-первых, пробел был бы крайне странным выбором для идентификации чего-либо в строке текста, а во-вторых это вовсе не пробел. SIGNBYTE определён как 0x7F, или управляющий символ DEL. И если ваш браузер достаточно осмотрителен и хотя бы показывает, что между кавычками что-то есть, то Visual Studio 2015 вероломно рисует '', между которыми курсор «спотыкается» на один символ.

Пожалуйста, если уж вы используете непечатаемые символы, то указывайте их в коде через шестнадцатеричное значение, а не как символ.

Правовой аспект

Всякий раз удивляюсь, когда для запуска игры требуются права администратора. И всякий раз думаю что-то вроде «ну как так можно игры программировать-то». Но в этот раз у меня и код на руках был, и собирал его я сам, а окно UAC всё так же не давало мне покоя.

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

Оказывается, ОС Windows начиная с Windows Vista применяет эвристический алгоритм для определения приложений, которым может потребоваться повышение привилегий. Называется эта функция «Технологией обнаружения установщика» (см. статью в ИТ-центре Windows), и обычно она реагирует на ключевые слова вроде install или setup. Но в нашем случае виновником оказался параметр CompanyName — если он содержит строку "-GSC-\0", то просыпается UAC и требует прав администратора.

Как уберечь своё приложение от такой эвристики со стороны Майкрософт, существующей и грядущей? А никак. Сегодня вы разрабатываете игры, а завтра уже стоите в одном ряду с Inno Setup и InstallShield.

Партизанский sscanf()

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

Опущу скучные детали отладки и перейду к самому соку. Настройки игры, вид карты и данные о выбранных нациях передаются через имя файла случайной карты, которое имеет формат RN0 1A3C 12345 0KFH31CJ 4501326.m3d, где

RN0: префикс RN и размер карты (0 — 2)
1A3C: значение ГПСЧ для инициализации случайной карты
12345: Вид карты (ландшафт, горы, месторождения и пр.)
0KFH31CJ: Нации, которые выбрали игроки (0 — K)
4501326: Настройки игры (Артиллерия, PT и пр.)

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

int v1, v2, v3, ADD_PARAM;
char ccc[32];
int z = sscanf( Name, "%s%x%x%x%d", ccc, &v1, &v2, &v3, &ADD_PARAM );
if ( z == 5 ) { /* Интерпретация настроек из ADD_PARAM */ }

Загвоздка здесь в том, что для четвёртой переменной указан тип %x, в то время как диапазон символов в ней выходит за рамки шестнадцатиричной системы и простирается до буквы K. Если в игре присутствуют игроки, которые выбрали нации с индексом выше F, то sprintf() преждевременно закончит парсинг и вернёт 4. Параметры не будут интерпретированы, у ИИ будет неправильная информация об игре и он будет принимать другие решения, что приведёт к рассинхронизации.

В дополнение к этому идёт тот факт, что sprintf() вызывается исключительно для ADD_PARAM — остальные переменные нигде не используются. Решение проблемы относительно простое:

int options = 0;
int z = sscanf( Name, "%*s %*s %*s %*s %d", &options );
if ( 1 == z ) { /* Интерпретация настроек из ADD_PARAM */ }

Флаг * указывает функции, что значение не следует сохранять в переменной. Кстати, пoсмотреть, каким образом я реализовал кодирование 10 игровых настроек на месте тех же 7 цифр можно здесь. «Зачем?» — спросите вы. А потому что менять длину строки с именем файла карты по своему усмотрению показалось мне не очень хорошей идеей (см. выше в «Весёлой арифметике»).

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

Памятка: Следовать в первую очередь требованиям документации, а не принципу «рабочий код — правильный код».

Тонкости языка

Локализация это отдельная тема для любого разработчика, но такое я увидел впервые:

#define RUSSIAN

#define _CRYPT_KEY_ 0x78CD

#ifdef RUSSIAN
#undef _CRYPT_KEY_
#define _CRYPT_KEY_ 0x4EBA
#endif

Если вам этого мало, предлагаю заглянуть под спойлер и посмотреть, для чего же нужен этот «крипто-ключ».

Не делайте так. Пожалуйста.
Вот таким нехитрым образом можно запороть локализацию на этапе компилирования. Если, например, «английскому» dmcr.exe подсунуть архив с ресурсами из русской версии, то всё, что останется от игры — окно ошибки access violation. Потому что ни до, ни после «isi memory decryption» содержимое буфера не проверяется. А вот если мы распакуем архив all.gsc, заменим файлы и запакуем его обратно, то в игре нас будет ждать русский интерфейс.

Посмотрев на эту XOR-вакханалию я решил ограничиться английской версией, но с поддержкой кириллицы в чате. Так как весь текст отрисовывается через собственные шрифты игры, я скопировал из русской версии ресурс mainfont.gp. Осталось только отловить символы, выходящие за пределы диапазона ascii, и правильно сопоставить коды букв с «индексом кадров» этого файла (формат GP используется в игре повсеместно для хранения графики, в том числе и для анимации). Не самое элегантное решение, зато работает безотказно, причём на сервере в чате с игроками под версией 1.35 тоже.

UDP без дырок

К сожалению, в оригинальных «Казаках» не был реализован механизм UDP hole punching, который позволил бы игрокам подключаться к игровым комнатам, даже если их хост находится по ту сторону NAT своего провайдера.

К счастью, товарищ, известный под ником [-RUS-]AlliGator, запустивший и поддерживающий сервер cossacks-server.net, выделил немного своего времени и мы договорились о дополнительном протоколе, по которому хост игры будет поддерживать UDP соединение с сервером, и по которому сервер сможет сообщать внешний UDP порт хоста игрокам, желающим к нему подключиться.

Все детали реализации можно посмотреть в классе UdpHolePuncher. Соединение инициализируется при создании игровой комнаты хостом, после чего он вплоть до старта игры поддерживает связь, отправляя небольшие пакеты. Это нужно, т.к. NAT может при каждом новом UDP соединении присваивать другой внешний порт, а так сервер наверняка знает, что в данный момент времени хост доступен из-за NAT по тому порту, с которого приходят пакеты.

Соответствующие изменения были внесены и в процедуру обработки команд сервера и в структуру RoomInfo в библиотеке IChat.dll. Поддерживаются следующие дополнительные переменные при создании игровой комнаты:

%PROF: идентификатор игрока. С помощью него сервер сможет различать хостов
%CG_HOLEHOST: адрес сервера, обрабатывающего UDP пакеты
%CG_HOLEPORT: порт сервера, на котором слушается UDP
%CG_HOLEINT: интервал, с которым клиент должен отправлять пакеты

Получив эти данные, хост игры открывает дополнительное соединение и поддерживает его. Желающие присоединиться получают дополнительную переменную %CG_PORT при подключении к комнате. И только если её нет, используется константа DATA_PORT.

Хотя весь этот механизм уже имплементирован в клиенте игры, протестировать мне его ещё не удалось, т.к. контакт с Аллигатором оборвался. Незадолго до этого он выложил в открытый доступ исходный код своего сервера, — спасибо тебе за это! — так что если кто-то готов поднять эстафетную палочку, я буду только за.

Послесловие

Статья и так уже вышла длиннее, чем я планировал, так что буду краток. Хотя этот проект занял продолжительное время и ощутимо истощил запас моего энтузиазма к реверс-инжинирингу и анализу исходного кода, я рад, что взялся за него. Рад, что написал тогда статью на Хабр с описанием своего первого, ассемблерного, костыля для «Казаков». Рад, что в комментарии пришёл Максим fsou11 и выложил в свободный доступ исходный код игры. Также я благодарен сообществу LCN за ценные советы, объяснения и помощь в тестировании.

Автор статьи: Эреб

Категория: Казаки модострой | Добавил: EVIL_NOOB | Скачать игру: Моды в игре Казаки, Кодинг Казаков, Программный код игры Казаки
Просмотров казаков: 1079 | Загрузок: 0 | Комментарии тКазакеров: 5 | Рейтинг статьи: 0.0/0
Всего комментариев: 5
1 EVIL_NOOB  
0
Спасибо за статью и за ваш труд, отличная работа.

Если позволите, вопрос по внутренности реплеев: 

1. Правильно ли я понимаю, что в файле реплея содержатся ничто иное, как последовательность действий каждого из игроков, которые передаются интерпретатору (движку игры) как если бы игра происходила в real-time?

2. Итоговая статистика (сколько каждый из игроков добыл, произвёл, убил) «аккумулятивна»? Другими словами, вычисляется в процессе воспроизведения записи или же хранится в виде «summary» файле?

2 EVIL_NOOB  
0
Всегда пожалуйста :)

1. Да, именно так. rec-файл в основном состоит из буфера команд (ExBuf), которые передавались по сети. Действия ИИ рассчитываются заново при просмотре, так что игра как будто играется заново.

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

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

3 EVIL_NOOB  
0
Здравствуйте! 
Ветка комментариев заставила меня задуматься на одну тему, я не знаю пробовали ли вы играть в довольно новую игру от Blizzard «Heroes of the Storm», но говорят, она сделано на основе движка SCII, т.е. RTS. Так вот в игре есть занимательная вещь, если во время игры игрок по каким-то причинам вылетел (ошибка, дисконнект), то при повторной загрузке в игру, игрок сидит и ждет пока всё, что происходило без его участия (или вообще все действия с начала игры) воспроизводятся перед ним в ускоренном виде и когда этот «реплей» воспроизведется до текущего момента, игрок может продолжить игру. Меня это решение всегда ставило в ступор, ведь столько онлайн игр возвращают игрока без всего этого сразу и в текущий момент времени, а тут такое. 

Возможно ли такое именно из движка и такого вот подхода к воспроизведению реплеев в нем? 
Хотя с другой стороны есть сам SCII, в котором при дисконнекте такого не происходит.

4 EVIL_NOOB  
0
Здравствуйте,

с «Heroes of the Storm» я не знаком, но судя по вашему описанию всё обстоит именно так.

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

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

Вообще там очень много разнообразных фиксов, которые можно применять к приложениям. Многим старым программам прописано SingleProcAffinity, например, что заставляет выполняться их на одном ядре (иначе некоторые программы эпохи до появления многоядерных процессоров просто зависают). Или, например, есть фикс, который «врёт» программе о свободном пространстве на диске, гарантируя, что число не будет больше 2 гигабайт, что помогает некоторым старым программам работать без ошибок. Эту базу можно редактировать, добавлять или удалять свои фиксы каким-то программам. Для запуска некоторых старых игр часто предлагается инсталлировать в систему *.sdb патчи — это как раз оно, добавляет новые записи в эту базу.

И да, я не совсем понял, где собственно у вас был реверс-инжиниринг? У вас же были исходные коды.

Добавлять комментарии могут только зарегистрированные игроки
[ Регистрация нового пользователя | Вход тут ]
Казаки снова война

Торент Казаки

Найду все

Мой любимый клип

Ваши любимые фильм

Слушать Злую муз

Казаки windows 7


Игра Казаки © 2024
Бесплатный конструктор сайтов - uCoz
Расширенный поиск