Final Fantasy III SNES disassembled (C source reimplementation in progress)

  • Thread starter Thread starter Givemesteak
  • Start date Start date
  • Views Views 429
  • Replies Replies 10
  • Likes Likes 5

Givemesteak

Active Member
Newcomer
Joined
Aug 4, 2024
Messages
40
Reaction score
61
Trophies
0
Age
37
XP
229
Country
Sweden
Hi, not sure if I am shooting myself in the foot by opening this thread but here we go.

I have fully disassembled Final Fantasy III/VI and it builds the identical input ROM.
Work has started on converting it to C. Several functions are already reimplemented in C and build the identical SNES rom.

I don't know how much interest there is in this kind of project but it's one of my favorite games of all time so I hope to eventually have complete, readable and documented source code for this game.

The end goal is being able to port this to other platforms.

I'll keep this thread updated when I have things to share and milestones are reached and so on.

The reason why I am not yet sharing any repo to the source is that it's still full of extracted game-assets and that's copyright material.
Eventually I can remove the assets and just provide the scripts to extract the assets.
But for now I am running frequent rebuilds and tests so that's not a priority at the moment.
 
Last edited by Givemesteak,
Gracias por tu trabajo; a mí también me gusta mucho este juego y me encantaría jugarlo en la Nintendo Switch con herramientas modernas.
 
+8KB across 443 routines are now C owned. And builds the exact rom with matching hashes.

With a lot of compiler stuff getting sorted out I am hoping that progress will speed up. However I am like 1.2% done translating it to C. And once that's done then there will be a lot of clean-up and documentation to make sure we have a nice, tidy and readable source.

I expect this will take a long time to complete.
 
How can you build a C version with the exact matching hashes, as the game was written in 65816 assembly ?
Well, if you match the functions written in assembly and make a ton of compiler tweaks then they should compile the same bytes. At least that's what has been working for me so far.

My pipeline uses Clang to parse C, followed by My own 65816 code generator, alongside modified compiler paths.
So we are therefor doing both reverse engineering and compiler development
 
Last edited by Givemesteak,
Well, if you match the functions written in assembly and make a ton of compiler tweaks then they should compile the same bytes. At least that's what has been working for me so far.

My pipeline uses Clang to parse C, followed by My own 65816 code generator, alongside modified compiler paths.
So we are therefor doing both reverse engineering and compiler development
That’s not how it works. No C compiler can generate the same code as a hand-written 65816 assembly.
You don’t seems to understand how the various decomp projects works : you have to find the exact compiler used for the project, and compare the generated assembly code from your reimplementation, with the binary from the game’s executable.
There is no such thing in SNES games written in assembly (you can have a look at various snes games leaks).

