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
[spoiler=“I remember when I used to try to do useful things in this language…”]
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;
}
[/spoiler]
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?