<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Keber Flores</title>
    <description>The latest articles on DEV Community by Keber Flores (@keber).</description>
    <link>https://dev.to/keber</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1396399%2Ff56e7376-85c4-426b-866d-49cf1113fa92.jpeg</url>
      <title>DEV Community: Keber Flores</title>
      <link>https://dev.to/keber</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/keber"/>
    <language>en</language>
    <item>
      <title>The game that corrupted its own help files: fixing Civilization's 30-year-old CHANGE freeze</title>
      <dc:creator>Keber Flores</dc:creator>
      <pubDate>Fri, 23 Oct 2026 03:36:10 +0000</pubDate>
      <link>https://dev.to/keber/the-game-that-corrupted-its-own-help-files-fixing-civilizations-30-year-old-change-freeze-2160</link>
      <guid>https://dev.to/keber/the-game-that-corrupted-its-own-help-files-fixing-civilizations-30-year-old-change-freeze-2160</guid>
      <description>&lt;p&gt;If you have played the original &lt;strong&gt;Civilization&lt;/strong&gt; (MicroProse, 1991) in DOSBox, you may have hit this one: you open a city, press &lt;strong&gt;CHANGE&lt;/strong&gt; to pick something new to build, and the game freezes. No error, no crash, just a hung screen. The &lt;code&gt;READ.ME&lt;/code&gt; that ships with version 474.03 knows about it (item 23) and suggests a workaround: turn off &lt;em&gt;Instant Advice&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That avoids the symptom, but not the cause. I wanted to know what was actually happening, so I took it apart. The short version: &lt;strong&gt;the game corrupts its own data files&lt;/strong&gt;, a little more every time it hits a specific code path, until a lookup loop has no way out.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;The freeze is an infinite loop in the game's indexed text lookup. Three defects combine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Incomplete shipped indexes.&lt;/strong&gt; Each indexed &lt;code&gt;.TXT&lt;/code&gt; file omits its &lt;em&gt;last&lt;/em&gt; entry from its hash index. In &lt;code&gt;PRODUCE.TXT&lt;/code&gt; that entry is &lt;code&gt;*Caravan&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Off-by-one write-back.&lt;/strong&gt; When a lookup falls back to a sequential scan, the game "helpfully" writes the found offset back into the index. But the offset comes from &lt;code&gt;ftell()&lt;/code&gt; on a text-mode stream and is usually 1 byte late, so it never matches. Every later lookup repeats the scan and burns another empty bucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unbounded probing.&lt;/strong&gt; The lookup probes buckets until it finds a match or an empty bucket. Once every bucket is full, a key that can't be matched loops forever.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix: rebuild the indexes of seven &lt;code&gt;.TXT&lt;/code&gt; files (text unchanged), plus an optional &lt;strong&gt;1-byte patch&lt;/strong&gt; to &lt;code&gt;CIV.EXE&lt;/code&gt; that disables the write-back so they can never fill up again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Only happens with &lt;strong&gt;Instant Advice&lt;/strong&gt; on.&lt;/li&gt;
