Skip to content

Latest commit

 

History

History
265 lines (212 loc) · 9.75 KB

File metadata and controls

265 lines (212 loc) · 9.75 KB

The executables

Nine hunk files: four on the disc as files, five inside the packed level blobs.

File Hunks Code Alloc Mem Reloc32
/Legends 2 37,848 37,848 + 1,016 BSS chip 1,020
/InstallHD 3 8 8 + 4,076 DATA + 1,628 BSS any 113
/c/SetPatch 2 6,640 6,640 + 32 DATA any 154
/c/Assign 1 3,184 3,184 any 0
/c/ShutDown 1 44 44 any 1
Level1/Legends.Pak 1 572,984 packed 1,531,604 chip 0
Level2/Legends.Pak 1 506,560 packed 1,469,276 chip 0
Level3/Legends.Pak 1 328,444 packed 1,143,092 chip 0
Level4/Legends.Pak 1 337,116 packed 1,142,964 chip 0
Level5/Legends.Pak 1 298,964 packed 1,085,840 chip 0

None of them carries a HUNK_SYMBOL table and none carries HUNK_DEBUG, so there are no symbols to read anywhere on the disc.

/Legends -- the front end

One code hunk, 37,848 bytes, entirely in chip RAM, 1,020 relocations, and the whole thing is compiled to absolute addressing: move.l $1aa.l,d0 rather than register-relative. Entry point is jmp $9330 at offset 0, jumping over a table of hardware register/value pairs that sits immediately after it.

It identifies itself, once, at hunk offset 0x41AD:

Legends A1200 V1.00

A1200, on a CD32 disc. The five level executables disagree with it -- see below.

Sixty library calls

tools/m68k.py finds sixty 4E AE instructions. Sorted, they are a small and very deliberate set:

Call Times On
OldOpenLibrary (-552) 3 exec
Open (-30), Read (-42), Seek (-66), Close (-36) 1 each dos
OpenDevice (-444), DoIO (-456), SendIO (-462), AbortIO (-480), WaitIO (-474) 1-2 each exec
OwnBlitter / DisownBlitter / WaitBlit 5 / 5 / 4 graphics
AllocMem (-198), FreeMem (-210) 1 each exec

One Open and one Read in the entire program. There is exactly one file loader, and it is the routine at hunk 0x749E:

749E  bsr     lock_screen
74A2  jsr     DisownBlitter(GfxBase)     ; give the blitter back to the OS
74AC  move.l  #name,d1
74B2  movem.l d1-d7/a0-a6,-(a7)          ; save everything: the game runs
74B6  move.l  #1005,d2                   ;   with its own register conventions
74BC  jsr     Open(DOSBase)              ; MODE_OLDFILE
74D6  jsr     Seek(file, 0, OFFSET_END)
74EA  jsr     Seek(file, 0, OFFSET_BEGINNING) -> d3 = the file's size
7526  jsr     Read(file, heap, d3)
753C  jsr     Close(file)
7542  movem.l (a7)+,d1-d7/a0-a6
754C  jsr     OwnBlitter(GfxBase)        ; take the blitter back
7556  jsr     WaitBlit(GfxBase)

The size comes from seeking to the end and then to the start: the second Seek returns the previous position. The game owns the blitter permanently and hands it back to the operating system only for the duration of a DOS call.

The heap

At hunk 0x728C:

move.l  #$200000,d6              ; ask for 2 MB
.retry:
subi.l  #$400,d6                 ; ... 1 KB less each time
move.l  d6,d0
move.l  #MEMF_CHIP|MEMF_CLEAR,d1
jsr     AllocMem(SysBase)
tst.l   d0
beq.b   .retry

The largest chip block it can get, up to 2 MB, in 1 KB steps. On a CD32 that is essentially all of chip RAM. Everything the game loads goes into that block through a bump allocator ($1AA is the pointer, $1AE the size of the last thing loaded).

The resource table

At hunk offset 0 -- overwriting its own entry point once the game is running -- the program builds a table of up to 64 entries tagged with four-byte names:

'RAMC'  chip block      'RAMP'  public
'RAMF'  fast            'RAMD'  (a fourth class)

The lookup at 0x7108 scans the table for the tag RA M, sums the size field at +6 of each match, and a second routine at 0x7128 finds an entry by full tag and returns its base and length. It is a 64-slot memory directory built by hand on top of one AllocMem.

CD audio

The only CD32-specific subsystem in the program. At hunk 0x47F6:

lea     cd_device_name,a0        ; "cd.device"
lea     ioreq,a1
moveq   #0,d0                    ; unit 0
moveq   #0,d1
jsr     OpenDevice(SysBase)
...
move.w  #35,IO_COMMAND(a1)       ; read the table of contents
move.l  #100,IO_LENGTH(a1)
jsr     DoIO(SysBase)

and playback, at 0x812A:

jsr     AbortIO(SysBase)         ; stop whatever is playing
jsr     WaitIO(SysBase)
move.l  8(a0),d1                 ; end   of the wanted track
move.l  2(a0),d0                 ; start of the wanted track
sub.l   d0,d1                    ; length
move.w  #39,IO_COMMAND(a1)       ; play
move.l  d0,IO_OFFSET(a1)
move.l  d1,IO_LENGTH(a1)
jsr     SendIO(SysBase)          ; asynchronous -- the game does not wait

The game asks cd.device for the TOC, reads a track's start and end out of it, and plays by absolute position rather than by track number. Stopping is AbortIO plus WaitIO. That is the whole of the CD32 audio support: about 120 bytes of code.

Taking the machine

At hunk 0x4798 the front end saves INTENA, INTREQ and CIA-B's control register B, installs its own level-2 autovector at $68, enables every DMA channel except audio (DMACON = $87F0), and runs. On exit it clears INTENA and DMACON with $7FFF, restores the saved values and puts the old vector back. ciaa.resource and ciab.resource are both named in the string table, so the CIA timers are reserved properly rather than simply grabbed.

AmigaDOS stays alive underneath all of it -- it has to, because the only way the game loads a file is dos.library.

LoadSeg, written out by hand

The front end never calls LoadSeg -- there is no jsr -150(a6) anywhere in it -- and it loads five hunk files anyway. At hunk 0x6E02 it implements the loader itself:

6E02  lea     $80,a5                  ; the loader's state block
6E08  bsr     $7018                   ; point the stream reader at the file
6E0C  .next:  bsr $702C               ; read a longword
6E10  subi.w  #$3E7,d0                ; HUNK_UNIT is the first entry
6E14  add.w   d0,d0
6E16  add.w   d0,d0
6E18  jsr     $6E28(pc,d0.w)          ; a 15-entry dispatch table
6E1C  bpl.b   .next

The table runs from HUNK_UNIT (0x3E7) to 0x3F5. UNIT and NAME are skipped, CODE and DATA share a handler, BSS, RELOC32 and END have their own, HEADER has its own, and RELOC16, RELOC8, EXT, SYMBOL and DEBUG all land on the same rejection stub.

The reason for writing it out is in the CODE/DATA handler:

6E7E  bsr     $6F7E             ; allocate the hunk; d0 = its base
6E84  subi.l  #$20,d0           ; 32 bytes below it
6E8A  movea.l d0,a0
6E8C  move.l  8(a5),d1          ; the longword count out of the stream
6E90  .copy:  bsr $702C
6E94  move.l  d0,(a0)+          ; the *packed* body, landing 32 bytes low
6E98  bne.b   .copy
6E9C  lea     -$20(a0),a1
6EA0  bsr     $703E             ; unpack, in place, into the hunk

followed, only then, by the ordinary RELOC32 loop:

6EB8  move.l  $C(a5,d0.w),d2    ; base of the target hunk
6EC2  .l: bsr $702C             ; an offset
6EC6  add.l   d2,(a0,d0.l)      ; relocate
6ECC  bne.b   .l

Decompression has to happen between reading the hunk body and applying the relocations, and AmigaDOS does those two things back to back. The level files' relocation offsets run as high as 940,974 while their packed bodies are only 572,984 bytes, so LoadSeg would be writing into memory that the decruncher had not filled yet and then having its work overwritten. Krisalis's answer was to re-implement LoadSeg in about four hundred bytes with the decruncher spliced into the middle of it. The 32-byte back-off is so that the .Pak header sits just below the hunk base and the unpacked data lands exactly on it.

The decruncher is called from ten places in the front end, so it is the general asset codec as well as the level loader.

The five level executables

Each LevelN/Legends.Pak is a well-formed hunk file -- one HUNK_CODE holding a packed stream, a HUNK_RELOC32 block, and HUNK_END:

Level Reloc32 entries Offsets Hunk allocation
1 3,982 2 - 940,974 1,531,604
2 3,726 2 - 925,664 1,469,276
3 2,863 2 - 787,842 1,143,092
4 3,324 2 - 762,134 1,142,964
5 2,443 2 - 742,448 1,085,840

The first relocation on each is at offset 2 -- the operand of the jmp the unpacked blob starts with. The front end locks the file, Examines it for fib_Size, places it sixteen kilobytes below the top of its heap, runs its own loader over it, and then calls the level with jsr (a0) and gets control back when the level is done.

Unpacked, each blob starts with jmp and is therefore code, not data:

Level1  4E F9 000E 5212   jmp $e5212

They are five builds of one program. Every one of them names:

Legends 1200/CD32 Version 0.99
cd.device        intuition.library   graphics.library
trackdisk.device dos.library         arp.library
ciaa.resource    ciab.resource       DiskDirectory

Version 0.99, on a shipped disc, against the front end's V1.00. And trackdisk.device, arp.library and DiskDirectory are floppy-era names that survive into a CD build -- arp.library is the AmigaDOS Replacement Project library from the late 1980s.

Each level also names exactly one floppy volume, and it is a different one per level. That, and what it reconstructs, is in 11-leftovers.md.

/InstallHD -- eight bytes of code

The odd one out. Three hunks: a HUNK_CODE of 8 bytes, a HUNK_DATA of 4,076, and 1,628 of BSS. The code hunk is a single relocated jmp into the data hunk, so the whole program lives in a hunk marked as data. It is the A1200 hard-disk installer, and it is documented in 11-leftovers.md.