Pull to refresh

Not 'Doom' ported

Level of difficulty Easy
Reading time 8 min
Views 1.7K
Programming *Java *Assembler *Development for Android *
Case

Cruel time. Nothing I can do.
I needed positive emotions. Distract myself, do some useless work.

Coding a game might help.
There are a lot of ports for "Doom". But I like the old cool game "Stunt Car Racer".
Recognizable graphics, cool car dynamics and tracks.
Why is everyone fixated on "Doom"?

There is an 'Amiga' emulator, 'WinUAE', so it is not a problem to play the game on PC. There are Android ports of the emulator too. Unfortunately, file access was restricted in the latest Android versions, so I was not able to start the game on my Android device. Also, I do not think that the game is playable because it requires a keyboard or joystick to play.

Some time ago a windows version appears here: https://stuntcarracerwin32.bravesites.com/
Unfortunately, it is not exact the same. I doubt that the original code uses 'atan' to compute a car position. But there is a disassembled Amiga code there. The file contains more than 27000 lines. Huge amount of code!

I decided to port this code on PC and Android to enjoy this game again.

A lot of work has already been done. Someone recognized variables, data, pieces of code.

But look at the code:

acceleration.adjust
road.height	dc.w	0	; actually a longword, but bytes have more than 1 use
factor1	dc.b	0
value
product
	dc.b	0

cbits	incbin	graphics_data/cbits.bin

sin.table	incbin	tables.bin
perspective.table	equ	sin.table+516
perspective.table2	equ	perspective.table+4096

Binary data, sprites are not included in the code. Variables located at the same address have different sizes and different purposes. I also did not want to learn how the 'Amiga' hardware worked. All these things stopped me. But it was a challenge, and finally I decided to try.

So, the start conditions:

  • Partially commented disassembled code written in Motorola 68000 assembler;

  • There are no extra binary data required in the code;

  • No tools will be used to compile and debug the original assembler code;

Which programming language to choose?

I think that the best choice is C or C++.

Pros are obvious: efficiency, there will be no problems with types of variables. The code will work almost on any modern CPU.
"Stunt Car Racer" worked even on 'ZX Spectrum' with 3.5 MHz 8-bit Z80 and 48 kB RAM.

But there are cons too.
A GUI library is required which can be used on Windows, Linux, Mac OS, Android.
Modern CPUs prefer to use 32- or 64-bit memory alignment for variables which can create extra difficulties because the original code uses bytes and words.

Rust?

It would be a good way to learn this language better, but 'Visual Studio Code' crashed when I tried to start debugging the simplest code.

fn main() {
    let guess = String::from("test\r\n");

    let guess = guess.trim();// causes LLDB to crash
    println!("{}", guess);
}

There is no debugging in 'IntelliJ'. Too complex language, problems with IDE. 'CLion' is a paid. So, no.

Java or Kotlin.

I have rich experience in porting C libraries on Java. But it is an unknown assembler.

Java does not have unsigned types except 'uint16' and extends 'int8', 'int16', 'uint16' into 'int32' that creates some extra level of difficulties.
It is obvious that will be problems with memory pointers.
In C, it is not a problem to return several values from a function, but in Java it creates additional difficulties.
Java does not support aliases or typedefs. If you use 'int32' to represent 'color8888', there is no way to say that 'int' means 'color'.

And Java does not have 'goto'!
Why, why does not Java allow to use 'goto'?
It is really necessary sometime. IDEs may simply have settings to highlight or show an error or warning.

Look at this piece of code and try to call it from different entry points without 'goto':

entry point 1:
do {
    if( condition 1 ) {
        entry point 2:
        ...
    }
    if( condition 2 ) {
        entry point 3:
        ...
    }
    if( condition 3 ) {
        entry point 4:
        ...
    }
    if( condition 4 ) {
        entry point 5:
        ...
    }
} while(...);

Pros are that a code will work on every OS where Java works. Even for 'Android', only small changes will be required.

Kotlin is a multi-platform language. But it is high level object-oriented language, it is not a good choice for bit oriented operations.
I also made measurements. I converted one of my Android tools to Kotlin.
Kotlin took twice more time for code compiling than Java and there is trash in the compiled code.

Here is a code example after compiling and obfuscation:

ArrayList<String[]> mo429d = abstractC1628a2.mo429d(mo432a.f5056f, dArr);
C0992g.m1648e(mo429d, "list");
c0806c.f3202d = mo429d;

Here, 'C0992g.m1648e' contains a null check. Why? 'NullPointerException' will be thrown anyway.

