# Ocarina of Time Gateway changes ## Introduction The build date of the newly-discovered Gateway rom is `99-01-19 11:37:17`. This is later than every released Nintendo 64 version (the last of these being in November 1998): the last NTSC version (commonly referred to as 1.2 or NN2) was built on `98-11-12`, and the last PAL version (the second PAL version, referred to as NP1) on `98-11-18`. ## Summary The vast majority of the rom is the same as NN2. A small number of the changes made between NN2 and NP1 are present, but plenty of others are not (such as the Gerudo who gives you Gerudo card's clothes, which are patched to be stay a single colour in NP1 and later). These are noted below. - The most visible change is that the new Gerudo symbol is used instead of the original crescent and star, in both scene model textures and actor model textures. This change was originally thought to come from Majora's Mask first; this rom is now the earliest known build of a Zelda game to use it.[^gerudo] Most interestingly, this also includes textures in Dampe's race and Iron Knuckles, which are not patched in any other known rom version. - The file choice screen is changed to only display a single file, and no copy/erase functionality. A new file (which we may guess is called `gateway_title_static`) contains 3 new textures for this, a `Start` button, and textures for `Please Select.`, `Press A button to decide.`, `Press B button to exit.`. After entering a name, the game starts immediately `END` is pressed. - The `Save` option is disabled in the pause menu, and the Game Over screen goes straight to the "Continue playing?" choice, skipping the save screen. Since these are the only way to save, saving is impossible. No text is changed, so the tutorial signs still explain how to save the game. - Rumble is completely disabled (so just as in the Virtual Console release, the Stone of Agony is completely useless). - The three antipiracy checks are removed, specifically: - Fishing would disable catching. - Zelda's hair would turn into an afro. - The gates in the castle collapse escape sequence would not open. [^gerudo]: This change has a somewhat storied history: The symbol was first seen in Majora's Mask in a single place. Gamecube releases of Ocarina of Time used it to replace the original crescent and star symbol everywhere; these edits were made in the rom itself. Virtual Console releases have also made this change, but the edits are done by the emulators themselves; the roms are identical to NN2 or NP1. This also added the new symbol to Dampe race, whereas Gateway simply removes it. Strangest of all is the iQue release, where the symbol is the new one on actors and the old one on scenes; we may speculate that the wrong files were used by accident. ## Detailed analysis Section titles are the segment name. ### Rom header Region byte is `'G'`. ### `boot` - z_locale: switch changed to cover `'L'` and `'G'` as the region byte instead of `'J'`/`'E'`/`'P'` - CIC6105: first two functions stubbed (removing antipiracy checks, the second one is used in `ovl_title`) - bcmp: moved here from `code` - build: build date is different ### `code` N.B. the segment is called `code`, but there is executable code in many other segments, this is just the largest one that is always loaded. - z_parameter: Changed to not draw the B button if the DoAction is "Save".[^parameter] - z_ss_sram: extra function used by the new function in `graph` (see below): ```c void func_80091BC4_neg(u32 addr, void* dramAddr, s32 direction) { SsSram_Init(addr, DEVICE_TYPE_SRAM, PI_DOMAIN2, HALT_REG_latency, HALT_REG_pageSize, HALT_REG_relDuration, HALT_REG_pulse, 0); SsSram_Dma(dramAddr, 2, direction); } ``` this essentially just writes 2 bytes to the given address. - z_play: "Empty Epona cutscene" patch (as in NP1) (https://github.com/zeldaret/oot/blob/8e04ae917f013f3944114fddd1286dfea86a010a/src/code/z_play.c#L1757-L1761 is not present in NN2) - audioMgr: `AudioMgr_Init` as in GC versions (setting `SREG(20)` is removed). - graph: function to conform to the gateway halting requirements, i.e. pause gameplay when joypad Up+Down is pressed and released, resume when joypad Left+Right is pressed and released. This is called near the top of `graph_main`(`Graph_Update`) ```c void func_800A1EF8_neg(GameState* game) { Input* input = game->input; u16 payload; if ((input->cur.button & (U_JPAD | D_JPAD)) == (U_JPAD | D_JPAD)) { SREG(20) = 2; // Set halt bit payload = 0xFFFF; func_80091BC4_neg(HALT_REG_START_ADDR, &payload, OS_WRITE); // Wait for Up or Down to be released do { usleep(1000000 / 60); game_get_controller(game); } while ((input->cur.button & (U_JPAD | D_JPAD)) == (U_JPAD | D_JPAD)); // Sit in this loop until Left and Right detected do { usleep(1000000 / 60); game_get_controller(game); } while ((input->cur.button & (L_JPAD | R_JPAD)) != (L_JPAD | R_JPAD)); // clear halt bit payload = 0; func_80091BC4_neg(HALT_REG_START_ADDR, &payload, OS_WRITE); // Wait for Left or Right to be released do { usleep(1000000 / 60); game_get_controller(game); } while ((input->cur.button & (L_JPAD | R_JPAD)) == (L_JPAD | R_JPAD)); SREG(20) = 0; } } ``` `SREG(20)` is used to tell the audio thread to not update (see https://github.com/zeldaret/oot/blob/8e04ae917f013f3944114fddd1286dfea86a010a/src/code/audioMgr.c#L11-L44) - padmgr: Essentially anything rumble-related removed: - `PadMgr_UpdateRumble`, `PadMgr_RumbleStop`, `PadMgr_RumbleReset` are removed. - `PadMgr_RumbleSetSingle` and `PadMgr_RumbleSet` still exist (presumably because they are used elsewhere). - `PadMgr_HandleRetrace`: `PadMgr_RumbleStop` block at the bottom is gone. (For some reason IDO unrolled the loop in this version only.) - `PadMgr_HandlePreNMI`: call to `PadMgr_RumbleReset` removed - z_game_over: Nayru's Love timer change from NP1 (https://github.com/zeldaret/oot/blob/8e04ae917f013f3944114fddd1286dfea86a010a/src/code/z_game_over.c#L63 is 0 on versions pre-NP1). Libultra: most changes are removal of pfs files - motor: removed - pfsselectbank: removed - contramwrite: removed - pfsgetstatus: removed - contpfs: removed - bcmp: moved to `boot` - contramread: removed - crc: removed - pfsisplug: removed [^parameter]: Also if the DoAction state is `0x2B`, which is well outside the range of normal DoAction states. ### Overlays - ovl_file_choose: changed to only have one file selectable, uses new file textures, etc. Note struct is shifted relative to PAL (but consistent with NTSC) - FileSelect_DrawNameEntry ```diff Audio_PlaySfxGeneral(NA_SE_SY_FSEL_DECIDE_L, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale, &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); - gSaveContext.fileNum = this->buttonIndex; - dayTime = ((void)0, gSaveContext.dayTime); - Sram_InitSave(this, &this->sramCtx); - gSaveContext.dayTime = dayTime; this->configMode = CM_NAME_ENTRY_TO_MAIN; this->nameBoxAlpha[this->buttonIndex] = this->nameAlpha[this->buttonIndex] = 200; - this->connectorAlpha[this->buttonIndex] = 255; - Rumble_Request(300.0f, 180, 20, 100); + this->actionTimer = 8; + this->selectedFileIndex = this->buttonIndex; + this->menuMode = FS_MENU_MODE_SELECT; + this->nextTitleLabel = FS_TITLE_OPEN_FILE; + this->selectMode = SM_FADE_OUT; ``` - New file for gateway stuff: - `func_8080BCC0_neg`: duplicate of Sram_InitNewSave - `func_8080BDEC_neg`: stub - `func_8080BDF4_neg`: stub - `func_8080BDFC_neg`: Similar to the top half of `Sram_InitSave`, (above setting the "ZELDAZ") - `func_8080BF40_neg`: Similar to the bottom half of Sram_InitSave - `func_8080C290_ne2`/`func_8080C780_neg` (FileSelect_UpdateMainMenu): vast majority of function removed ```diff - if (this->buttonIndex <= FS_BTN_MAIN_FILE_3) { + if (this->buttonIndex == FS_BTN_MAIN_FILE_0) { - if (!SLOT_OCCUPIED(sramCtx, this->buttonIndex)) { Audio_PlaySfxGeneral(NA_SE_SY_FSEL_DECIDE_L, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale, &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); ``` Below the first MemCpy the rest of the function is just ```c Audio_PlaySfxGeneral(NA_SE_SY_FSEL_DECIDE_L, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale, &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); this->prevConfigMode = this->configMode; this->configMode = CM_MAIN_TO_OPTIONS; this->kbdButton = 0; this->kbdX = 0; this->kbdY = 0; this->charBgAlpha = 0; this->newFileNameCharCount = 0; this->nameEntryBoxPosX = 120; this->actionTimer = 8; } else { if (ABS(this->stickAdjY) > 30) { Audio_PlaySfxGeneral(NA_SE_SY_FSEL_CURSOR, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale, &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); this->buttonIndex ^= 1; } } ``` - `func_8080D138_ne2`/`func_8080CFD0_neg` (`FileSelect_SetWindowContentVtx`): phi_t5 is 0x274, or 0x40 if `buttonIndex == 0`, the other logic is gone. Also an extra set of vertices is set above that block, using `VREG(28)` and `VREG(30)`. - `func_8080EB78_ne2`/`func_8080EA44_neg` (`FileSelect_DrawWindowContents`): large differences throughout - `sTitleLabels[gSaveContext.language][this->titleLabel]` replaced by `D_2000800_neg` in the first `gDPLoadTextureBlock` - Next block (the `nextTitleLabel` one) is gone. - The file button/box loop is gone, replaced by a single iteration, that uses the `Start` texture, and all the DD stuff removed. - `sActionButtonTextures` loop is gone. - `FileSelect_LoadGame`: save opening replaced by custom version. ```diff -Audio_PlaySfxGeneral(NA_SE_SY_FSEL_DECIDE_L, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale, - &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); -gSaveContext.fileNum = this->buttonIndex; -Sram_OpenSave(&this->sramCtx); +func_8080BDFC_neg(this); gSaveContext.gameMode = GAMEMODE_NORMAL; SET_NEXT_GAMESTATE(&this->state, Play_Init, PlayState); this->state.running = false; ``` - `FileSelect_Main`: ```diff FileSelect_PulsateCursor(&this->state); sFileSelectUpdateFuncs[this->menuMode](&this->state); sFileSelectDrawFuncs[this->menuMode](&this->state); - if ((this->configMode <= CM_NAME_ENTRY_TO_MAIN) || (this->configMode >= CM_UNUSED_DELAY)) { Gfx_SetupDL_39Opa(this->state.gfxCtx); gDPSetCombineLERP(POLY_OPA_DISP++, PRIMITIVE, ENVIRONMENT, TEXEL0, ENVIRONMENT, TEXEL0, 0, PRIMITIVE, 0, PRIMITIVE, ENVIRONMENT, TEXEL0, ENVIRONMENT, TEXEL0, 0, PRIMITIVE, 0); gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 100, 255, 255, this->controlsAlpha); gDPSetEnvColor(POLY_OPA_DISP++, 0, 0, 0, 0); - gDPLoadTextureBlock(POLY_OPA_DISP++, controlsTextures[gSaveContext.language], G_IM_FMT_IA, G_IM_SIZ_8b, 144, 16, - 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, - G_TX_NOLOD, G_TX_NOLOD); - gSPTextureRectangle(POLY_OPA_DISP++, 90 << 2, 204 << 2, 234 << 2, 220 << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, - 1 << 10); + if ((this->configMode <= CM_NAME_ENTRY_TO_MAIN) || (this->configMode >= CM_UNUSED_DELAY)) { + gDPLoadTextureBlock(POLY_OPA_DISP++, D_2001000_neg, G_IM_FMT_IA, G_IM_SIZ_8b, 144, 16, + 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, + G_TX_NOLOD, G_TX_NOLOD); + gSPTextureRectangle(POLY_OPA_DISP++, 90 << 2, 204 << 2, 234 << 2, 220 << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, + 1 << 10); + } else if (this->configMode == CM_OPTIONS_MENU) { + gDPLoadTextureBlock(POLY_OPA_DISP++, D_2001900_neg, G_IM_FMT_IA, G_IM_SIZ_8b, 144, 16, + 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, + G_TX_NOLOD, G_TX_NOLOD); + gSPTextureRectangle(POLY_OPA_DISP++, 90 << 2, 204 << 2, 234 << 2, 220 << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, + 1 << 10); } gDPPipeSync(POLY_OPA_DISP++); gSPDisplayList(POLY_OPA_DISP++, sScreenFillSetupDL); gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 0, 0, 0, sScreenFillAlpha); gDPFillRectangle(POLY_OPA_DISP++, 0, 0, gScreenWidth - 1, gScreenHeight - 1); ``` (presumably the new gateway_texture_static images) - `FileSelect_Init`: ```diff - DmaMgr_RequestSync(this->parameterSegment, (uintptr_t)_parameter_staticSegmentRomStart, size); + DmaMgr_RequestSync(this->parameterSegment, (uintptr_t)_gateway_title_staticSegmentRomStart, size); + VREG(28) = 0; + VREG(30) = 35; Matrix_Init(&this->state); ``` - ovl_kaleido_scope: - `func_80816078_ne2`/`func_80815B28_neg` (`Kaleidoscope_DrawDebugEditor`): ```diff if (CHECK_BTN_ALL(input->press.button, BTN_CUP) || CHECK_BTN_ALL(input->press.button, BTN_CLEFT)) { gSaveContext.inventory.gsTokens++; - if (gSaveContext.inventory.gsTokens >= 100) { - gSaveContext.inventory.gsTokens = 100; - } } else if (CHECK_BTN_ALL(input->press.button, BTN_CDOWN) || CHECK_BTN_ALL(input->press.button, BTN_CRIGHT)) { gSaveContext.inventory.gsTokens--; if (gSaveContext.inventory.gsTokens <= 0) { gSaveContext.inventory.gsTokens = 0; } } ``` (present in all other N64 versions, gone in iQue and GC) - `func_8081F4B8_ne2`/`func_8081EF48_neg` (`KaleidoScope_SwitchPage`): ```diff - gSaveContext.buttonStatus[0] = D_8082AB6C[pauseCtx->pageIndex + pt][0]; + gSaveContext.buttonStatus[0] = BTN_DISABLED; gSaveContext.buttonStatus[1] = D_8082AB6C[pauseCtx->pageIndex + pt][1]; gSaveContext.buttonStatus[2] = D_8082AB6C[pauseCtx->pageIndex + pt][2]; gSaveContext.buttonStatus[3] = D_8082AB6C[pauseCtx->pageIndex + pt][3]; gSaveContext.buttonStatus[4] = D_8082AB6C[pauseCtx->pageIndex + pt][4]; ``` - `func_80825CD0_ne2`/`func_8082574C_neg` (`func_808265BC`) ```diff gSaveContext.buttonStatus[0] = D_8082AB6C[pauseCtx->pageIndex][0]; gSaveContext.buttonStatus[1] = D_8082AB6C[pauseCtx->pageIndex][1]; gSaveContext.buttonStatus[2] = D_8082AB6C[pauseCtx->pageIndex][2]; gSaveContext.buttonStatus[3] = D_8082AB6C[pauseCtx->pageIndex][3]; gSaveContext.buttonStatus[4] = D_8082AB6C[pauseCtx->pageIndex][4]; pauseCtx->pageIndex = D_8082ABEC[pauseCtx->mode]; pauseCtx->unk_1E4 = 0; pauseCtx->state++; pauseCtx->alpha = 255; + gSaveContext.buttonStatus[0] = BTN_DISABLED Interface_LoadActionLabelB(play, DO_ACTION_SAVE); ``` - `func_808263C0_ne2`/`func_80825E44_neg` (`Kaleidoscope_Update`) ```diff switch (pauseCtx->unk_1E4) { case 0: if (CHECK_BTN_ALL(input->press.button, BTN_START)) { Interface_SetDoAction(play, DO_ACTION_NONE); pauseCtx->state = 0x12; WREG(2) = -6240; func_800F64E0(0); } else if (CHECK_BTN_ALL(input->press.button, BTN_B)) { - pauseCtx->mode = 0; - pauseCtx->promptChoice = 0; - Audio_PlaySfxGeneral(NA_SE_SY_DECIDE, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale, - &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); - gSaveContext.buttonStatus[0] = gSaveContext.buttonStatus[1] = gSaveContext.buttonStatus[2] = - gSaveContext.buttonStatus[3] = BTN_DISABLED; - gSaveContext.buttonStatus[4] = BTN_ENABLED; - gSaveContext.hudVisibilityMode = HUD_VISIBILITY_NO_CHANGE; - Interface_ChangeHudVisibilityMode(HUD_VISIBILITY_ALL); - pauseCtx->unk_1EC = 0; - pauseCtx->state = 7; } break; ``` and similarly in cases 5 and 8. - ovl_En_Zl2: antipiracy check removed: ```diff Matrix_Push(); - if (osCicId != 6105) { - Matrix_Scale(2.0f, 1.0f, 2.0f, MTXMODE_APPLY); - } Matrix_Translate(pos->x, pos->y, pos->z, MTXMODE_APPLY); Matrix_RotateZYX(rot->x, rot->y, rot->z, MTXMODE_APPLY); Matrix_Push(); ``` (famously this turns adult Zelda's hair into an afro). - ovl_Fishing: antipiracy check removed (this would check the value at a particular RAM address and prevent reeling a fish in if incorrect) - ovl_En_Ganon_Mant (Ganondorf's cape): Gerudo symbol (model is in the data of this actor rather than an object) - ovl_Bg_Zg (Gates in Ganon's Tower Collapse): antipiracy check removed (the gate will not open if the check fails) - ovl_En_Go2 (Gorons): https://github.com/zeldaret/oot/blob/8e04ae917f013f3944114fddd1286dfea86a010a/src/overlays/actors/ovl_En_Go2/z_en_go2.c#L1581 the flag clear is the same as NP1 on, rather than NN2. ### Asset files - icon_item_static: Gerudo symbol on Mirror Shield - gateway_title_static: New file with textures for a `Start` button, `Please Select.`, `Press A button to decide.`, `Press B button to exit.` ### Actor Objects All just have the Gerudo symbol replaced - gameplay_dangeon_keep (General dungeon objects) - object_link_boy (Adult Link) - object_menkuri_objects (Gerudo Training Ground objects) - object_haka_objects (Shadow Temple objects) - object_heavy_object (Huge liftable pillar) - object_mir_ray (Reflected light beam) - object_gi_gerudo (Gerudo Card) - object_gi_shield_3 (Mirror Shield) - object_jya_obj (Spirit Temple objects) - object_ik (Iron Knuckle) object_ik is notable because the change does not exist on any other version. #### Scenes - spot06_room_0 (Lake Hylia): Water Temple gate and lock moved down 200 units (as in NP1 and later) - spot12_scene (Gerudo's Fortress): Gerudo symbol - Bmori1_room_11 (Forest Temple): Gerudo symbol - men_scene + rooms (Gerudo Training Ground): A palette is a slightly different size. This palette change causes a lot of changes in room files that are unimportant due to colour indexed textures having their indices changed. - jyasinzou_scene + rooms (Spirit Temple): A palette is a slightly different size (and shifted) from GC versions. - jyasinboss_room_1,3 (Twinrova's lair): Gerudo symbol - hakasitarelay_room_0--6 (Dampe Race ("?")): PolygonType 2 is used instead of 0 hakasitarelay (Dampe race) deserves special mention, RoomShapeCullable is used instead of RoomShapeNormal. For example, in room 0: All other versions: ```c RoomShapeNormal hakasitarelay_room_0RoomShapeNormal_0000F0 = { 0, 1, hakasitarelay_room_0RoomShapeDListsEntry_0000FC, hakasitarelay_room_0RoomShapeDListsEntry_0000FC + ARRAY_COUNTU(hakasitarelay_room_0RoomShapeDListsEntry_0000FC) }; RoomShapeDListsEntry hakasitarelay_room_0RoomShapeDListsEntry_0000FC[1] = { { hakasitarelay_room_0DL_003218, hakasitarelay_room_0DL_0062A8 }, }; ``` neg: ```c RoomShapeCullable hakasitarelay_room_0RoomShapeCullable_0000F0 = { 2, 6, hakasitarelay_room_0RoomShapeCullableEntry_0000FC, hakasitarelay_room_0RoomShapeCullableEntry_0000FC + ARRAY_COUNTU(hakasitarelay_room_0RoomShapeCullableEntry_0000FC) }; RoomShapeCullableEntry hakasitarelay_room_0RoomShapeCullableEntry_0000FC[6] = { { { -59, -460, -1602 }, 56, hakasitarelay_room_0DL_000230, NULL }, { { -60, -335, -1760 }, 400, hakasitarelay_room_0DL_0010E8, NULL }, { { 155, -355, -2385 }, 1110, hakasitarelay_room_0DL_002610, NULL }, { { 155, -540, -2385 }, 1100, hakasitarelay_room_0DL_002EF0, NULL }, { { -59, -80, -1602 }, 82, hakasitarelay_room_0DL_0031C8, NULL }, { { -59, -300, -1602 }, 169, NULL, hakasitarelay_room_0DL_006220 }, }; ``` This is the only known version that is this way. (What does this actually change?) This is also the only known version of the original where the Gerudo symbol is removed in this scene rather than replaced: GC misses it, VC replaces it with the new one.