Ecco II Disassembly: Random Bits and Bobs

If you're working on a project that involves hacking the games, or making a new Ecco game, post it here!
Post Reply
User avatar
Moduvator
Site Admin
Posts: 86
Joined: 16 Dec 2025, 22:10
Location: Atlantis
Contact:

Ecco II Disassembly: Random Bits and Bobs

Post by Moduvator »

Johnny did a marvelous job reverse engineering some of the Ecco aspects. The problem was that there was never much of it documented and with Dark Sea long gone there isn't really a good reliable source of information for the others to pick up.

Since I am following up on some of the sections of Tides of Time, such as password system and related elements, I will be documenting my findings here.
The posts will not be following any structure in particular at the moment and I will merely be posting things as I run into and disassemble them.

This first message in the topic can serve as an index and I will likely put a table of contents here.
User avatar
Moduvator
Site Admin
Posts: 86
Joined: 16 Dec 2025, 22:10
Location: Atlantis
Contact:

Re: Ecco II Disassembly: Random Bits and Bobs

Post by Moduvator »

First random bit of Ecco II code analysis.

The Asterite.
Curiously enough, he is actually stored as two halves inside two 32-bit variables at RAM addresses 0xD434⁩⁩ and 0xD438⁩⁩. Every globe is represented by a single bit in those: e.g., 0x0F is 4, 0xFF is 8, etc., up to 32 in each helix.

Code: Select all

// Global variables to store Asterite's status
// ⁨⁨RAM addresses 0xD434⁩⁩ and ⁨⁨0xD438
uint32_t gAsteriteGlobesLeft, gAsteriteGlobesRight;
image.png
It is possible by manipulating those variables to spawn an "asymmetrical" Asterite, as well as making "holes" in him: the game does not mind this. However, none of the functions that I have met so far appear to be using this and in fact the one responsible for counting globe pairs will return -1 if pairs mismatch. It's possible that they were intending to manipulate individual globes, such as in Ecco 1, but feature ended up never used.

Storing data like this is very inefficient otherwise, but they can get away with this since that is really only used when saving the game in a password and restoring it from that.
The following is a reconstruction of the function at ROM offset 0xB0BA4 that counts the globes:

Code: Select all

int countAsteriteGlobePairs(void) {
    int retval = -1;
    uint32_t MASK = 0x80000000;
    int16_t globeCount = 0;

    if (gAsteriteGlobesLeft == gAsteriteGlobesRight) {
        for (uint16_t i = 32; i > 0; i--) {
            if ((gAsteriteGlobesLeft & MASK) != 0) {
                globeCount++;
            }
            MASK = MASK >> 1;
        }
        retval = globeCount;
    }
    return retval;
}
Function to populate Asterite at ROM offset 0xB0BD8:

Code: Select all

void setAsteriteGlobes(uint8_t numPairs) {
	uint8_t i;
	uint32_t MASK = 1;
	gAsteriteGlobesLeft = 0;
	
	for(i = numPairs; i > 0; i--) {
		gAsteriteGlobesLeft |= MASK;
		MASK = MASK << 1;
	}
	
	gAsteriteGlobesRight = gAsteriteGlobesLeft;
	return;
}
Following that is a function at ROM offset 0xB0BFC that initializes Asterite, setting both globe variables to 1, but it also takes care to fill 196 bytes at 0xD35E with zeroes. It actually makes more sense to post the raw disassembly of it for now since it's very short:

Code: Select all

clearAsteriteGlobes:
    moveq      #0x0,D1
    lea        (DAT_ffffd35e).w,A1
    movea.w    #0x31,A0
    moveq      #0x0,D0
.loop:
    move.l     D0,(A1)+
    addq.w     #0x1,D1w
    cmp.w      A0w,D1w
    blt.b      .loop
    moveq      #0x1,D0
    move.l     D0,(gAsteriteGlobesLeft).w
    move.l     D0,(gAsteriteGlobesRight).w
    rts
Essentially a memset() and two assignments, really. I have not yet worked out what said location does exactly but so far my hunch is that the actual globe objects are to be stored there for animating them. This array is referenced in a lot of functions:

Code: Select all

fClearAsteriteGlobes:000b0bfe(*), 
fClearAsteriteGlobes:000b0c08(W), 
FUN_000b1536:000b1542(*), 
FUN_000b1536:000b154e(*), 
FUN_000b1554:000b1562(*), 
FUN_000b1554:000b156e(*), 
FUN_000b1574:000b1580(*), 
FUN_000b1574:000b158c(*), 
FUN_000b1f60:000b20f8(*), 
FUN_000b2208:000b2260(*)
User avatar
Moduvator
Site Admin
Posts: 86
Joined: 16 Dec 2025, 22:10
Location: Atlantis
Contact:

Re: Ecco II Disassembly: Random Bits and Bobs

Post by Moduvator »

Just another quick note for posterity. Whilst picking apart functions that generate and decode passwords, I've come across two functions that were an absolute hell to wrap my head around. Now, all they really do is 32-bit division and modulo respectively, however due to the fact 68000's div family of instructions are limited to 16 bit quotient and divisor there is A LOT of branching and jumping all over the program memory including hijacking of rts at some point via clever pushing of a return address onto the stack.

I have been suggested those may in fact be library functions that were linked in by compiler and the fact password related stuff does look like it was written in C may actually support this. Alternatively this could be Molnar just reusing some of the code he'd written earlier.

ROM offsets are as follows:
  • 0x00001080: D1_32div_D0
  • 0x000025D8: D1_32modulo_D0
Feel free to gawk at them in your free time. All the complexity does seem to come from the fact those functions are fully universal and support up to 32 bit divisor as well as returning up to 32 bit quotient. Probably the most ironic part that most of that ends up never being used in password routines since stuff is only divided by 26 there. For example, here is the flow of the division function with all the extra steps tossed out and sections rearranged (note ROM offsets) to make it easier to follow:

Code: Select all

; dword D1_div_D0(dword divisor, dword dividend)
; Preliminary diagnosis: this function does full 32-bit UNSIGNED division
; Arguments:
; 	D0: divisor,  dword
;	D1: dividend, dword
;
; Returns:
;	D1: dword
                     D1_div_D0                                    XREF[1]:     Generate_Password:000d1b90(c)  
00001080 41 fa 00 04     lea        (0x4,PC)=>.divisorCheck,A0		; Load label into A0
00001084 60 2e           bra.b      .beginDivide

                     .beginDivide
000010b4 48 40           swap       D0					; Swap D0
000010b6 32 40           movea.w    D0w,A1				; A1 = D0 >> 16;
000010b8 48 40           swap       D0					; Swap D0 back
000010ba 34 09           move.w     A1w,D2w				; Copy A1 to D2
000010bc 66 20           bne.b      .divisorOverflow			; Branch if A1 was not zero, meaning divisor is larger than 16 bit
000010be 82 c0           divu.w     D0w,D1				; Divide D1 by D0
000010c0 69 08           bvs.b      .quotOverflow			; Branch on overflow (quotient > 16-bit signed), D1 is NOT AFFECTED
000010c2 48 41           swap       D1					; Swap D1
000010c4 42 41           clr.w      D1w					; Clear lower 16 bits and put all back...
000010c6 48 41           swap       D1					; ... as we only need quotient. It's in D1 now.
000010c8 4e d0           jmp        (A0)				; A0 is loaded with .divisorCheck

		; Divide D1 by D0 in 16-bit slices as result was too large to fit into lower 16-bit of destination
                     .quotOverflow							
000010ca 3f 01           move.w     D1w,-(SP)				; Push lower 16 bits of D1 onto the stack
000010cc 42 41           clr.w      D1w					; Clear lower 16 bits of D1
000010ce 48 41           swap       D1					; D1 = D1 >> 16
000010d0 82 c0           divu.w     D0w,D1				; Divide high 16 bits of original D1 by D0 now
000010d2 24 01           move.l     D1,D2				; Copy result to D2, higher 16 bits has remainder
000010d4 34 1f           move.w     (SP)+,D2w				; Pop from stack  the lower original 16 bits into D2
000010d6 84 c0           divu.w     D0w,D2				; Divide D2 by D0
000010d8 48 41           swap       D1					; D1 = D1 << 16;
000010da 32 02           move.w     D2w,D1w				; D1 = (D1 & 0xFFFF0000) | (D2 & 0x0000FFFF)
000010dc 4e d0           jmp        (A0)				; A0 is loaded with .divisorCheck
                     .divisorCheck
00001086 b2 fc 00 00     cmpa.w     #0x0,A1				; A1 == 0?
0000108a 67 0a           beq.b      .endDivide				; Return if so
                     .endDivide
00001096 4e 75           rts
Post Reply