Back-инжиниринг Caesar III

Мне нравится играть в игры, особенно в экономические стратегии, хочу рассказать про градостроительный симулятор из детства — Caesar III, как принято говорить, тёплый и ламповый. Игра была выпущена в 1998 году, знатоками своего дела, Impressions Games®. Это экономический симулятор управления древнеримским городом в реальном времени. Через много лет я решил вновь пройти её, а затем постараться продлить удовольствие от игры, посмотреть ресурсы и вникнуть в игровую логику с точки зрения программиста.Под катом я опишу процесс извлечения текстур, поиск игровых алгоритмов и расскажу как хобби превратилось в самостоятельный проект. А еще будет палитра RGB555, IDA, HexRays и немного кода.301641ead8b3fb95255beb5768c07526.jpgМузыкаПро музыку я ничего писать не буду, ибо лежит она никем неупакованная на диске с игрой в формате .wav.

Графика

С графикой (текстурами) все намного сложнее, текстуры разбиты на несколько псевдоархивов с расширением .sg2 и .555.

Файл с расширением .sg2, назовем его «оглавлением», содержит параметры текстур: размеры, смещение в атласе, имя и номер, идентификатор, различные флаги.

Файл с расширением .555, назовем его «атласом», содержит сами изображения, в собственном формате описания, которые делятся на три типа: — простые (bmp)— изометрические— с альфа-каналомДля каждого типа текстур используется свой формат «сжатия». «Оглавление» может ссылаться на несколько атласов, при этом имя «атласа», должно соответствовать названию группы текстур, которые в нем содержатся. Простые текстуры читаются как массив цветов и их можно практически без обработки рисовать на экране, «обработка» состоит в преобразовании BGR555 цвета с глубиной 5 бит на канал, в более удобный для работы АRGB32. В игре Сaesar III текстуры с прозрачностью не используются, они будут задействованы позже в этой серии игр (Pharaoh, Cleopatra и др)

В файле С3.SG2 содержатся описания групп изображений.Если открыть этот файл в hex-редакторе, то можно увидеть следующий блок данных, e9ab09a4e6840bc9326529d86ebac44c.pngкоторый описывает группу из 44 (n_images: 0×0000002C) изображения с именем plateau, информация о которых начинается с индекса 201 (start_index: 0×000000C9). Всего в «оглавлении» есть место для 100 таких групп. После описания групп, идут описания конкретных изображений, перебирая которые можно восстановить сами картинки. Дело осталось за малым, прочитать оглавление, распаковать пожатые текстуры и собрать их в полноценные изображения. Вот что получилось при распаковке группы plateaubad71f31696f6de37a2d04c4a240f48b.pngfdd1315d444cc56ae2106d6a5a188008.png853ab541794892a5f53f4271618c49cc.png33597add2bb14950ade04b8752ee3ab1.png08de947067ba7cc69b913ccb886d5005.png0022357d0abffbd5fcaaf47c6f978713.pngeb6584a8a0173824a1e662d6fc50e735.png699fa0ce85bf620e0af86fafc6988892.png56e07b897f884e7c8ccdc350f0c8df0e.png685e40ec6a679f17bc145de0e1ff5c33.pngafe494d0a437b3b44e6cd3341aae7d6b.pngcf548682f9b1f942af2b9a48462b93ac.png5f4c832cca116d0d3387479a0d52c5f7.png

Вот еще несколько восстановленных текстур, в нативном формате, насколько это получилось, без фильтров.31dfb1e12af74098a55dce01e93d5a04.bmp3c7bc312c87947bb80a4cf8df0a46d90.bmpe64a2c6722894f4d93381b80ec0904e0.bmp828363b9d7b5352de8da446291cf8a02.png7fe08d076412fb5c08e74250cbf0f089.pngecd7b15a3a8c6121426c9b3646d97a12.png

А здесь обработанные текстуры с альфаканалом.13db2a344f364cf20dd5e0e507b1b618.png2445d86ccf6ed87d6a83510ac0b05f2f.png2547889b4c012090ba74f68c66cd012b.png

Если с атласом текстур и используемых в нем структурах данных еще можно разобраться, полагаясь на сообразительность, hex-редактор и долю везения, то с алгоритмами восстановления текстур такое не пройдет. И тут на помощь приходит Ильфак с незаменимым отладчиком IDA, и не менее полезным декомпилятором Hex-Rays. Открываем с3.exe в отладчике, видим картину отнюдь не радужную, я большую часть времени программирую на яве (java) или плюсах (c++) и для меня это, не то чтобы темный лес, но густой кустарник точно.e5b87779d73074a428bb6b5520cd929d.png

Тут нам поможет способность IDA восстанавливать asm в псевдокод plain-С. Нажимаем F5 и перед нами человеко-читабельный код, с которым уже можно работать.67e6d0f75c7c62b9ef9927c2058908cf.png.

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

А спустя некоторое время (день, неделю, месяц и тд) он станет вот таким. Согласитесь, теперь намного удобнее искать алгоритмы7110009fa58790cd1e329267169050fe.png.

Исполняемый файл игры Caesar III © был собран с отладочной информацией компилятором Visual C++ 5.0, что также позволяет восстанавливать логику приложения более продуктивно. Используя отладчик, декомпилятор и собственные серые клетки можно добраться до функции чтения изображений из архива

Много кода int __cdecl fun_drawGraphic (signed int graphicId, int xOffset, int yOffset) { int result; // eax@2 LONG v4; // [sp+50h] [bp-8h]@43

drawGraphic_graphicId = graphicId; drawGraphic_xOffset = xOffset; drawGraphic_yOffset = yOffset; if (graphicId <= 0 ) return 0; if ( graphicId >= 10000) return 0; drawGraphic_fileOffset = c3_sg2[graphicId].offset; if (drawGraphic_fileOffset <= 0 ) return 0; LOWORD(drawGraphic_width) = c3_sg2[graphicId].width; LOWORD(drawGraphic_height) = c3_sg2[graphicId].height; drawGraphic_type = c3_sg2[graphicId].type; graphic_xOffset = xOffset; graphic_yOffset = yOffset; drawGraphic_visiblePixelsClipX = (signed __int16)drawGraphic_width; if ( c3_sg2[graphicId].extern_flag && (signed __int16)drawGraphic_width <= ddraw_width ) { strcpy(drawGraphic_555file, &c3sg2_bitmaps[200 * c3_sg2[graphicId].bitmap_id]); j_fun_changeFileExtensionTo(drawGraphic_555file, &extension_555[4 * graphics_format_id]); if ( !j_fun_readDataFromFilename( drawGraphic_555file, screen_buffer, c3_sg2[graphicId].data_length, c3_sg2[graphicId].offset - 1) ) { j_fun_changeFileExtensionTo(drawGraphic_555file, "555"); if ( !j_fun_readDataFromFilename( drawGraphic_555file, screen_buffer, c3_sg2[graphicId].data_length, c3_sg2[graphicId].offset - 1) ) return 0; if ( c3_sg2[graphicId].compr_flag ) j_fun_convertCompressedGraphicToSurfaceFormat(screen_buffer, c3_sg2[graphicId].data_length); else j_fun_convertGraphicToSurfaceFormat(screen_buffer, c3_sg2[graphicId].data_length); } j_fun_setGraphicXClipCode(); j_fun_setGraphicYClipCode(); if ( drawGraphic_clipYCode == 5 ) return 0; if ( drawGraphic_type ) { if ( drawGraphic_clipYCode == 5 ) return 0; drawGraphic_fileOffset = 2 * (signed __int16)drawGraphic_width * drawGraphic_invisibleHeightClipTop; drawGraphic_fileOffset += 2 * drawGraphic_invisibleWidthClipLeft; if ( drawGraphic_clipXCode == 1 ) { j_fun_drawGraphicUncompressedClipLeft((char *)screen_buffer + drawGraphic_fileOffset); } else { if ( drawGraphic_clipXCode == 2 ) j_fun_drawGraphicUncompressedClipRight((char *)screen_buffer + drawGraphic_fileOffset); else j_fun_drawGraphicUncompressedClipY((char *)screen_buffer + drawGraphic_fileOffset); } } else { if ( c3_sg2[graphicId].compr_flag ) { if ( drawGraphic_clipXCode == 1 ) { j_fun_drawGraphicCompressedClipLeft((char *)screen_buffer); } else { if ( drawGraphic_clipXCode == 2 ) j_fun_drawGraphicCompressedClipRight((char *)screen_buffer); else j_fun_drawGraphicCompressedFull((char *)screen_buffer); } } else { drawGraphic_fileOffset = 2 * (signed __int16)drawGraphic_width * drawGraphic_invisibleHeightClipTop; drawGraphic_fileOffset += 2 * drawGraphic_invisibleWidthClipLeft; if ( drawGraphic_clipXCode == 1 ) { j_fun_drawGraphicUncompressedClipLeft((char *)screen_buffer + drawGraphic_fileOffset); } else { if ( drawGraphic_clipXCode == 2 ) j_fun_drawGraphicUncompressedClipRight((char *)screen_buffer + drawGraphic_fileOffset); else j_fun_drawGraphicUncompressedClipY((char *)screen_buffer + drawGraphic_fileOffset); } } } result = (signed __int16)drawGraphic_width; } else { if ( c3_sg2[graphicId].extern_flag ) { if ( window_id == 21 || window_id == 20 ) { drawGraphic_visiblePixelsClipX = fullscreenImage_width; drawGraphic_visiblePixelsClipY = fullscreenImage_height; drawGraphic_copyBytesInBufferForClipX = 2 * ((signed __int16)drawGraphic_width - drawGraphic_visiblePixelsClipX); drawGraphic_skipBytesInBufferForClipX = 2 * (ddraw_width - drawGraphic_visiblePixelsClipX); j_fun_drawGraphicUncompressedFull(&c3_555[2 * fullscreenImage_xOffset + 13000000] + 2 * (signed __int16)drawGraphic_width * fullscreenImage_yOffset); return drawGraphic_visiblePixelsClipX; } v4 = 2 * (signed __int16)drawGraphic_width * fullscreenImage_yOffset + 2 * fullscreenImage_xOffset; drawGraphic_visiblePixelsClipX = fullscreenImage_width; drawGraphic_visiblePixelsClipY = fullscreenImage_height; strcpy(drawGraphic_555file, &c3sg2_bitmaps[200 * c3_sg2[graphicId].bitmap_id]); j_fun_changeFileExtensionTo(drawGraphic_555file, &extension_555[4 * graphics_format_id]); if ( !j_fun_readUncompressedImageData( drawGraphic_555file, screen_buffer, 2 * drawGraphic_visiblePixelsClipX, drawGraphic_visiblePixelsClipY, v4) ) { j_fun_changeFileExtensionTo(drawGraphic_555file, "555"); if ( !j_fun_readUncompressedImageData( drawGraphic_555file, screen_buffer, 2 * drawGraphic_visiblePixelsClipX, drawGraphic_visiblePixelsClipY, v4) ) return 0; j_fun_convertGraphicToSurfaceFormat( screen_buffer, drawGraphic_visiblePixelsClipY * 2 * drawGraphic_visiblePixelsClipX); } drawGraphic_copyBytesInBufferForClipX = 0; drawGraphic_skipBytesInBufferForClipX = 0; j_fun_drawGraphicUncompressedFull((char *)screen_buffer); result = drawGraphic_visiblePixelsClipX; } else // internal { if ( (unsigned __int8)drawGraphic_type == 30 )// isometric { switch ( (signed __int16)drawGraphic_width ) { case 58: LOWORD(drawGraphic_height) = 30; break; case 26: LOWORD(drawGraphic_height) = 14; break; case 10: LOWORD(drawGraphic_height) = 6; break; default: if ( (signed __int16)drawGraphic_width == 118 ) return j_fun_drawBuildingFootprintSize2(); if ( (signed __int16)drawGraphic_width == 178 ) return j_fun_drawBuildingFootprintSize3(); if ( (signed __int16)drawGraphic_width == 238 ) return j_fun_drawBuildingFootprintSize4(); if ( (signed __int16)drawGraphic_width == 298 ) return j_fun_drawBuildingFootprintSize5(); break; } } j_fun_setGraphicXClipCode(); j_fun_setGraphicYClipCode(); if ( drawGraphic_clipYCode == 5 ) { result = 0; } else { if ( drawGraphic_type ) { if ( (unsigned __int8)drawGraphic_type == 30 ) { if ( drawGraphic_clipXCode == 1 ) { switch ( (signed __int16)drawGraphic_width ) { case 58: j_fun_drawBuildingFootprint_xClipRight(&c3_555[drawGraphic_fileOffset], drawGraphic_clipYCode); break; case 26: j_fun_drawBuildingFootprint_26px_xClipRight(); break; case 10: j_fun_drawBuildingFootprint_10px_xClipRight(); break; default: j_fun_drawGraphicUncompressedClipLeft(&c3_555[drawGraphic_fileOffset]); break; } } else { if ( drawGraphic_clipXCode == 2 ) { switch ( (signed __int16)drawGraphic_width ) { case 58: j_fun_drawBuildingFootprint_xClipLeft(&c3_555[drawGraphic_fileOffset], drawGraphic_clipYCode); break; case 26: j_fun_drawBuildingFootprint_26px_xClipLeft(); break; case 10: j_fun_drawBuildingFootprint_10px_xClipLeft(); break; default: j_fun_drawGraphicUncompressedClipRight(&c3_555[drawGraphic_fileOffset]); break; } } else { switch ( (signed __int16)drawGraphic_width ) { case 58: j_fun_drawBuildingFootprint_xFull(&c3_555[drawGraphic_fileOffset], drawGraphic_clipYCode); break; case 26: j_fun_drawBuildingFootprint_26px_xFull(); break; case 10: j_fun_drawBuildingFootprint_10px_xFull(); break; default: j_fun_drawGraphicUncompressedClipY(&c3_555[drawGraphic_fileOffset]); break; } } } } else { if ( (unsigned __int8)drawGraphic_type == 13 && drawGraphic_clipXCode ) { j_fun_drawImage_32x32((int *)&c3_555[drawGraphic_fileOffset]); } else { if ( (unsigned __int8)drawGraphic_type == 12 && drawGraphic_clipXCode ) { j_fun_drawImage_24x24((int *)&c3_555[drawGraphic_fileOffset]); } else { if ( (unsigned __int8)drawGraphic_type == 10 && drawGraphic_clipXCode ) { j_fun_drawImage_16x16((int *)&c3_555[drawGraphic_fileOffset]); } else { if ( (unsigned __int8)drawGraphic_type == 2 && drawGraphic_clipXCode ) { j_fun_drawGraphicType2(&c3_555[drawGraphic_fileOffset]); } else { if ( (unsigned __int8)drawGraphic_type == 20 ) { if ( drawGraphic_clipXCode == 1 ) { j_fun_drawGraphicLetterColoredClipLeft(&c3_555[drawGraphic_fileOffset]); } else { if ( drawGraphic_clipXCode == 2 ) j_fun_drawGraphicLetterColoredClipRight(&c3_555[drawGraphic_fileOffset]); else j_fun_drawGraphicLetterColoredFull(&c3_555[drawGraphic_fileOffset]); } } else { drawGraphic_fileOffset += 2 * (signed __int16)drawGraphic_width * drawGraphic_invisibleHeightClipTop; drawGraphic_fileOffset += 2 * drawGraphic_invisibleWidthClipLeft; if ( drawGraphic_clipXCode == 1 ) { j_fun_drawGraphicUncompressedClipLeft(&c3_555[drawGraphic_fileOffset]); } else { if ( drawGraphic_clipXCode == 2 ) { j_fun_drawGraphicUncompressedClipRight(&c3_555[drawGraphic_fileOffset]); } else { if ( drawGraphic_clipYCode ) j_fun_drawGraphicUncompressedClipY(&c3_555[drawGraphic_fileOffset]); else j_fun_drawGraphicUncompressedFull(&c3_555[drawGraphic_fileOffset]); } } } } } } } } } else // type == 0 { if ( c3_sg2[graphicId].compr_flag ) { if ( drawGraphic_clipXCode == 1 ) { j_fun_drawGraphicCompressedClipLeft(&c3_555[drawGraphic_fileOffset]); } else { if ( drawGraphic_clipXCode == 2 ) j_fun_drawGraphicCompressedClipRight(&c3_555[drawGraphic_fileOffset]); else j_fun_drawGraphicCompressedFull(&c3_555[drawGraphic_fileOffset]); } if ( drawGraphic_colorMask ) { if ( drawGraphic_clipXCode == 1 ) { j_fun_drawGraphicCompressedColorMaskClipLeft(&c3_555[drawGraphic_fileOffset], drawGraphic_colorMask); } else { if ( drawGraphic_clipXCode == 2 ) j_fun_drawGraphicCompressedColorMaskClipRight(&c3_555[drawGraphic_fileOffset], drawGraphic_colorMask); else j_fun_drawGraphicCompressedColorMaskFull(&c3_555[drawGraphic_fileOffset], drawGraphic_colorMask); } } } else // not compressed { drawGraphic_fileOffset += 2 * (signed __int16)drawGraphic_width * drawGraphic_invisibleHeightClipTop; drawGraphic_fileOffset += 2 * drawGraphic_invisibleWidthClipLeft; if ( drawGraphic_clipXCode == 1 ) { j_fun_drawGraphicUncompressedClipLeft(&c3_555[drawGraphic_fileOffset]); } else { if ( drawGraphic_clipXCode == 2 ) j_fun_drawGraphicUncompressedClipRight(&c3_555[drawGraphic_fileOffset]); else j_fun_drawGraphicUncompressedClipY(&c3_555[drawGraphic_fileOffset]); } } } result = drawGraphic_visiblePixelsClipX; } } } return result; } На основе это кода можно будет построить приложение, которое сможет отображать текстуры используемые в игре.

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

Каковы цели у ремейка+ Дать возможность другим людям поиграть в забытую игру и не только под Windows.+ Играть в Caesar III без эмуляторов, танцев с бубном, возни с запуском игры под Wine, дикого на текущий момент разрешения 800×600.+ Повысить качество текстур, шрифтов и скорости игры.+ Получить удовольствие от разработки — я люблю играть в игры, особенно экономические, и мне очень не нравится когда игра глючит, вылетает или работает неправильно. Мне проще сделать ремейк, чем писать свою игру, ведь к свои программам я отношусь очень критично, стараясь убрать глюки и по максимуму настроить баланс. Но результат всегда чуть хуже, чем ожидаешь, наверное поэтому на создание своего проекта уходит времени в разы больше.+ Добавить наконец сетевую игру, которой мне так не хватало в детстве.+ В век планшетов побить варваров, стоя в пробке — согласитесь намного интереснее, чем донатить в ферму.+ Сделать хороший перевод, не только для русскоговорящих, а например для французов, до них игра дошла на английском.

Что делать с авторскими правамиВариантов немного:1. Забить и делать то, что хочешь — не наш путь, мы ведь цивилизованные люди, не хочется тратить громадное количество времени на ремейк, чтобы авторы оригинала запретили его на финише.2. Писать на почту правообладателем и просить разрешения (устное, разрешение на использование ресурсов или бренда, «на бумаге» и пр.). Тут еще хуже, цивилизованные авторы, или держатели прав (на данный момент это Activision), как правило держатся за них до последнего, даже если игра не приносит прибыли. Права есть — значит ремейка не будет. Точка.3. Позиционировать игру как мод, которому для работы нужна оригинальная игра, скачанная с торрента честно купленная на GOG.com, так поступили например Corsix TH, выпустив ремейк Theme Hospital©. Самый оправданный и безопасный путь, хотя…

Старые игры не значит плохие. Многие старые игры, если с них сдуть пыль, подчистить, подмазать и подклеить… Эти игрушки заткнут за пояс многие современные поделки.

Вадим БалашовБлагодарю, что дочитали до конца!

P.S. Отдельное спасибо людям, которые помогают в развитии ремейка.Bianca van Schaik (http://pecunia.nerdcamp.net/), back-инжиниринг оригинальной игрыGregoire Athanase (http://sourceforge.net/projects/opencaesar3/), автор рендера и многих алгоритмовGeorge Gaal (https://github.com/gecube/opencaesar3) back-инжиниринг сейвови многие другие коммитеры

© Habrahabr.ru