VOGONS


First post, by Wilczek_h

User metadata
Rank Newbie
Rank
Newbie

Hi,

This first article is going to be an unorganized dump of my mind. Maybe later notes will be more organized. Anyway, let's get started. 😀

I spent quite some time in the last 3 months reverse engineering Sid Meier's Railroad Tycoon. Why would I do that you ask? I stopped playing that awesome game when I hit the upper limit of the money amount the game can handle even on the hardest level. (Why couldn't they just pick a 4-byte integer to represent money?) My kids also like this game, but for them the challenge is not hitting the limit, but rather the competition, which is tough for them. They do not care about competition, they just want to build railroads and manage trains and that is it. I also find the 320x200 resolution quite suboptimal on large screens nowadays.
So, I thought I would give it a try and after 3 months of debugging in DOSBox (and DOSBox too), identifying code that is relevant for porting and actually porting it I have got this far (I could not figure how to embed this video here, so I am sharing the link):

Video showing the ported parts of the game (Make sure to bump up the quality settings in the player to actually see something)

What do I have already?
- I know how to extract images (PIC). This took me more or less a week. However, this does not seem to be a big achievement as others have already figured out the format itself.
- BUT, I could also figure out how to load and play PAN animation files, see the video above 😀. (This was around a month, pretty tough, and I cannot say that I can fully understand it, even though it is working…)
- It was pretty easy to figure out the font file format, just by looking at the binary.
- I also know how to load maps and parts of saved games. I can render the map with rails and stations, including the rails of the opponents at any resolution.
- I can also render the signal lights on stations.
- I can render the city names (pixel perfect).

I started with building DOSBox from source. Why? Because sometimes I also had to debug DOSBox itself to be able to progress. Example: I wanted to know the CS:IP values when a file gets open. Solution: breakpoint in DOSBox (dos_files.cpp/DOS_OpenFile), get the register variables and call it a day. (Btw., IDA Pro 5.0 was not helpful at all.)

The good things: Railroad Tycoon has been relatively easy to debug, understand and port. From the memory handling point of view the game is pretty static. So far I bumped into 2 malloc calls only and those only happened during PAN file processing. (Counting the malloc calls from the point when the micro prose logo is displayed.) Most of the things is loaded or written to fixed memory locations.
Another good thing is that only a part of the code needs to be disassembled as many functions or behaviors can be simply developed, for example: menus, file handling, etc.
However, there are some gray zones. To understand how tracks, stations and trains are rendered I had to debug the drawing code. Interestingly, the game transforms the tracks.pic into another format to save on iterations when drawing as it contains a lot of transparent pixels. That is absolutely not modern hardware friendly, so I had to change the drawing logic and decided not to port the tracks.pic preprocessor code and the infra asset drawing code as they would not be needed at all. Here it is how the original game stores the infra assets in memory:
IQRHevVWFqtaQ7hepqK59G0wAWFZSmxLmeeskobwixJDkTA?width=256

Another mind blowing drawing solution I discovered is how the game renders the "blue" assets. Like tracks in the ocean or river landing parts. Uhh. It renders the original assets first, then replaces one or more of the on screen colors with different ones in the given cell. So, while the ported GDI version did the same, in the final(-ish) OpenGL version I simply precalculated all the blue assets in memory, created a texture from it and only addressed the texture at runtime. I had to be careful though, an infra asset is 20x20 pixels large, while a cell is only 16x16, so I only had to convert the inner 16x16 pixels of each asset to "blue".
IQRdtDyaicDoR7E1AG-Xi2cNAZtL1d5qcBbIiowcKq8hRTo?width=256

The bad thing: RRT uses overlay extensively. I mean a lot. Therefore, many times I went through code that was handling overlays, that turned out to be a huge waste of time. After a while you get the pattern and can just skip huge chunks of ASM code. However, the real pain is that if you just step over code that does the overlay load and replaces code segments in memory, then you usually get a crash from the game saying: "Overlay not found". This makes debugging pretty difficult from time to time. Also, code gets overwritten, so if you previously had a function at XXXX:YYYY the next time you might have something else there, yet the code calling the same location. I had to be careful and check each and every function each time it got called after an overlay interrupt. (because call XXXX:YYYY before the overlay might have called a different function then call XXXX:YYYY after the overlay interrupt.)

The signal drawing code was harder to port to OpenGL. The original code draws the station and then calculates the lights at one or both ends and then picks an 8x8 pixels square in which the teal color is replaced with the calculated signal light color. Pretty easy when you can read from A000:xxxx and write to A000:xxxx directly. I did not want to do glReadPixels, FBO and co., so I decided to write a pixel shader that gets the source and destination colors as parameters and draws the station again at a given location, but discarding all pixels that do not match the source color.

Another weird thing is that the game stores a lookup table at 2815:1660 that are actually byte offsets of each row on the screen. For example: [2815:1660]=0, [2815:1662]=0x140… I did not need these offsets in the end, but it took some time to eliminate most of the references.

The map: the map is just a PIC file actually. It utilizes only 15 "colors", but there are more than 15 possible cell types in the game. Like industries and producers. Not everything is stored in the map, some tables are hardcoded in the game's executable. So far, I could not decide whether the logic that generates industries and producers is extremely clever or utterly insane. There is a very important uint16_t value at 17ED:8CF6 at runtime, or at 0x3736 in the SVE file. I call it the seed value. This seed value, the map color at the given location and the cell coordinates are all used to generate an index into the lookup table to get the industry or producer index. Take a look at the function in memory at 02BB:3033. Originally, I would have expected all this information to be stored in the map, as there would be enough room I guess, but no. How did I figure this out? The problem was that city sizes, industries and producers all changed each time a new map was generated in the original game. So, I saved a generated world for each scenario and loaded them back for investigation. This gave me a little bit of deterministic behavior 😀. The lookup tables are in the game code and there are 3 of them:
IQTe6Kfdaj1VSIY1HDbyv9_8AZWzg_nborNLbJt_XltpXvQ?width=256
Interestingly, the opponents' stations are encoded into the map, but the tracks are stored separately.

Other great sources that were essential to my research:
- https://stanislavs.org/helppc/int_21.html
- Intel's CPU manual. Yes, this is a very important read when CPU instructions need to be implemented as C/C++ functions. 😀

What do I consider an MVP (minimum viable product)?
- all features of the original are working in the port (except sound)
- resolution independent map rendering (already working). This screenshot was taken on my other monitor (max. res. 3440x1440) utilizing all the available space to render the map:
IQQ3lOE3JP5tSaz7aoehVwSMAeRtIiecQC1KaxpPcuMBHLA?width=430
- money bug fixed
- "unlimited" saved games (at least more than 4 😀)
- play alone feature
Once this is done, other limitations can be eliminated, like the limited number of stations and trains.

How far will I get? I do not know. I will have less time for this project until ~October comes again, but I will try to keep this project alive. The hard thing will be to find and port the non-deterministic, but essential code fragments, like the code that drives opponents, or the code that washes bridges away.

This became quite a long and random "article" scratching the surface, but I hope you enjoyed it. I will try to share more information in the future. Debugging, identifying code and porting take weeks/months, so do not expect daily reports here 😀, but once I have something, I will post it here.

Take care and best regards,
Wilczek

Reply 1 of 10, by Wilczek_h

User metadata
Rank Newbie
Rank
Newbie

Dear Vogons,

Here is the 2nd article. This time we will look at how trains are drawn on the "sidebar".
IQTHzBwAvYXWRZGcTNCkDEkRAbcvqG3OVC9B81slINUB2XU?width=256
The function code that draws the trains on the side bar can be found in memory at 02BB:7D10.
So, where is the train data? In the SVE file train data starts at offset 0x50 and the data is 0x1580 bytes long. If you take a look at the code in memory at 02BB:7DE6…

	02BB:7DE6  B8AC00              mov  ax,00AC
02BB:7DE9 F76EF2 imul word [bp-0E] // AX=0x00AC * 0=0
02BB:7DEC 8BD8 mov bx,ax // BX=AX=0
02BB:7DEE 83BF1297FF cmp word [bx-68EE],FFFF // READING TRAIN DATA! BX-68EE=0x9710+2, train data starts at 0x9710!, ds:[9712]==0003, if FFFF -> no train at this location

… then you can see that each train is described by 0xAC=172 bytes. If we divide 0x1580 by 32 (which is the maximum number of trains you can build) then we also get 0xAC or 172 in decimal. In memory the train data is loaded at 17ED:9710. From the code it is not immediately obvious, but if you calculate the used address of 0-0x68EE that is referenced by the code at 02BB:7DEE then it will give you 0x9712 (which means 2 bytes are read from offset 2 of the first train's data).
Now we know that each train is described by 172 bytes, but what are those bytes?
At offset 2 we have a 2 byte index that is -1 (or 0xFFFF) if the train at the given location is not built. If the train is built, then it gives you the engine index. (This index can be used to get the engine info of the train, such as the name of the engine, the type of the engine, and so on. More on this later.)
What about the gray and green horizontal lines. The gray bar represents the track. It can be red as well (if the engine is held at a signal tower or station). Drawing of the "horizontal line" can be found at memory location 02BB:6A9D.
Each train data starts with a 2-byte long flag (offset 0). To check if the train is held check bit 4 (assuming that the first bit index is 0). If the bit is set then the track color will be light red instead of dark gray:

// 02BB:6AD3  F684109710          test byte [si-68F0],10      ds:[9710]=41A5 // FIRST 2 BYTES OF THE TRAIN DATA!!!
// 02BB:6AD8 7405 je 00006ADF ($+5) (down)
// 02BB:6ADA B80C00 mov ax,000C
// 02BB:6ADD EB03 jmp short 00006AE2 ($+3) (down)
// 02BB:6ADF B80800 mov ax,0008
uint16_t trackColor = ((train.GetFlags() & 0x10) == 0) ? 0x8 : 0xC;

Next, what is the green color? The answer is in the official manual:
IQSoYtuOe9LgR5r0ArsYUZ6RASBDQCOP-0FWcLCPS53oofU?height=256
That is the train speed indicator. The train speed can be found at offset 4 and it is represented by 2 bytes. The length of the speed indicator is the speed multiplied by 2. Check the code at 02BB:6B0D-02BB:6B27. I had to change the original logic in the port though to allow resolution independent rendering. Otherwise the side bar would always be drawn horizontally from 256 to 319.

OK, now we have the tracks and the speed indicators drawn. What about the engine and the cars?
In the original code the train and the cars are drawn by the function located at 02BB:83C5, but in a very weird way. The function has 4 parameters, the first one is a color index, which is 0xFFFF if the engine is drawn. The 2nd and 3rd parameters are the x and y coordinates while the last one is either a train index (if the color parameter is 0xFFFF), or 0/1/2 if the color parameter is not 0xFFFF.
Let's start with the case when the engine is drawn on the side bar (color param = 0xFFFF): The engine can either be black or green. It is green, if the train is shipping a bonus shipment. If bit 8 is set (assuming that the first bit is called bit 0) then that train is shipping a bonus shipment.

// 02BB:83EE  C746FE0000          mov  word [bp-02],0000      ss:[FEF4]=0000
// 02BB:83F3 B8AC00 mov ax,00AC // AC=172 dec, EACH train is described by 172 bytes!
// 02BB:83F6 F76E0A imul word [bp+0A] ss:[FF00]=0000 // calculate the starting byte of the train descriptor, 172*train index, here index=0 (first train!)
// 02BB:83F9 8BD8 mov bx,ax // BX=AX=0000 (previously BX=4F53)
const auto& train = this->configuration.trains[trainIndexOrSpecial];
// CHECK IF IT IS SHIPPING BONUS SHIPMENT!
//02BB:83FB F78710970001 test word [bx-68F0],0100
if (train.GetFlags() & 0x100) {
draw green engine
}

Now one weird thing here. The original code draws the engine 5 times! Why? I do not know, but in the port I only draw it once and the result seems to be just fine.
How is this drawing function invoked? The caller (02BB:7D10) first invokes it with color parameter = 0xFFFF and the last parameter is set to the train index. Then the caller asks for the number of cars attached to the engine. This value can be retrieved from the train data at offset 0x4F and the value is a single byte. If there are cars attached to the engine then the code starts looping over the cars asking for each car's type (mail, passenger, coal, etc.). The info on pulled cars start at offset 0x50 and the info is 8 bytes long, each byte representing a car type. For example: 0=mail, 3=passenger, etc.
How is the car color calculated? Interestingly multiple car types can get the same color. The car type is divided by 3, which will be an index into an array of car colors. However, there is a twist here. Let's assume the processed car is a "passenger" car. Its type is 3. If we divide this number we get 1, which will be a color index. We have 2 arrays of car colors though. If the passenger car is full (or more precisely above a certain load amount), the car color will get a light cyan color. If the car is empty or almost empty, the car color will be dark cyan. Therefore, we also need to consider the car load. Car load amounts are stored at offset 0x58 and the data is 8 bytes long (1 byte per car). While the game displays load amounts of 0 up to 40, the game actually uses a wider range, from 0 to 0xA1. If the load is below or equal to 0x50 then the darker colors are used to draw the car, otherwise the lighter ones.
At runtime the original game stores the car colors in memory at 17ED:1450. The lighter colors precede the darker colors.
IQS0lnpyag4oRaSn6_Lqg4HLAZdyNXsjIKX8uSjb_XV3WnU
In the port I separated them so that the same index can be used to address colors:

const std::vector<uint8_t> fullCarColorIndices = { 0x0F, 0x0B, 0x0E, 0x0C, 0x00, 0x00};
const std::vector<uint8_t> halfFullCarColorIndices = { 0x07, 0x03, 0x0A, 0x04, 0x08, 0x00 };

Now we have the car color, but how do we know whether the car icon is a solid rectangle,
IQRKljk0hV9PQoSbktzmClqrASsAkdXQ-cDv2vP_Ubx7-g8?width=123&height=86
or open on the top,
IQS3-uoyTE67SqLQVcaiSD6gAQSoYlcMO4rLYjLxpm0ldk8?width=119&height=86
or hollow and closed on the top
IQRiWY8KIGEJSo_JwCQ56UNtAax_JzOtyxb3TSjMuXi-mpk?width=117&height=86
?
The answer is simple, simple do a "carType % 3" and pass it as the last parameter to the function (0 = solid car, 1 = hollow, 2 = open on the top).

Now we have the tracks, speed indicators, engines and cars drawn on the side bar. What about the destination city abbreviations?
The destination station index is stored at offset 0x91 and it is a single byte value.
Here is visually what has been discussed so far:
IQR4KhUYdjjkS6JNbyRgPP5NAegpB5wBwGvmg8TZTF0Jrw4?width=660
The function code that creates the abbreviation can be found in memory at runtime at 02BB:96E1. The station index is passed to this function. Station data is stored in the SVE file at 0x1ED0 and the data is 0x900 bytes long. Each station is described by 0x18 = 24 bytes. At runtime the station data is loaded at 17ED:AFF0.
The function reads the city index of the station (the city index tells us in which city the station was built). The city index is stored at (0xAFF0 + (station index * 0x18)) + 6, in other words at offset 6. The city index is stored on 2 bytes. Why is it like that? There can only be 100 cities on a map and indices 0..99 would fit on a byte. The reason is that stations (including signal towers and depots) can be built outside a city, so flags are stored in the high-byte. The actual city index is stored on the low-byte. If the high-byte is non-zero then the name of the station is the city name extended with one of the following values: " Junction", " Crossing", " Central", " Annex", " Transfer", " Valley", " Hills", " Woods". To figure out which extension needs to be added to the city name the game uses the following formula:

// 02BB:971E  8B46FA              mov  ax,[bp-06]             ss:[FEF6]=010F
// 02BB:9721 B108 mov cl,08
// 02BB:9723 D3F8 sar ax,cl
// 02BB:9725 0346FE add ax,[bp-02] ss:[FEFA]=000F
// 02BB:9728 250700 and ax,0007
// 02BB:972B 8946FC mov [bp-04],ax ss:[FEF8]=7E6F
const auto additionalNameIndex = (cityIndex / 256 + (cityIndex & 0x00FF)) & 0x0007;

And then the code replaces the 3rd character with the initial letter of the selected additional name.

name[2] = additionalNames[additionalNameIndex][1];

However, there is yet another twist to this logic:

// 02BB:973C  837EFC01            cmp  word [bp-04],0001      ss:[FEF8]=0000
// 02BB:9740 7505 jne 00009747 ($+5) (no jmp)
// 02BB:9742 C6068E8958 mov byte [898E],58 ds:[898E]=004A
// reversing the condition to avoid a goto...
if (additionalNameIndex == 1) name[2] = 0x58;

I hope you enjoyed this writeup. Be prepared for the 3rd article, because I also figured most of the logic that draws trains on the map itself 😀.
Teaser: (alignment of engines and the cars are not yet perfect as some code is not yet ported, but you get the idea…)
IQQAfCvZnaP3TY4DWX5ZP3kAAfTUV52NlGi0bWScjSAqZSg?width=660

Best regards,
Wilczek (Zoltan Farkas)

Reply 2 of 10, by Wilczek_h

User metadata
Rank Newbie
Rank
Newbie

Dear Vogons,

Here is the 3rd article, but this time I will keep it quite short. Today I will cover rendering trains on the detailed map. The entry point of the code that starts drawing the trains can be found at memory location 02BB:14D3, but the real logic starts at 02BB:1521 that lasts till 02BB:167A. Locating the code in memory was a bit harder than usual as the game draws the trains to a back buffer first and once all the visible trains are drawn then the content from the back buffer is merged with the pixels of the front buffer (A000:xxxx). In the end I figured out that the ES register just needs to be changed to A000 when hitting code at 2815:0C49 in the DOSBox debugger and then each drawing step can be seen (engine and cars).
Also, since I do not slice up the image containing the assets into smaller pieces and then further convert these to a special format (size, number of transparent pixels, color data, repeat, see the details in my previous articles) I also had to figure out how asset indices need to be remapped. Long story short, not all assets are converted by the original game. Anyway, the indices relevant for drawing trains on the detailed map are the following:
IQRFTaUXzcYBTKpy27Ix5gxGAdzAfUL8ZRZPat1-ePvPJeE?width=660
Assets with "??" are not used by the game.

The main drawing logic at a high level is very simple. It loops over the train array, skipping the ones whose engine index is -1 (0xFFFF). If a valid train is found then the code asks for the number of cars the train is pulling and the code adds 1 to this value. (+1 so that the engine is also covered). Then, in a loop the absolute position of the engine and the cars of the given train are determined by calling a function at 02BB:1B42. The returned coordinates are translated to screen coordinates and the code checks if those coordinates are within the view. If yes, the engine or the car is drawn.
Pretty simple, right? Well, it is way more complicated then this when going into the details.
Let's start with the function (02BB:1B42) that returns the position of an engine or one of its cars and the relative asset index. In the original code the function has 2 parameters, the first one is the train index, the second one is the car index multiplied by 12, which is the relative distance from the front of the train. By the way, each train and car assets are 12x12 pixels in size, hence the multiplication. In the port I replaced the train index with "const GameConfiguration::TrainInfo& train", but this fact does not change the logic of the function.
The function reads the train's X and Y positions that are located at offset 0x14 and 0x16 of the train data (base ptr=17ED:9710). Unfortunately, there are 3 values per train that I could not yet figure out exactly what they mean, but they affect alignment of engines and cars. These values are at offset 0x08, 0x18 and 0x10.
For example, the value at 0x08 seems to be flags, because if bit 0 is set, then the engine or car is on a diagonal track. However, it is also used as an index into the cell X and cell Y offset tables (used to determine neighboring cells). If the value at offset 0x18 is 0x10 (16 in decimal), then the X and Y positions lower 3 bits are cleared and 8 is added to both. The value at offset 0x10 is used to calculate the relative asset index of the car or engine.
Once the aligned X an Y positions of the engine or car are calculated it needs to be checked if the engine or car is on a station or double track, because then further alignments are needed.
Anyway, here is the entire code including the assembly code too, if someone can help me make it cleaner I would greatly appreciate it:

GameContent::TrainPos GameContent::function_02BB_1B42_GetTrainPosAndRelativeAssetIndex(const GameConfiguration::TrainInfo& train, int16_t carIndexMulBy12) const {
TrainPos pos;
// 02BB:1B49 B8AC00 mov ax,00AC // each train is described by 172 bytes
// 02BB:1B4C F76E04 imul word [bp+04] ss:[FF40]=0000
// 02BB:1B4F 8BF0 mov si,ax
// 02BB:1B51 8B842497 mov ax,[si-68DC] ds:[9724]=0AD1 // READING TRAIN DATA offset=0x14
// 02BB:1B55 8946FA mov [bp-06],ax ss:[FF36]=0AD1
auto xPosition /*bp-06*/ = static_cast<int16_t>(train.GetXPosition());
// 02BB:1B58 8B842697 mov ax,[si-68DA] ds:[9726]=09A8 // READING TRAIN DATA offset=0x16
// 02BB:1B5C 8946F8 mov [bp-08],ax ss:[FF34]=09A8
auto yPosition /*bp-08*/ = train.GetYPosition();
// 02BB:1B5F 8B841897 mov ax,[si-68E8] ds:[9718]=0006 // READING TRAIN DATA offset=0x08
// 02BB:1B63 8946F4 mov [bp-0C],ax ss:[FF30]=0006
auto local_BP_minus_0C = train.ReadUnknownValue<int16_t>(0x08);
// 02BB:1B66 8B842897 mov ax,[si-68D8] ds:[9728]=0007 // READING TRAIN DATA offset=0x18
// 02BB:1B6A 8946FC mov [bp-04],ax ss:[FF38]=0007
auto local_BP_minus_04 = train.ReadUnknownValue<int16_t>(0x18);
// 02BB:1B6D 8B842097 mov ax,[si-68E0] ds:[9720]=0007 // READING TRAIN DATA offset=0x10
// 02BB:1B71 8946FE mov [bp-02],ax ss:[FF3A]=0007
auto local_BP_minus_02 = train.ReadUnknownValue<int16_t>(0x10);
do {
// 02BB:1B74 837EFC10 cmp word [bp-04],0010 ss:[FF38]=0007
// 02BB:1B78 7516 jne 00001B90 ($+16) (down)
// reversing the condition to avoid a goto...
if (local_BP_minus_04 == 0x0010) {
// 02BB:1B7A 8B46FA mov ax,[bp-06]
// 02BB:1B7D 24F0 and al,F0
// 02BB:1B7F 050800 add ax,0008
// 02BB:1B82 8946FA mov [bp-06],ax
xPosition = (xPosition & 0xFFF0) + 8;
// 02BB:1B85 8B46F8 mov ax,[bp-08]
// 02BB:1B88 24F0 and al,F0
// 02BB:1B8A 050800 add ax,0008
// 02BB:1B8D 8946F8 mov [bp-08],ax
yPosition = (yPosition & 0xFFF0) + 8;
}
// 02BB:1B90 F646F401 test byte [bp-0C],01 ss:[FF30]=0006
// 02BB:1B94 743E je 00001BD4 ($+3e) (down)
// reversing the condition to avoid a goto...
if (((local_BP_minus_0C & 0x00FF) & 0x01) != 0) {
// align engine and car positions on diagonal tracks>>>>>>>
// 02BB:1B96 B80300 mov ax,0003
// 02BB:1B99 F76EFC imul word [bp-04]
auto tmp = 3 * local_BP_minus_04;
// 02BB:1B9C 99 cwd
auto sign = (local_BP_minus_04 & 0x8000) ? -1 : 0;
// 02BB:1B9D 2BC2 sub ax,dx
tmp -= sign;
// 02BB:1B9F D1F8 sar ax,1
tmp /= 2;
// 02BB:1BA1 8946FC mov [bp-04],ax
local_BP_minus_04 = tmp;
// 02BB:1BA4 8B4606 mov ax,[bp+06]
tmp = carIndexMulBy12;
// 02BB:1BA7 3946FC cmp [bp-04],ax
// 02BB:1BAA 7E03 jle 00001BAF ($+3)
// inverting the condition to avoid a goto...
if (local_BP_minus_04 > tmp) {
// 02BB:1BAC 8946FC mov [bp-04],ax
local_BP_minus_04 = tmp;
Show last 149 lines
			}
// 02BB:1BAF 8B76F4 mov si,[bp-0C]
// 02BB:1BB2 D1E6 shl si,1
// 02BB:1BB4 8B841A0B mov ax,[si+0B1A]
// NOTE: do not multiply bp-0C by 2 as it already uses int16_t*
// 02BB:1BB8 F76EFC imul word [bp-04]
// 02BB:1BBB D1E0 shl ax,1
tmp = (this->cellXOffsetTable[local_BP_minus_0C] * local_BP_minus_04) * 2;
// 02BB:1BBD 99 cwd
// 02BB:1BBE B90300 mov cx,0003
// 02BB:1BC1 F7F9 idiv cx
tmp /= 3;
// 02BB:1BC3 2946FA sub [bp-06],ax
xPosition -= tmp;
// 02BB:1BC6 8B847A0B mov ax,[si+0B7A]
// 02BB:1BCA F76EFC imul word [bp-04]
// 02BB:1BCD D1E0 shl ax,1
// 02BB:1BCF 99 cwd
// 02BB:1BD0 F7F9 idiv cx
tmp = ((this->cellYOffsetTable[local_BP_minus_0C] * local_BP_minus_04) * 2) / 3;
// 02BB:1BD2 EB21 jmp short 00001BF5
// 02BB:1BF5 2946F8 sub [bp-08],ax ss:[FF34]=09A8
yPosition -= tmp;
// align engine and car positions on diagonal tracks<<<<<<<<<<
} else {
// 02BB:1BD4 8B4606 mov ax,[bp+06] ss:[FF42]=0000
// 02BB:1BD7 3946FC cmp [bp-04],ax ss:[FF38]=0007
// 02BB:1BDA 7E03 jle 00001BDF ($+3) (no jmp)
// reversing the condition to avoid a goto...
if (local_BP_minus_04 > static_cast<int16_t>(carIndexMulBy12)) {
// 02BB:1BDC 8946FC mov [bp-04],ax ss:[FF38]=0000
local_BP_minus_04 = static_cast<int16_t>(carIndexMulBy12);
}
// 02BB:1BDF 8B76F4 mov si,[bp-0C] ss:[FF30]=0006
// 02BB:1BE2 D1E6 shl si,1 // SI=0006<<1 == 000C
// 02BB:1BE4 8B841A0B mov ax,[si+0B1A] ds:[0B26]=FFFF // 0B1A = xCellOffset table!
// 02BB:1BE8 F76EFC imul word [bp-04] ss:[FF38]=0000
// 02BB:1BEB 2946FA sub [bp-06],ax ss:[FF36]=0AD1
// NOTE: do NOT multiply bp-0C by 2 here, because the ptr points to 2 byte values already!
xPosition -= this->cellXOffsetTable[local_BP_minus_0C] * local_BP_minus_04;
// 02BB:1BEE 8B847A0B mov ax,[si+0B7A] ds:[0B86]=0000 // 0B7A = yCellOffset table!
// 02BB:1BF2 F76EFC imul word [bp-04] ss:[FF38]=0000
// 02BB:1BF5 2946F8 sub [bp-08],ax ss:[FF34]=09A8
yPosition -= this->cellYOffsetTable[local_BP_minus_0C] * local_BP_minus_04;
}
// 02BB:1BF8 8B46FC mov ax,[bp-04] ss:[FF38]=0000
// 02BB:1BFB 294606 sub [bp+06],ax ss:[FF42]=0000
carIndexMulBy12 -= local_BP_minus_04;
// 02BB:1BFE B8AC00 mov ax,00AC // each train is represented by 172 bytes!
// 02BB:1C01 F76E04 imul word [bp+04] ss:[FF40]=0000 // train index
// 02BB:1C04 8BF0 mov si,ax
// 02BB:1C06 8B5EFE mov bx,[bp-02] ss:[FF3A]=0007
// 02BB:1C09 8A804A97 mov al,[bx+si-68B6] ds:[9751]=0006 // READING TRAIN DATA offset=0x3A+BX, SINGLE BYTE!
// 02BB:1C0D 98 cbw
// 02BB:1C0E A3F8F6 mov [F6F8],ax ds:[F6F8]=0006
pos.RelativeAssetIndex = static_cast<uint16_t>(train.GetRelativeAssetIndex(local_BP_minus_02));
// 02BB:1C11 8BC3 mov ax,bx // AX=BX=0007
// 02BB:1C13 48 dec ax // AX=0006
// 02BB:1C14 250700 and ax,0007 // AX&=0007
// 02BB:1C17 8946FE mov [bp-02],ax ss:[FF3A]=0007
local_BP_minus_02 = (local_BP_minus_02 - 1) & 0x0007;
// 02BB:1C1A 8BD8 mov bx,ax
// 02BB:1C1C 8A804A97 mov al,[bx+si-68B6] ds:[9750]=06 // READING TRAIN DATA offset=0x40, SINGLE BYTE!
// 02BB:1C20 98 cbw
// 02BB:1C21 8946F4 mov [bp-0C],ax ss:[FF30]=0006
local_BP_minus_0C = static_cast<uint16_t>(train.GetRelativeAssetIndex(local_BP_minus_02));

// 02BB:1C24 C746FC1000 mov word [bp-04],0010 ss:[FF38]=0000
local_BP_minus_04 = 0x0010;
// 02BB:1C29 837E0600 cmp word [bp+06],0000 ss:[FF42]=0000
// 02BB:1C2D 7E03 jle 00001C32 ($+3) (down)
} while (carIndexMulBy12 > 0); // inverting the condition to avoid an extra goto..

auto SignExtend = [](const int16_t value) -> uint16_t {
// From the Intel's Instruction Manual:
/*
The CWD instruction copies the sign (bit 15) of the value in the AX register
into every bit position in the DX register.
*/
return (value & 0x8000) ? 0xFFFF : 0x0000;
};
// 02BB:1C53 E86322 call 00003EB9 ($+2263) // calls function_infra_02BB_3EB9(cellX=0xAD,cellY=9A)
int16_t xPositionSign = static_cast<int16_t>(SignExtend(xPosition));
int16_t yPositionSign = static_cast<int16_t>(SignExtend(yPosition));
// 02BB:1C3A B90400 mov cx,0004
const auto cellType = this->configuration.function_infra_02BB_3EB9(
// 02BB:1C44 8B46FA mov ax,[bp-06] ss:[FF36]=0AD1
// 02BB:1C47 99 cwd
// 02BB:1C48 33C2 xor ax,dx
// 02BB:1C4A 2BC2 sub ax,dx
// 02BB:1C4C D3F8 sar ax,cl
// 02BB:1C4E 33C2 xor ax,dx
// 02BB:1C50 2BC2 sub ax,dx // AX=00AD
// 02BB:1C52 50 push ax
(((xPosition ^ xPositionSign) - xPositionSign) / 16) ^ xPositionSign - xPositionSign,
// 02BB:1C32 8B46F8 mov ax,[bp-08] ss:[FF34]=09A8
// 02BB:1C35 99 cwd
// 02BB:1C36 33C2 xor ax,dx // AX=09A8, DX=0
// 02BB:1C38 2BC2 sub ax,dx // AX=09A8-0
// 02BB:1C3D D3F8 sar ax,cl // AX/=16; ==> AX=009A
// 02BB:1C3F 33C2 xor ax,dx // AX^=DX; AX=009A
// 02BB:1C41 2BC2 sub ax,dx // AX-=DX==009A
// 02BB:1C43 50 push ax
(((yPosition ^ yPositionSign) - yPositionSign) / 16) ^ yPositionSign - yPositionSign
);
// 02BB:1C59 A810 test al,10
// 02BB:1C5B 7438 je 00001C95 ($+38) (down)
// inverting the condition to avoid a goto...
if (((cellType & 0x00FF) & 0x10) != 0) {
// THIS CODE OFFSETS THE ENGINE OR CAR IF THE IT IS IN A STATION OR ON A DOUBLE TRACK>>
// 02BB:1C5D C746F60200 mov word [bp-0A],0002
int16_t tmp = 2;
// 02BB:1C62 833EF8F605 cmp word [F6F8],0005
// 02BB:1C67 7407 je 00001C70 ($+7)
// 02BB:1C69 833EF8F607 cmp word [F6F8],0007
// 02BB:1C6E 7505 jne 00001C75 ($+5)
// inverting the condition to avoid a goto
if ((pos.RelativeAssetIndex == 5) || (pos.RelativeAssetIndex == 7)) {
// 02BB:1C70 C746F60100 mov word [bp-0A],0001
tmp = 1;
}
// 02BB:1C75 8B36F8F6 mov si,[F6F8]
// 02BB:1C79 83C602 add si,0002
// 02BB:1C7C 83E607 and si,0007
// 02BB:1C7F D1E6 shl si,1
uint16_t offsetIndex = ((pos.RelativeAssetIndex + 2) & 0x0007); // DO NOT MULTIPLY BY 2 BECAUSE THE OFFSET TABLE ALREADY USES int16_t*!
// 02BB:1C81 8B841A0B mov ax,[si+0B1A]
// 02BB:1C85 F76EF6 imul word [bp-0A]
// 02BB:1C88 0146FA add [bp-06],ax
xPosition += this->cellXOffsetTable[offsetIndex] * tmp;
// 02BB:1C8B 8B847A0B mov ax,[si+0B7A]
// 02BB:1C8F F76EF6 imul word [bp-0A]
// 02BB:1C92 0146F8 add [bp-08],ax
yPosition += this->cellYOffsetTable[offsetIndex] * tmp;
// THIS CODE OFFSETS THE ENGINE OR CAR IF THE IT IS IN A STATION OR ON A DOUBLE TRACK<<
}
// 02BB:1C95 8B46FA mov ax,[bp-06] ss:[FF36]=0AD1
// 02BB:1C98 A3A8B9 mov [B9A8],ax ds:[B9A8]=0AD1
pos.XPosition = xPosition;
// 02BB:1C9B 8B46F8 mov ax,[bp-08] ss:[FF34]=09A8
// 02BB:1C9E A3AAB9 mov [B9AA],ax ds:[B9AA]=09A8
pos.YPosition = yPosition;
// 02BB:1CA1 5E pop si
// 02BB:1CA2 8BE5 mov sp,bp
// 02BB:1CA4 5D pop bp
// 02BB:1CA5 C3 ret
return pos;
}

Back to the main logic: when the position of the engine or car and the relative asset index are determined the code checks if an engine or car needs to be drawn. If it is a car then the code checks the car type (offset (0x50 + carIndex*2)) which is divided by 3 then multiplied by 4 and 0x0018 is added. Then the final asset index of the car is calculated like this: take the previously calculated value add 0x50 and add the relativeAssetIndex & 0x0003 value to it. Pretty simple, huh?!
If the logic is dealing with an engine, then the final asset index is calculated the following way: if the engine index is less or equal to 2, then the asset index is 0 else 16. However, if the engine flags are set to 0xFFFF then the asset index is 8. Then add 0x50 to this index and the relative asset index returned by function at 02BB:1B42.
Remember, that I just use the asset picture as a texture without any slicing and conversion, so I need to further offset the asset index, which is done this way:

auto RemapAssetIndex = [](const uint16_t index) -> uint16_t {
// remap the indices to match the tiles in the texture
if ((index >= 0x50) && (index <= 0x5F)) return index + 0x10;
else if ((index >= 0x60) && (index <= 0x6B)) return index + 0x10;
else if ((index >= 0x6C) && (index <= 0x6F)) return index + 0x14;
else if ((index >= 0x70) && (index <= 0x73)) return index + 0x18;
else if ((index >= 0x74) && (index <= 0x77)) return index + 0x1C;
else if ((index >= 0x78) && (index <= 0x7B)) return index + 0x20;

return index;
};

There is a small piece of code executed before drawing the engine on the map that I can only guess what it might be doing (see the comments in the code). I could only port it the "low-level" way, so if you already know what this might be doing, please let me know. I will post it here as a piece of "food for thought":

const auto speed = train.GetSpeed();
// 02BB:155C 7451 je 000015AF ($+51) (no jmp)
// inverting the condition to avoid a goto...
if (speed) {
// 02BB:155E B80300 mov ax,0003
// 02BB:1561 F76EF4 imul word [bp-0C] ss:[FF52]=0000 // train index
// this one is loaded from the SVE file, maybe it is the time variable???
// 02BB:1564 0206A695 add al,[95A6] ds:[95A6]=9D
// 02BB:1568 A80F test al,0F
// 02BB:156A 7543 jne 000015AF ($+43) (down)
// inverting the condition to avoid a goto...
if ((((3 * trainIndex) + ReadMemory<uint8_t>(0x17ED, 0x95A6)) & 0x000F) == 0) {
// THIS CODE IS UNKNOWN, BUT EXECUTED MAINLY WHEN TRAIN MOVEMENT HAPPENS>>
// 02BB:156C B82A00 mov ax,002A
// 02BB:156F F7AC1297 imul word [si-68EE] // ds:[9712] TRAIN DATA (9710 + 2)
// 02BB:1573 8BD8 mov bx,ax
// 02BB:1575 83BF2E02FF cmp word [bx+022E],FFFF // ds:[0230] ENGINE DATA (0212 + engine index*0x2A + 1C)=flags
// 02BB:157A 7433 je 000015AF ($+33)
// inverting the condition to avoid a goto...
if (engine.GetFlags() != 0xFFFF) {
// 02BB:157C 8B3E9E95 mov di,[959E] // Unknown value, not from SVE
// 02BB:1580 D1E7 shl di,1
auto value_17ED_959E = ReadMemory<uint16_t>(0x17ED, 0x959E);
uint16_t di = value_17ED_959E * 2;
// 02BB:1582 8B46FA mov ax,[bp-06]
// 02BB:1585 89858CDF mov [di-2074],ax // 0-2074 = DF8C
WriteMemory<uint16_t>(0x17ED, di - 0x2074, screenX);

// 02BB:1589 8B46F6 mov ax,[bp-0A]
// 02BB:158C 8985B8E1 mov [di-1E48],ax // 0-1E48 = E1B8 -> an array of FF FF FF values (see RRTLib.cpp)
WriteMemory<uint16_t>(0x17ED, di - 0x1E48, screenY);
// 02BB:1590 C785BED90000 mov word [di-2642],0000 // 0-2642 = D9BE
WriteMemory<uint16_t>(0x17ED, di - 0x2642, 0);

// 02BB:1596 8B841897 mov ax,[si-68E8] // READ TRAIN DATA! Offset = 8, Unknown
// 02BB:159A 40 inc ax
// 02BB:159B 250700 and ax,0007
// 02BB:159E 89850095 mov [di-6B00],ax
WriteMemory<uint16_t>(0x17ED, di - 0x6B00, (train.ReadUnknownValue<uint16_t>(8) + 1) & 7);

// 02BB:15A2 FF069E95 inc word [959E]
// 02BB:15A6 A19E95 mov ax,[959E]
// 02BB:15A9 250F00 and ax,000F
// 02BB:15AC A39E95 mov [959E],ax
WriteMemory(0x17ED, 0x959E, (value_17ED_959E + 1) & 0x000F);
}
// THIS CODE IS UNKNOWN, BUT EXECUTED MAINLY WHEN TRAIN MOVEMENT HAPPENS<<
}
}

Yeah, quite a mess, BUT the result makes up for it: the engines and cars are properly aligned, the right engine type is drawn as well as the cars are also displayed nicely. For example, the engine that is leaving London has just 1 smoke stack, the engine at Lille is indeed an electric engine (Crocodile) and the engine whose destination is Lille has 2 smoke stacks. 😀
Add to this that the ported code was slightly modified to support resolution independent rendering (independent from resolution and scaling):
IQSFuoO3VeP5SIKf6Q1-k2E7ARU4YYmUIj_3s2kjwU1VPBQ?width=1024

Next we will cover the Train Income (F6) screen. While that seems to be boring, going through the code that produces the train income table revealed a lot about trains, stations and modern cars. Stay tuned 😀.

Best regards,
Wilczek (Zoltan Farkas)

Reply 3 of 10, by carlostex

User metadata
Rank l33t
Rank
l33t

This is all very interesting. I’m specially interested in how the sound drivers work. With MicroProse there seems to be an OPL2 which handles single and Dual OPL2 cards (ASOUND), an OPL3 driver (PSOUND), and a Roland driver (RSOUND).

There’s also usually a digital driver. I’m very interesting in knowing the ins and outs of these drivers, that could potentially help to patch them to use cards that are not on their standard address port. In my case I managed to patch some Microprose games to support my AdLib Gold clone on a non standard port, but not all. I think I did have success with Railroad Tycoon though.

Reply 4 of 10, by Wilczek_h

User metadata
Rank Newbie
Rank
Newbie
carlostex wrote on 2025-04-27, 14:58:

This is all very interesting. I’m specially interested in how the sound drivers work. With MicroProse there seems to be an OPL2 which handles single and Dual OPL2 cards (ASOUND), an OPL3 driver (PSOUND), and a Roland driver (RSOUND).

There’s also usually a digital driver. I’m very interesting in knowing the ins and outs of these drivers, that could potentially help to patch them to use cards that are not on their standard address port. In my case I managed to patch some Microprose games to support my AdLib Gold clone on a non standard port, but not all. I think I did have success with Railroad Tycoon though.

Hi carlostex,

Thank you for your post.
I have not looked into sound handling yet as it is not part of the MVP. In fact, I always start the game in DOSBox using "MCGA mode", "no sound" and "keyboard only mode" to do the debugging and disassembly work and to minimize the "extra code" that runs. By "extra code" I mean any code that is not relevant for data handling and game logic (image, map, trains, time, reports, etc.). MCGA mode is also good to avoid running the extra code that does the per pixel animation to make a new image appear, for example, the one that would happen when you start the game or when you improve stations.
Therefore, at the moment I cannot share anything about sound as I have not looked into how RRT handles it.

Best regards,
Wilczek (Zoltan Farkas)

Reply 5 of 10, by Wilczek_h

User metadata
Rank Newbie
Rank
Newbie

Dear Vogons,

It's been quite a while since I last posted anything about this project, but be assured the project was not forgotten. Quite the opposite, a lot of things got reverse engineered and ported! This time I will not go into the technical details, however.

The most exciting thing is that TRAINS ARE MOVING in the port! 😀
HERE is a video, take a look. It's so cool to see so many trains moving on a large screen 😀. (The smoke puffs are not yet rendered though.) (Note: make sure to bump up the quality settings in the player.)

It's amazing how little code the original developers needed to get the trains on track and get them moving, as well as implementing the collision detection logic. In the port - at the moment - trains run freely without considering their schedule and cargo. Ah yes, I almost forgot: while the "train moving" code is pretty small, the "handling arrival trains at a station" code is brutally complex and large. Finally, I managed to collect most of the necessary code that does this, so the next challenge is to port it. That will take quite some time.

Other features that got ported since the last video:
- ability to list and load saved games (any number of saved games! You can see in the video that the port already lists more than 4.)
- load the tutorial map
- cash and date display
- game ticks, date calculation, get things moving 😀
- functional train roster
- find city
- income statement report
- train income report
- accomplishments report
- efficiency report

Btw., I praised the developers earlier how neat the train moving code is, but, well, I just don't know what they were thinking when the implemented the income statement report. The current port fixes all the calculations by using 32-bit integers, but the original - workaround - logic can be enabled if needed. It is visible how much they struggled with the 16-bit signed integer limitations. Anyway, this is for a future article, until then enjoy the video and the running trains 😀.

Best regards,
Wilczek (Zoltan Farkas)

Reply 6 of 10, by gerry

User metadata
Rank l33t
Rank
l33t

Great milestone in an interesting project. Workaround code for 16bits is always a reminder of different times

Reply 7 of 10, by Wilczek_h

User metadata
Rank Newbie
Rank
Newbie
gerry wrote on 2025-08-25, 08:08:

Great milestone in an interesting project. Workaround code for 16bits is always a reminder of different times

Thank you, gerry. 😀

Regarding the 16-bit signed integer usage: what puzzles me is that a lot of CWD instructions are scattered around in the binary, so it would have been possible to combine two 16-bit registers (DX:AX) to work with 32-bit integers. Yet they continued to use 16-bit integers. Even when a CWD instruction is executed the DX part is just ignored most of the cases. In other cases DX is used to check if a value is negative and then turn the lower 16-bit part into a positive integer. Weird.

Also, here is a very obvious example of their struggle: this screenshot was taken of the original game running in DOSBox. Can you spot the issue?
IQTuiousal3yQqEMfg85M4oAASHBxxz7-GEEwooNuy0JhZk?width=642&height=432

When they calculate the sum of the total revenues they integer divide the actual value by 10 and sum these values and then just add the necessary zeros to the screen. So, in this example the game does this: 36+40+3=79, then it is displayed as 790,000.
The same logic is done for the total expenses column.

And here is the ported version using 32-bit integers: no division happens, because the variable that holds the sum is a 32-bit integer. Of course, this "fix" can be switched off in the port if full authenticity is required 😀.
IQRngGvg9lWeTJMKVnBusCs7Abv1jDbFmB-DP470HrGKuyg?width=642&height=432

Another example: they tried to cap the cash at 30,000 (displayed as 30,000,000 in the game) when a train produces revenue:
Here is the ported code fragment that also contains the assembly code (with debug info too):

void GameConfiguration::function_02BB_9287_ApplyRevenue(const uint16_t revenueRaw, const uint16_t cargoType) {
// 02BB:9287 55 push bp
// 02BB:9288 8BEC mov bp,sp
// 02BB:928A B90200 mov cx,0002
// 02BB:928D 8B4604 mov ax,[bp+04] ss:[FEB4]=001A // revenueRaw, @2nd call w. passenger info: 0005, @3rd call, the car hasn't changed, still car at index 1 which is a passenger car: revenueRaw=002E
// 02BB:9290 99 cwd
// 02BB:9291 F7F9 idiv cx // AX=000D, @2nd call: 0005/0002=0002, @3rd call:002E/0002=0017
// 02BB:9296 0106AA95 add [95AA],ax ds:[95AA]=F797+000D=F7A4, CURRENT CASH! SIGNED! //F797=-2,153

this->SetCurrentCashAmount(this->GetCurrentCashAmount() + (revenueRaw / 2) * 1000);

// update revenue per cargo type!
// // 2nd call: [95AA]=F7A4+0002=F7A6
// // 3rd call: [95AA]=F7A6+0017=F7BD
// 02BB:929A 8B5E06 mov bx,[bp+06] ss:[FEB6]=0000 // cargoType, //@2nd call: 0005, //@3rd call: 0001
// 02BB:929D D1E3 shl bx,1 // @2nd call: 0005*2=000A // @3rd call: 0001*2=0002
// 02BB:929F 0187DEE1 add [bx-1E22],ax ds:[E1DE]=0063+000D=>0070 // REVENUE VALUES of group cargoType

this->revenueValues[cargoType] += revenueRaw / 2; // do not multiply the index by 2 as we are already addressing 2-byte words!

// // @2nd call: [E1E8]=0014+0002=0016
// // @3rd call: [E1E0]=0074+0017=008B
// ** TRY TO ** LIMIT THE CASH AMOUNT TO 30,000 -> NO LONGER NEEDED, WE USE 32-bits!!! >>
// 02BB:92A3 813EAA953075 cmp word [95AA],7530 ds:[95AA]=F7A4 // 0x7530=30000 dec
// 02BB:92A9 7E06 jle 000092B1 ($+6) (down)
// // LIMIT CASH TO 30000>>
// 02BB:92AB C706AA953075 mov word [95AA],7530 ds:[95AA]=F7A4 ==> -2,140
// // LIMIT CASH TO 30000 -> NO LONGER NEEDED, WE USE 32-bits!!! <<

// 02BB:92B1 5D pop bp
// 02BB:92B2 C3 ret
}

You can see the instructions from 02BB:92A3-02BB:92AB which could be translated as: if (cash > 30000) cash = 30000; These instructions cost more than the extra 2 bytes that would be needed to store the upper 16 bits of the cash amount.
It would be so nice to hear from the original developers why this design choice was made. Maybe one day Sid will reach out? 😀
Anyway, the port internally uses a 32-bit integer (in a weird way though, to remain compatible with the original's memory layout) to count the cash the player has.

Best regards,
Wilczek (Zoltan Farkas)

Reply 8 of 10, by gerry

User metadata
Rank l33t
Rank
l33t

that's interesting, at first glance it looks like a kind of naivete - like new programmers who do a great job but sometimes construct awkward workarounds.

Maybe the 16 bit stuff is for 8088 compatibility and performance or just a determination to stick with 16 bit values no matter what, to stay with 'easy' compare/move structures without worrying about 32 bits and managing upper/lower halves of the value. Maybe the CWD for the sign and the one mov cap was their way of keeping the capping as simple as possible

that income statement issue is an odd one though, an actual bug. It would be interesting to see what the developers would say, or whether they just compromised knowingly. I wonder if players at the time commented on it

Reply 9 of 10, by BaronSFel001

User metadata
Rank Member
Rank
Member

Sid Meier is one of those adept programmers who went in Indiana Jones style: instead of crafting a plan in advance, he made it up as he went along.

System 20: PIII 600, LAPC-I, GUS PnP, S220, Voodoo3, SQ2500, R200, 3.0-Me
System 21: G2030 3.0, X-fi Fatal1ty, GTX 560, XP-Vista
Retro gaming (among other subjects): https://baronsfel001.wixsite.com/my-site

Reply 10 of 10, by Wilczek_h

User metadata
Rank Newbie
Rank
Newbie

Dear Vogons,

I'm happy to report that handling of already existing trains is almost complete in the port.

The code of the following features were found and ported:
- Trains run according to their schedule. Finally 😀
- Train movement is the same as in the original game now. There were several bugs in the port that made some trains run faster than they should have.
- Trains are handled at the stations which includes:
○ trains passing through a station
○ arriving at the destination station and hence stopping
○ unloading cars (including leaving some cargo at the station and decreasing the available amount of cargo at the station by the amount picked up by the train)
○ changing cars
○ loading cars
○ picking up bonus/priority shipment
○ station signals are updated when trains arrive (indicating - for example - whether it is safe to enter a section between 2 stations)
○ revenue is calculated when a train arrives
○ load times of train cars are calculated and applied to trains
○ statistics are updated (ton miles traveled, etc.)
- Train messages now appear in the right-top box 😀 saying when and where they arrived and what cargo they delivered.
- Game logic is stopped while coins clinks happen.
- Local news is displayed when a priority shipment is delivered.
- Smoke puffs of steam engines got implemented, but the rendering is not pixel perfect. If I need to name some of the worst code fragments in the game, then smoke puffs handling would be one of them (next to the 16-bit cash calculations). It feels like a quick and dirty solution. I will have to replace it with custom/new code to get it working properly, but more on this later. On the other hand the routing code is pretty elegant. (Although the original code contains a bug that affects routing. For example, you start a train at station X, but you immediately replace the starting station with another one in the schedule and if the route contains a Y section before the destination station, then the train will miss the turn and just wonder around…)

Note: in the port stations do not yet produce cargo; that is in the main loop somewhere. So, if the trains pick up and deliver all available cargo then they will run empty in the end 😁.

I must say that the amount of code that does train routing and handles trains at stations is enormous. It took me quite a lot of time to figure out what the disassembled code does. Also, I consider myself a seasoned player, but the code still taught me a lot of things about what happens with trains at stations 😀.

Here is a screenshot of the port that displays the train message as well as the local news about bonus delivery (click on the image to see a larger version):
IQQi8U3_7_fGQYQOGDuAo3VPAU63-zlp2vK5LGrKVvW8uWE?width=660

Here is a video about a train delivering a bonus shipment (and rendering smoke puffs):
VIDEO: delivering bonus shipment

And here is a video about trains running according their schedules and that cars are changed:
VIDEO: changing cars

What made debugging very difficult is that there are many conditions as well as overlay calls in the train handling code. Regarding overlays: when I stepped on an "int 3F" call and tried to step over it the game would just crash with an "overlay not found" error. I know how to - more or less - solve this, but it's not a nice debugging experience. (Here is how I do it, maybe there is a simpler way? Anyway: I put a breakpoint before the interrupt instruction; when it hits I slow down DOSBox to ~19 cycles, then I continue running the game. When the overlay is loaded and/or visible changes appear I press ALT+Pause to start debugging again. Then, I try to locate the start of the code that got executed after the overlay code was loaded…)

Another challenge is that the game does not separate data and the presentation of the data. In the port I use a state machine(-like thing). I update the state and render the given state as many times as needed. This simplifies the code dramatically, but also complicates it sometimes. For example: in the original code if a train arrives at a station it produces the train message that is immediately displayed, or, if a service is inaugurated then the game shows the related animation, or it re-renders the trains on the "side bar" immediately. BUT, there are drawbacks of this approach too. Since many states are not stored, if something changes those states cannot be presented on the screen again. For example, the train message appears and then you press 'C' (the focused rect should be the center of the displayed area) then a redraw occurs and the train message is lost. The game here and there saves the displayed image to a memory segment and copies the pixels back to A000:xxxx from there, but it is not always the case. They used this technique to speed up redrawing larger parts of the screen without recalculation (like after dismissing a menu or returning from another screen). Also, the game in DOS is the only program that normally would control, well, basically the entire hardware including the A000:xxxx memory area. This is definitely not the case for the port. While something happens, you could just resize the window of the port and then nothing would appear as the render loop would have to be stopped. So, the state machine is needed; I save all train state changes in queues instead of immediately displaying them and then states are taken from the appropriate queue letting the render loop display the given state. When the state is over the next state is taken by the "update state" logic from a queue and the render loop renders the actual state. The render loop does not modify the state. EXCEPT, oh well, the smoke puffs, it's just so horrible, I'm looking forward to getting rid of the original logic and replace it with a decent one that does not cr*p itself when more than 4 steam engines appear on the screen and is independent from the number of redraws. The original code allows handling of smoke puffs of up to 4 engines. The data of "puff" frames are updated when displayed(!). If the screen changes (because you move the focus rectangle and the map needs to be redrawn) then the code just moves the position of the puffs off-screen by setting their Y coords to -1 and let the rendering catch up and reuse and realign the puffs for the trains visible on the updated screen. The original code is also sensitive to the number of redraws, so imagine you render at 500 frames per sec and smoke puffs just go wild. I fixed this in the port, but it is an ugly workaround. I tried to move the update of smoke puff coords to the update logic, but then there was a frame delay and the result felt unnatural. While all these were fine when 4 trains could hardly fit on the screen as the display area of the map was 255x190 (320-sidebar-1px left white border, 200-8(menu)-2(white border top and bottom)), this is not fine for the port that can render at any resolution. There is more, but well… enough about smoke puffs and let's focus on trains again.
By the way the smoke puff assets and their indices:
IQTaPp6ReL1ES4NMzJs7Mz41AeMCD8qTe4TPW6rqIBcZ4Bo?width=221&height=76

What other train handling features need to be ported? (Note: memory addresses mentioned here are addresses of the first missing instructions at runtime)
- Train collision is detected, but not handled (that would be code that starts at 02BB:6121). This affects functionality of course.
- There is some code at 02BB:66CA-02BB:670B that I still do not know what it does. (Never run, but I can change the IP register when debugging the original to get it executed and see what happens.)
- Another branch that starts at 02BB:5166 and ends up doing overlay stuff.
- New speed record case when you can enter a name for your train (starts at 02BB:53B3). The rename train screen is missing, but the core functionality will not really be affected. (Named trains arriving at a station produces a little bit of extra revenue when carrying passengers, btw. 😀)
- Service inaugurated animation (that only appears when the difficulty level is set to investor or financier). This starts at 02BB:55E7 and ends up doing overlay stuff.
- The "first train arrives at XYZ, citizens celebrate" news headline is not yet displayed (02BB:57D4-02BB:57DE).
- "Impossible routing" is detected, but not handled fully. (02BB:6545-02BB:65CD)

Bugs:
- There is an issue in the port handling "Wait until full" orders. The train tries to leave the station, but stops immediately. This is clearly a bug, because - due to the state machine - rendering speed indicators of speed > 0 on the side bar and then speed == 0 is only possible if multiple update cycles have been executed on the train data.

Those who are interested: start memory addresses of the most important train handling functions at runtime that got ported:
- 02BB:6BA1: DetermineAndSetNextStopForTrain(trainIndex)
- 02BB:5EDC: MoveTrains()
- 02BB:6DE8: CheckTrainCollisionWithOtherTrains(trainIndex)
- 02BB:9EA4: CheckTrainRouteExists(stationIndex, p1, stationIndex, p3)
- 02BB:4D4D: HandleArrivalOfTrainAtStation(trainIndex, stationIndex)
- 02BB:6C76: HandleSignalsOfStations(p0, p1)
- 02BB:5823: CalculateApproxDistanceBetweenStations(stationIndex0, stationIndex1)
- 02BB:5E38: ResetBonusRelatedVariables()
- 02BB:5B33: BuildTrainArrivalHeaderString(trainIndex)
- 02BB:5854: ChangeCarsAtStation(trainIndex, stationIndex)

One more interesting thing: the priority shipment value calculation code is done twice via a more or less copy-paste code, just to waste precious base memory. This could have been moved to a function and called, but nope: (there is some extra stuff around, and a different max clamp value, but you can clearly see the calculation logic code is ~identical; click for a larger image):
IQTwonEu4iN9Q7CLpSBpCABVAQMadunkcBGMDytFnxRFKGM?width=660

Anyway I'm sooooo happy that a huge portion of the core game logic got reverse engineered and ported. Next I'll tackle and port something simpler, but more spectacular (like station rendering). 😀

Stay tuned.

Best regards,
Wilczek (Zoltan Farkas)

(EDIT: I added the missing pictures to the article)