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 4, 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 4, 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 4, 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 4, 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)