Skip to content

Instantly share code, notes, and snippets.

@Dragorn421
Last active February 16, 2022 11:39
Show Gist options
  • Save Dragorn421/969e950d44db754fa13037696ce32bfe to your computer and use it in GitHub Desktop.
Save Dragorn421/969e950d44db754fa13037696ce32bfe to your computer and use it in GitHub Desktop.
Introduction to OoT64 code

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 :)

Contents

The ROM

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.

The file system

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 file system (Advanced)

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.

How files are defined in oot decomp

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

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.

Game boot (Advanced)

This is simplified, many steps from the boot sequence are omitted.

Checksum

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.

Running game code

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.

Game main loop

The "code" file

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.

Game main loop 1/2

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 different game states

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

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.

Game main loop 2/2 (Advanced)

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.

Game state sequence after boot (Advanced)

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).

The Gameplay state

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.

TODO

overlays, relocation

actors, objects, actor overlay table, object table

entrance table, scenes, rooms, alternate headers

collision

camera

colliders

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment