← Devblog

Reverse-engineering Caesar III: Game Logic (Part 3)

The algorithms for extracting Caesar III© textures and rendering the city have already been covered — now for the "sweetest" part that has been drawing ancient Roman architects back for over 15 years: the game logic. Using various analysis approaches, I present the results of this small investigation. I apologise in advance for the length — some songs can't be shortened. At the end there are a few words about the fate of the source code recovered from the original game's executable.

This is a translation of the original article published on Habr.

A Brief History

The game is written in C and available for Windows and Mac. The official release was in October 1998. For Windows it was most likely compiled with Visual Studio 97 — VS6 hadn't gained popularity yet. In 1999 the game was updated to version 1.0.0.3, already built with Visual Studio 6, which became the most widespread version. Later, patch 1.1 (c3 1.0.1.0) was released for English-language copies.

Build metadata (v1.0.0.3)
Date compiled: Fri, September 3, 1999, 7:31:17 AM
Linker version: 6.0
Machine type:   Intel 386 or later
PE format:      PE32
Subsystem:      Windows GUI
Min OS:         Windows 95 (4.0)
File version:   0.0

Game fans have also modded the executables to run at higher resolutions. The game uses the graphics engine from Caesar II — there are screenshots of an intermediate (possibly debug) build where elements of the old UI are still visible.

Caesar III UI overview

A Closer Look

Many variables in the game are hardcoded as "magic numbers." The code examples here are taken from the c3_rev catalogue — the sources compile and run in debug mode, so you can step through the logic. Maintenance was apparently done by a fellow Russian speaker who brought the recovered sources to a working state.

One curiosity: Russian localizers patched the binary, replacing c3.eng with c3.rus, accidentally breaking compatibility with later patches.

Recovered main() function
int main(int argc, char* argv[])
{
  int result;           // eax@2
  signed int v5;        // [sp+4Ch] [bp-20h]@7
  struct tagMSG Msg;    // [sp+50h] [bp-1Ch]@21

  HINSTANCE hInstance = GetModuleHandle(0);

  byte_6606BC = 0;
  if ( fun_loadSettings() )
  {
    if ( fun_readLanguageTextFiles("c3.eng", "c3_mm.eng") )
    {
      fun_loadDefaultNames();
      fun_logDebugMessage("OK :Game text loaded.", 0, 0);
      if ( fun_setupMainWindow(hInstance) )
      { /* ... */ }
      /* ... */
    }
  }
}

Text Resources / Localisation

Most of the game's text — except mission text data — lives in C3.eng and C3_mm.eng. C3.eng holds building descriptions, events, residents, and so on. It consists of a 28-byte header followed by an 8,000-byte index table. Each index entry stores the byte offset of a null-terminated string. To retrieve the title of the Education Advisor window (TITLE_ADVISOR_EDUCATION = 16), read the 16th index entry and follow its offset.

C3.eng header and index-entry structs
struct C3EngHeader
{
    char tag[16];    // file description string
    int  numGroups;  // number of string groups
    int  numStrings; // total string count
    int  unknown;
};

struct C3EngIndexEntry
{
    int offset; // byte offset to the string
    int inUse;
};

User Interface

All UI element sizes except buttons are hardcoded, which is why you can't drag info windows or help panels around. The background of a window is drawn first; then its controls are drawn on top.

Buttons have a fixed height and are rendered using three textures: the left cap, a repeating middle fill, and a right cap. Windows are drawn in a similar pattern: specialised edge textures frame the border, and a seamless tile fills the centre.

Scrollbar click handler (file-open dialog)
int fun_dialogFile_handleScrollbarClick()
{
  signed int result;
  int v1, v2, v3;

  if ( filelist_numFiles > 12 )
  {
    if ( mouse_isLeftClick )
    {
      v2 = filelist_numFiles - 12;
      if ( mouseclick_x >= screen_640x480_x + 464 )
      if ( mouseclick_x <= screen_640x480_x + 496 )
      if ( mouseclick_y >= screen_640x480_y + 145 )
      if ( mouseclick_y <= screen_640x480_y + 300 )
      {
        v3 = mouseclick_y - (screen_640x480_y + 145);
        if ( v3 > 130 ) v3 = 130;
        v1 = fun_getPercentage(v3, 130);
        filelist_scrollPosition = fun_adjustWithPercentage(v2, v1);
        window_redrawRequest = 1;
        result = 1;
      } else result = 0;
    } else result = 0;
  } else result = 0;
  return result;
}

Small Cogs in a Large Machine

The game is built from a large number of subsystems — water availability, building maintenance, education, entertainment, and more. Each subsystem, when examined closely, turns out to be very simple and can be described in a few paragraphs. Their combined operation produces engaging gameplay with good balance.

Notably, the subsystems hardly interact with each other: the position of a theatre does not affect the neighbouring school or market; doctors know nothing about the actors walking past. Houses are the common meeting point — they accumulate the effects of all subsystems, and as more become available, the house level rises. House level in concrete terms means tax income and the number of available workers.

Walker Routes

Walker route planning

Different walker types use different routing logic. A prefect (or engineer) walks the path A1 → A2 → A1 — even if A2 is right next to the prefect's building — and then returns to base. A barber, by contrast, completes the full circuit to point B2.

Service walkers move exclusively on roads (as well as bridges, gates, granaries, the northern tiles of storage yards, gardens, and fort grounds). Enemies and wild animals cannot pass through gates. Merchants and mega-carriers can travel off-road but prefer roads when possible. Regular carriers cannot pass through gardens or fort grounds.

A route is calculated before the walker leaves the building. Starting from the exit tile, the game builds a map of all reachable road paths. Each candidate path is scored by walking it tile-by-tile and asking nearby buildings whether they need the walker's service. The path with the highest total need score is selected.

Route scoring diagram

For example, a prefect heads toward the area with the highest fire risk — but the algorithm favours large districts over small isolated ones even if the small district's peak risk is higher, because the total score of a large area exceeds the peak of a small one. This applies to all service workers since the route-importance algorithm is unified across all service types.

The bazaar trader is an exception: route selection accounts for the goods currently stocked in the market. The diagram below shows the possible routes a trader might take; each intersection multiplies the number of paths to evaluate. The white route wins because it sells the most goods. When the trader reaches the end of that route: if she has goods left, she retraces it; if not, she finds the shortest path back to the market.

Bazaar trader route

Maximum route lengths (long / short, in tiles)

Walker typeLong / Short
Prefect, Engineer52 / 43
Actor, Gladiator, Lion Tamer, Tax Collector43 / 35
Priest, Doctor, Surgeon, Teacher, Trader, Bathkeeper, Barber35 / 26

The switch between long and short routes happens under different conditions for different walkers — not all have been fully reversed. The trader, for instance, switches to the long route when a granary is placed next to the market.

Buildings have a separate exit point that may not coincide with the entrance. Priority for spawning is shifted downward; priority for entry is shifted upward — the path between entry and exit can be very long.

House Evolution

Houses below the Insula level occupy a single tile. Two adjacent single-tile houses of the same level can merge into a 2×2 house of the same level, with an additional condition: the first three bits of the pseudorandom value in byte minimap_info[26244] (also used during minimap generation) must match for both tiles. This means merging was not possible on every tile in the city.

When a house reaches a level whose next upgrade requires more space, it expands by capturing adjacent tiles. Valid neighbours are: empty land, a garden, a lower-level house, or a same-level house. Expansion can happen in four directions (N, W, S, E).

TransitionSize change
Medium Insula → Large Insula1×1 → 2×2
Medium Villa → Large Villa2×2 → 3×3
Medium Palace → Large Palace3×3 → 4×4

City Population

Population is tracked by age from 0 to 99. Each house stores a char peoples[100] array: the index is the age and the value is the number of residents at that age. At the start of each year the array shifts by one position, removing the oldest residents and opening a slot for newborns. Two functions then calculate the probability of new births in a house and the natural death rate from old age.

Population game-tick function (excerpt)
void fun_gametick_population()
{
  /* ... */
  cityinfo_happiness_immigrationAmount[4517 * ciid] = 0;
  cityinfo_happiness_emigrationValue[4517 * ciid]   = 0;
  if ( cityinfo_populationYearlyBirthsDeathsCalculationNeeded[4517 * ciid] )
  {
    fun_populationAdvanceAgesOneYear();
    fun_populationBirths();
    fun_updatePopulationAfterBirthsDeaths(ciid);
  }
  fun_calculateNumberOfWorkers();
  /* ... */
}

Service and Resource Consumption

Houses consume both resources (food, pottery, oil, furniture, wine) and services (religion, healthcare, education, etc.).

Resource storage capacity scales with house level — larger houses can hold more of each resource type. A house buys enough goods from a trader to last the next six months.

Religion