Finally, I chose Java.
If the code work on Java, the code can be rewritten in C or C++ without big problems.
Also, Java uses boundary check for arrays and it could prevent bugs.

Now, what program to use for coding?

I started with 'Eclipse', but something happened in the latest versions.
'Eclipse' was losing my code formatting settings, I also tried to switch off code formatting completely, but failed.

After several days I decided to switch to 'IntelliJ'.

It is also hell. 'Low memory' warning appears. CPU cooler running at full power. Narrow scrollbars, so I had to search over the Internet to find hidden settings to make them wider.

Look at the screenshot below and try to find a scrollbar position!

Inconvenient scroll bar in 'IntelliJ and the standard scroll bar.
Inconvenient scroll bar in 'IntelliJ and the standard scroll bar.

If you use a mouse without a wheel for scrolling, a trackball, for instance, it is a real problem.

Also, tooltips appear only on top of the text, hiding the line above.

Tooltip always hides a line above in 'IntelliJ'.
Tooltip always hides a line above in 'IntelliJ'.

Tooltips must take the position of the cursor into account. A programmer not always write code line by line, copy-paste is also used.

I tried 'Notepad++', but it does not show types of variables, so I had to got back to the IDE and just restart 'IntelliJ' sometimes.

M68k assembler is simple and straightforward. The only unusual thing is that destination is on the right.

I started with drawing scenery to understand how trigonometry in the game works and the game draws pictures.

The game uses 2 off-screen buffers, 32000 bytes each. 4 layers, 8000 bytes each. Each bit is a pixel. The layers define a color, and a pallete is set independently, so 16 colors can be used in the image.
'draw_horizon' is full of bit-setting operations, so I decided not to waste time and just rewrite the code completely.
'draw_mountains' fills polygons, but it was unclear how it happens.
'sin.table' and 'perspective.table' were absent, I had to extract them from a binary .adf file of the game.

A few days later, mountains appeared on the screen and rotation worked.
This success inspired me to go further.

The next step was the track preview.

The first attempt was disappointing. And the second, and the tenth.
Just a couple of points on the screen instead of the road.
But it is also good news for me: I am not a robot. I made a lot of mistakes.
It became clear that a great deal of attention is required when converting the code.

The authors used access to variables in different ways. For example, this code tests bit 13 in an int32 value:

btst	#5,30(a1,d0.w)

And this code:

move.l	#lw_screen_data,a4
move.w	w_road_section_offset,d3
move.b	29(a4,d3.w),d0

in Java it is:

int d3 = w_road_section_offset;
final int d0 = (lw_screen_data[ (d3 + 29) / 4 ] & 0x00_FF_0000) >> 16;

Assembler can easily work with signed and unsigned values. But in Java, attention is required.

M68K code:

cmpi.w	#256,d0
bcc	mote2
...

Java:

if( (d0 >= 0) && (d0 < 256) )	// d0 is a signed value here
	...

And a lot of time was wasted because there is no 'goto' in Java.

Several days of code checking, and now part of the road appears on the screen. But the projection was wrong and all the pieces were rotated in the wrong direction. A few days later the bug was found and fixed, and preview was now working correctly!

Now, it is time for racing.

It was an awful time. The code crashed. The car was going in the wrong direction. Far road pieces were drawn incorrectly.

Look at this code piece, for example:

near.section.flags	ds.w	80
coord.visible.values	ds.w	160

	move.l	#coord.visible.values,a6
	move.b	coord.offset.zero.or.four,d1
	...
	move.w	-4(a6,d1.w),d3
	bpl	previous.left.y.positive

As I understood, this code works correctly only because 'near.section.flags' are filled initially with the same values as 'coord.visible.values'.
In Java it causes the 'ArrayIndexOutOfBounds' exception.
In C, it would work correctly only if there were no gap between these arrays.

And there is a difference between using 'int16' and 'int32' variables because of overflow.
It would be much easier if Java did not extend 'byte' and 'short' to 'int' automatically.

Another problem was pointers. In this code, like in C, 0 is used as the incorrect pointer value.
In Java, offsets are used instead of pointers, so 0 is a valid value.

Finding bugs without the ability to compare variable values with the original code values is a special kind of self-torture. I spent several days and found nothing.
But a lot of code had already been written and it was a shame to just throw it away.

I tried to write a m68k assembler to Java code converter. Here is a piece of the code in Java after converting:

