Canas' alternate death quote (spoilers?)

So for those of you who don’t know, apparently Canas has an alternate death quote in FE7:

Text of ID 2011
[OpenMidRight][LoadFace][0x3C][0x01]
Ah, what a shame.[.][A]
There was so much more
that I wanted to learn...[.][A][X]

Text of ID 2012
[OpenMidRight][LoadFace][0x3C][0x01]
Mother...[.]
Forgive me...[.][A]
Take care...
of...Hugh...[A][X]

I’d never seen the second one in-game, and apparently it isn’t known exactly what triggers it. @Primefusion gave me a lead on Skype, though:

ORG $CC0E6C
TEXTIFASM 0x7A3F9 0x7DC 0x7DB
REMA
ENDA

So I started tracing the ASM from 0x0807A3F8… it ended up spreading over about ten routines, including one that’s copied to IRAM ahead of time (had to boot the game and look at it with VBA to figure out the address and get the code, because it was indirected through an IRAM pointer). But I eventually got it all worked out, even “decompiled” to C more or less.

It looks like

typedef struct small_struct_s {
    char unknown[0x20];
    short interesting;
    short check_value;
} SMALL_STRUCT; /* 0x24 bytes */

typedef struct big_struct_s {
    char header[8];
    int header_2;
    short header_3;
    char unknown[6];
    char interesting[0xc];
    char unknown_2[0x30];
    char extra[0x10]; /* NB this part is skipped when calculating the checksum. */
    short check_value;
    char unknown_3[2]; /* Probably just padding for word alignment. */
} BIG_STRUCT; /* 0x64 bytes */

/* 0x0809E4C4 */
short checksum(short* struct_data, int byte_count) {
    short sum = 0, xor = 0;
    int short_count = byte_count / 2; /* not optimized :( */
    for (int i = 0; i < short_count; ++i) {
        /* A translation using pointer arithmetic would be more literal, but this is clearer. */
        sum += struct_data[i];
        xor ^= struct_data[i];
    }
    return sum + xor;
}

/* 0x080130B4 */
/* return 1 (true) for equal strings, 0 (false) for unequal strings. */
/* Note this is different from standard library strcmp(). */
int /* bool */ compare_strings(char* x, char* y) {
    while (1) {
        char a = *x++, b = *y++;
        if (!(a | b)) { return 1; }
        if (a != b) { return 0; }
    }
}

/* 0x030022F8 */
/* Copied into IRAM and run from there, after function pointer indirection. */
/* `src` should be somewhere in SRAM; `dst` is in WRAM probably (e.g. program stack) */
/* TODO: locate where in ROM this is (presumably) copied from. */
void sram_copy(char* src, char* dst, int amount) {
    short* WAITCNT = (short*)0x04000204; /* I/O register */
    *WAITCNT = (*WAITCNT & 0xfffc) | 3; /* set lowest two bits - SRAM Wait Control = 8 cycles */
    for (int i = 0; i < amount; ++i) {
        *dst++ = *src++;
    }
}

/* 0x0809E478 */
int /* bool */ allow_sram_read() {
    return *((char*)(0x0203E79A)); /* need to research */
}

/* 0x080A038C */
int count_interesting_things(BIG_STRUCT* big) {
    int result = 0;
    for (int i = 0; i < 0xc; ++i) {
        if (big.interesting[i] != 0) { ++result; }
    }
    return result;
}

/* 0x0809F000 */
int /* bool */ load_small(SMALL_STRUCT* ptr) {
    SMALL_STRUCT small;
    if (ptr == NULL) { ptr = &small; }
    if (!allow_sram_read()) { return 0; }
    /* This is done very indirectly. 
       The offset to SRAM is looked up in the ROM,
       then a function pointer is looked up in IRAM, 
       dereferenced, and fed to the bx ladder. */
    sram_copy((char*)0x0e0070d8, ptr, 0x24);
    return checksum((short*)(&small), 0x22) == small.check_value;
}

/* 0x0809E4F0 */
int /* bool */ load_big(BIG_STRUCT* ptr) {
    BIG_STRUCT big;
    if (ptr == NULL) { ptr = &big; }
    if (!allow_sram_read()) { return 0; }
    /* Same comment here as for load_small(). */
    sram_copy((char*)0x0e000000, ptr, 0x64);
    /* Verify header and checksum.
       The ROM at the specified address contains the ASCII "AGB-FE7\0". */
    if (compare_strings(&(ptr.header), (char*)0x0840f430)) { return 0; }
    if (ptr.header_2 != 0x30317) { return 0; }
    if (ptr.header_3 != 0x200A) { return 0; }
    return checksum((short*)(&big), 0x50) == big.check_value;
}

