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.
Ecco II Disassembly: Random Bits and Bobs
- Moduvator
- Site Admin
- Posts: 86
- Joined: 16 Dec 2025, 22:10
- Location: Atlantis
- Contact:
- Moduvator
- Site Admin
- Posts: 86
- Joined: 16 Dec 2025, 22:10
- Location: Atlantis
- Contact:
Re: Ecco II Disassembly: Random Bits and Bobs
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.
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:
Function to populate Asterite at ROM offset 0xB0BD8:
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:
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:
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;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;
}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;
}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
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(*)- Moduvator
- Site Admin
- Posts: 86
- Joined: 16 Dec 2025, 22:10
- Location: Atlantis
- Contact:
Re: Ecco II Disassembly: Random Bits and Bobs
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:
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
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