Jump to content

Photo

River Raid: adding a bridges counter


13 replies to this topic

#1 NicoLarve OFFLINE  

NicoLarve

    Space Invader

  • 12 posts
  • Location:France

Posted Wed Feb 13, 2008 10:57 AM

Hi,

As I said in this post, I'd like to add a passed bridge counter to River Raid (i.e. a kinf of level counter).

I plan to put it on the bottom right of the screen (opposite place to the lives counter):

Posted Image

I don't have any real experience in asm programming (even if I did some little thing at school), but I know really well C/C++ and some other languages.
I first obtained the River Raid asm file from Thomas Jentzsch which is well commented (thank you !).
Then I began read some docs:
  • Randy Hyde's 1981 book "Using 6502 Assembly Language" (for apple II): this is a great book !!!
  • Stella programmer's guide (mostly talk about the TIA and the PIA)
  • and of course the River Raid source code commented by T.J.
I know that I have to read "2600 Programming For Newbies"...!
Work in progress !

I first tried to understand the code and add some dummy code, modify sprites, etc.
Then I compiled the source of River Raid with just one more line of code (a dummy LDA for example), the bin file becomes 4352 bytes long:
ls -l
-rw-rw-r-- 1 NicoLarve NicoLarve   4352 fév 13 15:05 RiverRaid-mod.bin
But I only added 3 bytes of code...
Moreover, the ROM doesn't work anymore (testing with Linux/Stella) !

As far as I know, River Raid was really optimized to fit in the 4kB cartridge (thanks to Miss Shaw !).
So, what happens if I exceed 4kB of code ? Is there any problem with DASM ?

So my main question would be: "Can I expect to compile River Raid to a bigger file (8kB ?), and how ?"

PS: For the moment, I use a DOS version of DASM (DASM V2.20.07) run with wine under Linux (of course, it works very well when to compile the original source code of River Raid)

Edited by NicoLarve, Wed Feb 13, 2008 11:01 AM.


#2 mimo OFFLINE  

mimo

    Preppie!

  • 6,776 posts
  • It's easy living in a bubble

Posted Wed Feb 13, 2008 11:34 AM

A bridge counter would be really cool, I miss it after playing the 800 version.
Hope you can find a solution

#3 NicoLarve OFFLINE  

NicoLarve

    Space Invader

  • Topic Starter
  • 12 posts
  • Location:France

Posted Thu Feb 14, 2008 7:46 AM

Maybe I first have to read "2600 Programming For Newbies" instead of waiting for help now...
I've just printed it.

PS: If anyone is interested in a booklet PDF version (two pages per A4 page, duplex, ecological version !) of "2600 Programming For Newbies", I can put it anywhere accessible for others.

#4 accousticguitar OFFLINE  

accousticguitar

    Quadrunner

  • 5,683 posts
  • Sherlock made it to 15 before he left us.
  • Location:Idaho

Posted Fri Feb 15, 2008 4:20 PM

It's too bad nobody can help you on this. It's a great idea!

#5 NicoLarve OFFLINE  

NicoLarve

    Space Invader

  • Topic Starter
  • 12 posts
  • Location:France

Posted Sun Feb 17, 2008 12:28 PM

It's too bad nobody can help you on this. It's a great idea!

No matter with that for the moment: I'm reading "2600 programming for newbies", it's an excellent reading !
And I think I'm not yet ready to be helped !
This thread was initiated to first inform people about this hack, then I'll ask for help.

Now, I understand the principle of a kernel, WSYNC, VBLANK... and 6502 assembly instructions seems really easy to understand.
All I know for the moment is that I have to inspire myself from the scoring system of River Raid and "duplicate it" in order to count the destroyed bridges.
I also know that River Raid was really optimized to fit in the 4k... and I think this add on will lead to obligatorily generate a bigger rom than a 4k...
We'll see, I'm very motivated and I love to learn assembly & '2600 hardware !

Thanks for your encouragements !