get_road_data_byte
	d0 = (ba5[ ba5i + (d5 & 0xFFFF) ] & 0xFF) | (d0 & 0xFFFFFF_00);
	d5 = ((d5 + 1) & 0xFFFF) | (d5 & 0xFFFF_0000);
	d0 &= 0xFFFFFF_FF;
	return;

Although it could be used, there was still the problem with conditional branches and the lack of 'goto' in Java.
So I returned to studying the code to find bugs. To simplify, all variables were renamed and prefixes were added to see their sizes.
Line by lines, and bugs were localized and fixed.

The sprites were taken from screenshots using 'WinUAE'.
But I see 8 sprites for clouds in the code, and only found 7. 2 sprites for smashed holes, and only found 1.

Another problem was wheels.
If you start the game in 'WinUAE', you will see, that wheels sprites are updated each frame.
Amiga uses co-processor called "copper" to draw sprites.
In Java, 4 threads are used and 3 off-screen buffers.
GUI thread, a 20 ms timer thread to emulate TV screen update, a game thread, an audio thread.
While the game draws on the 1st off-screen buffer, the 2nd is showed. The 3rd is required to draw wheels each frame.

All worked relatively fine, but then I changed jdk version from 11 to 7.
The game behavior became strange, the image froze after starting to move, the car did not respond to the keys.
Another several hours were spent making it work.

Porting on 'Android' is not a big problem, but time consuming.

Start 'Android Studio', meet the same problems with navigation like in 'IntelliJ', spend an hour or two to install all required updates.
If 'IntelliJ' constantly complained about the lack of memory, then here a huge chunk of memory is used by the emulator.
Use an old image to reduce memory consuming.
Constant problems connecting to the emulator:

I/System.out: waiting for debugger to settle...
I/System.out: debugger has settled (1332)
Disconnected from the target VM, address: 'localhost:63352', transport: 'socket'
D/AndroidRuntime: Shutting down VM

Cold restart of the emulator, and now:

03/23 15:05:40: Launching 'app' on Nexus 4 API 21.
Timed out waiting for process (xxxxx) to appear on Nexus_4_API_21 [emulator-5554].

After several unsuccessful attempts, you give up, restart a computer, and then Windows remembers about housework.
And in hour or two, you finally can start your work, but you cannot remember what problem you were trying to solve?
Maybe it is one of the answers why programmers fail to meet deadlines?
And maybe it is better desugar a code and make it faster and less memory consuming?

'Image' is replaced by 'Bitmap', 'Graphics' by 'Canvas', 'AudioInputStream' by 'AudioTrack'.

One of the problems that it is impossible to access application resources in static context.

A view with specified aspect ratio can be created using 'ConstraintLayout' and the 'app:layout_constraintDimensionRatio' property. But 'ConstraintLayout' creates a gap ignoring defined parameters.

A problem with layout in 'ConstraintLayout'.
A problem with layout in 'ConstraintLayout'.

Unfortunately, there is a problem with on-screen keyboard which overlaps input text fields. So, menus should be changed.

Finally, after 2.5 month, the game works and I can enjoy the game on PC and 'Android'.

Modern CPUs are much faster, so it is possible to make the gameplay smoother.
All car moving constants are hard-coded, each constant should be recognized and redefined.
Computer link in the original game uses COM-port, so this part was not ported. A networks connection can be used in the ported game.
Maybe I will find time to do it all later.

Also, the game loop should be rewritten to adopted to the event-driven GUI.
Both tracks and related code should be rewritten to make the code simpler and clearer, like it was done in the Windows port.

Several years ago this converted code could be published on sites like 'github'.
But now, when different AIs use published codes as examples to help you write code, would you publish a code like this?

_label2:
do {
	...
	_label4: {
		do {
			do {
				d0 = waitForKey();
				encodeJoystick( d0 );
				if( 0 != b_joystick_directions ) {
					break _label4;
				}
			} while( KeyEvent.VK_ENTER != d0 );
		} while( ' ' == opponents_names[ 12 ][ 1 ] );
		if( R_5f4b6( g ) ) {
			continue _label2;
		}
		break _label2;
	}
} while( true );

Conclusion

This part of the work is finished.
One more useless experience. One more useless text on the 'habr.com'.
An now you know who can do useless work :):

'Stunt Car Racer' rewritten in Java
'Stunt Car Racer' rewritten in Java
'Stunt Car Racer' in Java on 'Android'
'Stunt Car Racer' in Java on 'Android'

Tags:
Hubs:
Total votes 2: ↑2 and ↓0 +2
Comments 9
Comments Comments 9

Articles