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.
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
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.
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.
Maximum route lengths (long / short, in tiles)
| Walker type | Long / Short |
|---|---|
| Prefect, Engineer | 52 / 43 |
| Actor, Gladiator, Lion Tamer, Tax Collector | 43 / 35 |
| Priest, Doctor, Surgeon, Teacher, Trader, Bathkeeper, Barber | 35 / 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).
| Transition | Size change |
|---|---|
| Medium Insula → Large Insula | 1×1 → 2×2 |
| Medium Villa → Large Villa | 2×2 → 3×3 |
| Medium Palace → Large Palace | 3×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.).
- Food: consumed once per month at 0.5 units per resident.
- Other resources (pottery, oil, furniture, wine): 1 unit per house, twice per month, regardless of resident count.
- Services: 1 unit consumed per game day; maximum 100 units of any service stored.
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:
- Festival factor = 12 − max(40, months_since_last_festival_for_this_god)
- Temple bonus = +50 to the god with the most temples; if tied, nobody gets it.
- Disrespect penalty = −25 to the god with the fewest temples; if tied, nobody gets it.
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
| Population | Minimum mood |
|---|---|
| 0 – 99 | 50 |
| 100 – 199 | 40 |
| 200 – 299 | 40 |
| 300 – 399 | 30 |
| 400 – 499 | 20 |
| 500 – 599 | 10 |
| 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:
- mood < 9 → +5 wrath points
- mood 10–19 → +2 wrath points
- mood 20–39 → +1 wrath point
- mood ≥ 50 → wrath points reset to 0
- Maximum wrath: 50 points
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
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:
| Type | Cost (denarii) | Wine required | Prep time |
|---|---|---|---|
| Small | population / 20 + 10 | — | 2 months |
| Medium | population / 10 + 20 | — | 3 months |
| Large | population / 5 + 40 | population / 500 + 1 | 4 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:
| Type | 1st festival | 2nd 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:
| Difficulty | Tax multiplier |
|---|---|
| Very Easy | 300% |
| Easy | 200% |
| Normal | 150% |
| Hard | 100% |
| Very Hard | 75% |
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:
| Population | No profit / With profit |
|---|---|
| 0 – 500 | 0 / 50 |
| 501 – 1,000 | 0 / 150 |
| 1,001 – 2,000 | 100 / 225 |
| 2,001 – 3,000 | 200 / 300 |
| 3,001 – 5,000 | 200 / 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 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 # | Modest | Generous | Lavish |
|---|---|---|---|
| 1st | +3 | +5 | +10 |
| 2nd | +1 | +3 | +5 |
| 3rd | 0 | +1 | +3 |
| 4th | 0 | 0 | +1 |
| 5th+ | 0 | 0 | 0 |
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):
- Base change: −2
- Tribute: +1 if paid successfully; −3 / −5 / −8 for skipping 1 / 2 / more than 2 consecutive years
- Governor salary: +1 if salary is below rank level; −1 for each rank level above your current status
- Mission events: mission-dependent, e.g. +5 for reaching 1,000 residents
Culture Rating
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
| Coverage | Points |
|---|---|
| 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
| Coverage | Points |
|---|---|
| 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).
| Coverage | Schools | Academies | Libraries |
|---|---|---|---|
| 0 – 30% | 0 | 0 | 0 |
| 31 – 50% | 1 | 1 | 2 |
| 51 – 70% | 4 | 2 | 4 |
| 71 – 85% | 6 | 4 | 8 |
| 86 – 99% | 10 | 7 | 14 |
| 100%+ | 15 | 10 | 20 |
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.