&lt;li&gt;The last thing on screen is the &lt;em&gt;Military Advisor&lt;/em&gt; recommendation. The &lt;em&gt;Domestic Advisor&lt;/em&gt; box, normally shown next, never appears.&lt;/li&gt;
&lt;li&gt;Intermittent: some cities, some turns. Later saves of a game are affected while earlier ones are not. Forum folklore blames unit stacking in or near the city.&lt;/li&gt;
&lt;li&gt;Reproduced in DOSBox 0.74-3; &lt;code&gt;READ.ME&lt;/code&gt; says it happens on real hardware too.&lt;/li&gt;
&lt;li&gt;A useful clue: &lt;code&gt;PRODUCE.TXT&lt;/code&gt; and &lt;code&gt;ERROR.TXT&lt;/code&gt; had modification dates &lt;em&gt;later than the install&lt;/em&gt;, even though nobody had edited them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point turned out to be the whole story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup and approach
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Game&lt;/td&gt;
&lt;td&gt;Civilization for DOS, 474.03 (shown at start-up; &lt;code&gt;INTRO.TXT&lt;/code&gt; says 474.01)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executable&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CIV.EXE&lt;/code&gt;, 302,982 bytes, MD5 &lt;code&gt;8a574c67c722c34beb6f5c965742769f&lt;/code&gt;. Microsoft C runtime (1988), EXEPACK-compressed, overlay data appended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emulator&lt;/td&gt;
&lt;td&gt;DOSBox 0.74-3, 10,000 cycles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test harness&lt;/td&gt;
&lt;td&gt;Headless Linux + Xvfb, input driven by &lt;code&gt;xdotool&lt;/code&gt;, screenshots after each step&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The investigation went from data, to code, and back to the running game:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Save-file comparison.&lt;/strong&gt; I parsed the &lt;code&gt;.SVE&lt;/code&gt; format (28-byte city records, 12-byte unit records including the per-tile stack link) and compared an affected city with the same city in an unaffected save. Every stack link was well formed, so stacking corruption was ruled out as a direct cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unpacking the executable.&lt;/strong&gt; I wrote an EXEPACK decompressor. The stream unpacked cleanly to 201,520 bytes with its relocation table; the data segment base sits at linear &lt;code&gt;0x2A800&lt;/code&gt;, found via the C runtime copyright string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locating the code.&lt;/strong&gt; References to the strings &lt;code&gt;Military Advisor:&lt;/code&gt; and &lt;code&gt;produce.txt&lt;/code&gt; lead to the advisor routine and to a generic text lookup at &lt;code&gt;1ECB:01B3&lt;/code&gt;, disassembled with &lt;code&gt;ndisasm&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format analysis and simulation.&lt;/strong&gt; I decoded the index at the top of the &lt;code&gt;.TXT&lt;/code&gt; files, reimplemented the lookup in Python and ran it on all 73 unit, improvement and wonder names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduction.&lt;/strong&gt; Load a save, open the city, press CHANGE, check for a freeze. Repeated with original and corrected files, diffing the files before and after to catch writes by the game.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Diagnosis
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The advisor flow
&lt;/h3&gt;

&lt;p&gt;With Instant Advice on, CHANGE computes a military and a domestic recommendation. For each one the game builds a key of &lt;code&gt;*&lt;/code&gt; plus the item name (e.g. &lt;code&gt;*Caravan&lt;/code&gt;) and looks it up in &lt;code&gt;PRODUCE.TXT&lt;/code&gt; to get the advice text ("We should build a …"). The military lookup succeeds and its box is drawn. The domestic lookup never returns.&lt;/p&gt;

&lt;p&gt;In every frozen case, the domestic recommendation was &lt;strong&gt;Caravan&lt;/strong&gt;. Unit stacking only matters indirectly: the defenders in and around a city change what the advisors recommend.&lt;/p&gt;

&lt;h3&gt;
  
  
  The indexed text file format
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PRODUCE.TXT&lt;/code&gt;, &lt;code&gt;ERROR.TXT&lt;/code&gt;, &lt;code&gt;KING.TXT&lt;/code&gt;, &lt;code&gt;HELP.TXT&lt;/code&gt; and &lt;code&gt;BLURB0&lt;/code&gt;–&lt;code&gt;BLURB4.TXT&lt;/code&gt; all start with a &lt;strong&gt;528-byte hash index&lt;/strong&gt;: 256 little-endian 16-bit buckets, written as 8 lines of 32 buckets, each line ending in CR LF. Entries start at offset &lt;code&gt;0x212&lt;/code&gt; (530) as lines beginning with &lt;code&gt;*&lt;/code&gt;, and the file ends with &lt;code&gt;*END&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A bucket holds the file offset of an entry's &lt;code&gt;*&lt;/code&gt;. The value &lt;code&gt;0x3030&lt;/code&gt; (ASCII &lt;code&gt;00&lt;/code&gt;) means &lt;em&gt;empty&lt;/em&gt;. The hash of a key of n bytes is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;h = sum(i * c[i] for i in 0..n-1)  mod 256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Collisions are resolved by linear probing. Bucket &lt;code&gt;b&lt;/code&gt; lives at file offset &lt;code&gt;2 * (b + b/32)&lt;/code&gt; (the &lt;code&gt;b/32&lt;/code&gt; accounts for the CR LF after every 32 buckets).&lt;/p&gt;

&lt;h3&gt;
  
  
  The lookup routine (&lt;code&gt;1ECB:01B3&lt;/code&gt;)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Compute &lt;code&gt;h&lt;/code&gt;. Open the file in binary mode, read the 16-bit value at &lt;code&gt;2 * (h + h/32)&lt;/code&gt;, close it, reopen it in text mode (&lt;code&gt;"rt"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;If the value is &lt;code&gt;&amp;lt;= 0x1FF&lt;/code&gt; or equals &lt;code&gt;0x3030&lt;/code&gt;, the bucket is empty: go to the sequential scan.&lt;/li&gt;
&lt;li&gt;Otherwise seek to the value, read one line (&lt;code&gt;%[^\n]\n&lt;/code&gt;) and compare it with the key. Match → found.&lt;/li&gt;
&lt;li&gt;No match → &lt;code&gt;h = (h + 1) &amp;amp; 0xFF&lt;/code&gt; (&lt;code&gt;1ECB:0398&lt;/code&gt;) and go back to step 1. &lt;strong&gt;There is no probe counter and no other exit.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequential scan:&lt;/strong&gt; seek to &lt;code&gt;0x200&lt;/code&gt;; for each line remember &lt;code&gt;ftell()&lt;/code&gt;, read the line, compare. Stop on a match or at EOF.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write-back (&lt;code&gt;1ECB:03DC&lt;/code&gt;):&lt;/strong&gt; if the scan found the key, reopen the file as &lt;code&gt;"r+b"&lt;/code&gt; and write the remembered &lt;code&gt;ftell()&lt;/code&gt; value into the empty bucket where probing stopped.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 6 is a self-healing cache: the index repairs itself the first time a missing key is looked up. In theory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;           ┌──────────────────────────────────────────────┐
           ▼                                              │
  read bucket[h] ──empty?──yes──► linear scan ──found──► write ftell() into bucket[h]
           │                          │                   (off by one byte!)
           no                      not found
           ▼                          ▼
   line at offset == key? ──yes──► return found      return not found
           │
           no
           ▼
    h = (h+1) &amp;amp; 0xFF ───────────────────────────────────────┘
    (no counter: if no bucket is ever empty, this never ends)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The three defects, together
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Incomplete shipped index.&lt;/strong&gt; The last entry of every indexed file is missing from its index. &lt;code&gt;*Caravan&lt;/code&gt; is the last entry of &lt;code&gt;PRODUCE.TXT&lt;/code&gt;. So the first Caravan lookup misses the hash, scans, finds it, and writes back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Off-by-one write-back.&lt;/strong&gt; The offset from &lt;code&gt;ftell()&lt;/code&gt; on a text-mode stream is usually 1 byte &lt;em&gt;past&lt;/em&gt; the &lt;code&gt;*&lt;/code&gt;: 2099 instead of 2098 for &lt;code&gt;*Caravan&lt;/code&gt; (and in one file, 1 byte early). From offset 2099 the game reads &lt;code&gt;Caravan&lt;/code&gt; without the &lt;code&gt;*&lt;/code&gt;, which never matches &lt;code&gt;*Caravan&lt;/code&gt;. So the &lt;em&gt;next&lt;/em&gt; lookup probes past that bucket, reaches the next empty one, scans again, and writes &lt;code&gt;2099&lt;/code&gt; again. &lt;strong&gt;Each Caravan recommendation consumes one empty bucket.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unbounded probing.&lt;/strong&gt; After roughly 205 Caravan recommendations no empty bucket remains, and every one of them holds &lt;code&gt;0x0833&lt;/code&gt; (2099). Now any key whose probe chain never matches spins forever, opening and closing the file on every step. In the fully corrupted file this happens for &lt;strong&gt;23 of the 73 keys&lt;/strong&gt;: Caravan, Palace, and all 21 Wonders.&lt;/p&gt;

&lt;p&gt;That explains everything that looked random: it depends on how many times the game has recommended a Caravan &lt;em&gt;since install&lt;/em&gt;, and on whether this particular city's advisor recommends one of the 23 doomed keys. It also explains why the problem seems to get worse the longer you play (the damage accumulates in &lt;code&gt;PRODUCE.TXT&lt;/code&gt; itself, not in the save), and why the file's modification date changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirmation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Reproduction
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Save&lt;/th&gt;
&lt;th&gt;Original files&lt;/th&gt;
&lt;th&gt;Corrected files&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Latest affected save&lt;/td&gt;
&lt;td&gt;Freezes after the Military Advisor box&lt;/td&gt;
&lt;td&gt;Works; Domestic Advisor recommends Caravan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Earlier affected save&lt;/td&gt;
&lt;td&gt;Freezes&lt;/td&gt;
&lt;td&gt;Works; recommends Caravan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A third reported save&lt;/td&gt;
&lt;td&gt;No freeze in 3 tries (advisor recommended City Walls, which resolves through the hash)&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unaffected save&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Watching the corruption happen
&lt;/h3&gt;

&lt;p&gt;I started from a correct index with only the &lt;code&gt;*Caravan&lt;/code&gt; bucket emptied, then pressed CHANGE three times on an affected city. After each press the game had written &lt;code&gt;2099&lt;/code&gt; (&lt;code&gt;0x0833&lt;/code&gt;) into one more bucket: 117, then 118, then 119. That's the corruption, step by step, live.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audit of all indexed files
&lt;/h3&gt;

&lt;p&gt;Every bucket was checked against the real entry offsets. &lt;em&gt;Unindexed&lt;/em&gt; = entry missing from the index. &lt;em&gt;Bad buckets&lt;/em&gt; = values not pointing at an entry's &lt;code&gt;*&lt;/code&gt;, all off by one byte.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Entries&lt;/th&gt;
&lt;th&gt;Unindexed&lt;/th&gt;
&lt;th&gt;Bad buckets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PRODUCE.TXT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*Caravan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;206 (every empty bucket)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ERROR.TXT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;*NOMINE&lt;/code&gt;, &lt;code&gt;*NOIRR&lt;/code&gt;, &lt;code&gt;*ADDCITY&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;4 (&lt;code&gt;NOIRR&lt;/code&gt; twice)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KING.TXT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;*UMAX&lt;/code&gt;, &lt;code&gt;*PMAX&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BLURB1.TXT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*CURE FOR CANCER2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BLURB2.TXT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*SETTLERS2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BLURB4.TXT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*TRADE ROUTES2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BLURB0.TXT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*HORSEBACK RIDING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HELP.TXT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;KING&lt;/code&gt; and &lt;code&gt;BLURB&lt;/code&gt; traces were probably already in the distributed archive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix #1: rebuild the indexes
&lt;/h2&gt;

&lt;p&gt;Replace the 528-byte index of the seven damaged files with one rebuilt from their actual entries. The text is untouched; only bytes 0–527 change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Rebuild the hash index at the top of a CIV .TXT file (256 buckets, CRLF every 32)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finditer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\n(\*[^\r\n]+)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;tab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x3030&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*END&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mh"&gt;0x3030&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;
    &lt;span class="n"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt;
&lt;span class="n"&gt;hdr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;32H&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;528&lt;/span&gt;
&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;528&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hdr&lt;/span&gt;
&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;keys indexed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fidelity check.&lt;/strong&gt; The rebuilt indexes match every &lt;em&gt;correct&lt;/em&gt; bucket of the originals exactly (50 in PRODUCE, 9 in ERROR, 58 in KING, and 67/89/55/41 in BLURB0/1/2/4). That confirms the hash and probing order are the game's own. The only differences are the bad buckets (fixed or cleared) and the missing entries (added).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it works.&lt;/strong&gt; Every key in the file is now found through the hash and never reaches the write-back. Keys &lt;em&gt;absent&lt;/em&gt; from the file, like Palace and the Wonders, reach an empty bucket, scan to EOF, return "not found", and write nothing. The probe loop always terminates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulation.&lt;/strong&gt; 23 of 73 keys loop forever with the original &lt;code&gt;PRODUCE.TXT&lt;/code&gt;; zero with the rebuilt one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emulator.&lt;/strong&gt; All saves, Instant Advice on: no freeze, and the Domestic Advisor shows its Caravan advice. Across six CHANGE presses the files stayed byte-identical: the game no longer writes to them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fix #2: a 1-byte patch to &lt;code&gt;CIV.EXE&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Rebuilt indexes stop the freeze, but the self-healing write-back is still a loaded gun. So &lt;code&gt;CIVFIX.EXE&lt;/code&gt; is &lt;code&gt;CIV.EXE&lt;/code&gt; with one byte changed so the lookup never writes back.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Original&lt;/th&gt;
&lt;th&gt;Patched&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;File offset&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0x1F072&lt;/code&gt; = &lt;code&gt;74 6B&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0x1F072&lt;/code&gt; = &lt;code&gt;EB 6B&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unpacked address&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1ECB:03D3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1ECB:03D3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instruction&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jz 0x0440&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jmp 0x0440&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effect&lt;/td&gt;
&lt;td&gt;Write back only when the scan found the key&lt;/td&gt;
&lt;td&gt;Always skip the write-back block (&lt;code&gt;03D5&lt;/code&gt;–&lt;code&gt;043F&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MD5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8a574c67c722c34beb6f5c965742769f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;27eecc3fb17bb7dac950909445752962&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The guard at &lt;code&gt;1ECB:03C7&lt;/code&gt; skips the write-back when the hash found the key (&lt;code&gt;jnz 0x0440&lt;/code&gt;) or when the scan didn't (&lt;code&gt;jz 0x0440&lt;/code&gt;). Turning the second jump into an unconditional &lt;code&gt;jmp&lt;/code&gt; skips it every time. The return value at &lt;code&gt;0x0440&lt;/code&gt; is unaffected, so callers still get found/not-found.&lt;/p&gt;

&lt;h3&gt;
  
  
  Patching inside a compressed executable
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CIV.EXE&lt;/code&gt; is EXEPACK-compressed, which normally means you can't just poke a byte. But EXEPACK is a run-length scheme that stores non-repeating bytes as literal copy blocks. Mapping each unpacked byte back to its source showed that unpacked &lt;code&gt;0x1F083&lt;/code&gt; is stored &lt;em&gt;literally&lt;/em&gt; at file offset &lt;code&gt;0x1F072&lt;/code&gt;. So it can be changed in place: the compressed stream, relocation table, header and overlays all stay the same, and EXEPACK has no checksum. Unpacking the patched file yields an image that differs from the original at exactly that one byte.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not just fix the offset?
&lt;/h3&gt;

&lt;p&gt;The written offset comes from &lt;code&gt;ftell()&lt;/code&gt; on a text-mode stream. It was 1 byte late in most cases and 1 byte early in one. Writing &lt;code&gt;ftell() - 1&lt;/code&gt; would fix most entries and break the rest. Skipping the write-back is simpler and always safe: unindexed entries are found by the sequential scan on each lookup, about 2 KB of reading, which is not noticeable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Starting from an index with &lt;code&gt;*Caravan&lt;/code&gt; removed, CHANGE pressed three times on an affected city: advice shown, no freeze, file unchanged.&lt;/li&gt;
&lt;li&gt;Run together with the rebuilt files: game starts, loads a save, shows advice, writes nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to apply it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Back up your game folder (at least &lt;code&gt;CIV.EXE&lt;/code&gt; and the seven &lt;code&gt;.TXT&lt;/code&gt; files).&lt;/li&gt;
&lt;li&gt;Rebuild the index of &lt;code&gt;PRODUCE.TXT&lt;/code&gt;, &lt;code&gt;ERROR.TXT&lt;/code&gt;, &lt;code&gt;KING.TXT&lt;/code&gt;, &lt;code&gt;BLURB0.TXT&lt;/code&gt;, &lt;code&gt;BLURB1.TXT&lt;/code&gt;, &lt;code&gt;BLURB2.TXT&lt;/code&gt; and &lt;code&gt;BLURB4.TXT&lt;/code&gt; with the script above.&lt;/li&gt;
&lt;li&gt;Optionally, apply the 1-byte patch: check that offset &lt;code&gt;0x1F072&lt;/code&gt; holds &lt;code&gt;74 6B&lt;/code&gt;, change &lt;code&gt;74&lt;/code&gt; to &lt;code&gt;EB&lt;/code&gt;. Keep the original as &lt;code&gt;CIV_ORIG.EXE&lt;/code&gt; and keep the patched file named &lt;code&gt;CIV.EXE&lt;/code&gt; (the runtime refers to its own image as &lt;code&gt;civ.exe&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The patch prevents new damage but doesn't repair an index that is already full, so use it &lt;strong&gt;with&lt;/strong&gt; the rebuilt files, not instead of them. After that you can turn Instant Advice back on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I didn't trace exactly why the C runtime's text-mode &lt;code&gt;ftell()&lt;/code&gt; is off by ±1. The fix doesn't depend on it.&lt;/li&gt;
&lt;li&gt;The probe loop is still unbounded in the patched executable. It's unreachable as long as indexes keep their empty buckets, but an index damaged by hand or another tool could still trigger it.&lt;/li&gt;
&lt;li&gt;Tested in DOSBox 0.74-3 only. The behaviour lives in the game's own code and data, so real hardware should behave the same.&lt;/li&gt;
&lt;li&gt;Version 474.03 only. Other releases may place the routine elsewhere: check for &lt;code&gt;74 6B&lt;/code&gt; at the offset before patching.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key addresses
&lt;/h2&gt;

&lt;p&gt;Unpacked image; data segment base at linear &lt;code&gt;0x2A800&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Address&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1ECB:01B3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Indexed text lookup: hash, probe, scan, write-back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1ECB:026C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Empty-bucket test (&lt;code&gt;&amp;lt;= 0x1FF&lt;/code&gt; or &lt;code&gt;0x3030&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1ECB:0398&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Probe step: &lt;code&gt;h = (h + 1) &amp;amp; 0xFF&lt;/code&gt;, loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1ECB:03C7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Write-back guard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1ECB:03D3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Patched jump (&lt;code&gt;jz&lt;/code&gt; → &lt;code&gt;jmp&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1ECB:03DC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Write-back: open &lt;code&gt;"r+b"&lt;/code&gt;, seek to bucket, write 2 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;linear &lt;code&gt;0x0B8EE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Military advice lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;linear &lt;code&gt;0x0B98A&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Domestic advice lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;A cache that writes back a slightly wrong key, a hash table with no probe limit, and an index that ships one entry short. Each one is harmless on its own; together they make a game slowly eat its own help files until it hangs. A nice reminder that "self-healing" code needs a test for what happens when the healing is wrong.&lt;/p&gt;

</description>
      <category>reverseengineering</category>
      <category>retrogaming</category>
      <category>dos</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