There are five gods: Ceres, Neptune, Mars, Mercury, and Venus. The mood calculation algorithm is the same for all of them. Coverage is counted by population, not territory — so all temples can be placed anywhere on the map without affecting the gods' moods directly. Temple placement does affect which houses the priests reach, however.

Coverage formula:

base_value = 100 × (500 × oracles + 750 × working_small_temples + 1500 × working_large_temples) / population

"Working" means at least one employee is assigned.

Additional mood factors:

Note: Venus does not participate in bonus distribution. This is likely a bug.

Final mood = base_value + festival_factor + temple_bonus + disrespect_penalty

Minimum mood floor by city population

PopulationMinimum mood
0 – 9950
100 – 19940
200 – 29940
300 – 39930
400 – 49920
500 – 59910
600+0

God wrath mechanics

Each game day (25 ticks), each god's current mood moves one unit toward its true mood. Additionally, a random god index 0–6 is drawn; indices 0–4 map to the five gods. If the selected god's mood is below thresholds, wrath points accumulate:

Monthly divine event logic (900 ticks)
if ( god_id == [5, 6, 7] )
{
  // find the god with the most wrath points,
  // or mood < 40, or a random god if none qualify
  current_god = find_least_happy_god();

  if ( current_god )
  {
    if ( current_god.mood > 100 && current_god.blessingDone == 0 )
    {
      current_god.doBlessing();
      current_god.mood       = 50;
      current_god.blessingDone = 1;
    }

    if ( current_god.wrathPoints > 20 && current_god.smallCurseDone == 0 )
    {
      current_god.doSmallCurse();
      current_god.smallCurseDone = 1;
      current_god.wrathPoints    = 0;
      current_god.mood          -= 12;
    }

    if ( current_god.wrathPoints == 50 && current_god.lastFestival > 3_months )
    {
      current_god.doWrath();
      current_god.wrathPoints = 0;
      current_god.mood       -= 30;
    }
  }
}

For example: suppose we have angered Mars and do nothing to fix it. Current mood = 100, true mood = 0. Each day current mood drops by 1. A game month is 16 days, so at that rate Mars will act in approximately 100/16 ≈ 6 in-game months — though the "sacred RNG" may intervene before then.

Festivals and City Mood

Festival screen

Three festival sizes are available: Small, Medium, and Large. Each is dedicated to one god and improves that god's mood. Festival costs scale with population:

TypeCost (denarii)Wine requiredPrep time
Smallpopulation / 20 + 102 months
Mediumpopulation / 10 + 203 months
Largepopulation / 5 + 40population / 500 + 14 months

Beyond the god-specific mood boost, festivals also improve citizen happiness. Only the first two festivals within any 12-month window contribute to city mood — further festivals have no effect:

Type1st festival2nd festival
Small+7+2
Medium+9+3
Large+12+5

Finances

The maximum debt a city can carry while still being allowed to build is −5,000 denarii. This is checked on every financial action.

Debt check during road placement
void fun_drawBuildingGhostRoad()
{
  /* ... */
  if ( cityinfo_treasury[4517 * ciid] <= -5000 )
    cannotBuild = 1;
  if ( cannotBuild )
  {
    sub_4D0B70(graphic_fire_almost, v2, v1 - iso_tile_half_height, (ColorMask)-1949);
  }
  /* ... */
}

The annual wage shown in the Labor Advisor (e.g. "30") is actually 10× the real value in denarii — it displays 30 but pays 3 denarii per year. This is done because all financial calculations are in integers.

Monthly wage payment formula:

wage_paid = (workers_at_month_end × worker_wage) / 10 / 12

This sum leaves the city treasury and disappears (unlike, say, Tropico, where money flows into residents' pockets).

If the treasury is negative, the city pays 10% annual interest to Rome, deducted monthly:

monthly_interest = (−treasury) × 10 / 100 / 12

When debt first reaches 5,000, Rome offers a loan (mission-dependent, typically 10,000 denarii). A second loan is possible, but it is tracked as debt.

Taxes

Each month, every house generates a tax based on house level and resident count. The tax collector gathers it when she passes by; uncollected taxes expire at the end of the year. The base tax rate is set in c3_model.txt; the difficulty modifier scales the final amount:

DifficultyTax multiplier
Very Easy300%
Easy200%
Normal150%
Hard100%
Very Hard75%

Final tax formula:

TotalTax = (tax_base / 2) × city_tax_rate / (12 × 100)

Imperial Tribute

At the end of each year, 25% of the city's annual profit is sent to Rome. If the city is in debt, no tribute is paid — but Emperor favor decreases. Minimum tribute amounts are defined by population:

PopulationNo profit / With profit
0 – 5000 / 50
501 – 1,0000 / 150
1,001 – 2,000100 / 225
2,001 – 3,000200 / 300
3,001 – 5,000200 / 400
5,000+200 / 500

Profit here is the difference between income (donations, exports) and expenses (construction, imports, debt interest, governor salary, wages, other).

Emperor Favor

Emperor favor screen

Emperor favor is primarily driven by city profitability and fulfilling (or ignoring) imperial requests. Gifts significantly boost favor, but with diminishing returns — all gifts given in the past 12 months are counted:

Gift #ModestGenerousLavish
1st+3+5+10
2nd+1+3+5
3rd0+1+3
4th00+1
5th+000

Fulfilling requests on time gives the full favor bonus; completing them late yields half. Ignoring a request costs 8 favor: −3 when the deadline is missed, −5 more after 24 months of continued inaction.

Annual favor update (applied at year start):

Culture Rating

Culture advisor

The Culture rating is a composite score reflecting city attractiveness. It comprises theater, temple, school, academy, and library coverage. A culture rating above 30 indirectly signals that affluent citizens are present.

Culture = theaters + temples + schools + academies + libraries

Theater coverage → culture points (max 25)

theater_coverage = 500 × working_theaters / population

CoveragePoints
0 – 30%0
31 – 50%3
51 – 70%8
71 – 85%12
86 – 99%18
100%+25

Temple coverage → culture points (max 30)

temple_coverage = 150 × small_temples + 300 × large_temples + 500 × oracles

CoveragePoints
0 – 30%0
31 – 50%3
51 – 70%9
71 – 85%14
86 – 99%22
100%+30

Education coverage → culture points

Schools serve children (75 pupils each, max 15 pts); academies serve students (100 each, max 10 pts); libraries serve all residents (800 each, max 20 pts).

CoverageSchoolsAcademiesLibraries
0 – 30%000
31 – 50%112
51 – 70%424
71 – 85%648
86 – 99%10714
100%+151020

Resources and Production

Resources are the backbone of the economy. Some require processing, creating simple production chains (e.g. iron mine → weapons workshop). All resources are inexhaustible except fishing spots, which have a finite fish supply.

Wheat is the first resource the game introduces. In central provinces wheat farms are more efficient than other farms, but this advantage disappears in northern or desert regions. Farms are placed on fertile soil (shown on the map as yellow seedlings) and deliver their harvest to a granary or storage yard.

Farms, extraction buildings, and workshops all follow the same production tick logic. Every 25 ticks (one game day), production progress is updated:

Daily production progress formula
// productionRate  — carts of goods per year at 100% staffing
// numberWorkers   — current worker count
// maximumWorkers  — maximum workers for this building
// 12 * 16         — months/year × game days/month

progress += 100 * (productionRate * numberWorkers) / (12 * 16 * maximumWorkers);

When progress reaches 100, a carrier is spawned to deliver the goods to the nearest storage facility. If no space is available, the production halts until the carrier is freed. If a new batch completes while the carrier is still busy, production stops until the carrier returns.

Storage and Distribution

Two types of storage participate in the supply chain: granaries store food only and are where bazaar traders collect goods; storage yards hold any resource type and sell to visiting merchants.

Both facility types stop accepting deliveries when staffed at less than 50% capacity. Neither actively fetches goods by default — except when the special Get Goods order is active, which causes the facility to pull goods from other storage facilities. This mechanism allows players to create cross-district supply chains.

The combination of resource extraction, processing, and distribution ended up quite well designed. A small number of resource types avoids overwhelming the player with micromanagement, while automation of routine decisions (storage assignment, routing) leaves the governor room for strategic planning.

Closing Thoughts

This article leaves two large topics untouched: inter-city trade and military mechanics — both are substantial enough to deserve their own write-ups, and the research there is incomplete.

I should note that dedicated fans of the game could certainly add many details I missed or overlooked while recovering the original sources. My goal was to build a solid understanding of the game's balance — so carefully polished by the original authors — before risking introducing regressions in the remake.

As for the recovered source code: I personally ran out of patience to clean it up, but AntonBaracuda completed this genuinely difficult work. At that address you will find the sources with partially separated structures and functions — it all compiles, runs, and can be stepped through in a debugger up to the early startup functions.

Links