Edited by NicoLarve, Sun Feb 17, 2008 12:29 PM.


#6 accousticguitar OFFLINE  

accousticguitar

    Quadrunner

  • 5,683 posts
  • Sherlock made it to 15 before he left us.
  • Location:Idaho

Posted Sun Feb 17, 2008 6:27 PM

Well good luck! Hacking is fun. It will be a challenge, but it's worth it.

#7 Nukey Shay OFFLINE  

Nukey Shay

    Sheik Yerbouti

  • 20,921 posts
  • Location:The land of Gorch

Posted Fri Apr 18, 2008 6:12 AM

As I said in this post, I'd like to add a passed bridge counter to River Raid (i.e. a kinf of level counter).

I plan to put it on the bottom right of the screen (opposite place to the lives counter):

It's not as simple as that, because the lives counter + Activision logo are already using all 6 sprites (using the existing routine and ram requirements). If 30hz flicker is acceptable, you could do it on alternating frames. Or you could do away with the logo.


I first tried to understand the code and add some dummy code, modify sprites, etc.
Then I compiled the source of River Raid with just one more line of code (a dummy LDA for example), the bin file becomes 4352 bytes long:

ls -l
-rw-rw-r-- 1 NicoLarve NicoLarve   4352 fév 13 15:05 RiverRaid-mod.bin
But I only added 3 bytes of code...