/* 0x0809EFBC */
short wrapped_canas_asmc() {
    BIG_STRUCT big;
    SMALL_STRUCT small;
    /* If we can load a BIG_STRUCT, count the interesting things. */
    if (load_big(&big)) {
        int count = count_interesting_things(&big);
        if (count > 9) { return 2; }
        if (count > 7) { return 1; }
    }
    /* Loading failed, or not enough interesting things. */
    return load_small(&small) ? small.interesting : 0;
}

/* 0x0807A3F8 */
/* Entry point that gets referred to in the event code. 
   Canas' death quote is dependent on the result. */
int canas_asmc() {
    return wrapped_canas_asmc() ? 1 : 0;
}

tl;dr - it does depend pretty much entirely on something to do with your save data:

  • The in-IRAM routine is responsible for copying chunks of data from the SRAM (save file) to other RAM locations (it also has to set an I/O control register to adjust communication with the battery save, to make sure it doesn’t try to read faster than the data can actually be provided).

  • There are a couple of calls to a routine that checks a byte value at 0x0203E79A in WRAM. My best guess is that this is set to indicate that save data is actually available.

  • And then there are a couple of functions that appear to load a couple of structures from the save data, perform some checksumming and other validation, and then look at certain key values.

So what I’m looking for now is any documentation on the format of the save data. In particular:

  • A 100-byte structure is read from the very beginning of the save file. Twelve bytes are checked, from 20 (inclusive) to 32 (exclusive). If at least 7 of these are nonzero, the ASMC will return true.

  • A 36-byte structure is read from offset 0x70d8 in the save file (so, a little over 28kb in). The last two bytes are a checksum; the two bytes before that (so, a short value at offset 0x70f8) is important to the code - if that value is nonzero, the ASMC will also return true.

So. Anyone happen to know what’s stored at those locations in the save file?

3 Likes

I’m just guessing, but maybe it has something to do with the connection with FE6.
I mean, it makes reference to Canas’ Mother (Niime) and Hugh.

1 Like

Routine 080A038C is called when you view the Status screen.
The “Play” counter that appears in the upper-right corner will be 1 + the return value of 080A038C (do not display Play counter if 080A038C returns 0)

Looks like you need to complete the game 8 times to unlock Canas’s second death quote.


FE6 data transfer:
Chapter 22 ending unlocks the 8 playthroughs bonus scene.
Final chapter ending unlocks the 10 playthroughs bonus scene.
So it’s likely data transfer would have also unlocked Canas’s second death quote.

2 Likes

That… makes sense, except now I’m wondering why it’s determining “number of playthroughs” by iterating through an array and counting the nonzero values. o_O

1 Like

I am overexcited that someone finally figured this out. Cool stuff :0

I think I figured it out. It’s counting an array of unique playthrough IDs.

The game doesn’t want the player to be able to increase the playthrough counter simply by replaying the final chapter or epilogue from the same playthrough.

Near the end of the epilogue, the game calls routine 080A03C8 which adds parameter r1 (0202BBF8+0x18) to the playthrough ID array if the array has less than 12 elements and parameter r1 doesn’t appear in the array.

I think it is used to display the B/W/L here (red box).

And I remember its routine is at the end of the one for the status screen.

Now I wonder if we could just give one playthrough ID while creating the save file so that players can begin our hack from the 2nd playthrough to enjoy more advanced functions such as a quick movement of units.

Probably, it shouldn’t be hard to find the routine that writes back to the SRAM. But then you have to figure out when to call that… maybe it’s easier to just disable the checks :slight_smile:

Anyway, I guess the “36-byte structure” has something to do with the FE6 save data transfer?

Yes.

A quick experiment:


The left one is FE7(J), and the right one is FE6(J). I read the 2nd save from FE6 to FE7.

It writes data here during connection:

– Maybe you can trace the process by vba-sdl-h. –

btw, FE6/7/8 can also connect with FE9. You can emulate it with vba and dolphin. I did it 2 years ago,

1 Like

Nice (and the checksum works out like it should, too). I didn’t even know you could emulate GBA-GBA link with VBA :slight_smile: