Компания Electronic Arts открыла исходный код первой Command & Conquer, а также Command & Conqueror: Red Alert. Скачать его можно с GitHub.
Всё содержимое имеет лицензию GPL v3; кроме того, в исходном коде сохранены все комментарии. Отсутствует только changelog использовавшейся при разработке системы контроля версий. Похоже, всё просто недавно выложили на Git.
Я решил изучить, что же происходит внутри этого игрового движка. Не буду описывать каждую строку кода, но, по крайней мере, будет интересно взглянуть на то, какой была разработка на C++ в начале 1990-х.
Изучать мы будем только исходный код «Command & Conquer: Red Alert», потому что он похож на форк первой игры. В репозитории он находится в папке REDALERT
.
Статистика
- 290 файлов заголовков C++
- 296 файлов реализации на C++
- 14 файлов ассемблера, содержащих инструкции ассемблера x86
- 222090 строк кода на C++
Количество строк кода я получил, подсчитав непустые строки, после чего вычел из них те строки, которые очевидно были комментариями.
Почти все файлы имеют имена в верхнем регистре.
Кроме того, есть файл «RedAlert.vcxproj», поэтому можно предположить, что проект можно собрать в более новых версиях Visual Studio, но этого я не проверял.
Это всё?
Начнём с того, что в репозитории нет ресурсов, даже тестовых. Поэтому придётся каким-то законным образом получить их, например, купить игру у EA. Пока вы этого не сделаете, даже компиляция кода не может подтвердить правильность работы игры.
Код переполнен #ifdef WIN32
, есть по крайней мере 430 вхождений этой строки. В репозитории находится папка WIN32LIB
, содержащая реализации для платформы Microsoft Windows. Больше никаких платформ не реализовано. Директивы компилятора для #ifdef WIN32
не имеют #else
, которые я видел для других платформ. Не совсем понимаю, в чём смысл этого, но игра поддерживала DOS. Поэтому, возможно, все проверки позволяют собрать игру так, чтобы она работала под DOS, но не под Windows.
Я подумал, что нашёл два файла, LCW.CPP
и LCWUNCMP.CPP
, в которых косвенно упоминается платформа Playstation:
// From LCW.CPP
* Project Name : WESTWOOD LIBRARY (PSX) *
Этот файл реализует алгоритм сжатия LCW.
Но на самом деле сокращение «PSX» относится к стандарту POSIX. Есть даже ещё одна реализация той же функции сжатия LCW_Comp
из LCW.CPP
, находящаяся в LCWCOMP.ASM
. Думаю, реализация на C++ использовалась на некоторых платформах, где не работал бы ассемблерный код.
Поэтому мне кажется, что компания не опубликовала код порта на Playstation. Я даже не знаю, создавался ли порт на Playstation самой Westwood, или был передан подрядчикам.
Интересно, что также реализованы алгоритмы сжатия LZO и LZW. Наверно, стратегия Westwood Studios по выбору технологий была такой: «берём всё». Алгоритма LCW было бы достаточно для того, чтобы избежать патентных притязаний Unisys на LZW, но, возможно, код LZW просто остался в исходниках. Код LZW используется только в LZWPIPE.CPP
, определяющем класс LZWPipe
. Единственный случай использования этого кода закомментирован.
// From SAVELOAD.CPP:423
LZOPipe pipe(LZOPipe::COMPRESS, SAVE_BLOCK_SIZE);
// LZWPipe pipe(LZWPipe::COMPRESS, SAVE_BLOCK_SIZE);
// LCWPipe pipe(LCWPipe::COMPRESS, SAVE_BLOCK_SIZE);
Похоже, что в этом случае разработчики пробовали разные алгоритмы, в результате остановившись на LZO. То есть LZW был готов к работе, но не использовался.
Срок патентных притязаний компании Unisys на LZW уже давно истёк, поэтому об этом мы можем не волноваться.
Странные заголовки
В LZW.H и других файлах есть подобные странные заголовки:
// From the top of LZW.H
/* $Header: /CounterStrike/LZW.H 1 3/03/97 10:25a Joe_bostic $ */
«CounterStrike» относится к дополнению «Command And Conquer: Counterstrike». В коде есть 215 вхождений строки #ifdef FIXIT_CSII
, изменяющих его работу для дополнения. Полное объяснение изменений написано пользователем «awj» 28 сентября 1998 года в комментарии:
// From DEFINES.H
#define FIXIT_CSII // Adds Aftermath CounterStrike II units
// ajw 9/28/98 - Note about FIXIT_CSII. Changes seem to have been made for Aftermath ("Counterstrike II") that: a) were
// bug fixes that should never be rolled back, b) change the nature of the game, at least in multi-player. This meant
// that the "Red Alert" executable ( == Counterstrike executable ) could no longer be built. Apparently, at the time,
// this was justified, as it was believed that no further patches to the RA executable would ever be necessary.
// Given that Denzil's DVD changes and my WOLAPI integration are essentially a patch, we've got a problem.
// We've decided to level the field and make sure every who gets or patches to the new version of Red Alert, CS, AM, (and
// their DVD equivalent(s)) will have the same executable. So we're assuming that all of the FIXIT_CSII changes are
// permanent (as, in fact, all prior FIXIT_'s are - makes me wonder why the old non-compiling code has to hang around
// forever), and fixing the code so that the assumption "this is an Aftermath game" is no longer hard-coded, but can
// change at runtime. (Which is what should have been done when Aftermath was created.)
//
#define FIXIT_CARRIER // Adds Aftermath aircraft carrier
#define FIXIT_PHASETRANSPORT // Adds Aftermath cloaking APC
// ajw - Discovered that engineer changing fields were specifically left out of aftrmath.ini, thus this has no effect.
// Engineer changes (and other game rule changes) are in mplayer.ini, which was loaded before aftermath-only mplayer games.
#define FIXIT_ENGINEER // Adds Engineer rules.ini overrides
Отсутствующий исходный код
Вопросы вызывает библиотека WOLAPI.dll
, название которой, похоже, расшифровывается как «Westwood Online API». Она использовалась для поиска игроков для онлайн-боёв. Кажется, автору Command & Conquer: Red Alert так не нравилась эта библиотека, что он написал для неё целую обёртку, находящуюся в WOLAPIOB.H
. Вероятно, можно найти оригинальный файл wolsetup.exe
и извлечь DLL из него. Но, скорее всего, она не будет особо полезной.
Забавно, что WOLAPI.dll
, похоже, 9 лет назад была подвергнута реверс-инжинирингу.
Это не релиз полного исходного кода, но его должно быть достаточно, чтобы понять внутреннюю механику игры. EA заявила, что опубликовала код для упрощения жизни моддеров, и, кажется, справилась с этой задачей. Приложив значительные усилия, можно собрать самостоятельную игру.
Как игра запускается?
Я решил начать с изучения процесса запуска игры. Код запуска обычно даёт хорошее представление о том, как игровой движок взаимодействует с ОС. Описание представлено примерно в том же порядке, что и в самом коде, но не полностью. Все неинтересные части пропущены.
Функция main на C++
Функция main
объявляется в STARTUP.CPP
. Похоже, игру недавно пересобрали как DLL, вероятно, для выпускаемой сейчас переработанной версии. Поэтому первым делом она присваивает RunningAsDLL = true;
.
Функция main
повсюду проверяет #ifdef MPEGMOVIE
, а также #ifdef MCIMPEG
. В DEFINES.H
эти строки полностью закомментированы.
// Define DVD to turn on RADVD additions/changes - Denzil
#ifdef DVD
//#define INTERNET_OFF
//#define MPEGMOVIE //PG
//#define MCIMPEG
#endif
То есть, похоже, из игры убрана функциональность воспроизведения MPEG-видеовставок.
Первым делом игра проверяет, не запущена ли ещё одна копия; если это так, то она отказывается запускаться. Похоже, это отключено при помощи строки #if (0)
. В коде есть много частей, отключенных таким образом, и каждая из них содержит //PG
. Один из коммиттеров в Git имеет ник PG-SteveT
; думаю, тот же человек, который выпустил проект в Git, отвечал за портирование игры в DLL вместо исполняемого файла. В функции main
есть много других подобных частей, в дальнейшем я просто буду называть их отключенными.
Следующей идёт проверка свободной памяти. Первая проверка выделяет 13 мегабайта памяти; если это срабатывает, код освобождает эту память. Есть ещё одна отключенная проверка, которая пытается выделить 13 мегабайт памяти в цикле. На каждой итерации цикла запрашиваемая выделяемая память уменьшается на 1 килобайт, пока не удастся её выделить. Думаю, можно спокойно считать, что у современных компьютеров, на которых запускают Command & Conquer, найдутся необходимые 13 мегабайт памяти, поэтому логично, что эту проверку отключили.
Далее код проверяет, содержит ли командная строка f:\projects\c&c0
или F:\PROJECTSC&C0
, и отказывается запускаться, если это так. При этом открывается диалоговое окно «Playing off of the network is not allowed» и игра закрывается. Краткое изучение какого-то заголовка привело меня к этому:
"/* $Header: F:projectsc&c0vcscodewwalloc.h_v 4.9 07 May 1996 17:14:00 JOE_BOSTIC $ */" in WWALLOC.H.
Так как в пути содержится «vcs», похоже, что использовалась какая-то система контроля версий, которая действовала как примонтированный в Microsoft Windows диск. В последний раз, когда я пользовался IBM Rational ClearCase, всё было так же. Вероятно, это сделано для того, чтобы люди не запускали игру с сетевого ресурса и не перезаписывали постоянно чужие сохранения.
Игра реализует собственную логику парсинга командной строки, написанную как встроенный в функцию main
код:
// From STARTUP.CPP
/*
** Get pointers to command line arguments just like if we were in DOS
**
** The command line we get is cr/zero? terminated.
**
*/
command_scan=0;
do {
/*
** Scan for non-space character on command line
*/
do {
command_char = *( command_line+command_scan++ );
} while ( command_char==' ' );
if ( command_char!=0 && command_char != 13 ) {
argv[argc++]=command_line+command_scan-1;
/*
** Scan for space character on command line
*/
bool in_quotes = false;
do {
command_char = *( command_line+command_scan++ );
if (command_char == '"') {
in_quotes = !in_quotes;
}
} while ( (in_quotes || command_char!=' ') && command_char != 0 && command_char!=13 );
*( command_line+command_scan-1 ) = 0;
}
} while ( command_char != 0 && command_char != 13 && argc<20 );
Я не стал заморачиваться проверкой его работы; похоже, код сканирует элементы, указанные в командной строке в кавычках, а затем пытается интерпретировать их как разделённые кавычками ("
) группы. Думаю, командная строка Windows не обрабатывала бы такую схему правильно, поэтому обработка выполнена в самой игре. На самом деле я не уверен, реализована ли в cmd.exe
логика работы кавычек даже сегодня.
Затем функция main
сохраняет исходную рабочую папку и диск, с которого была запущена игра, а затем переключается на папку с исполняемым файлом. Любопытно, что по умолчанию она выбирает диск A:
, который в Windows обычно используется для привода гибких дисков. Теперь этот путь выполнения кода полностью отключен.
Далее идёт первая #ifdef WOLAPI_INTEGRATION
. Эта директива не определена и код Westwood Online API недоступен. Если бы она была определена, то игра бы пыталась найти и запустить wolsetup.exe
, а затем проверяла бы ключ регистра Windows, сообщающий о завершении настройки. Здесь встречается довольно любопытный комментарий:
// From STARTUP.CPP
// I've been having problems getting the patch to delete "conquer.eng", which is present in the game
// directory for 1.08, but which must NOT be present for this version (Aftermath mix files provide the
// string overrides that the 1.08 separate conquer.eng did before Aftermath).
// Delete conquer.eng if it's found.
if( FindFirstFile( "conquer.eng", &wfd ) != INVALID_HANDLE_VALUE )
DeleteFile( "conquer.eng" );
Похоже, разработчики после версии 1.08 игры очень не хотели, чтобы среди файлов присутствовал «conquer.eng». Поэтому при каждом запуске игры они пытаются его удалить. Я не знаю, почему он находится вместе с кодом Westwood Online.
Далее мы переходим к проверке директивы #if(TEN)
, которая не определена. Если бы она была определена, игра бы настраивалась на поддержку Total Entertainment Network.
Сразу после этой проверки есть проверка на #if(MPATH)
, которая тоже не определена. Если бы она была определена, то игра бы имела поддержку MPlayer.
Во второй половине 1990-х обе они были довольно популярными платформами для онлайн-игр. Думаю, что существует специальная сборка для поддержки MPlayer и TEN. Ни той, ни другой уже не существует. Подробнее о них я расскажу позже.
Затем игра запускает таймер Windows, ожидает (sleep
) в течение 1000 миллисекунд, а затем проверяет, что системные часы идут. Если это не так, то игра завершается. Думаю, реализация sleep
на некоторых платформах была настолько глючной, что приводила к сбоям проверки при запуске. Теперь этот код отключен.
// From STARTUP.CPP
#if (0)//PG
int time_test = WindowsTimer->Get_System_Tick_Count();
Sleep (1000);
if (WindowsTimer->Get_System_Tick_Count() == time_test){
MessageBox(0, TEXT_ERROR_TIMER, TEXT_SHORT_TITLE, MB_OK|MB_ICONSTOP);
return(EXIT_FAILURE);
}
#endif
Затем игра проверяет свободное место на диске и если его недостаточно, выполняет выход. Этот код теперь отключен.
Далее она переходит к загрузке конфигурации, сгенерированной из программы ccsetup
из комплекта оригинальной игры. Она не делает ничего особо интересного, но я заметил это:
: cpp
// From STARTUP.CPP
if (!Debug_Quiet) {
Audio_Init(NewConfig.DigitCard,
NewConfig.Port,
NewConfig.IRQ,
NewConfig.DMA,
PLAYBACK_RATE_NORMAL,
// (NewConfig.Speed) ? PLAYBACK_RATE_SLOW : PLAYBACK_RATE_NORMAL,
NewConfig.BitsPerSample,
// 4,
(Get_CPU() < 5) ? 3 : 5,
// (NewConfig.Speed) ? 3 : 5,
NewConfig.Reverse);
SoundOn = true;
} else {
Audio_Init(0, -1, -1, -1, PLAYBACK_RATE_NORMAL, 8, 5, false);
}
Была добавлена конфигурация «DebugQuiet»; похоже, она выбирает аудиовыход, который ничего не делает. Думаю, довольно сильно раздражало бы, если бы при тестировании игры постоянно слышался шум.
После этого игра готова к настройке разрешения экрана. Этот код чрезвычайно интересен. Первым делом проверяется высота экрана 400, что означает выбранный режим 640 x 400. Если его нельзя установить, то игра откатывается к 640 x 480. Все остальные разрешения используются напрямую. Что же такого особенного в режиме 640 x 400?
Для выполнения рендеринга игре нужен доступ к графической памяти. Если выбрана ширина экрана 320, то игра идёт по особому пути выполнения кода, используемого для инициализации буферов дисплея. Я предполагаю, что если кто-то запускает игру в режиме 320x240 (который тоже является VGA), то он работает на таком старом «железе», что код даже не пытается использовать аппаратные буферы. При всех остальных высотах экрана код пытается выделить аппаратные буферы под страницу видимой памяти и страницу скрытой памяти. Подозреваю, что используется двойная буферизация графики, отсюда и два буфера. Для страницы видимой памяти обязательна доступность аппаратной памяти. С выделением этой памяти связана довольно интересная логика:
// From STARTUP.CPP
VisiblePage.Init( ScreenWidth , ScreenHeight , NULL , 0 , (GBC_Enum)(GBC_VISIBLE | GBC_VIDEOMEM));
/*
** Check that we really got a video memory page. Failure is fatal.
*/
memset (&surface_capabilities, 0, sizeof(surface_capabilities));
VisiblePage.Get_DD_Surface()->GetCaps(&surface_capabilities);
if (surface_capabilities.dwCaps & DDSCAPS_SYSTEMMEMORY) {
/*
** Aaaarrgghh!
*/
WWDebugString(TEXT_DDRAW_ERROR);WWDebugString("n");
MessageBox(MainWindow, TEXT_DDRAW_ERROR, TEXT_SHORT_TITLE, MB_ICONEXCLAMATION|MB_OK);
if (WindowsTimer) delete WindowsTimer;
return (EXIT_FAILURE);
}
Можно только предположить, что комментарий «Aaaarrgghh!» был добавлен после долгой отладки проблем с производительностью на отдельных машинах. На каком-то этапе кто-то догадался, что DirectDraw API (часть DirectX) может возвращать страницу системной памяти, даже когда запрашивается страница аппаратной видеопамяти.
Работа со скрытой страницей более расслабленная, но её логика и комментарии ещё лучше:
// From STARTUP.CPP
/*
** If we have enough left then put the hidpage in video memory unless...
**
** If there is no blitter then we will get better performance with a system
** memory hidpage
**
** Use a system memory page if the user has specified it via the ccsetup program.
*/
unsigned video_memory = Get_Free_Video_Memory();
unsigned video_capabilities = Get_Video_Hardware_Capabilities();
if (video_memory < (unsigned int)(ScreenWidth*ScreenHeight) ||
(! (video_capabilities & VIDEO_BLITTER)) ||
(video_capabilities & VIDEO_NO_HARDWARE_ASSIST) ||
!VideoBackBufferAllowed) {
HiddenPage.Init (ScreenWidth , ScreenHeight , NULL , 0 , (GBC_Enum)0);
} else {
//HiddenPage.Init (ScreenWidth , ScreenHeight , NULL , 0 , (GBC_Enum)0);
HiddenPage.Init (ScreenWidth , ScreenHeight , NULL , 0 , (GBC_Enum)GBC_VIDEOMEM);
/*
** Make sure we really got a video memory hid page. If we didnt then things
** will run very slowly.
*/
memset (&surface_capabilities, 0, sizeof(surface_capabilities));
HiddenPage.Get_DD_Surface()->GetCaps(&surface_capabilities);
if (surface_capabilities.dwCaps & DDSCAPS_SYSTEMMEMORY) {
/*
** Oh dear, big trub. This must be an IBM Aptiva or something similarly cruddy.
** We must redo the Hidden Page as system memory.
*/
AllSurfaces.Remove_DD_Surface(HiddenPage.Get_DD_Surface()); // Remove the old surface from the AllSurfaces list
HiddenPage.Get_DD_Surface()->Release();
HiddenPage.Init (ScreenWidth , ScreenHeight , NULL , 0 , (GBC_Enum)0);
} else {
VisiblePage.Attach_DD_Surface(&HiddenPage);
}
}
Такая же «двойная проверка» применяется и к этой странице, но API может вернуться к использованию системной памяти. В комментарии источником таких проблем называют IBM Aptiva. Подозреваю, что IBM Aptiva был причиной множества шуток во время разработки — в DEFINES.H
даже есть строка #define FIXIT_APTIVA_MODEM
, которая использует специальную логику для работы с модемом компьютера IBM Aptiva.
Затем игра присваивает ScreenHeight точное значение 3072. Графический буфер, привязанный к видимой и скрытой страницам, имеет размер 3072 x 3072. Насколько я понимаю, весь вышеприведённый код, связанный с получением аппаратной памяти, на самом деле отключен при помощи конструкций #if
. Он такой запутанный, что сложно сказать, что именно включено, а что нет. Но во всех пресс-релизах о ремастере упоминается поддержка «4K». Поэтому, вероятно, реальное разрешение «4K»-ремастера равно 3072 x 3072.
Совсем недавно, в 2019 году, в комментарии были внесены изменения с указанием даты и времени. Некто с инициалами JAS добавил проверку, выполняется ли запуск из редактора (вероятно, редактора уровней), чтобы в этом случае не производился перехват мышки. О ремастере было заявлено в ноябре 2018 года, поэтому логично, что за этот временной интервал должны были реализовать различные изменения и протестировать поддержку 4K с новыми файлами ресурсов.
В коде присутствует всевозможная отключенная логика о принудительном воспроизведении вступительного видеоролика при первом запуске. Такое ощущение, что у разработчика была биполярка и он не мог решить, должен ли пользователь на самом деле увидеть этот ролик. На каком-то этапе вся эта логика была закомментирована; скорее всего, во время разработки ремастера. Думаю, разработчики и тестеры быстро устали от просмотра вступительного видеоролика, поэтому подозреваю, что и во время разработки оригинала его тоже отключали.
Теперь можно начинать играть! Далее код выполняет Main_Game
(из CONQUER.CPP
), которая и является самим игровым циклом. В комментарии к этому файлу указана дата начала «3 апреля 1991 года», то есть для выпуска первой Command & Conquer в 1995 году потребовалось четыре года разработки!
После завершения игры функция Main_Game
может выполнить возврат. В ней есть код очистки, но прежде чем мы к нему приступим, взглянем на это:
// From STARTUP.CPP
if (RunningAsDLL) { //PG
return (EXIT_SUCCESS);
}
Итак, предположительно, графика после DLL остаётся в странном состоянии, что, скорее всего, приводит к всевозможным утечкам памяти.
Другие открытия
Я решил подробнее изучить и другие заинтересовавшие меня по именам файлов аспекты.
Трассировки стеков Windows 95
Файл W95TRACE.CPP
содержит комментарий:
/ * Implementation of Win95 tracing facility to mimic that of NT". */
Похоже, он свидетельствует о том, что разработка велась на Windows NT, но тестирование проводилось на Windows 95, чтобы гарантировать работу игры у целевой аудитории. Это кажется логичным, потому что NT могла быть доступной разработчикам игры ещё в июле 1993 года. Подозреваю, что трассировки стеков Windows 95 до ужаса бесполезны по сравнению с тем, что предоставляет Windows NT.
Компилятор Watcom
Существует также WATCOM.H
, в котором определяется множество директив "#pragma", специфичных для очень популярного тогда компилятора Watcom.
Это довольно интересно, потому что первый релиз компилятора Watcom с поддержкой C++ был выпущен в 1993 году. Есть другие примечания разработчиков, говорящие нам, что проект был начат ещё в 1991 году. Начинался ли он как проект на C, а затем перешёл на C++? Или у разработчиков был другой компилятор C++?
В том же файле есть и другое сокровище — кажется, некоторые разработчики компиляторов тогда не могли утруждать себя реализацией true и false:
// From WATCOM.H
// Fix deficiency in Watcom so that true/false will be defined.
#ifndef __BORLANDC__
#ifndef TRUE_FALSE_DEFINED
#define TRUE_FALSE_DEFINED
enum {false=0,true=1};
typedef int bool;
#endif
#endif
Шифрование
В BLOWFISH.CPP реализовано шифрование Blowfish, которое, скорее всего, является хорошо известным шифром Blowfish. Я могу вообразить только один способ его применения — обфускация сетевого трафика.
Система сущностей
Класс CCPtr
в файле CCPTR.H — это тип-указатель, использующий ID
в качестве смещения кучи памяти. Это довольно стандартный подход, потому что он упрощает сохранение файлов на диск, если все объекты отслеживаются из одного места. Он используется как очень простой способ реализации системы сущностей. Если вам любопытны преимущества использования системы сущностей в видеоиграх, то подробнее об этом можно прочитать здесь.
Векторы
Класс VECTOR.H
определяет класс шаблона, приблизительно напоминающий std::vector. Предположительно, он был разработан на ранних этапах для использования возможностей STL.
Дифтонги
Дифтонг — это сочетание двух соседних гласных звуков в одном слоге. Как это связано с кодом видеоигры? Честно говоря — никак. Самый странный файл имеет название WIN32LIHB/DIPTHONG.H
. Его описание находится в заголовке:
// From WIN32LIB/DIPTHONG.CPP
* DIGRAM or DIATOMIC encoding is the correct term for this method. *
* This is a fixed dictionary digram encoding optimized for English text. *
По сути, это простой алгоритм сжатия, предполагающий, что мы сжимаем тексты, и выполняющий замену последовательностей.
В коде встречается магическое число 4567
, используемое при создании нескольких define:
// From WIN32LIB/DIPTHONG.CPP
#define TXT_GUEST 4567+3
#define TXT_LOGIN 4567+4
#define TXT_LOGIN_TO_INTERNET 4567+5
#define TXT_YOUR_HANDLE 4567+6
#define TXT_YOUR_PASSWORD 4567+7
#define TXT_INTERNET_HOST 4567+8
#define TXT_INTERNET_JOIN 4567+9
#define TXT_INTERNET_GAME_TYPE 4567+10
#define TXT_JOIN_INTERNET_GAME 4567+11
#define TXT_ENTER_IP_ADDRESS 4567+12
#define TXT_WINSOCK_CONNECTING 4567+13
#define TXT_WINSOCK_NOT_CONNECTING 4567+14
#define TXT_WINSOCK_UNABLE_TO_CONNECT_TO_SERVER 4567+15
#define TXT_WINSOCK_CONTACTING_SERVER 4567+16
#define TXT_WINSOCK_SERVER_ADDRESS_LOOKUP_FAILED 4567+17
#define TXT_WINSOCK_UNABLE_TO_ACCEPT_CLIENT 4567+18
#define TXT_WINSOCK_UNABLE_TO_CONNECT 4567+19
#define TXT_WINSOCK_CONNECTION_LOST 4567+20
#define TXT_WINSOCK_RESOLVING_HOST_ADDRESS 4567+21
Следующий код выполняется как часть распаковки сжатых данных:
// From WIN32LIB/DIPTHONG.CPP
if (string >= 4567) return (InternetTxt[string-4567]);
ptr = (unsigned short int const *)data;
return (((char*)data) + ptr[string]);
Массив «InternetTxt» — это просто набор заранее заданных строк. Похоже, всё, что равно или больше «4567», считается заранее заданной строкой. Я думаю, эту константу разработчик выбрал, просто нажав на клавиши 4-5-6-7 строки с цифрами на клавиатуре. Когда мне нужно сгенерировать случайное целочисленное значение в качестве константы, я предпочитаю этот способ.
Total Entertainment Network и MPlayer
Как я говорил выше, похоже на то, что существовали специальные сборки для поддержки Total Entertainment Network и MPlayer. Это сервисы для многопользовательских онлайн-игр того времени.
Каждый сервис имел собственный код инициализации и настройки.
Файлы Total Entertainment Network (TEN)
- CCTEN.CPP
- TENMGR.H
Используются следующие функции, не имеющие реализации в исходном коде:
- tenArSendToPlayer
- tenArExitArena
- tenArIdleArena
- tenArReturnGameOptions
- tenArReturnPlayerOptions
- tenArSendToOtherPlayers
- tenArSendToPlayer
- tenArSetAlertMessageRoutine
- tenArSetIncomingPacketRoutine
- tenArSetOption
- tenArSetPlayerEnteredRoutine
- tenArSetPlayerState
- tenArSetPregameHookRoutine
- tenArUnreliableSendToOtherPlayers
- tenArUnreliableSendToPlayer
Файлы MPlayer (MPATH)
- CCMPATH.CPP
- MPMGRW.H
- MPMGRD.H
Используются следующие функции, не имеющие реализации в исходном коде:
- MGenMoveTo
- MGenGetMasterNode
- MGenFlushNodes
- MGenMCount
- MGenSanityCheck
- MGenGetNode
- MGenGetQueueCtr
Я не смог найти никаких подробностей об отсутствующих в коде TEN функциях.
Код «MPATH» вызывает множество функций «MGen», которые тоже отсутствуют. Однако мы можем легко найти их в исходном коде Quake. В Quake тоже можно было играть в обеих этих сетях. К сожалению, в публичных исходниках Quake нет кода TEN. Предположу, что интеграция MPlayer была реализована передачей студии-разработчику стандартной библиотеки и интеграции её в код игры.
Думаю, что EA была аккуратной, потому что понимала, что не владеет этим кодом, а id Software на момент выпуска Quake это просто не волновало.
Разумеется, на самом деле этот код никому не нужен. Просто это единственное, что осталось от стандартных для конца 1990-х игровых сервисов.
Дополнение: со мной связался Ларри Гастингс и внёс коррективы в некоторые мои догадки. Оказывается, исходный код, который, по моему мнению, был реализацией MPlayer, ею не является. На самом деле это ПО, которое называется «chunnel»; оно позволяет приложению, выполняемому в Windows 95 в режиме DOS, взаимодействовать с сетевым стеком, предоставляемым Windows. Также он поделился некоторыми подробностями об интеграции MPlayer с играми. Воспроизвожу их здесь без упоминания имён третьих лиц:
Во-первых, у большинства компаний не было собственной интеграции с Mplayer; они просто присылали нам фрагмент кода, и мы должны были разбираться, как собрать его и выполнить всю работу по интеграции. Но я думаю, что Westwood стала исключением из правил. Помню, что один из наших «инженеров по портированию» съездил перед выпуском Red Alert в Westwood. Его очень впечатлила сетевая модель RA!
Во-вторых, все активы Mplayer были проданы компании GameSpy, которой не нужны были никакие онлайн-сервисы (Mplayer / POP.X / Global Rankings). Всё, что ей было нужно — это база пользователей и… отдел продаж, возможно? Как бы то ни было, в 2013 году GameSpy закрылась, а все её активы были проданы Glu Mobile, поэтому, теоретически, всей интеллектуальной собственностью теперь владеет эта компания. TEN в 1998 году сделала поворот в сторону «классических» онлайн-игр, в 1999 году выполнила ребрендинг, превратившись в Pogo.com, а в 2001 году её приобрела… EA! Поэтому EA, скорее всего, могла спокойно выпускать код TEN. И я сомневаюсь, что сейчас кого-то волнует код Mplayer, этот сервис давно мёртв.
В-третьих, как я говорил, компании обычно присылали нам код и Mplayer сама занималась работой по интеграции. В случае id Software было так же. Код Mplayer, который есть в Quake 1 — это не интеграция с игровым сервисом Mplayer! На самом деле это технология, которую мы лицензировали компании id. Она называется «chunnel»; её написал британский программист по имени ***. «Chunnel» — это библиотека, которая позволяла DOS-программам в защищённом режиме 386 выполняться под Windows, совершая вызовы сетевого стека Winsock операционной системы Windows 95. Нужно помнить, что это было примерно в 1996 году, и экосистема видеоигр под Windows ещё не сложилась. Quake изначально был выпущен как программа для DOS, и его портировали под Win32 только спустя один-два года. «Chunnel» позволила id решить серьёзную проблему — обеспечила её DOS-программе в защищённом режиме 386 возможность участвовать в сетевой игре при работе под Windows. Частично благодарность компании выразилась в том, что каждая игра id использовала Mplayer (несмотря на то, что это всегда вызывало сомнения). При запуске первых версий Quake под DOS, например, первой shareware-демо, на экране заставки даже присутствовал логотип Mplayer (золотая осциллограмма) и пометка о «лицензированной технологии». Это также объясняет параметр командной строки "-mpath" в DOS-сборках Quake 😉
Перевод и локализация
Игры обычно портируют на другие языки, чтобы их можно было продавать не только на англоязычных рынках. Интересно, что по крайней мере частично локализация была реализована при помощи разбросанных по коду конструкций #ifdef GERMAN
, а также проверкой LANGUAGE.H
перед заданием таких аспектов, как TEXT_SETUP_FIRST
.
Проверка #ifdef GERMAN
встречается в коде 28 раз, однако #ifdef FRENCH
встречается 37 раз. Возможно, перевод на французский просто был чуть более тщательным.
Разумеется, в DEFINES.H
представлена самая лучшая реализация перевода.
// From DEFINES.H
//#define SPAIN 1 (never used)
Похоже, до перевода на испанский разработчики так и не добрались.
Защита донглом
На определённом промежутке времени в прошлом стандартным способом DRM-защиты различного ПО был аппаратный ключ-донгл. По сути, для защиты использовалось закрытое и обфусцированное аппаратное устройство, подключаемое к PC. Программа передаёт ему некий «запрос» и отказывается запускаться, если не получит нужный ответ.
Не думаю, что Command & Conquer когда-то продавалась с таким устройством, но в DEFINES.H
есть одна-две закомментированные строки про него:
// From DEFINES.H
/**********************************************************************
** ColinM
** Set this to enable dongle protection
*/
//#define DONGLE
Virgin Interactive
Command & Conquer разрабатывала Westwood Studios, но этой компанией владела Virgin Interactive. В коде есть такая проверка с уникальным названием:
// From DEFINES.H
/**********************************************************************
** If this is defined, the special Virgin limited cheat keys
** are enabled. This allows the "cheat" parameter and then only
** allows the ALT-W to win the mission.
*/
#ifdef PLAYTEST_VERSION
#define VIRGIN_CHEAT_KEYS
#endif
// From INIT.CPP
#ifdef VIRGIN_CHEAT_KEYS
case PARM_PLAYTEST:
Debug_Playtest = true;
break;
#endif
Думаю, что перед релизом Virgin Interactive получила специальную сборку, чтобы провести плейтестинг продукта.
Конец
Вот и всё. Я не исследовал весь код, просто посмотрел то, что мне показалось мне интересным и на что хватило времени. Я узнал немного больше о разработке видеоигр начала 1990-х, и, возможно, смогу использовать это знание в настоящем.
Возможно, в дальнейшем я вернусь к изучению этого кода. Мне бы очень хотелось сравнить его с портом игры на Playstation, но не думаю, что EA сможет выпустить этот код, даже если он у неё ещё есть.