If the reverse-engineered disassembly was reassembled while retaining all of the original code, then added bytes would push it beyond the 4k boundry. You don't get something for nothing. Try eliminating 3 bytes for the 3 you've added (like changing JMP's to unconditional branches, for example). Naturally, any ORG or ALIGN commands inside the disassembly would need to be taken into consideration.


Moreover, the ROM doesn't work anymore (testing with Linux/Stella) !

...and it shouldn't. The added bytes pushed the START vector out of it's proper address. The 2 bytes right at the specific address $FFFC & D point to where the program should go on console powerup.



As far as I know, River Raid was really optimized to fit in the 4kB cartridge (thanks to Miss Shaw !).
So, what happens if I exceed 4kB of code ? Is there any problem with DASM ?

Dasm won't have any problem with it, because it's designed to create binary files for more platforms than just a 2600. However, the 2600 was not designed to be able to "see" more than 4k of code at any time. To employ more rom memory, a bankswitching scheme must be used to swap all or part of this 4k block with other memory.



So my main question would be: "Can I expect to compile River Raid to a bigger file (8kB ?), and how ?"

The easiest way is to swap the entire 4k block by using the F8 scheme. Alter the disassembly to exist in 2 4k blocks, and have the program switch between them at some non-critical moment. I've found that seperating the display (and all of it's data tables and routines) from the rest of the program (and all of IT'S data tables and routines) is the easiest way to accomplish this. Then you simply bankswitch right before the display is drawn, and bankswitch back when the display is finished...usually yielding 2k of free memory in each 4k block (depending on display complexity and the amount of redundancy needed).


However, just by eliminating the Activision logo bitmaps and it's setup routine you might reclaim enough space to add a bridge counter if you want to keep the 4k size limit. Grab a subroutine that uses roughly the same amount of space and move it there. You might even reclaim enough space just by employing Thomas' code optimizations - to do the 30hz flicker display mentioned earlier.

#8 Thomas Jentzsch OFFLINE  

Thomas Jentzsch

    Thrust, Jammed, SWOOPS!, Boulder Dash

  • 18,583 posts
  • Always left from right here!
  • Location:Düsseldorf, Germany

Posted Fri Apr 18, 2008 1:00 PM

You could just remove the screensaver code and get quite some free extra bytes.

#9 NicoLarve OFFLINE  

NicoLarve

    Space Invader

  • Topic Starter
  • 12 posts
  • Location:France

Posted Sun Oct 12, 2008 3:52 AM

You could just remove the screensaver code and get quite some free extra bytes.

I really thank you two guys !

I was absent these pasts months because of PhD thesis... that will end in this month... Too much job here !

Since my last post, I learned several things on DASM.
I think I won't need to use the bankswitiching trick.
In fact, the main problem I encounter for the moment is the amount free RAM (a few bytes, maybe 2 or 3 are free in the original game !). One or two of them are always zeros, so I freed them. But I have good plans to do the job !

I will seriously consider removing the screen saver and/or the logo stuff in my project.
As I saw in the code, displaying more digits would be really hard or impossible to do: the code is so compact (both in bytes used and in execution timing !) and at the same time seems to use (as far as I can understand) every free pointers (the famous 6 sprites ?) to display the 5+1 digits (the LSB digit zero is always zero !).
As Nukey Shay said, this is the same on the live + logo line & should be the same on the fuel line... no more "place" !

What is the 30Hz flicker ? Does it induce complications to run on real hardware ?

I'll be programming back again on it soon.
Hope you'll be there again ! ;-)

PS: I work exclusively under Linux platforms, but I use a win32 DASM version thanks to wine.
I'd like to build a DASM soft package for GNU/Linux Debian distros but DASM sources seems buggy for Linux versions... Do you know something interesting about this issue ?

Edited by NicoLarve, Sun Oct 12, 2008 4:07 AM.


#10 Nukey Shay OFFLINE  

Nukey Shay

    Sheik Yerbouti

  • 20,921 posts
  • Location:The land of Gorch

Posted Sun Oct 12, 2008 4:48 AM

Watch out...variables zero1 and zero2 ($F3 and $F6) are always zero, that is true. But there are also 3 places that expect them to be zero (loading a zero value into a register using exactly 3 cycles). These routines need to be altered. Perhaps loading them in immediate mode and throwing that extra cycle into a nearby instruction? Screensaver mode also wastes a byte at $F7...immediately usable if you disable the screensaver in Thomas' disassembly.

And as mentioned before, you don't need 30hz flicker. Just rewrite the copyright routine to display the counter instead. 2 bytes of ram is enough BCD space for 4 digits. Finding one more in addition to the above 3 gives each player a 4-digit counter.


Incidentally, since the scores of both players is always set up as full vectors (12 bytes each) you could reclaim some RAM there by changing it to use BCD (decimal) mode - 3 bytes per player and 12 bytes of temp ram. That gives you six more bytes of free ram (possibly 8...if the temp ram is moved to share the 2-byte stack). The trick is to find enough time to update the temps to the bitmap vectors specified by the current player's BCD variables before it's time to print the score to the screen. But it's that much better if a generic routine is made...so that the same routine can be used for additional displays (like the bridge counter idea) without wasting romspace.

#11 NicoLarve OFFLINE  

NicoLarve

    Space Invader

  • Topic Starter
  • 12 posts
  • Location:France

Posted Sun Oct 12, 2008 5:15 AM

Watch out...variables zero1 and zero2 ($F3 and $F6) are always zero, that is true. But there are also 3 places that expect them to be zero (loading a zero value into a register using exactly 3 cycles). These routines need to be altered. Perhaps loading them in immediate mode and throwing that extra cycle into a nearby instruction? Screensaver mode also wastes a byte at $F7...immediately usable if you disable the screensaver in Thomas' disassembly.

And as mentioned before, you don't need 30hz flicker. Just rewrite the copyright routine to display the counter instead. 2 bytes of ram is enough BCD space for 4 digits. Finding one more in addition to the above 3 gives each player a 4-digit counter.

The scoring system in River Raid uses 12 bytes per player: half of them are used for absolute addressing and the other half consist in digits offsets to point to digits graphics directly.
How would you convert this mechanism into a BCD based one ?
Does it consists in a conversion from 3 BCD bytes to 12 Hex (the temp you described below) ones just before displaying the digits ?

Incidentally, since the scores of both players is always set up as full vectors (12 bytes each) you could reclaim some RAM there by changing it to use BCD (decimal) mode - 3 bytes per player and 12 bytes of temp ram. That gives you six more bytes of free ram (possibly 8...if the temp ram is moved to share the 2-byte stack). The trick is to find enough time to update the temps to the bitmap vectors specified by the current player's BCD variables before it's time to print the score to the screen. But it's that much better if a generic routine is made...so that the same routine can be used for additional displays (like the bridge counter idea) without wasting romspace.

Do you mean there's so enough free CPU cycles in River Raid to call a subroutine (spare too much time isn't it) ?
I didn't analyzed the code enough to answer this but the CPU seems to be really busy in this game !

