Can build banks C0 and EE

No battle, menu, sound, or cutscene code. Game is playable up to Sabin's scenario, but softlocks because Gau can't be recruited on the Veldt.
This commit is contained in:
everything8215 2022-08-18 15:48:03 -04:00
parent 8cc165843c
commit 2b18ca7e8e
48 changed files with 83633 additions and 25859 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.DS_Store
*.sfc
*.o
*.lst
*.map
node_modules/*
ff6-*-data.json

97
Makefile Normal file
View File

@ -0,0 +1,97 @@
# exported variables used by module makefiles
export ASM = ca65
export ASMFLAGS =
export VERSION_EXT
# the linker
LINK = ld65
LINKFLAGS =
# list of ROM versions
VERSIONS = ff6-jp ff6-en ff6-en1
ROM_DIR = rom
ROMS = $(foreach V, $(VERSIONS), $(ROM_DIR)/$(V).sfc)
# list of modules
MODULES = field menu btlgfx battle sound cutscene world
.PHONY: all rip encode-jp encode-en clean $(VERSIONS) $(MODULES)
# disable default suffix rules
.SUFFIXES:
# make all versions
all: $(VERSIONS)
# rip data from ROMs
rip:
node tools/decode-ff6.js
# encode-jp: ff6-jp-data.json
# node tools/encode-ff6.js ff6-jp-data.json
#
# encode-en: ff6-en-data.json
# node tools/encode-ff6.js ff6-en-data.json
# clean module subdirectories
MODULES_CLEAN = $(foreach M, $(MODULES), $(M)_clean)
%_clean:
$(MAKE) -C $* clean
clean: $(MODULES_CLEAN)
$(RM) -r $(ROM_DIR)
# ROM filenames
FF6_JP_PATH = $(ROM_DIR)/ff6-jp.sfc
FF6_EN_PATH = $(ROM_DIR)/ff6-en.sfc
FF6_EN1_PATH = $(ROM_DIR)/ff6-en1.sfc
ff6-jp: $(FF6_JP_PATH)
ff6-en: $(FF6_EN_PATH)
ff6-en1: $(FF6_EN1_PATH)
# set up target-specific variables
ff6-jp: VERSION_EXT = jp
ff6-jp: ASMFLAGS += -D ROM_VERSION=0
ff6-en: VERSION_EXT = en
ff6-en: ASMFLAGS += -D LANG_EN=1 -D ROM_VERSION=0
ff6-en1: VERSION_EXT = en1
ff6-en1: ASMFLAGS += -D LANG_EN=1 -D ROM_VERSION=1
# target-specific object filenames
OBJ_FILES_JP = $(foreach M, $(MODULES), $(M)/obj/$(M)_jp.o)
OBJ_FILES_EN = $(foreach M, $(MODULES), $(M)/obj/$(M)_en.o)
OBJ_FILES_EN1 = $(foreach M, $(MODULES), $(M)/obj/$(M)_en1.o)
# rules for making ROM files
$(FF6_JP_PATH): ff6-jp.lnk encode-jp $(OBJ_FILES_JP)
@mkdir -p rom
$(LINK) $(LINKFLAGS) -m $(@:sfc=map) -o $@ -C $< $(OBJ_FILES_JP)
node tools/calc-checksum.js $@
$(FF6_EN_PATH): ff6-en.lnk encode-en $(OBJ_FILES_EN)
@mkdir -p rom
$(LINK) $(LINKFLAGS) -m $(@:sfc=map) -o $@ -C $< $(OBJ_FILES_EN)
node tools/calc-checksum.js $@
$(FF6_EN1_PATH): ff6-en.lnk encode-en $(OBJ_FILES_EN1)
@mkdir -p rom
$(LINK) $(LINKFLAGS) -m $(@:sfc=map) -o $@ -C $< $(OBJ_FILES_EN1)
node tools/calc-checksum.js $@
# run sub-make to create object files for each module
$(OBJ_FILES_JP): $(MODULES)
$(OBJ_FILES_EN): $(MODULES)
$(OBJ_FILES_EN1): $(MODULES)
# rules for making modules in subdirectories
define MAKE_MODULE
$1/obj/$1_%.o:
$$(MAKE) -C $1
endef
$(foreach M, $(MODULES), $(eval $(call MAKE_MODULE,$(M))))

225
README.md
View File

@ -1,2 +1,223 @@
# ff6
Disassembly and ROM info for Final Fantasy VI
# Final Fantasy VI Disassembly
This is a disassembly of Final Fantasy VI for the Super Famicom (i.e. Final
Fantasy III for the SNES). It is a work in progress which partially builds
the following ROMs:
- Final Fantasy III 1.0 (U), CRC32: `0xA27F1C7A`
- Final Fantasy III 1.1 (U), CRC32: `0xC0FA0464`
The Japanese version is not currently supported.
## Build Instructions
You will need a Unix-like shell to build the ROM. If you are on a Mac or
Linux, simply open a terminal. If you are using Windows, you will need to
use a Unix-like runtime environment such as Cygwin: https://www.cygwin.com.
### Install Dependencies
First, install the following dependencies if you don't have them already.
- GNU Make: https://www.gnu.org/software/make/
- cc65: https://cc65.github.io
- node: https://nodejs.org
Here is a very nice tutorial explaining how to set up Cygwin and cc65 on
a Windows machine: https://github.com/SlithyMatt/x16-hello-cc65
### Clone the Repo
If you have git installed, run `git clone
https://github.com/everything8215/ff4.git`. Otherwise, click on "Code" in
GitHub and select "Download ZIP" to copy the repo to your computer.
### Install Node.js Modules
In the root directory, run `npm install` to install Node.js modules needed
to build the ROMs.
### Rip ROM Data
Copy an unmodified FF6 ROM file into the "vanilla" directory. If you want to
build both the Japanese version and the English version, you will need to copy
both ROMs. If your ROMs have a 512-byte copier header, you will need to remove
if. There are many tools available on romhacking.net that can detect and
remove a copier header from a ROM file.
All of the data can be extracted from either a v1.0 ROM or a v1.1 ROM. For
this step there is no difference between the two versions.
Next, run `make rip` in the root directory to extract all of the required data
from your ROMs. You should only need to do this once. The extracted data will
be saved in the module directories. If you later wish to revert any of these
files to their original state, simply delete those files and run `make rip`
again, as it will only create files that do not exist and will not affect
existing files.
All ROM data will also be decoded and saved to a json file in the root
directory called either ff6-en-data.json or ff6-jp-data.json. Data in these
files can be modified and then encoded into source files when a ROM is
assembled. Editing the data file will eventually be done with the FFTools
editor, which is currently in development
(https://github.com/everything8215/fftools).
It is also possible to change simple things like text and monster HP
by editing the json file. A top-level object called `"obj"` contains all of
the data objects. After editing an object, find the corresponding entry in the
top-level object called `"assembly"` and add the following property:
`"isDirty": true`. This will notify the encoding script that the assembly
file containing this object's data needs to be updated. This procedure will
be automated by FFTools, but for now it needs to be done manually.
### Assemble and Link ROM File
Run `make <version>` to make the version of the ROM that you want, where
`<version>` is one of the following values:
- `ff6-en`: Final Fantasy III 1.0 (U)
- `ff6-en1`: Final Fantasy III 1.1 (U)
The ROM will be created in the `rom` directory. If you have ripped data from
both the Japanese and English versions, you can also run `make all` to make
all four ROMs.
After building the vanilla ROMs, you are free to modify the code and data as
you like, then run make again to rebuild the ROM. Some switchable config
options can be found in the file `include/const.inc`. This includes several
bugfixes and options to skip the intro and disable random battles.
## Distributing ROM Hacks
To avoid legal troubles, I believe that it's important to avoid distributing
copyrighted intellectual property. This repository does not contain any such
material, and instead allows you to extract all necessary data from your ROM
files.
ROM hacks are typically distributed by generating a patch file which
modifies a few bytes when applied to a ROM file. However, reassembling an
entire ROM from scratch can cause large blocks of data to be shuffled around,
resulting in patch files which contain copyrighted data when using the IPS
patch format. To avoid this, ROM hacks made from this code should be
distributed by creating forks of this repository or by using more sophisticated
patch formats which are able to differentiate data that has been modified from
data that has simply been relocated (i.e. XDelta or Delta BPS).
## Format and Organization
In order to create a somewhat cohesive and standardized disassembly, I try to
follow a set of rules for how files in the repo are formatted and organized.
### Assembler and Linker
The code for Final Fantasy games is typically split into several distinct
modules. The field or map module and the battle module are always present.
The battle code is often split into two parts, one for battle mechanics and
the other for battle graphics. There is also a menu module, though in some of
the early games the menu code was mixed in with the field code. The music and
sound code is also always in a separate module. Other modules can include
cutscenes, intro/ending credits, special effects, and the 3D world map for
FF6.
Because of this modular structure, I find it convenient to assemble each
module as a single object file and then link the modules together to create
the ROM file. These two steps are done using ca65 and ld65, respectively.
This strategy leads to a reasonable number of import/export commands, as the
separate modules only interact with one another via a small number of external
subroutines and data locations.
### File Formats, Names, and Extensions
Assembly files have the extension '.asm'. This includes files which define ROM
data, scripts, and memory labels but contain no actual code. In most cases,
assembly files should only be assembled once. The only exception is when
the ROM contains multiple identical copies of the same subroutine or data.
Include files have the extension '.inc'. These files should are meant to be
included multiple times and should not output any bytes to the assembler or
reserve any memory addresses. Examples include macro definitions and hardware
address definitions.
Assembly and include files should not have lines longer than 80 characters.
### File Organization
Each of the modules described above is in a separate directory. This mimics
my best guess as to how the original source code was organized based on e.g.
the Playstation releases where each module had a directory named after the
main programmer for that module ('NARITA', 'YOSHII', etc.). Each directory
contains all of the source code and data as well as a GNU Makefile to assemble
everything into a single object file. The root directory contains a Makefile
to link all of the object files together to create the ROM.
### Naming Conventions
As a naming convention for symbols, I've chosen to follow the example of the
Pokémon reverse engineering team (https://github.com/pret). Subroutine names
and labels for data in the ROM use PascalCase. Acronyms like RAM appear in all
capitals (i.e. InitRAM). This differs from conventional camel-case
capitalization rules.
External subroutines (which can be called by other modules) get the special
suffix '_ext'. Also, dummy subroutines called by another bank (typically a
jsr followed by rtl or jsl followed by rts on the 65c816) get the special
suffix '_far' or '_near', respectively.
Labels for unknown subroutines are the 6-digit ROM address of the subroutine
(including the bank) preceded by an underscore, e.g. `_c28566`.
Local labels inside subroutines use mixed case with a prepended '@' symbol,
which is ca65's default symbol to identify a local label. Most local labels
are unnamed, and instead use the 4-digit ROM address (excluding the bank). I
also typically include a local label at the start of each subroutine so that
it can be compared to the original ROM file, but these are just for convenience
and can be removed eventually.
WRAM and SRAM labels begin with a lowercase 'w' or 's' followed by a
descriptive name in PascalCase case, e.g. `wSpriteData`.
Hardware registers are a slight exception to the rule. I chose to use the
official register names from the SNES development manual in all caps,
prepended with a lowercase 'h' (i.e. `$2100` is `hINIDISP`).
Instruction mnemonics and macro names are in all lowercase. Macro names can
include underscores to improve readability. Constants are in all uppercase
with underscores between words.
To shorten subroutine and label names, the following shortened words may be
used:
- anim: animation
- btm: bottom
- char: character
- cmd: command
- ctrl: control or controller
- dec: decrement
- div: divide
- dlg: dialogue
- dur: duration
- elem: element
- exec: execute
- gfx: graphics
- grp: group
- inc: increment
- init: initialize
- mod: modify or modifier
- msg: message
- mult: multiply or multiplier
- obj: object
- qty: quantity
- pal: palette
- prop: properties
- ptr: pointer
- rand: random
- reg: register
- sfx: sound effect
- tbl: table
### Tabs, Spaces, and Comments
Never use tabs. Always use spaces. In assembly files, labels begin in column 1,
instructions and macros begin in column 9, operands begin in column 17, and
comments can begin in column 41. Long comments that would extend beyond the
80-character limit should be placed on their own line before the assembly
code that they describe.

26
battle/Makefile Normal file
View File

@ -0,0 +1,26 @@
NAME = battle
OBJ_DIR = obj
TARGET = $(OBJ_DIR)/$(NAME)_$(VERSION_EXT).o
SRC_MAIN = $(NAME).asm
SRC_FILES = $(wildcard *.asm)
INC_DIR = ../include
INC_FILES = $(wildcard $(INC_DIR)/*.inc)
DATA_FILES = $(wildcard data/*)
GFX_FILES = $(wildcard gfx/*)
TEXT_FILES = $(wildcard text/*)
.PHONY: all clean
# disable default suffix rules
.SUFFIXES:
all: $(TARGET)
clean:
$(RM) -r $(OBJ_DIR) data gfx text
$(TARGET): $(SRC_FILES) $(INC_FILES) $(DATA_FILES) $(GFX_FILES) $(TEXT_FILES)
@mkdir -p $(OBJ_DIR)
$(ASM) $(ASMFLAGS) -I $(INC_DIR) -l $(@:o=lst) $(SRC_MAIN) -o $@

65
battle/battle.asm Normal file
View File

@ -0,0 +1,65 @@
; +----------------------------------------------------------------------------+
; | |
; | FINAL FANTASY VI |
; | |
; +----------------------------------------------------------------------------+
; | file: battle.asm |
; | |
; | description: battle program |
; | |
; | created: 8/2/2022 |
; +----------------------------------------------------------------------------+
.p816
.include "const.inc"
.include "hardware.inc"
.include "macros.inc"
.include "battle_data.asm"
.export Battle_ext, UpdateBattleTime_ext
.export UpdateEquip_ext, CalcMagicEffect_ext
; ------------------------------------------------------------------------------
.segment "battle_code"
.a8
.i16
; ------------------------------------------------------------------------------
Battle_ext:
@0000: jmp BattleMain
UpdateBattleTime_ext:
@0003: jmp UpdateBattleTime
UpdateEquip_ext:
@0006: jmp UpdateEquip
CalcMagicEffect_ext:
@0009: jmp CalcMagicEffect
; ------------------------------------------------------------------------------
BattleMain:
@000c: rtl
; ------------------------------------------------------------------------------
UpdateEquip:
@0e77: rtl
; ------------------------------------------------------------------------------
UpdateBattleTime:
@111b: rtl
; ------------------------------------------------------------------------------
CalcMagicEffect:
@4730: rtl
; ------------------------------------------------------------------------------

29
battle/battle_data.asm Normal file
View File

@ -0,0 +1,29 @@
; +----------------------------------------------------------------------------+
; | |
; | FINAL FANTASY VI |
; | |
; +----------------------------------------------------------------------------+
; | file: battle_data.asm |
; | |
; | description: data for battle module |
; | |
; | created: 8/13/2022 |
; +----------------------------------------------------------------------------+
.export WorldBattleRate, SubBattleRate
; ------------------------------------------------------------------------------
.segment "battle_data"
; cf/0000
.res $5800
; cf/5800
.include "data/world_battle_rate.asm"
; cf/5880
.include "data/sub_battle_rate.asm"
; ------------------------------------------------------------------------------

26
btlgfx/Makefile Normal file
View File

@ -0,0 +1,26 @@
NAME = btlgfx
OBJ_DIR = obj
TARGET = $(OBJ_DIR)/$(NAME)_$(VERSION_EXT).o
SRC_MAIN = $(NAME).asm
SRC_FILES = $(wildcard *.asm)
INC_DIR = ../include
INC_FILES = $(wildcard $(INC_DIR)/*.inc)
DATA_FILES = $(wildcard data/*)
GFX_FILES = $(wildcard gfx/*)
TEXT_FILES = $(wildcard text/*)
.PHONY: all clean
# disable default suffix rules
.SUFFIXES:
all: $(TARGET)
clean:
$(RM) -r $(OBJ_DIR) data gfx text
$(TARGET): $(SRC_FILES) $(INC_FILES) $(DATA_FILES) $(GFX_FILES) $(TEXT_FILES)
@mkdir -p $(OBJ_DIR)
$(ASM) $(ASMFLAGS) -I $(INC_DIR) -l $(@:o=lst) $(SRC_MAIN) -o $@

26
btlgfx/btlgfx.asm Normal file
View File

@ -0,0 +1,26 @@
; +----------------------------------------------------------------------------+
; | |
; | FINAL FANTASY VI |
; | |
; +----------------------------------------------------------------------------+
; | file: btlgfx.asm |
; | |
; | description: battle graphics program |
; | |
; | created: 8/2/2022 |
; +----------------------------------------------------------------------------+
.p816
.include "const.inc"
.include "hardware.inc"
.include "macros.inc"
; ------------------------------------------------------------------------------
.segment "btlgfx_code"
.a8
.i16
; ------------------------------------------------------------------------------

26
cutscene/Makefile Normal file
View File

@ -0,0 +1,26 @@
NAME = cutscene
OBJ_DIR = obj
TARGET = $(OBJ_DIR)/$(NAME)_$(VERSION_EXT).o
SRC_MAIN = $(NAME).asm
SRC_FILES = $(wildcard *.asm)
INC_DIR = ../include
INC_FILES = $(wildcard $(INC_DIR)/*.inc)
DATA_FILES = $(wildcard data/*)
GFX_FILES = $(wildcard gfx/*)
TEXT_FILES = $(wildcard text/*)
.PHONY: all clean
# disable default suffix rules
.SUFFIXES:
all: $(TARGET)
clean:
$(RM) -r $(OBJ_DIR) data gfx text
$(TARGET): $(SRC_FILES) $(INC_FILES) $(DATA_FILES) $(GFX_FILES) $(TEXT_FILES)
@mkdir -p $(OBJ_DIR)
$(ASM) $(ASMFLAGS) -I $(INC_DIR) -l $(@:o=lst) $(SRC_MAIN) -o $@

65
cutscene/cutscene.asm Normal file
View File

@ -0,0 +1,65 @@
; +----------------------------------------------------------------------------+
; | |
; | FINAL FANTASY VI |
; | |
; +----------------------------------------------------------------------------+
; | file: cutscene.asm |
; | |
; | description: cutscene program |
; | |
; | created: 8/2/2022 |
; +----------------------------------------------------------------------------+
.p816
.include "const.inc"
.include "hardware.inc"
.include "macros.inc"
.export OpeningCredits_ext, TitleScreen_ext
.export FloatingIslandScene_ext, WorldOfRuinScene_ext
; ------------------------------------------------------------------------------
.segment "cutscene_code"
.a8
.i16
; ------------------------------------------------------------------------------
OpeningCredits_ext:
@6800: jmp OpeningCredits
TitleScreen_ext:
@6803: jmp TitleScreen
FloatingIslandScene_ext:
@6806: jmp FloatingIslandScene
WorldOfRuinScene_ext:
@6809: jmp WorldOfRuinScene
; ------------------------------------------------------------------------------
TitleScreen:
@680c: lda #$ff ; set return code to start a new game
sta $0200
rtl
; ------------------------------------------------------------------------------
OpeningCredits:
@6813: rtl
; ------------------------------------------------------------------------------
FloatingIslandScene:
@681a: rtl
; ------------------------------------------------------------------------------
WorldOfRuinScene:
@6821: rtl
; ------------------------------------------------------------------------------

73
ff6-en.lnk Normal file
View File

@ -0,0 +1,73 @@
memory {
ram: start = $000000, size = $2000, type = rw;
wram: start = $7e2000, size = $01e000, type = rw;
sram: start = $306000, size = $2000, type = rw;
bank_c0: start = $c00000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_c1: start = $c10000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_c2: start = $c20000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_c3: start = $c30000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_c4: start = $c40000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_c5: start = $c50000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_c6: start = $c60000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_c7: start = $c70000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_c8: start = $c80000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_c9: start = $c90000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_ca: start = $ca0000, size = $2e600, type = ro, fill = yes, fillval = $ff;
bank_cc: start = $cce600, size = $1a00, type = ro, fill = yes, fillval = $ff;
bank_cd: start = $cd0000, size = $20000, type = ro, fill = yes, fillval = $ff;
bank_cf: start = $cf0000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_d0: start = $d00000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_d1: start = $d10000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_d2: start = $d20000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_d3: start = $d30000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_d4: start = $d40000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_d5: start = $d50000, size = $40000, type = ro, fill = yes, fillval = $ff;
bank_d9: start = $d90000, size = $50000, type = ro, fill = yes, fillval = $ff;
bank_de: start = $de0000, size = $7fa00, type = ro, fill = yes, fillval = $ff;
bank_e5: start = $e5f400, size = $0600, type = ro, fill = yes, fillval = $ff;
bank_e6: start = $e60000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_e7: start = $e70000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_e8: start = $e80000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_e9: start = $e90000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_ea: start = $ea0000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_eb: start = $eb0000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_ec: start = $ec0000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_ed: start = $ed0000, size = $10000, type = ro, fill = yes, fillval = $ff;
bank_ee: start = $ee0000, size = $20000, type = ro, fill = yes, fillval = $ff;
}
segments {
# wram: load = wram, type = bss;
# sram: load = sram, type = bss;
field_code: load = bank_c0, type = ro;
field_data: load = bank_c0, type = ro, start = $c0dfa0;
interrupt: load = bank_c0, type = ro, start = $c0ff00;
snes_header_ext: load = bank_c0, type = ro, start = $c0ffb0;
snes_header: load = bank_c0, type = ro, start = $c0ffc0;
vectors: load = bank_c0, type = ro, start = $c0ffe0;
btlgfx_code: load = bank_c1, type = ro;
battle_code: load = bank_c2, type = ro;
cutscene_code: load = bank_c2, type = ro, start = $c26800;
menu_code: load = bank_c3, type = ro;
event_triggers: load = bank_c4, type = ro;
font_gfx: load = bank_c4, type = ro, start = $c487c0;
sound_code: load = bank_c5, type = ro;
dialogue_ptrs: load = bank_cc, type = ro, start = $cce600;
dialogue: load = bank_cd, type = ro;
event_script: load = bank_ca, type = ro;
battle_data: load = bank_cf, type = ro;
map_init_event: load = bank_d1, type = ro, start = $d1fa00;
world_pal: load = bank_d2, type = ro, start = $d2ec00;
map_sprite_gfx: load = bank_d5, type = ro;
mode7_cutscene_data: load = bank_d5, type = ro, start = $d8dd00;
map_tile_data: load = bank_d9, type = ro, start = $d9a800;
map_tile_data2: load = bank_de, type = ro, start = $de0000;
map_gfx: load = bank_de, type = ro, start = $dfda00;
map_gfx2: load = bank_e6, type = ro;
window_gfx: load = bank_ed, type = ro;
map_data: load = bank_ed, type = ro, start = $ed8f00;
world_code: load = bank_ee, type = ro;
world_data: load = bank_ee, type = ro, start = $eeb200;
world_sine: load = bank_ee, type = ro, start = $effef0;
}

26
field/Makefile Normal file
View File

@ -0,0 +1,26 @@
NAME = field
OBJ_DIR = obj
TARGET = $(OBJ_DIR)/$(NAME)_$(VERSION_EXT).o
SRC_MAIN = $(NAME).asm
SRC_FILES = $(wildcard *.asm)
INC_DIR = ../include
INC_FILES = $(wildcard $(INC_DIR)/*.inc)
DATA_FILES = $(wildcard data/*)
GFX_FILES = $(wildcard gfx/*)
TEXT_FILES = $(wildcard text/*)
.PHONY: all clean
# disable default suffix rules
.SUFFIXES:
all: $(TARGET)
clean:
$(RM) -r $(OBJ_DIR) data gfx text
$(TARGET): $(SRC_FILES) $(INC_FILES) $(DATA_FILES) $(GFX_FILES) $(TEXT_FILES)
@mkdir -p $(OBJ_DIR)
$(ASM) $(ASMFLAGS) -I $(INC_DIR) -l $(@:o=lst) $(SRC_MAIN) -o $@

24888
field/field.asm Normal file

File diff suppressed because it is too large Load Diff

265
field/field_data.asm Normal file
View File

@ -0,0 +1,265 @@
; +----------------------------------------------------------------------------+
; | |
; | FINAL FANTASY VI |
; | |
; +----------------------------------------------------------------------------+
; | file: field_data.asm |
; | |
; | description: data for field module |
; | |
; | created: 8/9/2022 |
; +----------------------------------------------------------------------------+
.export RNGTbl
.export ShortEntrancePtrs, EventTriggerPtrs
; ------------------------------------------------------------------------------
.segment "field_data"
; c0/dfa0
.include "data/dte_table.asm"
; c0/e0a0
.include "data/init_npc_switch.asm"
; c0/e120
.res $0180
; c0/e2a0
.include "gfx/overlay_gfx.asm"
.res $0c00+OverlayGfx-*
; c0/eea0
.include "data/overlay_tilemap.asm"
; c0/f4a0
OverlayPropPtrs:
make_ptr_tbl_rel OverlayProp, 45
.res $60+OverlayPropPtrs-*
; c0/f500
.include "data/overlay_prop.asm"
.res $0800+OverlayProp-*
; c0/fd00
.include "data/rng_tbl.asm"
; c0/fe00
.include "data/map_color_math.asm"
.res $40+MapColorMath-*
; c0/fe40
.include "data/map_parallax.asm"
; ------------------------------------------------------------------------------
.segment "event_triggers"
; c4/0000
EventTriggerPtrs:
make_ptr_tbl_rel EventTrigger, 416, EventTriggerPtrs
.addr EventTriggerEnd - EventTrigger
; c4/0342
.include "data/event_trigger.asm"
EventTriggerEnd:
.res $1a10+EventTriggerPtrs-*
; c4/1a10
NPCPropPtrs:
make_ptr_tbl_rel NPCProp, 416, NPCPropPtrs
.addr NPCPropEnd - NPCProp
; c4/1d52
.include "data/npc_prop.asm"
NPCPropEnd:
.res $50b0+NPCPropPtrs-*
; c4/6ac0
.res $0e00
; c4/78c0
.include "text/char_name_en.asm"
; ------------------------------------------------------------------------------
.segment "font_gfx"
; c4/87c0
.include "gfx/font_gfx_small.asm"
; c4/8fc0
.include "data/font_width.asm"
; c4/90c0
.include "gfx/font_gfx_large.asm"
; ------------------------------------------------------------------------------
.segment "event_script"
; ca/0000
.include "data/event_script.asm"
; ------------------------------------------------------------------------------
.segment "dialogue_ptrs"
; cc/e600
DlgBankInc:
.word $0626
; cc/e602
DlgPtrs:
make_ptr_tbl_rel Dlg1, 1574
make_ptr_tbl_rel Dlg2, 1510, Dlg1+$10000
; ------------------------------------------------------------------------------
.segment "dialogue"
; cd/0000
.include "text/dlg1.asm"
.include "text/dlg2.asm"
.res $01f100+Dlg1-*
; ce/f100
.include "text/map_title_en.asm"
; ------------------------------------------------------------------------------
.segment "map_init_event"
; d1/fa00
.include "data/map_init_event.asm"
; ------------------------------------------------------------------------------
.segment "map_sprite_gfx"
; d5/0000
.include "gfx/map_sprite_gfx.asm"
; d8/3000
.include "gfx/map_vehicle_gfx.asm"
; ------------------------------------------------------------------------------
.segment "map_tile_data"
; d9/a800
.include "data/map_tile_prop.asm"
MapTilePropEnd:
.res $2510+MapTileProp-*
; d9/cd10
MapTilePropPtrs:
make_ptr_tbl_rel MapTileProp, 42
.addr MapTilePropEnd - MapTileProp
.res $80+MapTilePropPtrs-*
; d9/cd90
SubTilemapPtrs:
make_ptr_tbl_far SubTilemap, 350
.faraddr SubTilemapEnd - SubTilemap
.res $420+SubTilemapPtrs-*
; d9/d1b0
.include "data/sub_tilemap.asm"
SubTilemapEnd:
; ------------------------------------------------------------------------------
.segment "map_tile_data2"
; de/0000
.include "data/map_tileset.asm"
.res $01b400+MapTileset-*
; df/b400
.res $0200
; df/b600
.res $0400
; df/ba00
MapTilesetPtrs:
make_ptr_tbl_far MapTileset, 75
.res $0100+MapTilesetPtrs-*
; df/bb00
ShortEntrancePtrs:
make_ptr_tbl_rel ShortEntrance, 416, ShortEntrancePtrs
.addr ShortEntranceEnd - ShortEntrancePtrs
; df/bf02
.include "data/short_entrance.asm"
ShortEntranceEnd:
; ------------------------------------------------------------------------------
.segment "map_gfx"
; df/da00
MapGfxPtrs:
make_ptr_tbl_far MapGfx, 82
.res $0100+MapGfxPtrs-*
; df/db00
.include "gfx/map_gfx.asm"
; ------------------------------------------------------------------------------
.segment "map_gfx2"
; e6/0000
.include "gfx/map_anim_gfx.asm"
; e6/8000
.include "gfx/map_sprite_pal.asm"
; e6/8400
MapTitlePtrs:
make_ptr_tbl_rel MapTitle, 73
.res $0380+MapTitlePtrs-*
; e6/8780
.include "gfx/map_gfx_bg3.asm"
.res $45E0+MapGfxBG3-*
; e6/cd60
MapGfxBG3Ptrs:
make_ptr_tbl_far MapGfxBG3, 18
.res $40+MapGfxBG3Ptrs-*
; e6/cda0
MapAnimGfxBG3Ptrs:
make_ptr_tbl_far MapAnimGfxBG3, 6
.res $20+MapAnimGfxBG3Ptrs-*
; e6/cdc0
.include "gfx/map_anim_gfx_bg3.asm"
; ------------------------------------------------------------------------------
.segment "map_data"
; ed/8f00
.include "data/map_prop.asm"
.res 1
; ed/c480
.include "gfx/map_pal.asm"
; ed/f480
LongEntrancePtrs:
make_ptr_tbl_rel LongEntrance, 416, LongEntrancePtrs
.addr LongEntranceEnd - LongEntrancePtrs
; ed/f882
.include "data/long_entrance.asm"
LongEntranceEnd:
; ------------------------------------------------------------------------------

91
field/header.asm Normal file
View File

@ -0,0 +1,91 @@
; +----------------------------------------------------------------------------+
; | |
; | FINAL FANTASY VI |
; | |
; +----------------------------------------------------------------------------+
; | file: header.asm |
; | |
; | description: snes cartridge header |
; | |
; | created: 8/2/2022 |
; +----------------------------------------------------------------------------+
.segment "interrupt"
JmpReset:
@ff00: sei
clc
xce
jml Reset
; ------------------------------------------------------------------------------
; [ nmi ]
.align $10
JmpNMI:
@ff10: jml $001500
; ------------------------------------------------------------------------------
; [ irq ]
JmpIRQ:
@ff14: jml $001504
; ------------------------------------------------------------------------------
.if LANG_EN
.segment "snes_header_ext"
SnesHeaderExt:
@ffb0:
.byte "C3" ; publisher: squaresoft
.byte "F6 " ; game code
.byte 0,0,0,0,0,0,0,0,0,0
.endif
; ------------------------------------------------------------------------------
.segment "snes_header"
SnesHeader:
@ffc0:
.if LANG_EN
.byte "FINAL FANTASY 3 " ; rom title (U)
.else
.byte "FINAL FANTASY 6 " ; rom title (J)
.endif
.byte $31 ; HiROM, FastROM
.byte $02 ; rom + ram + sram
.byte $0c ; rom size: 48 Mbit
.byte $03 ; sram size: 64 kbit
.if LANG_EN
.byte $01 ; destination: north america
.byte $33 ; use extended header
.else
.byte $00 ; destination: japan
.byte $c3 ; publisher: squaresoft
.endif
.byte ROM_VERSION ; revision number (0 or 1)
.word 0 ; checksum (calculate later)
HeaderChecksum:
.word $ffff ; inverse checksum
; ------------------------------------------------------------------------------
.segment "vectors"
Vectors:
@ffe0: .res 10
@ffea: .addr JmpNMI
.res 2
@ffee: .addr JmpIRQ
.res 12
@fffc: .addr JmpReset
.res 2
; ------------------------------------------------------------------------------

34
include/const.inc Normal file
View File

@ -0,0 +1,34 @@
; +----------------------------------------------------------------------------+
; | |
; | FINAL FANTASY VI |
; | |
; +----------------------------------------------------------------------------+
; | file: const.inc |
; | |
; | description: global constant definitions |
; | |
; | created: 8/2/2022 |
; | |
; | author: everything8215@gmail.com |
; +----------------------------------------------------------------------------+
.list off ; disable listing
; define a config constant if it's not already defined
.macro def_config _const, _value
.ifndef _const
_const = _value
.endif
.endmacro
def_config DEBUG,0
def_config LANG_EN,0
.if LANG_EN
.define LANG_SUFFIX "en"
.else
.define LANG_SUFFIX "jp"
.endif
.list on

241
include/hardware.inc Normal file
View File

@ -0,0 +1,241 @@
; +----------------------------------------------------------------------------+
; | |
; | FINAL FANTASY VI |
; | |
; +----------------------------------------------------------------------------+
; | file: hardware.inc |
; | |
; | description: snes hardware register definitions |
; | |
; | created: 8/2/2022 |
; | |
; | author: everything8215@gmail.com |
; +----------------------------------------------------------------------------+
.list off
; [ ppu registers ]
hINIDISP := $2100
hOBJSEL := $2101
hOAMADDL := $2102
hOAMADDH := $2103
hOAMDATA := $2104
hBGMODE := $2105
hMOSAIC := $2106
hBG1SC := $2107
hBG2SC := $2108
hBG3SC := $2109
hBG4SC := $210a
hBG12NBA := $210b
hBG34NBA := $210c
hBG1HOFS := $210d
hBG1VOFS := $210e
hBG2HOFS := $210f
hBG2VOFS := $2110
hBG3HOFS := $2111
hBG3VOFS := $2112
hBG4HOFS := $2113
hBG4VOFS := $2114
hVMAINC := $2115
hVMADDL := $2116
hVMADDH := $2117
hVMDATAL := $2118
hVMDATAH := $2119
hM7SEL := $211a
hM7A := $211b
hM7B := $211c
hM7C := $211d
hM7D := $211e
hM7X := $211f
hM7Y := $2120
hCGADD := $2121
hCGDATA := $2122
hW12SEL := $2123
hW34SEL := $2124
hWOBJSEL := $2125
hWH0 := $2126
hWH1 := $2127
hWH2 := $2128
hWH3 := $2129
hWBGLOG := $212a
hWOBJLOG := $212b
hTM := $212c
hTS := $212d
hTMW := $212e
hTSW := $212f
hCGSWSEL := $2130
hCGADSUB := $2131
hCOLDATA := $2132
hSETINI := $2133
hMPYL := $2134
hMPYM := $2135
hMPYH := $2136
hSLHV := $2137
hROAMDATA := $2138
hRVMDATAL := $2139
hRVMDATAH := $213a
hRCGDATA := $213b
hOPHCT := $213c
hOPVCT := $213d
hSTAT77 := $213e
hSTAT78 := $213f
hAPUIO0 := $2140
hAPUIO1 := $2141
hAPUIO2 := $2142
hAPUIO3 := $2143
hWMDATA := $2180
hWMADDL := $2181
hWMADDM := $2182
hWMADDH := $2183
; ------------------------------------------------------------------------------
; [ cpu registers ]
hNMITIMEN := $4200
hWRIO := $4201
hWRMPYA := $4202
hWRMPYB := $4203
hWRDIVL := $4204
hWRDIVH := $4205
hWRDIVB := $4206
hHTIMEL := $4207
hHTIMEH := $4208
hVTIMEL := $4209
hVTIMEH := $420a
hMDMAEN := $420b
hHDMAEN := $420c
hMEMSEL := $420d
hRDNMI := $4210
hTIMEUP := $4211
hHVBJOY := $4212
hRDIO := $4213
hRDDIVL := $4214
hRDDIVH := $4215
hRDMPYL := $4216
hRDMPYH := $4217
hSTDCNTRL1L := $4218
hSTDCNTRL1H := $4219
hSTDCNTRL2L := $421a
hSTDCNTRL2H := $421b
hSTDCNTRL3L := $421c
hSTDCNTRL3H := $421d
hSTDCNTRL4L := $421e
hSTDCNTRL4H := $421f
; ------------------------------------------------------------------------------
; [ dma registers ]
hDMAP0 := $4300
hDMAB0 := $4301
hDMAAL0 := $4302
hDMAAH0 := $4303
hDMAAB0 := $4304
hDMADL0 := $4305
hDMADH0 := $4306
hDMADB0 := $4307
hDMATL0 := $4308
hDMATH0 := $4309
hDMAL0 := $430a
hDMAP1 := $4310
hDMAB1 := $4311
hDMAAL1 := $4312
hDMAAH1 := $4313
hDMAAB1 := $4314
hDMADL1 := $4315
hDMADH1 := $4316
hDMADB1 := $4317
hDMATL1 := $4318
hDMATH1 := $4319
hDMAL1 := $431a
hDMAP2 := $4320
hDMAB2 := $4321
hDMAAL2 := $4322
hDMAAH2 := $4323
hDMAAB2 := $4324
hDMADL2 := $4325
hDMADH2 := $4326
hDMADB2 := $4327
hDMATL2 := $4328
hDMATH2 := $4329
hDMAL2 := $432a
hDMAP3 := $4330
hDMAB3 := $4331
hDMAAL3 := $4332
hDMAAH3 := $4333
hDMAAB3 := $4334
hDMADL3 := $4335
hDMADH3 := $4336
hDMADB3 := $4337
hDMATL3 := $4338
hDMATH3 := $4339
hDMAL3 := $433a
hDMAP4 := $4340
hDMAB4 := $4341
hDMAAL4 := $4342
hDMAAH4 := $4343
hDMAAB4 := $4344
hDMADL4 := $4345
hDMADH4 := $4346
hDMADB4 := $4347
hDMATL4 := $4348
hDMATH4 := $4349
hDMAL4 := $434a
hDMAP5 := $4350
hDMAB5 := $4351
hDMAAL5 := $4352
hDMAAH5 := $4353
hDMAAB5 := $4354
hDMADL5 := $4355
hDMADH5 := $4356
hDMADB5 := $4357
hDMATL5 := $4358
hDMATH5 := $4359
hDMAL5 := $435a
hDMAP6 := $4360
hDMAB6 := $4361
hDMAAL6 := $4362
hDMAAH6 := $4363
hDMAAB6 := $4364
hDMADL6 := $4365
hDMADH6 := $4366
hDMADB6 := $4367
hDMATL6 := $4368
hDMATH6 := $4369
hDMAL6 := $436a
hDMAP7 := $4370
hDMAB7 := $4371
hDMAAL7 := $4372
hDMAAH7 := $4373
hDMAAB7 := $4374
hDMADL7 := $4375
hDMADH7 := $4376
hDMADB7 := $4377
hDMATL7 := $4378
hDMATH7 := $4379
hDMAL7 := $437a
; ------------------------------------------------------------------------------
; [ joypad button masks ]
JOY_A = %10000000
JOY_X = %01000000
JOY_L = %00100000
JOY_R = %00010000
JOY_B = %10000000
JOY_Y = %01000000
JOY_SELECT = %00100000
JOY_START = %00010000
JOY_UP = %00001000
JOY_DOWN = %00000100
JOY_LEFT = %00000010
JOY_RIGHT = %00000001
; ------------------------------------------------------------------------------
.list on

343
include/macros.inc Normal file
View File

@ -0,0 +1,343 @@
; +----------------------------------------------------------------------------+
; | |
; | FINAL FANTASY VI |
; | |
; +----------------------------------------------------------------------------+
; | file: macros.inc |
; | |
; | description: macro definitions |
; | |
; | created: 8/2/2022 |
; | |
; | author: everything8215@gmail.com |
; +----------------------------------------------------------------------------+
.list off
; ------------------------------------------------------------------------------
; [ clr_a/x/y ]
; clear accumulator, x, y registers
.macro clr_a
tdc
.endmacro
.macro clr_ax
tdc
tax
.endmacro
.macro clr_ay
tdc
tay
.endmacro
.macro clr_axy
tdc
tax
tay
.endmacro
.macro clr_ayx
tdc
tay
tax
.endmacro
; ------------------------------------------------------------------------------
; [ set the size of accumulator/index registers ]
PSW_A := %00100000
PSW_I := %00010000
PSW_C := %00000001
.macro longa
.a16
rep #PSW_A
.endmacro
.macro shorta
.a8
sep #PSW_A
.endmacro
.macro shorta0 ; battle code almost always clears
clr_a ; accumulator before a shorta
shorta
.endmacro
.macro longi
.i16
rep #PSW_I
.endmacro
.macro shorti
.i8
sep #PSW_I
.endmacro
.macro longai
.a16
.i16
rep #PSW_A|PSW_I
.endmacro
.macro shortai
.a8
.i8
sep #PSW_A|PSW_I
.endmacro
.macro longac
.a16
rep #PSW_A|PSW_C
.endmacro
.macro shortac
.a8
sep #PSW_A|PSW_C
.endmacro
; ------------------------------------------------------------------------------
; [ repeated no operation ]
.macro nop_n n
.repeat n
nop
.endrep
.endmacro
.define nop2 nop_n 2
.define nop3 nop_n 3
.define nop4 nop_n 4
.define nop5 nop_n 5
.define nop6 nop_n 6
.define nop7 nop_n 7
.define nop8 nop_n 8
; ------------------------------------------------------------------------------
; [ repeated shift left ]
.macro asl_n n
.repeat n
asl
.endrep
.endmacro
.define asl2 asl_n 2
.define asl3 asl_n 3
.define asl4 asl_n 4
.define asl5 asl_n 5
.define asl6 asl_n 6
.define asl7 asl_n 7
.define asl8 asl_n 8
; ------------------------------------------------------------------------------
; [ repeated shift right ]
.macro lsr_n n
.repeat n
lsr
.endrep
.endmacro
.define lsr2 lsr_n 2
.define lsr3 lsr_n 3
.define lsr4 lsr_n 4
.define lsr5 lsr_n 5
.define lsr6 lsr_n 6
.define lsr7 lsr_n 7
.define lsr8 lsr_n 8
; ------------------------------------------------------------------------------
; [ repeated rotate left ]
.macro rol_n n
.repeat n
rol
.endrep
.endmacro
.define rol2 rol_n 2
.define rol3 rol_n 3
.define rol4 rol_n 4
.define rol5 rol_n 5
.define rol6 rol_n 6
.define rol7 rol_n 7
.define rol8 rol_n 8
; ------------------------------------------------------------------------------
; [ repeated rotate right ]
.macro ror_n n
.repeat n
ror
.endrep
.endmacro
.define ror2 ror_n 2
.define ror3 ror_n 3
.define ror4 ror_n 4
.define ror5 ror_n 5
.define ror6 ror_n 6
.define ror7 ror_n 7
.define ror8 ror_n 8
; ------------------------------------------------------------------------------
; [ repeated increment ]
.macro inc_n n
.repeat n
inc
.endrep
.endmacro
.define inc2 inc_n 2
.define inc3 inc_n 3
.define inc4 inc_n 4
.define inc5 inc_n 5
.define inc6 inc_n 6
.define inc7 inc_n 7
.define inc8 inc_n 8
; ------------------------------------------------------------------------------
; [ repeated decrement ]
.macro dec_n n
.repeat n
dec
.endrep
.endmacro
.define dec2 dec_n 2
.define dec3 dec_n 3
.define dec4 dec_n 4
.define dec5 dec_n 5
.define dec6 dec_n 6
.define dec7 dec_n 7
.define dec8 dec_n 8
; ------------------------------------------------------------------------------
; [ repeated increment x ]
.macro inx_n n
.repeat n
inx
.endrep
.endmacro
.define inx2 inx_n 2
.define inx3 inx_n 3
.define inx4 inx_n 4
.define inx5 inx_n 5
.define inx6 inx_n 6
.define inx7 inx_n 7
.define inx8 inx_n 8
; ------------------------------------------------------------------------------
; [ repeated decrement x ]
.macro dex_n n
.repeat n
dex
.endrep
.endmacro
.define dex2 dex_n 2
.define dex3 dex_n 3
.define dex4 dex_n 4
.define dex5 dex_n 5
.define dex6 dex_n 6
.define dex7 dex_n 7
.define dex8 dex_n 8
; ------------------------------------------------------------------------------
; [ repeated increment y ]
.macro iny_n n
.repeat n
iny
.endrep
.endmacro
.define iny2 iny_n 2
.define iny3 iny_n 3
.define iny4 iny_n 4
.define iny5 iny_n 5
.define iny6 iny_n 6
.define iny7 iny_n 7
.define iny8 iny_n 8
; ------------------------------------------------------------------------------
; [ repeated decrement y ]
.macro dey_n n
.repeat n
dey
.endrep
.endmacro
.define dey2 dey_n 2
.define dey3 dey_n 3
.define dey4 dey_n 4
.define dey5 dey_n 5
.define dey6 dey_n 6
.define dey7 dey_n 7
.define dey8 dey_n 8
; ------------------------------------------------------------------------------
; [ make pointer table (absolute) ]
.macro make_ptr_tbl_abs label, length
.repeat length, i
.addr .ident(.sprintf("%s_%04x", .string(label), i))
.endrep
.endmacro
; ------------------------------------------------------------------------------
; [ make pointer table (relative) ]
.macro make_ptr_tbl_rel label, length, offset
.repeat length, i
.ifblank offset
.addr .ident(.sprintf("%s_%04x", .string(label), i)) - label
.else
.addr .ident(.sprintf("%s_%04x", .string(label), i)) - offset
.endif
.endrep
.endmacro
; ------------------------------------------------------------------------------
; [ make pointer table (far) ]
.macro make_ptr_tbl_far label, length, offset
.repeat length, i
.ifblank offset
.faraddr .ident(.sprintf("%s_%04x", .string(label), i)) - label
.else
.faraddr .ident(.sprintf("%s_%04x", .string(label), i)) - offset
.endif
.endrep
.endmacro
; ------------------------------------------------------------------------------
.list on

26
menu/Makefile Normal file
View File

@ -0,0 +1,26 @@
NAME = menu
OBJ_DIR = obj
TARGET = $(OBJ_DIR)/$(NAME)_$(VERSION_EXT).o
SRC_MAIN = $(NAME).asm
SRC_FILES = $(wildcard *.asm)
INC_DIR = ../include
INC_FILES = $(wildcard $(INC_DIR)/*.inc)
DATA_FILES = $(wildcard data/*)
GFX_FILES = $(wildcard gfx/*)
TEXT_FILES = $(wildcard text/*)
.PHONY: all clean
# disable default suffix rules
.SUFFIXES:
all: $(TARGET)
clean:
$(RM) -r $(OBJ_DIR) data gfx text
$(TARGET): $(SRC_FILES) $(INC_FILES) $(DATA_FILES) $(GFX_FILES) $(TEXT_FILES)
@mkdir -p $(OBJ_DIR)
$(ASM) $(ASMFLAGS) -I $(INC_DIR) -l $(@:o=lst) $(SRC_MAIN) -o $@

808
menu/menu.asm Normal file
View File

@ -0,0 +1,808 @@
; +----------------------------------------------------------------------------+
; | |
; | FINAL FANTASY VI |
; | |
; +----------------------------------------------------------------------------+
; | file: menu.asm |
; | |
; | description: menu program |
; | |
; | created: 8/2/2022 |
; +----------------------------------------------------------------------------+
.p816
.include "const.inc"
.include "hardware.inc"
.include "macros.inc"
.include "menu_data.asm"
.import ExecSound_ext
.export OpenMenu_ext, UpdateCtrlBattle_ext, InitCtrl_ext, UpdateCtrlField_ext
.export IncGameTime_ext, LoadSavedGame_ext, EndingCutscene_ext
.export OptimizeEquip_ext, EndingAirshipScene_ext
; ------------------------------------------------------------------------------
.segment "menu_code"
.a8
.i16
; ------------------------------------------------------------------------------
OpenMenu_ext:
@0000: jmp OpenMenu
UpdateCtrlBattle_ext:
@0003: jmp UpdateCtrlBattle
InitCtrl_ext:
@0006: jmp InitCtrl
UpdateCtrlField_ext:
@0009: jmp UpdateCtrlField
IncGameTime_ext:
@000c: jmp IncGameTime
LoadSavedGame_ext:
@000f: jmp LoadSavedGame
EndingCutscene_ext:
@0012: jmp EndingCutscene
OptimizeEquip_ext:
@0015: jmp OptimizeEquip
EndingAirshipScene_ext:
@0018: jmp EndingAirshipScene
; ------------------------------------------------------------------------------
; [ open menu ]
OpenMenu:
@001b: longi
shorta
lda #$00 ; set data bank
pha
plb
ldx #$0000 ; set direct page
phx
pld
ldx #$0000 ; set $00
stx $00
lda #$7e
sta $2183 ; set wram bank
jsr InitInterrupts
lda $0200
cmp #$02
bne @003f ; branch if not opening game load menu
jsr InitSaveSlot
@003f: jsr InitMenu
jsr OpenMenuType
jsl InitCtrl
lda #$8f
sta $2100
stz $4200
stz $420b
stz $420c
lda $0200
cmp #$02
bne @006f ; branch if not restoring a saved game
lda $0205
bpl @006f ; branch if tent/warp/warp stone was used
lda $1d4e
and #$20
beq @006f ; branch if stereo mode
; lda #$ff
; jsr $3e3f ; set mono/stereo mode
@006f: lda $0200
; bne @00bf ; branch if not main menu
; lda $0205
; bpl @00bf ; return if return code is positive
; cmp #$fe
; bne @009e ; branch if not using rename card
;
; ; rename card
; lda #$01
; sta $0200
; lda $0201
; sta $020f
; ldy $0206
; sty $0201
; jsl OpenMenu
; lda $020f
; sta $0201
; stz $0200
; jmp @001b
;
; ; swdtech renaming (ff6j)
; @009e: lda #$06
; sta $0200
; lda $0201
; sta $020f
; lda $0206
; sta $0201
; jsl OpenMenu
; lda $020f
; sta $0201
; stz $0200
; jmp @001b
; return from menu
@00bf: rtl
; ------------------------------------------------------------------------------
; [ set up interrupt jump code ]
InitInterrupts:
@00c0: lda #$5c
sta $1500
sta $1504
ldx #.loword(MenuNMI)
stx $1501
ldx #.loword(MenuIRQ)
stx $1505
lda #^MenuNMI
sta $1503
sta $1507
rts
; ------------------------------------------------------------------------------
; [ open menu (type) ]
OpenMenuType:
@00dd: tdc
lda $0200
asl
tax
jmp (.loword(MenuTypeTbl),x)
; menu type jump table
MenuTypeTbl:
.addr MainMenu
.addr NameChangeMenu
.addr GameLoadMenu
.addr ShopMenu
.addr PartySelectMenu
.addr ItemDetailsMenu
.addr BushidoNameMenu
.addr ColosseumMenu
.addr FinalBattleOrderMenu
; ------------------------------------------------------------------------------
; [ init menu ]
InitMenu:
@00f8: ; jsr $68fa ; init menu ram
; jsr $6915 ; init party character data
; jsr $69a9 ;
; jsl $d4cd48 ; init menu hardware registers
jsl InitCtrl
tdc
; jsl $d4ca1d ; init hdma for menu window gradient bars
; jsr $69dc ; init window 1 position hdma
; jsr $1114 ; init threads
; jsr $6a87 ; init menu graphics
; jmp @3a87 ; init custom menu window
rts
; ------------------------------------------------------------------------------
; [ menu type $00: main menu ]
MainMenu:
@011a: lda #$04 ; menu state $04 (main menu init)
sta $26
jmp MenuLoop
; ------------------------------------------------------------------------------
; [ menu type $03: shop ]
ShopMenu:
@0121: ; jsr $3f99 ; init font color
lda #$24 ; menu state $24 (shop init)
sta $26
jmp MenuLoop
; ------------------------------------------------------------------------------
; [ menu type $04: party select ]
PartySelectMenu:
@012b: ; jsr $3f99 ; init font color
; jsr $0138 ; clear previous cursor position
lda #$2c ; menu state $2c (party select init)
sta $26
jmp MenuLoop
; ------------------------------------------------------------------------------
; [ clear previous cursor position ]
@0138: tdc
tay
sty $8e
rts
; ------------------------------------------------------------------------------
; [ menu type $05: item details ??? (unused) ]
ItemDetailsMenu:
@013d: ; jsr $3f99 ; init font color
; jsr $0138 ; clear previous cursor position
lda #$2f ; menu state $2f (item details)
sta $26
bra MenuLoop
; ------------------------------------------------------------------------------
; [ menu type $06: swdtech renaming (ff6j) ]
BushidoNameMenu:
@0149: ; jsr $3f99 ; init font color
lda #$3f ; menu state $3f
sta $26
bra MenuLoop
; ------------------------------------------------------------------------------
; [ menu type $07: colosseum ]
ColosseumMenu:
@0152: ; jsr $3f99 ; init font color
stz $79
stz $7a
stz $7b
lda #$ff
sta $0205
lda #$71 ; menu state $71 (colosseum item select init)
sta $26
bra MenuLoop
; ------------------------------------------------------------------------------
; [ menu type $08: final battle order ]
FinalBattleOrderMenu:
; @0166: jsr $3f99 ; init font color
lda #$73 ; menu state $73 (final battle order init)
sta $26
bra MenuLoop
; ------------------------------------------------------------------------------
; [ menu type $02: restore game ]
GameLoadMenu:
@016f: lda $307ff1 ; increment random number seed
inc
sta $307ff1
jsl InitCtrl
jsr CheckSRAM
; bcc @019f ; branch if sram is invalid
; lda #$01 ; song $01 (the prelude)
; sta $1301
; lda #$10
; sta $1300
; lda #$80
; sta $1302
; jsl ExecSound_ext
; lda #$ff
; sta $0205
; lda #$20 ; menu state $20 (restore game init)
; sta $26
; bra MenuLoop
; sram invalid
; @019f: jsr $2a21 ; clear game time
lda #1
sta $0224 ; current saved game slot = 1
stz $021f ; don't load a saved game
lda #$ff
sta $26 ; terminate menu
stz $0205 ; clear return code
bra MenuLoop
; ------------------------------------------------------------------------------
; [ menu type $01: name change ]
NameChangeMenu:
@01b3: ; jsr $3f99 ; init font color
lda #$5d ; menu state $5d (name change init)
sta $26
; fall through
; ------------------------------------------------------------------------------
; [ menu state loop ]
MenuLoop:
; @01ba: jsr $1412 ; update hardware registers
@01bd: tdc
; lda $26 ; return if menu state is $ff
; cmp #$ff
; beq @01d8
; longa
; asl
; tax
; shorta
; jsr ($01db,x) ; do menu state code
; jsr $11b0 ; execute threads
; jsr $134d ; wait for next frame
; jsr $02db ; check timer 0
; bra @01bd
@01d8: stz $43 ; disable hdma
rts
; ------------------------------------------------------------------------------
; [ menu nmi ]
MenuNMI:
@1387: rti
; ------------------------------------------------------------------------------
; [ menu irq ]
MenuIRQ:
@13c7: rti
; ------------------------------------------------------------------------------
; [ increment game time ]
IncGameTime:
@13c8: rtl
; ------------------------------------------------------------------------------
LoadSavedGame:
@14fe: lda $021f ; branch if not loading a saved game
; beq @1514
; jsr $1566 ; load saved game data
; jsr $19d1 ; calculate saved game data checksum
; jsr $19eb ; check saved game data checksum
; beq @1514 ; branch if checksum invalid
; jsr $1595 ; restore timer data
; tdc ; return $00
; bra @1518
@1514: shorta
lda #$ff ; return $ff
@1518: sta $0205
tdc
rtl
; ------------------------------------------------------------------------------
; [ load window palette ]
LoadWindowPal:
@6bbc: ldx #8
stx $e7
ldx #0
txy
longa
@6bc7: lda #7
sta $e9
@6bcc: lda f:WindowPal+2,x ; load wallpaper palettes
sta $1d57,y
inx2
iny
iny
dec $e9
bne @6bcc
txa
clc
adc #$0012
tax
dec $e7
bne @6bc7
shorta
rts
; ------------------------------------------------------------------------------
; [ check if sram is valid ]
CheckSRAM:
@7023: longa
lda #$e41b ; fixed value
cmp $307ff8
beq @7047
cmp $307ffa
beq @7047
cmp $307ffc
beq @7047
cmp $307ffe
beq @7047
shorta
jsr ClearSRAM
clc
rts
@7047: shorta
sec
rts
; ------------------------------------------------------------------------------
; [ clear sram ]
ClearSRAM:
@704b: phb
lda #$30
pha
plb
ldx #0
longa
@7055: stz $6000,x ; clear 30/6000-30/7fff
inx
inx
stz $6000,x
inx2
cpx #$2000
bne @7055
shorta
plb
rts
; ------------------------------------------------------------------------------
; [ init saved menu cursor positions ]
_c37068:
@7068: ldx #0
@706b: stz $022b,x ; clear saved menu cursor positions
inx
cpx #$001f
bne @706b
lda #$01 ; character skills cursors default to magic
sta $0237
sta $0239
sta $023b
sta $023d
rts
; ------------------------------------------------------------------------------
; [ make sram valid ]
ValidateSRAM:
@7083: longa
lda #$e41b ; set fixed value
sta $307ff8
sta $307ffa
sta $307ffc
sta $307ffe
shorta
rts
; ------------------------------------------------------------------------------
; [ init saved game data ]
InitSaveSlot:
@709b: jsr _c37068
ldy #$7fff ; set default font color
sty $1d55
lda #$12 ; set default button mappings
sta $1d50
lda #$34
sta $1d51
lda #$56
sta $1d52
lda #$06
sta $1d53
lda #$2a ;
sta $1d4d
tdc
tay
sty $1dc7 ;
stz $1d54
stz $1d4e
stz $1d4f
sty $1863 ; clear game time
stz $1865
sty $1860 ; clear gp
stz $1862
sty $1866 ; clear steps
stz $1868
sty $021b ; clear menu game time
sty $021d
jsr LoadWindowPal
rts
; ------------------------------------------------------------------------------
OptimizeEquip:
@96d2: rtl
; ------------------------------------------------------------------------------
; [ init controller ]
InitCtrl:
@a424: lda #$08 ; set direction button repeat delay (8 frames)
sta $0229
lda #$03 ; set button repeat rate (3 frames/repeat)
sta $022a
sta $0226
lda #$20 ; set a button repeat delay (32 frames)
sta $0225
lda $1d54 ; branch if no special button configuration
and #$40
beq @a441
jsr SetCustomBtnMap
rtl
@a441: jsr SetDefaultBtnMap
rtl
; ------------------------------------------------------------------------------
; [ update controller (battle) ]
UpdateCtrlBattle:
@a445: lda $1d54
bpl @a458
tdc
lda $0201
tax
lda $1d4f
and f:_c3a53d,x
bne @a45d
@a458: ldx hSTDCNTRL1L
bra @a462
@a45d: ldx hSTDCNTRL2L
bra @a462
@a462: jsr _c3a483
jsr _c3a4bd
jsr _c3a4f6
jmp _c3a527
; ------------------------------------------------------------------------------
; [ update controller (menu) ]
UpdateCtrlMenu:
@a46e: ldx hSTDCNTRL1L
jsr _c3a483
jsr _c3a4bd
jmp _c3a527
; ------------------------------------------------------------------------------
; [ update controller (field/world) ]
UpdateCtrlField:
@a47a: ldx hSTDCNTRL1L
jsr _c3a483
jmp _c3a527
; ------------------------------------------------------------------------------
; [ ]
joy_getsub:
_c3a483:
@a483: ldy $e0 ; push dp variables
sty $0213
ldy $e7
sty $0215
ldy $e9
sty $0217
ldy $eb
sty $0219
stx $eb
longa
lda $0c
and #$fff0
sta $e0
jsr _c3a541
lda $0c
eor #$ffff
and $06
sta $08
ldy $06
sty $0c
lda hSTDCNTRL1L
ora hSTDCNTRL2L
sta $04
shorta
rts
; ------------------------------------------------------------------------------
; [ ]
_c3a4bd:
@a4bd: longa
lda $06
and #$fff0
cmp $e0
shorta
bne @a4e5
lda $0227
beq @a4d4
dec $0227
bne @a4f1
@a4d4: dec $0228
bne @a4f1
lda $022a
sta $0228
ldy $06
sty $0a
bra @a4f5
@a4e5: lda $0229
sta $0227
lda $022a
sta $0228
@a4f1: ldy $08
sty $0a
@a4f5: rts
; ------------------------------------------------------------------------------
; [ ]
_c3a4f6:
@a4f6: lda $06
bit #$80
beq @a517
lda $0225
beq @a506
dec $0225
bne @a522
@a506: dec $0226
bne @a522
lda $022a
sta $0226
lda #$80
tsb $0a
bra @a526
@a517: lda #$20
sta $0225
lda $022a
sta $0226
@a522: lda $08
sta $0a
@a526: rts
; ------------------------------------------------------------------------------
; [ pop dp variables used for joypad ]
joy_sub3:
_c3a527:
@a527: ldy $0213
sty $e0
ldy $0215
sty $e7
ldy $0217
sty $e9
ldy $0219
sty $eb
tdc
rtl
; ------------------------------------------------------------------------------
_c3a53d:
@a53d: .byte $01,$02,$04,$08
; ------------------------------------------------------------------------------
; [ ]
.a16
_c3a541:
@a541: lda $eb
and #$0f00
sta $06
lda #$0080
sta $e7
lda #$8000
sta $e9
ldy $00
jsr _c3a581
lda #$0040
sta $e7
lda #$4000
sta $e9
iny
jsr _c3a581
lda #$0020
sta $e7
lda #$0010
sta $e9
iny
jsr _c3a581
lda #$1000
sta $e7
lda #$2000
sta $e9
iny
jmp _c3a581
.a8
; ------------------------------------------------------------------------------
; [ ]
_c3a581:
@a581: lda $eb
bit $e7
beq @a59b
tdc
shorta
lda $0220,y
and #$f0
longa
lsr3
tax
lda f:_c3a5b4,x
tsb $06
@a59b: lda $eb
bit $e9
beq @a5b3
tdc
shorta
lda $0220,y
and #$0f
longa
asl
tax
lda _c3a5b4,x
tsb $06
@a5b3: rts
; ------------------------------------------------------------------------------
_c3a5b4:
@a5b4: .word $1000,$0080,$8000,$0040,$4000,$0020,$0010,$2000
; ------------------------------------------------------------------------------
; [ set custom button mapping ]
SetCustomBtnMap:
@a5c4: ldy $1d50
sty $0220
ldy $1d52
sty $0222
rts
; ------------------------------------------------------------------------------
; [ set default button mapping ]
SetDefaultBtnMap:
@a5d1: ldy #$3412
sty $0220
ldy #$0656
sty $0222
rts
; ------------------------------------------------------------------------------
EndingCutscene:
@c51c: rtl
; ------------------------------------------------------------------------------
EndingAirshipScene:
@c51c: rtl
; ------------------------------------------------------------------------------

26
menu/menu_data.asm Normal file
View File

@ -0,0 +1,26 @@
; +----------------------------------------------------------------------------+
; | |
; | FINAL FANTASY VI |
; | |
; +----------------------------------------------------------------------------+
; | file: menu_data.asm |
; | |
; | description: data for menu module |
; | |
; | created: 8/12/2022 |
; +----------------------------------------------------------------------------+
.export WindowGfx, WindowPal
; ------------------------------------------------------------------------------
.segment "window_gfx"
; ed/0000
.include "gfx/window_gfx.asm"
; ed/1c00
.include "gfx/window_pal.asm"
; ------------------------------------------------------------------------------

File diff suppressed because it is too large Load Diff

View File

@ -303,7 +303,7 @@ EF9D17-EFB630 Serpent Trench Map Formation (compressed)
EFB631-EFC623 Serpent Trench Graphics (compressed)
EFC624-EFCE76 Some Chocobo Graphics (compressed)
EFCE77-EFCE96 Vector Approach Palette
EFCE97-EFCEB6 Data for Esper Terra
EFCE97-EFCEB6 Esper Terra Palette
EFCEB7-EFCFB8 Airship Shadow and Perspective Graphics (compressed)
EFCFB9-EFDC4B Various Sprites (ship, esper terra, figaro, etc., compressed)
EFDC4C-EFE49A More Chocobo Graphics (world map, compressed)

View File

@ -22,34 +22,34 @@ $0000-$00FF direct page
l: Left direction down
r: Right direction down
$19
$19
+$1E
$20
+$1E
$20
$22 target screen brightness
$23 current screen brightness (0-F)
$24 vblank flag
+$26 forward movement speed
++$29 rotation speed
+$2F altitude
$31
$31
+$34 current X position (in pixels)
+$38 current Y position (in pixels)
++$3A rotation angle (high 16-bits active)
+$3D m7a
+$3F m7b
+$41 m7c
+$43 m7d
+$44
+$46
+$44
+$46
$58-$64 scratchpad
$60 fdd?????
@ -60,7 +60,7 @@ $58-$64 scratchpad
u: vehicle is moving up
l: vehicle is turning left
r: vehicle is turning right
+$6A scratchpad
+$73 rotation frame counter
@ -69,14 +69,14 @@ $58-$64 scratchpad
+$79 bg1 vertical scroll
+$7B map horizontal scroll position, or maybe an offset based on the map sector ???
+$7D map vertical scroll position ???
+$83
+$85
+$87
+$89
$8B
+$83
+$85
+$87
+$89
$8B
$8C zoom level ??
$9F HDMA #4 (M7A) line count 1 (124 lines, no repeat)
+$A0 HDMA #4 (M7A) data pointer 1
$A2 HDMA #4 (M7A) line count 2 (100 lines, no repeat)
@ -103,14 +103,23 @@ $58-$64 scratchpad
+$C4 current tile index
+$C6 current X position (in pixels)
+$C8 current Y position (in pixels)
$CA character 1 graphic (00 = none, 01 = falcon, 02 = chocobo ???, 03 = character, 04 = ???, 05 = ship, 0C = esper terra, 12 = bird, 16 = smoking airship)
$CA character 1 graphic
00: none
01: falcon
02: chocobo ???
03: character
04: ???
05: ship
0C: esper terra
12: bird
16: smoking airship
$CB character 2 graphic
$CC character 3 graphic
$CD character 4 graphic
++$D2 decompression source address
++$D5 decompression destination address
+$DF X position (in pixels * 256)
+$E1 Y position (in pixels * 256)
+$E3 X movement speed (signed)
@ -131,30 +140,30 @@ $58-$64 scratchpad
$EF movement distance/pause counter
$F0 current event command
$F1 frame counter for pause event command
$F2
$F2
$F3 character movement speed
+$F4 destination map index ???
$F6 facing direction (0 = up, 1 = right, 2 = down, 3 = left)
$F7 graphical action
$FA 4 frame counter (magitek train ride)
+$FA origin X
+$FC origin Y
$0140-$09FF mode 7 HDMA data
$0A00-$0AFF dp stack
$0B00-$0B0F
$0B00-$0B0F
$11F0
$11F2
$11F3
+$11F4
$11F0
$11F2
$11F3
+$11F4
+$11F6 ???????? ?????gbm
g: map graphics are already loaded
b: enable battle
m: hide mini-map
$11F8
$11F8
$11F9 battle bg index
$11FA vehicle index (0 = none, 1 = airship, 2 = chocobo)
$11FB showing character's graphic index
@ -179,7 +188,7 @@ $1F81-$1FA0 saved object map indexes
$1FA2 random number pointer for monster formation
$1FA3 random number counter for monster formation
$1FA4 random number counter for random monster battle
$1FA5
$1FA5
+$1FA6 pointer to current showing character's object data
$1FA8-$1FBF saved counter data
+$1FC0 party XY position
@ -201,10 +210,10 @@ $9350-$93DF map tile palette assignments
$B5D0-$B64F
$B650 walking animation position (0-3)
$B750-$B84F water tile graphics
$B850-$B85F water tile movement counters (1 per line)
$B860
$B860
$E000-$E200 color palettes

116
package-lock.json generated Normal file
View File

@ -0,0 +1,116 @@
{
"name": "ff6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"crc-32": "^1.2.2",
"is-number": "^7.0.0",
"is-string": "^1.0.7",
"isarray": "^2.0.5"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"dependencies": {
"has-symbols": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-string": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
"integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
"dependencies": {
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
}
},
"dependencies": {
"crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="
},
"has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
},
"has-tostringtag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"requires": {
"has-symbols": "^1.0.2"
}
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
"is-string": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
"integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
"requires": {
"has-tostringtag": "^1.0.0"
}
},
"isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
}
}
}

8
package.json Normal file
View File

@ -0,0 +1,8 @@
{
"dependencies": {
"crc-32": "^1.2.2",
"is-number": "^7.0.0",
"is-string": "^1.0.7",
"isarray": "^2.0.5"
}
}

26
sound/Makefile Normal file
View File

@ -0,0 +1,26 @@
NAME = sound
OBJ_DIR = obj
TARGET = $(OBJ_DIR)/$(NAME)_$(VERSION_EXT).o
SRC_MAIN = $(NAME).asm
SRC_FILES = $(wildcard *.asm)
INC_DIR = ../include
INC_FILES = $(wildcard $(INC_DIR)/*.inc)
DATA_FILES = $(wildcard data/*)
GFX_FILES = $(wildcard gfx/*)
TEXT_FILES = $(wildcard text/*)
.PHONY: all clean
# disable default suffix rules
.SUFFIXES:
all: $(TARGET)
clean:
$(RM) -r $(OBJ_DIR) data gfx text
$(TARGET): $(SRC_FILES) $(INC_FILES) $(DATA_FILES) $(GFX_FILES) $(TEXT_FILES)
@mkdir -p $(OBJ_DIR)
$(ASM) $(ASMFLAGS) -I $(INC_DIR) -l $(@:o=lst) $(SRC_MAIN) -o $@

66
sound/sound.asm Normal file
View File

@ -0,0 +1,66 @@
; +----------------------------------------------------------------------------+
; | |
; | FINAL FANTASY VI |
; | |
; +----------------------------------------------------------------------------+
; | file: sound.asm |
; | |
; | description: sound program |
; | |
; | created: 8/2/2022 |
; +----------------------------------------------------------------------------+
.p816
.include "const.inc"
.include "hardware.inc"
.include "macros.inc"
.export InitSound_ext, ExecSound_ext, TfrSong_ext, TfrSamples_ext
; ------------------------------------------------------------------------------
.segment "sound_code"
.a8
.i16
; ------------------------------------------------------------------------------
InitSound_ext:
@0000: jmp InitSound
nop
ExecSound_ext:
@0004: jmp ExecSound
nop
TfrSong_ext:
@0008: jmp TfrSong
nop
TfrSamples_ext:
@000c: jmp TfrSamples
nop
; ------------------------------------------------------------------------------
InitSound:
@0034: rtl
; ------------------------------------------------------------------------------
ExecSound:
@0131: rtl
; ------------------------------------------------------------------------------
TfrSong:
@031c: rtl
; ------------------------------------------------------------------------------
TfrSamples:
@0374: rtl
; ------------------------------------------------------------------------------

50
tools/calc-checksum.js Normal file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env node
const fs = require('fs');
const hexString = require('./romtools/hex-string');
function calcSum(data) {
let sum = 0;
for (let i = 0; i < data.length; i++) sum += data[i];
return sum & 0xFFFF;
}
function mirrorSum(data, mask) {
mask = mask || 0x800000;
while (!(data.length & mask)) mask >>= 1;
const part1 = calcSum(data.slice(0, mask));
let part2 = 0;
let nextLength = data.length - mask;
if (nextLength) {
part2 = mirrorSum(data.slice(mask), nextLength, mask >> 1);
while (nextLength < mask) {
nextLength += nextLength;
part2 += part2;
}
}
return (part1 + part2) & 0xFFFF;
}
// load the ROM file
const romPath = process.argv[2];
const romBuffer = fs.readFileSync(romPath);
const romData = new Uint8Array(romBuffer);
// calculate the SNES checksum
const checksum = mirrorSum(romData);
const checksumInverse = checksum ^ 0xFFFF;
console.log(`SNES Checksum: ${hexString(checksum, 4)}`);
// set the checksum in the SNES header and write to the ROM file
romData[0xFFDC] = checksumInverse & 0xFF;
romData[0xFFDD] = checksumInverse >> 8;
romData[0xFFDE] = checksum & 0xFF;
romData[0xFFDF] = checksum >> 8;
fs.writeFileSync(romPath, romData);
process.exit(0);

65
tools/decode-ff6.js Normal file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env node
const fs = require('fs');
const CRC32 = require('crc-32');
const ROMDataCodec = require('./romtools/data-codec');
const ROMDecoder = require('./romtools/rom-decoder');
const ROMRange = require('./romtools/range');
const hexString = require('./romtools/hex-string');
const romInfoListFF4 = {
0x45EF5AC8: {
name: 'Final Fantasy VI 1.0 (J)',
ripPath: 'vanilla/ff6-jp-rip.json',
dataPath: 'ff6-jp-data.json'
},
0xA27F1C7A: {
name: 'Final Fantasy III 1.0 (U)',
ripPath: 'vanilla/ff6-en-rip.json',
dataPath: 'ff6-en-data.json'
},
0xC0FA0464: {
name: 'Final Fantasy III 1.1 (U)',
ripPath: 'vanilla/ff6-en-rip.json',
dataPath: 'ff6-en-data.json'
}
}
// search the vanilla directory for valid ROM files
const files = fs.readdirSync('vanilla');
let foundOneROM = false;
for (let filename of files) {
const filePath = `vanilla/${filename}`;
if (fs.statSync(filePath).isDirectory()) continue;
const fileBuf = fs.readFileSync(filePath);
const crc32 = CRC32.buf(fileBuf) >>> 0;
const romInfo = romInfoListFF4[crc32];
if (!romInfo) continue;
console.log(`Found ROM: ${romInfo.name}`);
console.log(`File: ${filePath}`);
// load the definition file
const ripDefinitionFile = fs.readFileSync(romInfo.ripPath);
const ripDefinition = JSON.parse(ripDefinitionFile);
const decoder = new ROMDecoder(ripDefinition);
// load the ROM file
const romData = new Uint8Array(fs.readFileSync(filePath));
const romObj = decoder.decodeROM(romData, ripDefinition);
romObj.crc32 = hexString(crc32, 8);
fs.writeFileSync(romInfo.dataPath, JSON.stringify(romObj, null, 2));
foundOneROM = true;
}
if (!foundOneROM) {
console.log('No valid ROM files found!\nPlease copy your valid FF6 ROM ' +
'file(s) into the "vanilla" directory.\nIf your ROM has a header, ' +
'please remove it first.');
}
process.exit(0);

18
tools/encode-ff6.js Normal file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env node
const fs = require('fs');
const ROMEncoder = require('./romtools/rom-encoder');
const ROMMemoryMap = require('./romtools/memory-map');
const ROMRange = require('./romtools/range');
const ROMDataCodec = require('./romtools/data-codec');
// load the data file
const dataPath = process.argv[2];
const romDefinitionFile = fs.readFileSync(dataPath);
const romDefinition = JSON.parse(romDefinitionFile);
const encoder = new ROMEncoder(romDefinition);
encoder.encodeROM(romDefinition);
process.exit(0);

1321
tools/romtools/data-codec.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
// data-manager.js
const ROMTextCodec = require('./text-codec');
const ROMMemoryMap = require('./memory-map');
class ROMDataManager {
constructor(definition) {
this.definition = definition;
// create the memory mapper
const mapMode = definition.mode || ROMMemoryMap.MapMode.none;
this.memoryMap = new ROMMemoryMap(mapMode);
// create text codecs
this.textCodec = {};
for (let key in definition.textEncoding) {
const encodingDef = definition.textEncoding[key];
this.textCodec[key] = new ROMTextCodec(encodingDef, definition.charTable);
}
// create string tables
}
getDefinition(key) {
return this.definition.assembly[key];
}
getObject(key) {
return this.definition.obj[key];
}
}
module.exports = ROMDataManager;

1829
tools/romtools/gfx.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
const isNumber = require('is-number');
function hexString(num, pad, prefix) {
if (prefix === undefined) prefix = '0x';
if (num < 0) num = 0xFFFFFFFF + num + 1;
let hex = num.toString(16).toUpperCase();
if (isNumber(pad)) {
hex = hex.padStart(pad, '0');
} else if (num < 0x0100) {
hex = hex.padStart(2, '0');
} else if (num < 0x010000) {
hex = hex.padStart(4, '0');
} else if (num < 0x01000000) {
hex = hex.padStart(6, '0');
} else {
hex = hex.padStart(8, '0');
}
return (prefix + hex);
}
module.exports = hexString;

View File

@ -0,0 +1,138 @@
const ROMRange = require('./range');
class ROMMemoryMap {
constructor(mode) {
this.mode = mode;
}
bankSize() {
switch (this.mode) {
case ROMMemoryMap.MapMode.mmc1: return 0x4000;
case ROMMemoryMap.MapMode.mmc3: return 0x2000;
case ROMMemoryMap.MapMode.loROM: return 0x8000;
case ROMMemoryMap.MapMode.hiROM: return 0x10000;
case ROMMemoryMap.MapMode.x16: return 0x2000;
case ROMMemoryMap.MapMode.wsc: return 0x10000;
default: return 0x10000;
}
}
// convert CPU address to ROM file address
mapAddress(address) {
switch (this.mode) {
case ROMMemoryMap.MapMode.mmc1:
var bank = address & 0xFF0000;
return (bank >> 2) + (address & 0x3FFF) + 0x10;
case ROMMemoryMap.MapMode.mmc3:
var bank = address & 0xFF0000;
return (bank >> 3) + (address & 0x1FFF) + 0x10;
case ROMMemoryMap.MapMode.loROM:
var bank = address & 0xFF0000;
return (bank >> 1) + (address & 0x7FFF);
case ROMMemoryMap.MapMode.hiROM:
if (address >= 0xC00000) {
return address - 0xC00000;
} else if (address >= 0x800000) {
return address - 0x800000;
} else {
return address;
}
case ROMMemoryMap.MapMode.gba:
if (address >= 0x08000000) {
return address - 0x08000000;
} else {
return address;
}
case ROMMemoryMap.MapMode.x16:
var bank = address & 0xFF0000;
bank -= 0x010000;
return (bank >> 3) + (address & 0x1FFF) + 2;
case ROMMemoryMap.MapMode.wsc:
if (address < 0x10000000) return address; // raw address
var bank = (address >> 12) & 0xFF0000;
return bank + (address & 0xFFFF);
case ROMMemoryMap.MapMode.None:
default:
return address;
}
}
// convert ROM file address to CPU address
unmapAddress(address) {
switch (this.mode) {
case ROMMemoryMap.MapMode.mmc1:
address -= 0x10; // iNES header
var bank = (address << 2) & 0xFF0000;
return bank | (address & 0x3FFF) | 0x8000;
case ROMMemoryMap.MapMode.mmc3:
address -= 0x10; // iNES header
var bank = (address << 3) & 0xFF0000;
if (bank & 0x010000) {
return bank | (address & 0x1FFF) | 0xA000;
} else {
return bank | (address & 0x1FFF) | 0x8000;
}
case ROMMemoryMap.MapMode.loROM:
var bank = (address << 1) & 0xFF0000;
return bank | (address & 0x7FFF) | 0x8000;
case ROMMemoryMap.MapMode.hiROM:
return address + 0xC00000;
case ROMMemoryMap.MapMode.gba:
return address | 0x08000000;
case ROMMemoryMap.MapMode.x16:
address -= 2; // header
var bank = (address << 3) & 0xFF0000;
bank += 0x010000;
return bank | (address & 0x1FFF) | 0xA000;
case ROMMemoryMap.MapMode.wsc:
if (address > 0x0F0000) return address; // raw address
var bank = (address & 0x0F0000) << 12;
return bank | (address & 0xFFFF);
case ROMMemoryMap.MapMode.None:
default:
return address;
}
}
// convert CPU address (exclusive) to ROM file address (inclusive)
mapRange(range) {
const begin = this.mapAddress(range.begin);
const end = this.mapAddress(range.end);
return new ROMRange(begin, end);
}
unmapRange(range) {
const begin = this.unmapAddress(range.begin);
const end = this.unmapAddress(range.end);
return new ROMRange(begin, end);
}
}
ROMMemoryMap.MapMode = {
none: "none",
mmc1: "mmc1",
mmc3: "mmc3",
loROM: "loROM",
hiROM: "hiROM",
gba: "gba",
x16: "x16",
psx: "psx",
wsc: "wsc"
}
module.exports = ROMMemoryMap;

115
tools/romtools/range.js Normal file
View File

@ -0,0 +1,115 @@
const hexString = require('./hex-string');
const isNumber = require('is-number');
const isString = require('is-string');
class ROMRange {
constructor(begin, end) {
// beginning of range (must be positive)
this.begin = 0;
// end of range (included in range)
this.end = -1;
if (isNumber(begin)) {
// assume single value for now
this.begin = begin;
this.end = begin;
} else if (isString(begin)) {
const bounds = begin.split('-');
if (bounds.length == 1) {
// single value
this.begin = Number(begin);
if (!isNumber(this.begin)) {
console.log(`Invalid range: ${begin}`);
this.begin = 0;
}
this.end = this.begin;
} else if (bounds.length == 2) {
// multiple values
this.begin = Number(bounds[0]);
this.end = Number(bounds[1]);
if (!isNumber(this.begin) || !isNumber(this.end)) {
console.log(`Invalid range: ${begin}`);
this.begin = 0;
this.end = -1;
}
} else {
console.log(`Invalid range: ${begin}`);
}
}
// specified end value supercedes
if (isNumber(end)) {
this.end = end;
} else if (isString(end)) {
this.end = Number(end);
if (!isNumber(this.end)) {
console.log(`Invalid range: ${end}`);
this.end = this.begin - 1;
}
}
// validate range
// if (this.begin < 0) {
// console.log(`Invalid range begin: ${this.begin}`);
// this.begin = 0;
// }
// if (this.end < this.begin) {
// console.log(`Invalid range: ${this.begin}-${this.end}`);
// this.end = this.begin;
// }
}
toString(pad) {
if (pad) {
pad = Number(pad);
} else if (this.end < 0x0100) {
return `${this.begin}-${this.end}`;
} else if (this.end < 0x010000) {
pad = 4;
} else if (this.end < 0x01000000) {
pad = 6;
} else {
pad = 8;
}
return `${hexString(this.begin, pad)}-${hexString(this.end, pad)}`;
}
contains(i) {
// true if this range includes i
return (i >= this.begin && i <= this.end);
}
offset(offset) {
// return a new range offset from this range
return new ROMRange(this.begin + offset, this.end + offset);
}
intersection(range) {
// return a new range that includes the intersection of this range
// with another range (can be empty)
const begin = Math.max(range.begin, this.begin);
const end = Math.min(range.end, this.end);
return new ROMRange(begin, end);
}
isEmpty() {
// true if the array is empty
return (this.end < this.begin);
}
get length() {
// return the length of the range
return (this.end - this.begin + 1);
}
set length(newLength) {
// set the length of the range by changing end
this.end = this.begin + newLength - 1;
}
}
module.exports = ROMRange;

View File

@ -0,0 +1,369 @@
// rom-decoder.js
const fs = require('fs');
const getDirName = require('path').dirname;
const isString = require('is-string');
const ROMDataCodec = require('./data-codec');
const ROMMemoryMap = require('./memory-map');
const ROMRange = require('./range');
const ROMTextCodec = require('./text-codec');
const hexString = require('./hex-string');
class ROMDecoder {
constructor(definition) {
// create a memory mapper
const mapMode = definition.mode || ROMMemoryMap.MapMode.none;
this.memoryMap = new ROMMemoryMap(mapMode);
// create text codecs
this.textCodec = {};
for (let key in definition.textEncoding) {
const encodingDef = definition.textEncoding[key];
this.textCodec[key] = new ROMTextCodec(encodingDef, definition.charTable);
}
this.currentIndex = 0;
this.indexStack = [];
}
decodeROM(data, definition) {
// set the ROM file
this.romData = data;
definition.obj = {};
for (let key in definition.assembly) {
const objDefinition = definition.assembly[key];
if (!objDefinition.file) continue;
definition.obj[key] = this.decodeParentObject(objDefinition);
definition.assembly[key].isDirty = false;
}
return definition;
}
decodeParentObject(definition) {
// calculate the appropriate ROM range using the mapper
const unmappedRange = new ROMRange(definition.range);
const mappedRange = this.memoryMap.mapRange(unmappedRange);
const asmSymbol = definition.asmSymbol;
console.log(`${unmappedRange} ${asmSymbol} -> ${definition.file}`);
// extract the array data
const objData = this.romData.slice(mappedRange.begin, mappedRange.end + 1);
// make a list of pointers
let pointers = [];
this.currentIndex = 0;
if (definition.type !== 'array') {
// single object
pointers.push(0);
} else if (definition.pointerTable) {
// extract the pointer table
const ptrDefinition = definition.pointerTable;
const isMapped = ptrDefinition.isMapped;
let ptrOffset = Number(ptrDefinition.offset);
if (!isMapped) {
// map pointer offset first, then add pointers
ptrOffset = this.memoryMap.mapAddress(ptrOffset);
}
// extract the pointer table data
let ptrRange = new ROMRange(ptrDefinition.range);
ptrRange = this.memoryMap.mapRange(ptrRange);
const ptrData = this.romData.subarray(ptrRange.begin, ptrRange.end + 1);
const pointerSize = ptrDefinition.pointerSize || 2;
for (let i = 0; i < (ptrData.length / pointerSize); i++) {
let pointer = 0;
pointer |= ptrData[i * pointerSize];
pointer |= ptrData[i * pointerSize + 1] << 8;
if (pointerSize > 2) {
pointer |= ptrData[i * pointerSize + 2] << 16;
}
pointer += ptrOffset;
if (isMapped) {
// map pointer after adding to pointer offset
pointer = this.memoryMap.mapAddress(pointer);
}
pointers.push(pointer - mappedRange.begin);
}
} else if (definition.itemRanges) {
for (let i = 0; i < definition.arrayLength; i++) {
const range = new ROMRange(definition.itemRanges[i]);
const pointer = this.memoryMap.mapAddress(range.begin);
pointers.push(pointer - mappedRange.begin);
}
} else if (definition.terminator !== undefined) {
// terminated items
const terminator = Number(definition.terminator);
pointers.push(0);
for (let p = 0; p < (objData.length - 1); p++) {
if (objData[p] === terminator) {
pointers.push(p + 1);
}
}
} else if (definition.itemLength) {
// fixed length items
const length = definition.itemLength;
for (let i = 0; i < definition.arrayLength; i++) {
pointers.push(i * length);
}
}
// remove duplicates and sort pointers
const sortedPointers = [...new Set(pointers)].sort(function(a, b) {
return a - b;
});
// create a list of pointer ranges (these may not correspond
// with item ranges in some cases)
let pointerRanges = {};
for (let p = 0; p < sortedPointers.length; p++) {
const begin = sortedPointers[p];
let end = objData.length - 1;
if (p !== (sortedPointers.length - 1)) {
end = sortedPointers[p + 1] - 1;
}
pointerRanges[begin] = new ROMRange(begin, end);
}
const itemRanges = [];
const labelOffsets = {};
for (let i = 0; i < definition.arrayLength; i++) {
const begin = pointers[i];
if (definition.terminator !== undefined) {
let end = begin;
const terminator = Number(definition.terminator);
while (end < objData.length) {
if (objData[end] === terminator) break;
end++;
}
const range = new ROMRange(begin, end);
itemRanges.push(range);
} else if (definition.isSequential) {
let end = objData.length - 1;
if (i !== definition.arrayLength - 1) {
end = pointers[i + 1] - 1;
}
const range = new ROMRange(begin, end);
itemRanges.push(range);
} else {
itemRanges.push(pointerRanges[begin]);
}
if (definition.type === 'array') {
// add a label offset
if (labelOffsets[begin]) {
labelOffsets[begin].push(i);
} else {
labelOffsets[begin] = [i];
}
}
}
if (!fs.existsSync(definition.file)) {
let asmString = '';
asmString += '.list off\n';
asmString += '\n';
asmString += `.define ${asmSymbol}Size`;
asmString += ` ${hexString(mappedRange.length, 4, '$')}\n`;
if (definition.type === 'array') {
asmString += `.define ${asmSymbol}ArrayLength`;
asmString += ` ${itemRanges.length}\n`;
}
asmString += `\n${definition.asmSymbol}:\n`;
for (let pointer of sortedPointers) {
// skip if pointer is out of range
if (pointer < 0 || pointer > objData.length) {
continue;
}
const itemList = labelOffsets[pointer] || [];
const range = pointerRanges[pointer];
// print the label
for (let i = 0; i < itemList.length; i++) {
const indexString = hexString(itemList[i], 4, '').toLowerCase();
asmString += `\n${definition.asmSymbol}_${indexString}:`;
}
// print the data
const pointerData = objData.subarray(range.begin, range.end + 1);
for (let b = 0; b < pointerData.length; b++) {
if (b % 16 == 0) {
asmString += '\n .byte ';
} else {
asmString += ',';
}
asmString += hexString(pointerData[b], 2, '$').toLowerCase();
}
asmString += '\n';
}
asmString += '\n.list on\n';
const asmPath = definition.file;
fs.mkdirSync(getDirName(asmPath), { recursive: true });
fs.writeFileSync(asmPath, asmString);
}
if (definition.type === 'array') {
// create each array item
const array = [];
for (let i = 0; i < itemRanges.length; i++) {
const itemRange = itemRanges[i];
const itemData = objData.slice(itemRange.begin, itemRange.end + 1);
array[i] = this.decodeObject(itemData, definition.assembly);
this.currentIndex++;
}
return array;
} else {
return this.decodeObject(objData, definition);
}
}
decodeObject(data, definition) {
// default definition is raw data
definition = definition || { type: 'data' };
// decode the data
if (definition.format) {
const dataCodec = new ROMDataCodec(definition.format);
data = dataCodec.decode(data);
}
if (definition.type === 'text') {
const encodingKey = definition.encoding;
const textCodec = this.textCodec[encodingKey];
const begin = definition.begin || 0;
const textData = data.slice(begin);
return textCodec.decode(textData);
} else if (definition.type === 'assembly') {
let obj = {};
for (let key in definition.assembly) {
const subDefinition = definition.assembly[key];
// skip category names
if (isString(subDefinition)) continue;
// skip external properties
if (subDefinition.external) continue;
obj[key] = this.decodeObject(data, subDefinition);
}
return obj;
} else if (definition.type === 'array') {
return this.decodeArray(data, definition);
} else if (definition.type === 'property') {
return this.decodeProperty(data, definition);
} else {
return Buffer.from(data).toString('base64');
}
}
decodeProperty(data, definition) {
// calculate the index of the first bit
let mask = Number(definition.mask) || 0xFF;
let bitIndex = 0;
let firstBit = 1;
while (!(firstBit & mask)) {
bitIndex++;
firstBit <<= 1;
}
let value = 0;
const begin = Number(definition.begin) || 0;
const end = Math.min(begin + 4, data.length - 1);
for (let i = end; i >= begin; i--) {
value <<= 8;
value |= data[i];
}
value = (value & mask) >> bitIndex;
if (definition.bool) return (value !== 0);
return value;
}
decodeArray(data, definition) {
let array = [];
this.indexStack.push(this.currentIndex);
this.currentIndex = 0;
if (definition.terminator === '\\0') {
// null-terminated text
if (!definition.assembly || !definition.assembly.encoding) {
console.log('Invalid null-terminated text definition');
return array;
}
const encodingKey = definition.assembly.encoding;
const textCodec = this.textCodec[encodingKey];
let begin = 0;
while (begin < data.length) {
const end = begin + textCodec.textLength(data.subarray(begin));
const itemData = data.slice(begin, end);
const itemObj = this.decodeObject(itemData, definition.assembly);
this.currentIndex++;
array.push(itemObj);
begin = end;
}
} else if (definition.terminator !== undefined) {
// terminated items
const terminator = Number(definition.terminator);
let begin = 0;
let end = 0;
while (end < data.length) {
if (data[end] === terminator) {
const itemData = data.slice(begin, end + 1);
const itemObj = this.decodeObject(itemData, definition.assembly);
this.currentIndex++;
array.push(itemObj);
begin = end + 1;
}
end++;
}
// data at end with no terminator
if (begin !== end) {
console.log(`Invalid terminated array item`);
}
} else if (definition.itemLength) {
// fixed length items
const length = definition.itemLength;
let begin = 0;
while (begin < data.length) {
const itemData = data.slice(begin, begin + length);
const itemObj = this.decodeObject(itemData, definition.assembly);
this.currentIndex++;
array.push(itemObj);
begin += length;
}
} else {
console.log('Invalid sub-array');
}
this.currentIndex = this.indexStack.pop();
return array;
}
}
module.exports = ROMDecoder;

View File

@ -0,0 +1,348 @@
// encode-rom.js
const fs = require('fs');
const getDirName = require('path').dirname;
const isArray = require('isarray');
const ROMDataCodec = require('./data-codec');
const ROMRange = require('./range');
const ROMTextCodec = require('./text-codec');
const hexString = require('./hex-string');
class ROMEncoder {
constructor(definition) {
// create text codecs
this.textCodec = {};
for (let key in definition.textEncoding) {
const encodingDef = definition.textEncoding[key];
this.textCodec[key] = new ROMTextCodec(encodingDef, definition.charTable);
}
this.currentIndex = 0;
this.indexStack = [];
}
encodeROM(definition) {
for (let key in definition.assembly) {
const objDefinition = definition.assembly[key];
if (!objDefinition.file || !objDefinition.isDirty) continue;
const obj = definition.obj[key];
console.log(`Encoding ${objDefinition.asmSymbol}`);
this.encodeParentObject(obj, objDefinition);
}
}
encodeParentObject(obj, definition) {
// const definition = this.dataMgr.getDefinition(key);
// const obj = this.dataMgr.getObject(key);
if (isArray(obj) ^ (definition.type === 'array')) {
console.log(`Object/definition mismatch: ${definition.asmSymbol}`);
return;
}
let objData;
let pointers = [];
this.currentIndex = 0;
if (definition.type !== 'array') {
// single object
objData = this.encodeObject(obj, definition, null);
pointers.push(0);
} else if (definition.isSequential || (definition.terminator !== undefined)) {
// sequential array items
let totalLength = 0;
let dataArray = [];
for (let i = 0; i < obj.length; i++) {
pointers.push(totalLength);
const itemData = this.encodeObject(obj[i], definition.assembly, null);
this.currentIndex++;
dataArray.push(itemData);
totalLength += itemData.length;
}
objData = new Uint8Array(totalLength);
for (let i = 0; i < obj.length; i++) {
objData.set(dataArray[i], pointers[i]);
}
} else if (definition.itemLength) {
// fixed-length array items
const length = definition.itemLength;
let totalLength = length * obj.length;
objData = new Uint8Array(totalLength);
for (let i = 0; i < obj.length; i++) {
const begin = i * length;
pointers.push(begin);
let itemData = new Uint8Array(length);
itemData = this.encodeObject(obj[i], definition.assembly, itemData);
this.currentIndex++;
objData.set(itemData, begin);
}
} else {
// shared array items
objData = new Uint8Array(0);
for (let i = 0; i < obj.length; i++) {
const itemData = this.encodeObject(obj[i], definition.assembly, null);
this.currentIndex++;
let offset = findSubarray(objData, itemData);
if (offset === -1) {
// data not found
offset = objData.length;
const newData = new Uint8Array(offset + itemData.length);
newData.set(objData);
newData.set(itemData, offset);
objData = newData;
}
pointers.push(offset);
}
}
// remove duplicates and sort pointers
const sortedPointers = [...new Set(pointers)].sort(function(a, b) {
return a - b;
});
// create a list of pointer ranges (these may not correspond
// with item ranges in some cases)
let pointerRanges = {};
for (let p = 0; p < sortedPointers.length; p++) {
const begin = sortedPointers[p];
let end = objData.length - 1;
if (p !== (sortedPointers.length - 1)) {
end = sortedPointers[p + 1] - 1;
}
pointerRanges[begin] = new ROMRange(begin, end);
}
const itemRanges = [];
const labelOffsets = {};
for (let i = 0; i < definition.arrayLength; i++) {
const begin = pointers[i];
if (definition.terminator !== undefined) {
let end = begin;
const terminator = Number(definition.terminator);
while (end < objData.length) {
if (objData[end] === terminator) break;
end++;
}
const range = new ROMRange(begin, end);
itemRanges.push(range);
} else {
itemRanges.push(pointerRanges[begin]);
}
if (definition.type === 'array') {
// add a label offset
if (labelOffsets[begin]) {
labelOffsets[begin].push(i);
} else {
labelOffsets[begin] = [i];
}
}
}
const asmSymbol = definition.asmSymbol;
let asmString = '';
asmString += '.list off\n';
asmString += '\n';
asmString += `.define ${asmSymbol}Size`;
asmString += ` ${hexString(objData.length, 4, '$')}\n`;
if (definition.arrayLength) {
asmString += `.define ${asmSymbol}ArrayLength`;
asmString += ` ${definition.arrayLength}\n`;
}
// if (definition.itemLength) {
// asmString += `.define ${asmSymbol}ItemLength`;
// asmString += ` ${definition.itemLength}\n`;
// }
asmString += `\n${definition.asmSymbol}:\n`;
for (let pointer of sortedPointers) {
// skip if pointer is out of range
if (pointer < 0 || pointer > objData.length) {
continue;
}
const itemList = labelOffsets[pointer] || [];
const range = pointerRanges[pointer];
// print the label
for (let i = 0; i < itemList.length; i++) {
const indexString = hexString(itemList[i], 4, '').toLowerCase();
asmString += `\n${definition.asmSymbol}_${indexString}:`;
}
// print the data
const pointerData = objData.subarray(range.begin, range.end + 1);
for (let b = 0; b < pointerData.length; b++) {
if (b % 16 == 0) {
asmString += '\n .byte ';
} else {
asmString += ',';
}
asmString += hexString(pointerData[b], 2, '$').toLowerCase();
}
asmString += '\n';
}
asmString += '\n.list on\n';
const asmPath = definition.file;
fs.mkdirSync(getDirName(asmPath), { recursive: true });
fs.writeFileSync(asmPath, asmString);
}
encodeObject(obj, definition, data) {
// default definition is raw data
definition = definition || { type: 'data' };
if (definition.type === 'text') {
const encodingKey = definition.encoding;
const textCodec = this.textCodec[encodingKey];
const begin = definition.begin || 0;
const textData = textCodec.encode(obj);
if (data) {
const padValue = textCodec.getPadValue();
data.fill(padValue, begin);
data.set(textData, begin);
} else {
data = textData;
}
} else if (definition.type === 'assembly') {
for (let key in obj) {
const subDefinition = definition.assembly[key];
// skip invalid assemblies
const index = this.currentIndex;
const invalid = subDefinition.invalid;
if (invalid && eval(invalid)) continue;
data = this.encodeObject(obj[key], subDefinition, data);
}
} else if (definition.type === 'array') {
data = this.encodeArray(obj, definition, data);
} else if (definition.type === 'property') {
data = this.encodeProperty(obj, definition, data);
} else {
data = new Uint8Array(Buffer.from(obj, 'base64'));
}
if (definition.format) {
const dataCodec = new ROMDataCodec(definition.format);
data = dataCodec.encode(data);
}
return data;
}
encodeProperty(obj, definition, data) {
let value = obj;
// modify the value if needed
if (definition.bool) value = (value ? 1 : 0);
// calculate the index of the first bit
let mask = Number(definition.mask) || 0xFF;
let bitIndex = 0;
let firstBit = 1;
while (!(firstBit & mask)) {
bitIndex++;
firstBit <<= 1;
}
// shift and mask the value
value = (value << bitIndex) & mask;
// find the beginning and end of this property
const begin = Number(definition.begin) || 0;
let end = begin;
let byteMask = 0xFF;
while (byteMask & mask) {
byteMask <<= 8;
end++;
}
// validate the data length
if (!data) {
data = new Uint8Array(end);
} else if (end > data.length) {
let newData = new Uint8Array(end);
newData.set(data);
data = newData;
}
// copy property value to data
let unmask = (~mask) >>> 0;
for (let i = begin; i < end; i++) {
data[i] &= (unmask & 0xFF);
data[i] |= (value & 0xFF);
unmask >>= 8;
value >>= 8;
}
return data;
}
encodeArray(obj, definition, data) {
this.indexStack.push(this.currentIndex);
this.currentIndex = 0;
let pointers = [];
if (definition.itemLength) {
// fixed-length array items
const length = definition.itemLength;
let totalLength = length * obj.length;
data = new Uint8Array(totalLength);
for (let i = 0; i < obj.length; i++) {
const begin = i * length;
pointers.push(begin);
let itemData = new Uint8Array(length);
itemData = this.encodeObject(obj[i], definition.assembly, itemData);
this.currentIndex++;
data.set(itemData, begin);
}
} else {
// sequential array items
let totalLength = 0;
let dataArray = [];
for (let i = 0; i < obj.length; i++) {
pointers.push(totalLength);
const itemData = this.encodeObject(obj[i], definition.assembly, null);
this.currentIndex++;
dataArray.push(itemData);
totalLength += itemData.length;
}
data = new Uint8Array(totalLength);
for (let i = 0; i < obj.length; i++) {
data.set(dataArray[i], pointers[i]);
}
}
this.currentIndex = this.indexStack.pop();
return data;
}
}
function findSubarray(arr, subarr) {
for (let i = 0; i < 1 + (arr.length - subarr.length); i++) {
let found = true;
for (let j = 0; j < subarr.length; j++) {
if (arr[i + j] !== subarr[j]) {
found = false;
break;
}
}
if (found) return i;
}
return -1;
}
module.exports = ROMEncoder;

View File

@ -0,0 +1,196 @@
const fs = require('fs');
const isString = require('is-string');
const isNumber = require('is-number');
const isArray = require('isarray');
const hexString = require('./hex-string');
class ROMTextCodec {
constructor(definition, charTables) {
this.encodingTable = {};
this.decodingTable = [];
if (!isArray(definition.charTable)) {
console.log('Invalid text encoding');
return;
}
for (let charTableKey of definition.charTable) {
const charTable = charTables[charTableKey];
if (!charTable) {
console.log('Character table not found: ${charTableKey}');
continue;
}
const keys = Object.keys(charTable.char);
for (let c = 0; c < keys.length; c++) {
const key = keys[c];
const value = charTable.char[key];
this.decodingTable[Number(key)] = value;
this.encodingTable[value] = Number(key);
}
}
}
decode(data) {
let text = '';
let i = 0;
let b1, b2, c;
while (i < data.length) {
c = null;
b1 = data[i++];
b2 = data[i++];
if (b1) c = this.decodingTable[(b1 << 8) | b2];
if (!c) {
c = this.decodingTable[b1];
i--;
}
if (!c) {
// unknown character
text += hexString(b1, 2, '\\x');
} else if (c == '\\0') {
// string terminator
break;
} else if (c.endsWith('[[')) {
// 2-byte parameter
text += c;
b1 = data[i++] || 0;
b2 = data[i++] || 0;
text += hexString(b1 | (b2 << 8), 4);
text += ']]';
} else if (c.endsWith('[')) {
// 1-byte parameter
text += c;
b1 = data[i++] || 0;
text += hexString(b1, 2);
text += ']';
} else {
// no parameter
text += c;
}
}
// remove padding from the end of the string
while (text.endsWith('\\pad')) {
text = text.substring(0, text.length - 4);
}
return text;
}
encode(text) {
let data = [];
let i = 0;
const keys = Object.keys(this.encodingTable);
while (i < text.length) {
var remainingText = text.substring(i);
var matches = keys.filter(function(s) {
return remainingText.startsWith(s);
});
if (!matches.length && remainingText.startsWith('\\x')) {
const parameter = `0${remainingText.substring(1, 4)}`;
i += 4;
const n = Number(parameter);
if (!isNumber(n)) {
console.log(`Invalid value: ${parameter}`);
} else {
data.push(n);
}
continue;
} else if (!matches.length) {
console.log(`Invalid character: ${remainingText[0]}`);
i++;
continue;
}
const match = matches.reduce(function (a, b) {
return a.length > b.length ? a : b;
});
// end of string
if (match === '\\0') break;
let value = this.encodingTable[match];
i += match.length;
if (match.endsWith('[[')) {
// 2-byte parameter
let end = text.indexOf(']]', i);
const parameter = text.substring(i, end);
let n = Number(parameter);
if (!isNumber(n) || n > 0xFFFF) {
console.log(`Invalid parameter: ${parameter}`);
n = 0;
end = i;
}
i = end + 2;
value <<= 16;
value |= n;
} else if (match.endsWith('[')) {
// 1-byte parameter
let end = text.indexOf(']', i);
const parameter = text.substring(i, end);
let n = Number(parameter);
if (!isNumber(n) || n > 0xFF) {
console.log(`Invalid parameter: ${parameter}`);
n = 0;
end = i;
}
i = end + 1;
value <<= 8;
value |= n;
}
if (value > 0xFF) {
data.push(value >> 8);
data.push(value & 0xFF);
} else {
data.push(value);
}
}
const terminator = this.encodingTable['\\0'];
if (isNumber(terminator) && data[data.length - 1] !== terminator) {
data.push(terminator);
}
return Uint8Array.from(data);
}
textLength(data) {
var i = 0;
var b1, b2, c;
while (i < data.length) {
c = null;
b1 = data[i++];
b2 = data[i++];
if (b1) c = this.decodingTable[(b1 << 8) | b2];
if (!c) {
c = this.decodingTable[b1];
i--;
}
if (!c) {
continue;
} else if (c === '\\0') {
break; // string terminator
} else if (c.endsWith('[[')) {
i += 2;
} else if (c.endsWith('[')) {
i++;
}
}
return Math.min(i, data.length);
}
getPadValue() {
return this.encodingTable['\\pad'] || 0;
}
}
module.exports = ROMTextCodec;

8077
vanilla/ff6-en-rip.json Normal file

File diff suppressed because it is too large Load Diff

26
world/Makefile Normal file
View File

@ -0,0 +1,26 @@
NAME = world
OBJ_DIR = obj
TARGET = $(OBJ_DIR)/$(NAME)_$(VERSION_EXT).o
SRC_MAIN = $(NAME).asm
SRC_FILES = $(wildcard *.asm)
INC_DIR = ../include
INC_FILES = $(wildcard $(INC_DIR)/*.inc)
DATA_FILES = $(wildcard data/*)
GFX_FILES = $(wildcard gfx/*)
TEXT_FILES = $(wildcard text/*)
.PHONY: all clean
# disable default suffix rules
.SUFFIXES:
all: $(TARGET)
clean:
$(RM) -r $(OBJ_DIR) data gfx text
$(TARGET): $(SRC_FILES) $(INC_FILES) $(DATA_FILES) $(GFX_FILES) $(TEXT_FILES)
@mkdir -p $(OBJ_DIR)
$(ASM) $(ASMFLAGS) -I $(INC_DIR) -l $(@:o=lst) $(SRC_MAIN) -o $@

17295
world/world.asm Normal file

File diff suppressed because it is too large Load Diff

234
world/world_data.asm Normal file
View File

@ -0,0 +1,234 @@
; +----------------------------------------------------------------------------+
; | |
; | FINAL FANTASY VI |
; | |
; +----------------------------------------------------------------------------+
; | file: world_data.asm |
; | |
; | description: data for world module |
; | |
; | created: 8/15/2022 |
; +----------------------------------------------------------------------------+
.segment "world_pal"
; d2/ec00
.include "gfx/world_bg_pal1.asm"
; d2/ed00
.include "gfx/world_bg_pal2.asm"
; d2/ee00
.include "gfx/world_sprite_pal1.asm"
; d2/ef00
.include "gfx/world_sprite_pal2.asm"
; ------------------------------------------------------------------------------
.segment "mode7_cutscene_data"
; d8/dd00 Pointers to Magitek Train Ride Tile Graphics (29 items, 12x2 bytes each, +$7E0000)
.res $02b8
; d8/dfb8 Vector Panorama Graphics (compressed)
.res $0607
; d8/e5bf Vector Panorama Tilemap (compressed)
.res $00fb
; d8/e6ba
.include "gfx/snake_road_pal.asm"
; ------------------------------------------------------------------------------
.segment "world_data"
; unused
@b200: .faraddr WorldBackdropGfx
@b203: .faraddr WorldBackdropGfx
WorldBackdropGfxPtr:
@b206: .faraddr WorldBackdropGfx
WorldBackdropTilesPtr:
@b209: .faraddr WorldBackdropTiles
AirshipGfx1Ptr:
@b20c: .faraddr AirshipGfx1
WorldTilemap1Ptr:
@b20f: .faraddr WorldTilemap1
WorldGfx1Ptr:
@b212: .faraddr WorldGfx1
TrainGfxPtr:
@b215: .faraddr TrainGfx
; unused
@b218: .faraddr TrainPal
TrainPalPtr:
@b21b: .faraddr TrainPal
WorldGfx2Ptr:
@b21e: .faraddr WorldGfx2
@b221: .faraddr WorldTilemap2
WorldTilemap2Ptr:
@b224: .faraddr WorldTilemap2
WorldTilemap3Ptr:
@b227: .faraddr WorldTilemap3
WorldGfx3Ptr:
@b22a: .faraddr WorldGfx3
; unused
@b22d: .faraddr WorldChocoGfx1
WorldChocoGfx1Ptr:
@b230: .faraddr WorldChocoGfx1
; unused
@b233: .faraddr VectorApproachPal
VectorApproachPalPtr:
@b236: .faraddr VectorApproachPal
; unused
@b239: .faraddr WorldEsperTerraPal
WorldEsperTerraPalPtr:
@b23c: .faraddr WorldEsperTerraPal
; unused
@b23f: .faraddr WorldSpriteGfx1
WorldSpriteGfx1Ptr:
@b242: .faraddr WorldSpriteGfx1
WorldSpriteGfx2Ptr:
@b245: .faraddr WorldSpriteGfx2
WorldChocoGfx2Ptr:
@b248: .faraddr WorldChocoGfx2
MinimapGfx1Ptr:
@b24b: .faraddr MinimapGfx1
MinimapGfx2Ptr:
@b24e: .faraddr MinimapGfx2
AirshipGfx2Ptr:
@b251: .faraddr AirshipGfx2
; ending airship palette
EndingAirshipScenePalPtr:
@b254: .faraddr EndingAirshipScenePal
.res 3*3
; ------------------------------------------------------------------------------
; ee/b260
WorldModDataPtrs:
.res 3*3
; ee/b269
VehicleEvent_00:
.faraddr $000068
VehicleEvent_01:
.faraddr $00004f
VehicleEvent_02:
.faraddr $000059
VehicleEvent_03:
.faraddr $000088
VehicleEvent_04:
.faraddr $00007f
VehicleEvent_05:
.faraddr $00008f
VehicleEvent_06:
.faraddr $000096
.res 6*3
; ------------------------------------------------------------------------------
; ee/b290 World Map Clouds Graphics (compressed)
.include "gfx/world_backdrop_gfx.asm"
; ee/c295 World Map Clouds Tile Formation (compressed)
.include "gfx/world_backdrop_tiles.asm"
; ee/c702 Blackjack Graphics (compressed)
.include "gfx/airship_gfx1.asm"
; ee/d434 World of Balance Map Formation (compressed)
.include "data/world_tilemap1.asm"
; ef/114f World of Balance Graphics (compressed)
.include "gfx/world_gfx1.asm"
; ef/3250 Magitek Train Ride Graphics (variable size tiles, compressed)
.include "gfx/train_gfx.asm"
; ef/4846 Magitek Train Ride Palettes
.include "gfx/train_pal.asm"
; ef/4a46 World of Ruin Graphics (compressed)
.include "gfx/world_gfx2.asm"
; ef/6a56 World of Ruin Map Formation (compressed)
.include "data/world_tilemap2.asm"
; ef/9d17 Serpent Trench Map Formation (compressed)
.include "data/world_tilemap3.asm"
; ef/b631 Serpent Trench Graphics (compressed)
.include "gfx/world_gfx3.asm"
; ef/c624 Some Chocobo Graphics (compressed)
.include "gfx/world_choco_gfx1.asm"
; ef/ce77 Vector Approach Palette
.include "gfx/vector_approach_pal.asm"
; ef/ce97 Esper Terra Palette
.include "gfx/world_esper_terra_pal.asm"
; ef/ceb7 Airship Shadow and Perspective Graphics (compressed)
.include "gfx/world_sprite_gfx1.asm"
; ef/cfb9 Various Sprites (ship, esper terra, figaro, etc., compressed)
.include "gfx/world_sprite_gfx2.asm"
; ef/dc4c More Chocobo Graphics (world map, compressed)
.include "gfx/world_choco_gfx2.asm"
; ef/e49b World of Balance Minimap Graphics (compressed)
.include "gfx/minimap_gfx1.asm"
; ef/e8b3 World of Ruin Minimap Graphics (compressed)
.include "gfx/minimap_gfx2.asm"
; ef/ed26 Falcon Graphics (compressed)
.include "gfx/airship_gfx2.asm"
; ef/fac8 Palettes for Ending Airship Scene
.include "gfx/ending_airship_scene_pal.asm"
; ------------------------------------------------------------------------------
.segment "world_sine"
; ef/fef0
.include "data/world_sine.asm"
; ------------------------------------------------------------------------------