This has been moved to https://github.com/Dragorn421/z64-romhack-tutorials/blob/master/oot64%20overview/index.md
This has been moved to https://github.com/Dragorn421/z64-romhack-tutorials/blob/master/oot64%20overview/index.md
This document aims at introducing concepts needed to understand how some parts of Ocarina of Time 64 work.
Some parts focus on the organization in the OoT64 decomp project specifically. (latest revision at time of writing: 05b2cbf)
Some details may only be true for the PAL MQ Debug version of OoT64 but most of the content here applies to other versions.
Don't worry if some parts are unclear, most stuff here is not needed to have fun modding! That said, feedback on this document is appreciated :)
A ROM is a single file, usually .z64, .n64 or .v64. Read more
To set up decomp, a rom is needed to be put into the oot directory as baserom_original.z64
, .n64
or .v64
.
It is then processed to a baserom.z64
rom which md5 must be f0b7f35375f9cc8ca1b2d59d78e35405
.
The size of this baserom.z64
is 54 MB.
A ROM contains several files (1533 files in PAL MQ Debug OoT64), just like an archive (for example a .zip
).
Each file takes a portion of the ROM.
For example the code
file spans from 0xA94000 to 0xBCEF30 (start inclusive, end exclusive), as can be seen when building decomp by looking in the map file build/z64.map
for _codeSegmentRomStart
and _codeSegmentRomEnd
.
In the oot decomp repo, the file names come from strings present in the Debug ROM. (specifically, from sDmaMgrFileNames
)
This is why for the time being some names are weird/funny, like ovl_En_Torch2
which is Dark Link's code, or ovl_Boss_Ganondrof
which is Phantom Ganon's code (forest temple boss).
The game references files by their VROM (Virtual ROM) offset. VROM offsets are translated to ROM offsets (which index into the actual ROM file) by a look-up inside the "DMA table" (dmadata
file, src/dmadata/dmadata.c
) which lists VROM and ROM offsets for each file.
In an uncompressed ROM, VROM and ROM offsets are typically identical.
Knowing ROM offsets, the game loads data from the cartridge by performing what is called DMA. If compressed, the file is decompressed.
The spec
defines which files contain what.
Files appear in the same order in the spec
as in the ROM.
For example this defines the "makerom" file, the first file in the ROM:
beginseg
name "makerom"
include "build/asm/rom_header.o"
include "build/asm/ipl3.o"
include "build/asm/entry.o"
endseg
"seg" stands for "segment". This file/segment contains the ROM header, the IPL3 boot code, and the entrypoint function.
It is possible to edit the spec
if you want to add or modify files for modding.
When building decomp, the spec
is processed to a linker script build/ldscript.txt
(relevant Makefile part).
The ROM header provides information useful to the N64 when booting the ROM.
In oot decomp it is located in asm/rom_header.s
.
One of your first edits may be to change the "Region" from "P"
(Europe) to "E"
(North America) (En64 Wiki: ROM Header) to make the ROM "NTSC" instead of "PAL", so the ROM runs at 20fps instead of the decreased PAL framerate.
This is simplified, many steps from the boot sequence are omitted.
The N64 computes a checksum from 1 MB of ROM, starting at 0x1000, and compares it to the checksums in the ROM header. If the checksums mismatch the game won't start on hardware. (emulators, like Project64, may break out of the infinite loop caused by bad checksums and run the game just fine)
This would of course be an issue for modding, for example the dmadata
file which lists the offsets of the files inside the ROM is inside that 1 MB used to compute the checksum. So changing any file size or location may change the checksum and prevent the ROM from booting.
So, in oot decomp the checksum is computed (tools/n64chksum.c
) after each build and written to the rom header (tools/elf2rom.c
), to keep it matching.
The TLDR is you don't have to worry about this with the oot decomp.
The N64 copies 1 MB of ROM to RAM, then jumps to (starts executing code at) the address specified in the ROM header (entrypoint) (Manual 6.10).
The entrypoint
function is written in assembly: asm/entry.s
. It is located inside the "makerom" file.
It then jumps to bootproc
, inside the "boot" file.
It should be noted that it is very important that the entrypoint
and "boot" files stay within the megabyte of ROM initially loaded by the N64. This is achieved by having the files at the start of the spec
. This also means the "boot" file size is limited, but there is a lot of leeway before that becomes an issue.
From bootproc
various initialization steps are performed, Idle_ThreadEntry
then Main_ThreadEntry
are executed (as new threads, but that doesn't really matter).
One of the main things that happens in these functions is Main_ThreadEntry
loads the "code" file from ROM into RAM, then calls the Main
function from "code".
More initialization happens in Main
, then the graphics thread is run. (Graph_ThreadEntry
)
The graphics thread is the thread where most of the game computations actually happen.
The "code" file, often just called "code", contains most of the game's main code. Non-exhaustive list of what is handled by code in the "code" file: audio, graphics utilities, maths utilities, map loading, collision, camera, cutscenes, animation utilities, crash debugger.
The Graph_ThreadEntry
function contains the game's main loop.
There actually are two loops, the second inside the first.
The first loop loops over game states.
The second loop executes the current game state's main function.
The game can be in different "game states".
The most important game state is the Gameplay
one, also known as "global" ("global context") or "play state". It is used to load maps, control Link, play cutscenes and so on.
The file select menu is also its own game state (FileChoose
, "ovl_file_choose" file).
The map select menu (a debug feature, for quickly warping to various maps) is also its own game state (Select
, "ovl_select" file).
Other game states play a minor role. Game states are listed in gGameStateOverlayTable
.
On retail versions (non-Debug), the Title
state ("ovl_title" file) is responsible for drawing the N64 logo when the game starts, but it only lasts a frame in the Debug ROM.
The remaining states only serve as transitions to other states.
There are also two unused game states (not in the game state table): PreNMI
and Sample
.
Game states have three main functions: init, main, destroy.
- The init function is called once before the state starts running.
- The main function is called over and over until the state stops running, typically once per frame.
- The destroy function is called once when the state stops running.
Some game states have their code not in the "code" file but in their own file instead, then the file containing their code is loaded from ROM to RAM when needed and unloaded when no longer needed.
Back to the two loops in Graph_ThreadEntry
.
Each game state is run by calling its main function over and over (inner loop). The state may stop running, by setting GameState#running
to false
.
The next game state to run is determined by the function GameState#init
is set to. (make sure to look at Graph_GetNextGameState
if you want to mess with game states)
The outer loop keeps switching to the next game state, whenever the previous game state stops running.
The first game state to run when the game boots is TitleSetup
. Mainly, it switches to the Title
state, which draws the N64 logo in retail versions.
Title
then switches to the Opening
state ("ovl_opening" file), which switches to the Gameplay
state and makes it load the title screen (with the cutscene in Hyrule Field and the game logo).
When the player does the adequate inputs on the title screen, the game state eventually switches to FileChoose
(file selection menu).
Then when a file is loaded, if it is file 1 and on a Debug ROM the Select
game state (map select) is loaded. When a map is picked, it then switches to the Gameplay
state with that specific map.
Otherwise (not file 1 or not a Debug ROM), the Gameplay
state is loaded to a map that depends on the file.
When the map changes (scene transitions), the Gameplay
state switches to Gameplay
(it stops running and the next state, also Gameplay
, starts with another map).
Again, the most important state (almost the only one that matters) is Gameplay
.
Its init function Gameplay_Init
does a lot, including loading the map.
The heart of the game lies in the Gameplay
state's main function Gameplay_Main
. This function calls Gameplay_Update
then Gameplay_Draw
.
Gameplay_Update
will update and compute data such as animations, player and NPCs movements, camera movement, cutscene and dialogue progress, collision, physics simulation...
Gameplay_Draw
will draw the skybox, the map, Link, NPCs, props... without performing logic.
overlays, relocation
actors, objects, actor overlay table, object table
entrance table, scenes, rooms, alternate headers
collision
camera
colliders