⬆️ ⬇️

Back-engineering Caesar III

I like to play games, especially in economic strategies, I want to tell you about a town-planning simulator from childhood - Caesar III, as they say, is warm and tube. The game was released in 1998, experts in their field, Impressions Games. This is an economic simulator of real-time control of an ancient Roman city. After many years, I decided to go through it again, and then try to extend the pleasure of the game, see the resources and understand the game logic from the point of view of the programmer.



Under the cut, I will describe the process of extracting textures, searching for game algorithms and tell you how a hobby turned into an independent project. And there will be a palette of RGB555, IDA, HexRays and a bit of code.





Music

I will not write anything about music, for it lies unpacked by anyone on the game disc in .wav format.



Graphics

')

With graphics (textures) everything is much more complicated, textures are divided into several pseudo archives with the extension .sg2 and .555 .



The file with the extension .sg2 , let's call it “table of contents,” contains texture parameters: dimensions, offset in atlas, name and number, identifier, various flags.



The file with the .555 extension, let's call it “atlas”, contains the images themselves, in their own description format, which are divided into three types:

- simple (bmp)

- isometric

- with alpha channel

Each type of texture uses its own “compression” format. “Table of contents” can refer to several atlases, and the name “atlas” must correspond to the name of the group of textures contained in it. Simple textures are read as an array of colors and they can be drawn on the screen with little or no processing. “Processing” consists in converting BGR555 colors with a depth of 5 bits per channel, more convenient for ARGB32 operation. In the game Caesar III, the textures with transparency are not used, they will be used later in this series of games (Pharaoh, Cleopatra, etc.)



The file C3.SG2 contains descriptions of groups of images.

If you open this file in a hex editor, you can see the following data block,



which describes a group of 44 (n_images: 0x0000002C) images named plateau , information about which begins with index 201 (start_index: 0x000000C9). In total in the "table of contents" there is a place for 100 such groups. After the description of the groups, the descriptions of specific images come up, going over which you can restore the images themselves. It remains for the small, read the table of contents, unpack the shrunk textures and assemble them into full-fledged images. This is what happened when unpacking the plateau group





Here are some more restored textures, in the native format, as far as it turned out, without filters.





And here is the processed texture with alpha channel.





If the atlas of the textures and the data structures used in it can still be sorted out, relying on ingenuity, the hex editor and the share of luck, then this will not work with texture restoration algorithms. And then Ilfak comes to the rescue with an indispensable debugger IDA, and no less useful Hex-Rays decompiler. We open c3.exe in the debugger, we see the picture is not at all rainbow, I program most of the time in Java (java) or pluses (c ++) and for me this is not so much a dark forest, but a dense bush for sure.





Here we can use the ability of IDA to restore asm into plain-C pseudocode. Press F5 and in front of us is a human-readable code with which you can already work.

.



With functions and variables, and a structured structure, and surely an astute reader noticed some pattern in the code above, so let's make it more readable. Press the N button, enter the normal name for the function, and the code looks much simpler.





And after some time (day, week, month, etc.), it will become like this. Agree, now it is much more convenient to look for algorithms.

.



The Caesar III executable file was compiled with debug information by the Visual C ++ 5.0 compiler, which also allows you to recover the application logic more productively. Using the debugger, decompiler and own gray cells you can get to the function of reading images from the archive

Lot of code
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; } 




Based on this code, it will be possible to build an application that can display the textures used in the game.



Hobby

It would be strange if the post about the back-engineering of the game ended with a link to someone else's program))) My passion for modding the game’s resources turned into writing a number of fixes fixing some errors, and now into a full- featured remake of the game .

If you are not very interested in reading thoughts about the objectives of the project and copyrights, you can go to the downloads section and see how I managed to get closer to the original.



What are the goals of the remake

+ To give other people the opportunity to play a forgotten game and not only under Windows.

+ Play Caesar III without emulators, dancing with a tambourine, messing with the launch of the game under Wine, the currently-wild resolution of 800x600.

+ Increase the quality of textures, fonts and game speed.

+ Get pleasure from the development - I like to play games, especially economic, and I really do not like when the game is buggy, crashes or does not work correctly. It's easier for me to make a remake than to write my game, because I’m very critical of my programs, trying to remove glitches and adjust the balance to the maximum. But the result is always a little worse than you expect, which is probably why it takes much more time to create your project.

+ Finally add a network game, which I lacked as a child.

+ On the tablet to beat the barbarians, standing in traffic - you will agree much more interesting than donating to the farm.

+ Make a good translation, not only for Russian speakers, but for example for the French, the game has reached them in English.



What to do with copyright

There are few options:

1. To score and do what you want is not our way, we are civilized people, I don’t want to spend an enormous amount of time on a remake for the authors of the original to ban it at the finish.

2. Write to the post office by the right holder and request permission (oral, permission to use resources or a brand, “on paper”, etc.). Even worse, civilized authors, or rights holders (at the moment it’s Activision), tend to hold onto them to the last, even if the game does not bring profit. There are rights - it means there will be no remake. Point.

3. Position the game as a mod, which needs an original game for work, downloaded honestly purchased from GOG.com from the torrent , for example Corsix TH did this by releasing a remake of Theme Hospital. The most justified and safest way, though ...



Old games don't mean bad. Many old games, if you blow off the dust from them, clean up, grease and glue ... These toys are stuck in the belt of many modern crafts.
Vadim Balashov



Thank you for reading to the end!



PS


Special thanks to the people who help in the development of the remake.

Bianca van Schaik (http://pecunia.nerdcamp.net/), back-engineering of the original game

Gregoire Athanase (http://sourceforge.net/projects/opencaesar3/), author of the renderer and many algorithms

George Gaal (https://github.com/gecube/opencaesar3) back-engineering saves

and many other committers




UPD1. If you are interested in the results of back-ing this game (exe + idb), it is probably best to contact via mail or PM, a topic that is called "gray legal area". IDA 5.5 + Hex-Rays 1.01 was used to get acquainted with the game. Files and materials posted with permission from Bianca van Schaik (http://caesar.biancavanschaik.nl/).



UPD2. Why this post got into the linux hub. OllyDbg u IDA are run on a Win7 virtual machine, QtCreator 3.0.1 + cmake + gcc 4.8 is used for development, the game is natively written for linux. For building under Windows, the mingw-w64 crosscompiler is used; for MacOSX and Haiku, virtual machines are raised. To build an android, the environment from libsdl-android is used.

Source: https://habr.com/ru/post/221913/



All Articles