SNES to C projects, like snesrev’s project (https://github.com/snesrev/smw) ane reimplementation. The closest thing you can have for FF6 is Everything8215’s project (https://github.com/everything8215/ff6).

So, either you show some proof, either you’re lying.

(And if you want some proof that I know what I’m talking about, you’d understand the screenshot below)

1791012482741.png
 
That’s not how it works. No C compiler can generate the same code as a hand-written 65816 assembly.
You don’t seems to understand how the various decomp projects works : you have to find the exact compiler used for the project, and compare the generated assembly code from your reimplementation, with the binary from the game’s executable.
There is no such thing in SNES games written in assembly (you can have a look at various snes games leaks).

SNES to C projects, like snesrev’s project (https://github.com/snesrev/smw) ane reimplementation. The closest thing you can have for FF6 is Everything8215’s project (https://github.com/everything8215/ff6).

So, either you show some proof, either you’re lying.

(And if you want some proof that I know what I’m talking about, you’d understand the screenshot below)

View attachment 593726

It's possible that you are right that this is an impossible task.
But I am going to continue exploring this approach as I think it's far more interesting.

I already disassembled the entire game

I'll post some proof when/if I am ready.

But I have released stuff based on disassembly work before, I ported features from Pokemon Yellow over to Pokemon Crystal and released the .ips patch.

I am not lying about working on this. But perhaps I will run into a wall where I have to realize that this is impossible, time will tell.

fade_in.c
C:
typedef unsigned char u8;

void func_C00F4D(void) {
    *(volatile u8 *)0x004a = 0x10;
    *(volatile u8 *)0x004c = 0x10;
}

fade_out.c
C:
typedef unsigned char u8;

void func_C00F56(void) {
    *(volatile u8 *)0x004a = 0x90;
    *(volatile u8 *)0x004c = 0xf0;
}

fade_update.c
C:
typedef unsigned char u8;

void func_C00F5F(void)
{
    if ((signed char)*(volatile u8*)0x4a >= 0) {
        if ((*(volatile u8*)0x4c & 0xf0) == 0xf0)
            goto complete;
        *(volatile u8*)0x4c = (*(volatile u8*)0x4a & 0x1f) + *(volatile u8*)0x4c;
    } else {
        if (*(volatile u8*)0x4c == 0)
            goto complete;
        *(volatile u8*)0x10 = *(volatile u8*)0x4a & 0x1f;
        *(volatile u8*)0x4c = *(volatile u8*)0x4c - *(volatile u8*)0x10;
    }
    goto upload;
complete:
    *(volatile u8*)0x4a = 0;
upload:
    *(volatile u8*)0x2100 = *(volatile u8*)0x4c >> 4;
}

So we are going lower level than the one you provided in the screenshot.
 
Last edited by Givemesteak,
I didn’t say it is an impossible task. I said you lied when you write tha you use a C compiler to build a identical SNES rom, because it is not possible with a 65816 C compiler. What you are doing is similar to a recomp project, which makes the game unmaintainable.
For example, look at my "fade_update.c" version of your work :

C:
void UpdateBrightness(void)
{
    /* ASM: screen.asm @UpdateBrightness */
    uint8_t fade = wram_read8(DP_FADE_ACTIVE);   /* $4A */
    uint8_t bright = wram_read8(DP_SCREEN_BRIGHT); /* $4C */

    if (fade & 0x80) {
        /* ASM: FadingOut */
        /* ASM: lda $4c; beq FadeComplete */
        if (bright == 0) {
            wram_write8(DP_FADE_ACTIVE, 0);
        } else {
            /* ASM: lda $4a; and #$1f; sta $10;
             *      lda $4c; sec; sbc $10; sta $4c */
            uint8_t speed = fade & 0x1F;
            bright = (uint8_t)(bright - speed);
            wram_write8(DP_SCREEN_BRIGHT, bright);
        }
    } else {
        /* ASM: lda $4c; and #$f0; cmp #$f0; beq FadeComplete */
        if ((bright & 0xF0) == 0xF0) {
            wram_write8(DP_FADE_ACTIVE, 0);
        } else if (fade) {
            /* ASM: lda $4a; and #$1f; clc; adc $4c; sta $4c */
            uint8_t speed = fade & 0x1F;
            bright = (uint8_t)(bright + speed);
            wram_write8(DP_SCREEN_BRIGHT, bright);
        }
    }

    /* ASM: :lda $4c; lsr4; sta hINIDISP */
    bright = wram_read8(DP_SCREEN_BRIGHT);
    hw_write(hINIDISP, bright >> 4);
}

For those interested, here is the ASM equivalent :

Code:
.proc UpdateBrightness
        lda     $4a
        bmi     FadingOut
        lda     $4c
        and     #$f0
        cmp     #$f0
        beq     FadeComplete
        lda     $4a
        and     #$1f
        clc
        adc     $4c
        sta     $4c
        bra     :+

FadingOut:
        lda     $4c
        beq     FadeComplete
        lda     $4a
        and     #$1f
        sta     $10
        lda     $4c
        sec
        sbc     $10
        sta     $4c
        bra     :+

FadeComplete:
        stz     $4a
:       lda     $4c
        lsr4
        sta     hINIDISP
        rts
.endproc  ; UpdateBrightness

You can see the difference. Mine has variable names, no goto’s, and easier to modify if needed.

Far from me to prevent you to do your project, you can do whatever you want. Just don’t write fake informations.
 
I didn’t say it is an impossible task. I said you lied when you write tha you use a C compiler to build a identical SNES rom, because it is not possible with a 65816 C compiler. What you are doing is similar to a recomp project, which makes the game unmaintainable.
For example, look at my "fade_update.c" version of your work :

C:
void UpdateBrightness(void)
{
    /* ASM: screen.asm @UpdateBrightness */
    uint8_t fade = wram_read8(DP_FADE_ACTIVE);   /* $4A */
    uint8_t bright = wram_read8(DP_SCREEN_BRIGHT); /* $4C */

    if (fade & 0x80) {
        /* ASM: FadingOut */
        /* ASM: lda $4c; beq FadeComplete */
        if (bright == 0) {
            wram_write8(DP_FADE_ACTIVE, 0);
        } else {
            /* ASM: lda $4a; and #$1f; sta $10;
             *      lda $4c; sec; sbc $10; sta $4c */
            uint8_t speed = fade & 0x1F;
            bright = (uint8_t)(bright - speed);
            wram_write8(DP_SCREEN_BRIGHT, bright);
        }
    } else {
        /* ASM: lda $4c; and #$f0; cmp #$f0; beq FadeComplete */
        if ((bright & 0xF0) == 0xF0) {
            wram_write8(DP_FADE_ACTIVE, 0);
        } else if (fade) {
            /* ASM: lda $4a; and #$1f; clc; adc $4c; sta $4c */
            uint8_t speed = fade & 0x1F;
            bright = (uint8_t)(bright + speed);
            wram_write8(DP_SCREEN_BRIGHT, bright);
        }
    }

    /* ASM: :lda $4c; lsr4; sta hINIDISP */
    bright = wram_read8(DP_SCREEN_BRIGHT);
    hw_write(hINIDISP, bright >> 4);
}

For those interested, here is the ASM equivalent :

Code:
.proc UpdateBrightness
        lda     $4a
        bmi     FadingOut
        lda     $4c
        and     #$f0
        cmp     #$f0
        beq     FadeComplete
        lda     $4a
        and     #$1f
        clc
        adc     $4c
        sta     $4c
        bra     :+

FadingOut:
        lda     $4c
        beq     FadeComplete
        lda     $4a
        and     #$1f
        sta     $10
        lda     $4c
        sec
        sbc     $10
        sta     $4c
        bra     :+

FadeComplete:
        stz     $4a
:       lda     $4c
        lsr4
        sta     hINIDISP
        rts
.endproc  ; UpdateBrightness

You can see the difference. Mine has variable names, no goto’s, and easier to modify if needed.

Far from me to prevent you to do your project, you can do whatever you want. Just don’t write fake informations.

Sure it's maybe similar to a recomp in some ways. However at this stage the goal is simply to have as much as possible owned by C while still compiling an identical rom. (and I AM so far compiling an identical rom)

I am not just feeding C into a standard compiler and expecting it to magically produce the original ROM. We are using existing compilers like cc65 and Calypsi where they work, and adding targeted compiler support where needed. For more complex routines, we’re using Clang/LLVM with a custom 65816 backend that understands the game’s register usage, memory banks, flags, stack and calling conventions.

The idea is to compile readable C into the exact same instructions as the original assembly, not just code that behaves similarly. We then build those routines together with the remaining assembly and assets and compare the entire ROM against the original, byte for byte and by hash. We’re not copying the original machine code into the compiler or patching the output to make it match. This is working for some routines already, but we haven’t yet shown that the approach can handle the whole game.

This is mainly for the purpose of cross-referencing.
And as long as I can output an identical rom then I always know the code working as intended.

Once that's done then I will start working on a Decompilation port for MacOS and PC.
This port should have source code that is much more in line with what you'd expect (as this won't need to match the rom)
But I'll use the disassembly and C-code for cross-reference and feed that to Codex.
Then I could have a pretty nice path where each routine is validated against the logic in the already completed work.
This should minimize mistakes and allow me to automate a lot of the work.

Now I am kinda new in the whole recomp and decomp scene, and I do use quite a bit of AI.

But I am not lying to anyone.

The end goal is still ports of Final Fantasy VI with readable and maintainable source code, But I guess my way to get there is different from how you would have done it.
 
Last edited by Givemesteak,

Site & Scene News

New Hot Discussed User Submitted