I love the 1990-ass design of Railroad Tycoon's save system.
not "which directory do you want to save your game?", but "which drive has your save game disk?"

You know, cause you'd put the game in drive A:, and if you were fancy, you'd save to a blank disk in drive B:

Too poor to have two drives? Gotta swap them to save, then swap back to keep playing.

okay, now that's interesting. So, backstory info:
Railroad Tycoon (1990) stores most images as .PIC files. The format is unknown but seems to start with either 06, 07, 0F, then a 00, then the width and height as uint16s. You can do the VN-trick to decode them: rename your target file to one of the ones the game shows at startup, and it'll display it for you. (credits2.pic, for example)

now when you save your game, it saves two files: RR1.SVE and RR1.MAP.
(The 1 can be 0-5 depending on which slot you save into/if it's an autosave).

Now the obvious guess is that the game saves things like "how much money you have" and "where your trains are" in the .SVE, and the current state of the map in the .MAP file, since the game world dynamically changes over time.

Here's the weird part: The .MAP file? It's actually a PIC!
I started a game in Western-US and saved my game after making a quick railroad, and then renamed my RR1.MAP to CREDITS2.PIC and started the game. This is what I got:

now that's super interesting because PIC files are compressed!

And the game can SAVE them, not just load them! that's unusual for games of this age!

interestingly the images in the MAP files are set to 320x200 (like all the other images), even though they're clearly actually 256x192. I guess it was easier to write a compressor/decompressor that just assumes 320x200.
anyway this also lets me do something I've always kinda wondered about but can finally tell for sure:
Yep, the game is dynamically generating the maps when you start, not just animating them fancy. Here's two starts of a Western-US map:
the rivers are identical, though.
And here's why: The WESTUS.PIC file is used as the base, and as you can see, it's got some basic stuff laid down already:
So the game takes that, then overlays mountains, industries, and cities, using some kind of procedural generation.

that's why it has to save the whole map when you make a save game: because your map is unique.

(and presumably re-generating it from the seed would take too long on some of the computers this game ran on)

arg. I can't easily hack the game's binary because it's compressed using LINK /EXEPACK AND it uses overlays, which apparently means the offsets to the overlays are encoded into the exe... based on the compressed version. So if I decompress it, the game won't load at all.
UNP says the game has 87365 bytes of image and 109305 bytes of overlay.
that's more overlay than non-overlay! HOW MUCH OVERLAY DO YOU NEED, MAN?
okay "unp e -g" seems to have fixed it. the regular mode messed up the overlay, but merging it in fixes it
awesome. I have officially broken it. But I have broken it CAREFULLY.

okay so I hacked it to go into an infinite loop after showing the first image of the intro sequence, then wrote a script to copy every image into the first image of the intro, start dosbox-x, then wait a couple seconds then screenshot it. So I now have all the images.

Here's the faces:

man I thought this game just had a bunch of random white-guy (well, redish-guys) portraits that it matched with some names of historical-guys-who-built-railroads.

NOPE! every name is matched to a face.

Here's all the names from the executable
I don't think these guys actually show up in the game. Interesting.
so according to the manual, the railroad operators come in three flavors: builders, robber barons, and mixed.
But looking at the profiles in the EXE, there's 4 numbers associated with each name, in the range 0-4. There's no correlation that I can see.
Interesting. There appears to be some code for verifying disk sectors: Given that there's no other code that works at the sector-level, I suspect that's disk-based copy protection. Like, they intentionally broke some sectors on the original disks, then made the game fail if those sectors weren't CRC-fails?
which is weird because the game has manual-based copy protection and supports being copied to the hard drive
I'm gonna have to dig up my original disks and image them
ahh, nope. not copy protection: I checked and the game calls this function to see if a disk is valid for saving onto, and it does it by looking if it can see sector #2 on the given disk drive.
help my train has negative horsepower
by manipulating the horsepower, you can get the grade/cars calculation to take some weird turns. This train cannot move if it's on flat ground with only 1 car, but as soon as there's a hill or more cars, it's a speed demon

interestingly, these values don't seem to actually correspond to train performance.

I set my train up to have a very high max speed with lots of (positive) horsepower, and it managed to drive 26 miles at 42 miles an hour.

that's... better than a plain 0-4-0 Grasshopper, but not by the amount you'd expect

there's 42 bytes of data for each train and I understand 26 of them. that's not good

I figured out two more bytes: There's a "year this train is invented" short int, and it seems that when you start a game, it adjusts the years randomly.

So although the EXE says the 4-2-0 Norris says 1836, when I started a game, it changed to 1833.

oh wow. they set an interrupt to trigger every time the mouse moves?
that's... that's a lot of times.
apparently Ghidra doesn't understand DOS overlays, which means it keeps getting confused by the fact that they involve data in the middle of a function
every "INT 3F" is actually followed by a byte and a word, but ghidra tries to decode that data as code. it doesn't end well
let's desync the variable-length instruction encoding! x86 is the best machine code!

"Interrupt 43: Note: This is not a callable vector!"

SO WHY IS INT 0x43 IN THIS CODE

I'm definitely a fan of the fact that ASCII text can be confused for x86 code. that's great.

Annoyingly it turns out I have two different versions of Railroad Tycoon here: 455.00 and 455.02

And all my copies of 445.00 are cracked

why does this code have "SUB AX,AX"?
that just changes AX to 0. But... the normal (and faster, I think) way to do that is XOR AX,AX.

Other than some flags (which this code doesn't use), they're the same. Strange.

can't believe GHIDRA doesn't have MSC 5.1 identification in its libraries.
but it looks like MSC 5.1 is the compiler used for this game

god I hate hacking 16bit games.

GET A LINEAR ADDRESS SPACE YOU FUCKERS

oh god they interleaved fwrite and fread because they both end up calling the same DOS interrupt.
They basically wrote fwrite and made the fread function do a JMP into it.

the tell tale signs of this being hand-written assembly and not a compiler, because no compiler would generate something this weird

@foone I made a bunch of FunctionID databases for old DOS compilers, including MSC 5.1 https://github.com/moralrecordings/ghidra-fidb-dos-win16
GitHub - moralrecordings/ghidra-fidb-dos-win16: Scripts for scraping vintage x86 C/C++ libraries in Ghidra, in order to generate FunctionId databases.

Scripts for scraping vintage x86 C/C++ libraries in Ghidra, in order to generate FunctionId databases. - moralrecordings/ghidra-fidb-dos-win16

GitHub
@foone since both XOR and SUB are really basic instructions, shouldn't they both be like 1 cycle (or whatever the minimum number of cycles is)? Anyway why use either of those over MOV AX, 0?

(genuinely asking, I have no idea why)

@sekoiatree MOV AX, 0 takes more bytes to encode than XOR AX, AX, so it's less compact and thus slower.

it looks like SUB AX, AX and XOR AX, AX are equivalently fast. I don't know why XOR AX,AX is the "standard" encoding for "clear AX", but it is.

@foone
@sekoiatree I wouldn't be surprised if that convention came from older processors that had similar instructions, where logic instructions were faster than arithmetic ones

@foone ah, right, x86 is variable length.

Looking it up a little:
https://randomascii.wordpress.com/2012/12/29/the-surprising-subtleties-of-zeroing-a-register/

It turns out that for x86 processors have for years handled xor of a register with itself specially.Kinda horrifying tbh.

Charlotte's probably right, since SUB has a carry and XOR is bitwise

The Surprising Subtleties of Zeroing a Register

Zeroing out a CPU register seems like the simplest and most basic operation imaginable, but in fact x86 CPUs contain a surprising amount of special logic to make this operation run smoothly. The mo…

Random ASCII - tech blog of Bruce Dawson
@foone according to https://old.reddit.com/r/programming/comments/15n5v6/the_surprising_subtleties_of_zeroing_a_register/ this random 10-year-old reddit post, XOR could potentially be faster than SUB, but in practice it never was. So... who knows.
The surprising subtleties of zeroing a register, and why 'sub' is sometimes faster than 'add'

Posted in r/programming by u/brucedawson • 333 points and 72 comments

reddit
@foone @sekoiatree Older Intel CPUs only recognised the XOR REG,REG idiom as dependency-breaking and treated SUB REG,REG as regular subtraction with all of the fun data dependencies that brought.
@[email protected] @foone sure, but that's a chicken-and-egg thing. If SUB was the standard, they'd have recognized that, no?
@foone loved Railroad Tycoon. My favorite among the tycoon games that came out.
@foone Well, but so can empty memory...
@wollman it's not empty, it's
ADD byte ptr [BX + SI],AL!
@foone DEBUG.COM left out the "byte ptr" bit.
@foone an ASCII research paper that is also an executable! https://youtu.be/LA_DrBwkiJA?si=lym_Q-3G9oOq53TM
Compiling C to printable x86, to make an executable research paper

YouTube
@foone EICAR Anti-Virus Test File for a variation of this theme.
(the whole dos .com executable is ascii that can be typed on a keyboard)

@foone

I remember copy protection systems with self modifying code relying on the icache needing explicit invalidate!

@foone stupid question: the R 1990 means serial mice, mostly, at 1.2 kbd 8N1? So, at most maybe 140 reports per second; I think the interrupt load is fine.

@foone ...isn't interrupt 33 one of the really old and basic ways to do mouse support under DOS?

Allegro has it as a fallback if that newfangled "mickeys" stuff doesn't work IIRC.

@LionsPhil Yep! This is a game from 1990, so that makes sense
@foone I thought all DOS mouse drivers were interrupt-based TSRs?
@stilescrisis they are! but one way you use them is telling them to call you
@foone not at the time. (The serial MS mouse protocol normally sent about 40 reports per second, and you only got the mouse move interrupts if you actually had an x- or y-delta.)

@foone this post has a documentation of the MS mouse protocol attached https://roborooter.com/post/serial-mice/

it's been like 25 years since I messed with this and I don't have my original references anymore but it looks right

Serial Mice Protocols | Roborooter.com

I remembered there was such a thing as a serial mouse and I wondered how they worked. A bit of googling found me this link (which as of this posting has an SSL ...

Roborooter.com
@foone it was 1200 bits/second serial link, 7 data + 1 stop bit (150 payload bytes per second containing 7 usable bits), and 3 bytes per status update packet, so the absolute max you could get was 50 updates per second
@foone Maybe it's stored with a standard deviation?
@foone I like that it's a bug. Namely, grasshopper 🦗
@foone That usually means the reverser lever is badly connected. /j