Edited by NicoLarve, Sun Oct 12, 2008 5:16 AM.


#12 Nukey Shay OFFLINE  

Nukey Shay

    Sheik Yerbouti

  • 20,921 posts
  • Location:The land of Gorch

Posted Sun Oct 12, 2008 7:24 AM

How would you convert this mechanism into a BCD based one ?

Take a look at how most games do it. Whenever scoring points, a SED instruction takes place - instructing the processor to do subsequent ADC's and SBC's in decimal mode. Normally, when $01 is added to $09 you get $0A. In decimal mode, hex numbers are treated as decimal...$01+$09=$10. Doing a CLD at the end of the scoring routine clears off this mode. So instead of updating each digit seperately, you are doing 2 at once.


Does it consists in a conversion from 3 BCD bytes to 12 Hex (the temp you described below) ones just before displaying the digits ?

Not necessarily "just before" (which can be tricky in the middle of a display kernal). But sometime before it's needed. What is involved is a loop that takes a BCD value, breaks it in half into seperate nybbles, and uses each nybble to update temp ram where the bitmap addresses go.
NOTE: a nybble is a 4-bit portion of an 8-bit byte. The 4 leftmost bits is the "high" nybble, and the 4 rightmost bits is the "low" nybble. Hex numbers follow this same pattern. In value $19 for example, the high nybble = 1, the low nybble = 9. This works out really well when coupled with BCD...because [a] the digits are already the proper values that you want to see, and [b] digits 0 through 9 are the same value in both hex AND decimal.

But in order to display them, these digits need to correspond to addresses of bitmaps in ROM. That is where temporary ram comes in...

First, a BCD score value (2 digits, 0 through 9) is loaded into the accumulator. There, you can use an AND #$0F instruction to strip away the high nybble - we are not interested in it just yet. This gives you a value of 0 to 9 (remember, BCD addition does away with hex values A through F).

LDA score_variable ;load one of the 2-digit variables
AND #$0F ;keep only the low digit

There's a couple of standard ways to deal with getting the address from that point. Some games multiply the value by how many bytes are in each bitmap, for example. In that case, all that needs to be done is perform 3 ASL instructions if the bitmaps are all 8 bytes long.

ASL ;multiply by 2
ASL ;multiply by 4 (2^2)
ASL ;multiply by 8 (2^3)

This method assumes that your bitmaps are all 8 bytes apart...running consecutively.

Other games use a lookup table that holds the addresses of the bitmaps. In that case, you'd transfer the value of the accumulator to one of the index registers (X or Y) and use it as an offset to read from this table.

TAX ;move the digit to the X register
LDA LookupTable,X

The lookup table itself can be located anywhere. All it needs to contain is the LSB (least-significant byte) address of each digit bitmap...

LookupTable:
.byte <Zero_Bitmap
.byte <One_Bitmap
.byte <Two_Bitmap
.byte <Three_Bitmap
.byte <Four_Bitmap
.byte <Five_Bitmap
.byte <Six_Bitmap
.byte <Seven_Bitmap
.byte <Eight_Bitmap
.byte <Nine_Bitmap

The clear advantage of doing it this way is that each bitmap can be located anywhere...even share bytes between digits. The disadvantage is that you need to waste an index register to get it (your routine might have the X & Y registers already busy with something else).

Anyway, now we have our LSB for the first digit...and we can plunk it down into temp ram! Huzzah!
STA Print_This_11 ;more about this "11" in a minute...

The MSB is easy. As long as all bitmaps are in a single stretch of 256 bytes (1 page of ROM), you can just get it from any of them...don't forget the # to instruct the opcode that it's an immediate value...

LDA #>Zero_Bitmap ;load the page number where digit bitmaps are
STA Print_This_12 ;whats this 12 already??

The numbers 11 and 12 indicate that these 2 numbers you stored represent only ONE (1) of the digits to be printed. "Arrgh!" Don't start celebrating just yet.

To get the next digit in line...you again grab the score variable in question, but this time you keep the high nybble and work with it.

LDA score_variable ;load one of the 2-digit variables

Just as before, there's 2 common ways of dealing with it. You can either waste ROM by multiplying it by the number of bytes in a bitmap, or waste a register loading from a lookup table.

But first, you need to realise that the digit is ALREADY multiplied by 16 (it's in the high nybble spot). You need to move it lower. How much lower depends on the method. If you were multiplying by 8 for the low nybble...you can just divide by 2 for the high...

AND #$F0 ;keep only the high nybble
LSR ;divide by 2

If you were using a lookup table, divide by the full 16 so it can be used in the index register (like before)...

LSR ;divide by 2
LSR ;divide by 4 (2^2)
LSR ;divide by 8 (2^3)
LSR ;divide by 16 (2^4)
TAX ;move the digit to the X register
LDA LookupTable,X

No need for an AND #$F0 on that one. 4 LSR's in a row wipe out the lower nybble and replace it with whatever was high.

Now it's handled like before...use the value to update the temp...

STA Print_This_9 ;store the LSB
LDA #>Zero_Bitmap
STA Print_This_10 ;store the MSB

Looks pretty good so far, eh?

Uh oh...this only did the first 2 digits of the score...but what if there are six?

Right, the whole shebang really belongs in a loop (unless you want to do all the digits with seperate code). Remember how I mentioned that you might have an index register busy with something else? Well, to do this loop efficiently you need to burn BOTH of them...

LDX #11 ;the temps Print_This are on 12 consecutive bytes
LDY #2 ;The score is on 3 consecutive bytes...index 0, 1, and 2

Loop: ;this is just a label to return to later


LDA #>Zero_Bitmap ;going in reverse order now...so do the MSB first...
STA Print_This1,X ;store the address using the X index
DEX ;reduce the Print_This index
LDA score_variable,Y ;load one of the 2-digit variables using the other index
AND #$0F ;keep only the low digit
ASL ;multiply by 2
ASL ;multiply by 4 (2^2)
ASL ;multiply by 8 (2^3)
STA Print_This1,X ;store the address
DEX ;reduce the Print_This index
LDA #>Zero_Bitmap ;load...
STA Print_This1,X ;store...
DEX ;reduce the Print_This index
LDA score_variable,Y ;load that same 2-digit variable using the Y index
AND #$F0 ;keep only the high nybble
LSR ;divide by 2
STA Print_This1,X ;yadda
DEX ;yadda

Now we are near the end of the loop. You just need a branch to tell it when to go back and when to stop. The Y index is already counting from 2 to 0...so use that.

DEY ;reduce the score index (finally!)
BPL Loop ;...and branch until it falls negative


But wait just one doggone minute. That only works for consecutive bitmaps that waste a lot of rom!


Since both indexes are going to be needed...you need to store one of them mid-loop to do it the other way. Fortunately, there are 2 handy RAM addresses that you can reuse to do this. The MSB never changes...we just need to stick it in the temps before needed. For simplicity, use the lowest ones...Print_This2 and/or Print_This4...
To save time, I'm just going to reduce X four steps at a time...

LDX #8 ;the loop is done in 4-byte chunks...so we only need the values 8, 4, and 0
LDY #2 ;The score is on 3 consecutive bytes...index 0, 1, and 2

Loop: ;this is just a label to return to later

LDA score_variable,Y ;load one of the score variables
STA Print_This1+3 ;store the full value temporarily
AND #$0F ;keep only the low nybble
STY Print_This1+1 ;store the Y index so we can reuse it mid-loop
TAY ;and there it goes...Y has been changed to the table offset
LDA LookupTable,Y ;load the LSB addy
STA Print_This1+2,X ;and store it to one half of the 4-byte chunk
LDA Print_This1+3 ;Get back the original 2-digit score value
LSR ;divide by 2
LSR ;divide by 4 (2^2)
LSR ;divide by 8 (2^3)
LSR ;divide by 16 (2^4)
TAY ;move the digit to the Y register
LDA LookupTable,Y
STA Print_This1+0,X ;and store it to the other half of the 4-byte chunk
STY Print_This1+1 ;Now that the Y register has been reused, we can reload the index
LDA #>Zero_Bitmap ;and do both of the MSB's in one go...
STA Print_This1+1,X
STA Print_This1+3,X
DEX ;...and reduce the X register in 4 steps...
DEX
DEX
DEX
DEY ;reduce the score index (finally!)
BPL Loop ;...and branch until it falls negative

On the passes though the loop, the Y and A temps are safely stuck in the last addresses updated. An effective reuse of the same ram. Not quite as short as the other way, but you save ROM in the long run. Cycle time used, however, is quite a bit more.


Either method gives you the stretch of 12 bytes to use in a kernal's printing routine. The names have been changed here for clarity...but you should be able to see how each loop accomplished the task.


The catch:
Cycle time. You only get 76 cycles per scanline...and the way that the score/fuel/logo gfx is set up doesn't allow for any. So the desired routine would need to be executed waaaay before those are drawn (most likely, before the top of the screen is drawn).


* drops the stick of chalk



Do you mean there's so enough free CPU cycles in River Raid to call a subroutine (spare too much time isn't it) ?
I didn't analyzed the code enough to answer this but the CPU seems to be really busy in this game !


Looks can be deceiving. You just need to find a spot where you can scrounge enough cycle time to handle it. Most games have a bit of "just-in-case" room for worst-case scenarios (i.e. when loops and things end up taking the long way on a single frame). Just need to find out where. Above time-wasting loops is a good hunch. Look for something like:

Loop:
LDA INTIM
BNE Loop


...and place your BCD conversion routine above it.


Another problem is that this only works for the first 6-digit deal. Fortunately, since the MSB's have already been dealt with...you can just rewrite the LSB's for an additional 6-digit deal without a loop. That saves cycle time, and should only take a couple of scanlines to update. (up to) 6 reads and (up to) 6 stores. Wastes ROM going the long way...but oh well.

Edited by Nukey Shay, Sun Oct 12, 2008 7:31 AM.


#13 supercat OFFLINE  

supercat

    Quadrunner

  • 6,394 posts

Posted Sat Nov 1, 2008 10:41 PM

BTW, how hard would it be to make River Raid always show both player scores at the end of the game (instead of doing so only in one-player mode), but make the game give player two a point every time a bridge is cleared in one player mode?

#14 Nukey Shay OFFLINE  

Nukey Shay

    Sheik Yerbouti

  • 20,921 posts
  • Location:The land of Gorch

Posted Sun Nov 2, 2008 10:21 AM

Much simpler :)

Program:
First, check if a bridge has not been hit or if a 2-player game is set (skip the rest if either is true)
Bump the 2nd score counter after a bridge explosion is set.

Display:
First, check if the game is in progress or if a 2-player game is set (skip the rest if either is true).
Check the frame counter for one of the high bits to determine if the score should be swapped (so that both are shown momentarily).
Swap the score variables.


That would display the total number of bridges at the end of a game...alternating with the final score.




0 user(s) are browsing this forum

0 members, 0 guests, 0 anonymous users