adventures in reverse engineering #1: the oregon trail ii jesus bug

the oregon trail ii jesus bug

reach out and touch faith

originally posted: 2023

Whenever people talk about What I Did In 2020, I am slightly embarrassed, because I haven't come up with a socially acceptable way (besides blogging, I guess) to spin the fact that what I did for much of 2020 was reverse engineering the 1995 entry in the colonially dubious but massively selling Oregon Trail series, namely, Oregon Trail II.

The project is nowhere close to finished -- you can track my work here -- but I've gotten far enough to have sloughed off a few anecdotes. Let's talk about the bug that canonically makes your character Jesus Christ!

This is a 1995 game, so in lieu of source code, how this has worked has been decompiling the code to semi-obfuscated C++ (in this case, using a tool called Snowman; others use Ghidra for similar work), cross-referencing the anonymized variables (variable names are not preserved during compilation) against known numbers and strings (e.g., "1849", "wagon"), and piecing things together from there. The low-level code and 1995-era internals are very much handwaved, and I have yet to figure out how images and sound are compressed and stored. (The important part, to many.) I have, however, worked out much of the gameplay logic, which is both more sophisticated than one might think and also more janky.

(Incidentally, the authentic source code to one of the earlier games is in the collection of the Strong Museum of Play in Rochester, and I went and checked it out during a break at Narrascope 2024. It's the BASIC-era code, though: a few snippets of a much simpler simulation.

Oregon Trail II, like its predecessors, is less luck-based than its reputation suggests. It's basically a choice-based resource management game: a choose-your-own-adventure where you manipulate the underlying stats. At the beginning of the game, you customize your character and run. As in the better-known prequel, you can choose, among other things, your occupation: doctor, blacksmith, teacher, journalist, etc. Some occupations come with skills -- medical, carpentry, farming/animals, and the like -- and you can also choose more.

Some of these skills -- like, say, being a doctor -- are unsurprisingly super useful. Others are less so. Some skills are useless because the code for them just doesn't work. Musical skill, for instance, is supposed to boost your morale; it doesn't. Some skills, meanwhile, become useless if you actually possess them IRL; for instance, there's a skill that will translate all Spanish dialogue to English, which of course is pointless if you yourself are fluent.

Then, in a category of its own, is riverwork, which makes accidents while crossing a river less common. The probability of accidents depends on many factors: the river's difficulty (a var in river location data), the type of crossing, the river's width and depth, the precipitation level, and several more. Ice crossings have their own set of calculations, including how long the freeze has lasted. For the most part, all of these are pretty commonsensical. If you take the ferry, you can trust the ferryman to not screw you over. (Usually.) If you try to ford a really deep river, you'll get swamped. (Always.) (Right?)

After this come even more checks, adjusting this base chance even further. Some are flat boosts -- for instance, if you took the time to check the river conditions, you get an automatic 10% to your survival chance no matter what. Some are flat penalties -- taking a compensatory bulky Conestoga wagon knocks 10% off, no matter what. A few adjustments, including the boost for riverwork skill, are scaled using what modern games would call "fairmath": boosting your odds more if the chance of success is low and less if it's already high, capping the odds at 100%.

Probability tables are relatively easy to spot even in the decompiled code, because they're a lot of sets of suspiciously probability-like numbers like 97/92/75/50/0; the tables for river and mountain crossings were among the first things I found. I'd worked the math out, stepped through the code in the debugger, and tested it in-game; all worked as expected. Then someone told me I must be missing something because they were regularly plunging into the 14-foot depths of the Missouri River, by foot, and coming out just fine. But I'd looked elsewhere in the code and stack traces in the debugger for anything else that touched river crossing data, and eventually was pretty sure I'd found it all.

The culprit turned out to be riverwork. Here's the relevant decompiled code for adjusting the chance of success for riverwork (comments and new variable names mine):

ecx433 = reinterpret_cast(0);
*reinterpret_cast(&ecx433) = *reinterpret_cast
  (*reinterpret_cast(v33 + 0xc20) + 0x8b); // Riverwork skill flag
if (ecx433) {
  __asm__("cdq ");
  eax434 = 100 - successChance - edx255 >> 1; 
  ecx433 = reinterpret_cast(static_cast(successChance));
  eax435 = *reinterpret_cast(&eax434) + 
    reinterpret_cast(ecx433); // 0.5x + 50
  successChance = *reinterpret_cast(&eax435); 
}

Even after renaming/annotating the important variables, this is probably a bit hard to follow, so let's go step by step. (Note: As above, the internals and data types are handwaved.)

Line 2: The reinterpret_casts and eax___s throughout this are the decompiler's interpretation of assembly language. The important part for us humans is the block checking a signed char*, which suggests that we are checking a Boolean flag. The + 0xc20 offset here is where the game simulation data is stored; from there, the flags for skills start at offset 0x8a. Riverwork is the second skill in the list of skills, which puts it at offset 0x8b.

Lines 3 on: If we do have riverwork skill, we do some assembly math to recalculate the chance of successfully crossing the river. I'll save you the trouble of converting bitshifts to algebra: the adjustment is 0.5x + 50, where x is the current odds. Take a second to run some numbers through this until you see the problem.

.

What if we have a 0% chance of successfully crossing that river -- i.e., the chance you have if you're not Jesus, or maybe two or three Paul Bunyans stacked on top of one another? According to the formula above, that chance goes up from 0% to 50%. A coin flip. You could even survive a decision so awful that it gives you less than 0% odds (which can happen).

Obviously, this information isn't super useful (insofar as minmaxing or dissecting a 1995 educational game can ever be "useful"). There's no benefit to making questionable decisions; there are no tradeoffs here. If you're crossing a river in a way that gives you 0% chance of survival, then you're doing it on purpose. But the idea of it amuses me. The game manual repeatedly stresses, to Freaky Friday levels, the importance of making good choices!!! But as many a bored student has discovered, sometimes yolo is the sololo.


return to blog index, or return home