- Bizhawk 1.13.0
- mGBA core
- casual profile
- Deep Freeze v1.4
The Game
Metroid Deep Freeze is a Metroid: Zero Mission ROM hack made by Captain Glitch. It won the 2017 Winter Hacking Contest on metroidconstruction.com. The author later added more content to it, resulting in the 1.4 version this TAS uses. Although there were rumors about a final patch no such thing was ever officially released.
This TAS aims to complete the game as quickly as possible, while also collecting all the items.
Since Deep Freeze and Zero Mission use the same base-engine I want to point out the notes on Dragonfang's publications here and here, as they cover a lot.
This hack introduces some slight changes to the actual gameplay. Regular missiles have a reduced cooldown as well as increased damage. Super Missiles do regular damage but have an increased cooldown. Therefore, using regular missiles is preferred during boss fights (Kraid being an exception, as for some reason it takes reduced damage from regular missiles).
The suits were also altered slightly (Gravity Suit is actually Varia Suit, while the Ice Suit is Gravity Suit) but these changes have no influence on the TAS. Finally, you start the game with five missiles and Power Grip enabled.
If you are interested in more information about the hack from a more casual perspective, I recommend having a look at the official forum page or this developer talk.
Some things worth pointing out:
- Imago
- Imago has to fly to a certain point in the room for being able to trigger it's death animation. Killing it before it reaches that point (which this TAS does) does not save additional time. However, it means that you can get an equally fast fight even without a super missile to finish off Imago.
Also, one egg was left alive. That way we can get a sequel.
- Kraid
- It is possible to charge up a Shinespark while running against Kraid after you have defeated him (the same can be done with Mother Brain). This usually does not see any use, as the Speedbooster is locked behind Kraid in the vanilla game. However, here it is used to leave the Kraid boss fight while carrying a Shinespark. And even though this seems slow to do, it actually turned out to be faster in the end.
- Odd transitions
- The first room transition in the Space-Jump underground section (the one to the right) takes one frame longer to load if Super Missiles are selected. The reason for this odd behavior is completely unknown. However, it seems to be the only transition in the game that behaves like this.
In addition, when closing the text box after having picked up a major upgrade, some room loading times vary. This seems to be influenced by the time it takes you to scroll through the text.
- Routing / RNG
- The exact order in which all the items are picked up has undergone numerous changes. Incorperating all the different shinesparks greatly limited the possibilities. For example, the missile pack in the spawning room used to be picked up on the way back after the Kraid fight. When the reverse Kraid spark turned out to be faster, the missile pack had to move to the very beginning of the run. The problem with changes like these is that they completely mess up ALL the following RNG. Therefore, a lot of time was spent figuring out a pick-up order that would allow for good RNG, as there are several make-or-break points.
The biggest one was the Power Bomb drop right before the Ridley fight. There are two possible drops you can get, both from the Flameheads. Their drop probabilities are 30/1024 and 50/1024, which is not a lot. Unfortunately, you absolutely have to hit one of those, as without the drop, there is no way to progress. But it gets worse. The RNG is not entirely random. It instead moves in kind of "waves" with a wavelength of two seconds. Unlikely events (such as Power Bomb drops) are only possible towards the end of such a wave. Meaning even if you manage to find a pick-up order that gives you good RNG all the way up to this point, you can get bad luck and arrive at the beginning of a wave. This means that you may have to wait up to two seconds for the wave to roll through, and you can do nothing about it.
RNG manipulation is always one of the less visible parts of a TAS but it is definitely one of the most important ones. In the end, thanks to many hours of planning and testing, only a handful of frames were lost to RNG manipulation. (By the way, all the manipulations in this TAS were deliberately put in places where the keen observer could find them. This technically loses game time but we go by real-time anyways.)
- Breaking the Speedbooster blocks
- When you unmorph during a Space Boost, Samus will keep her Shinespark state for one more frame, even though she is no longer spaceboosting. In addition, since she is now in an upright position, her hitbox extends upwards, making it possible to break Speedbooster blocks above her head that might otherwise be unreachable. This is used when picking up the Power Bomb tank at the top right corner of the map (the room next to the easter egg room). By doing this, it is possible to reach the tank without having to use a Shinespark.
The trick was discovered while timing the early Power Bombs route that was used for earlier speedruns of this category. That route was also able to do some unique Shinesparks in the Spacejump area due to having more Power Bombs at that point. Sadly, the hitbox extension trick is the only part of the old route which made it into the final route.
- Clipping through the gate
- During the escape, the Ice Beam is used to clip through a gate, saveing around 26 seconds. This type of clip is widely known and covered in Dragonfang's notes. In this case, it was quite a challenge to get the pirate into position.
Also, it is safe to say that this trick will not be RTA viable. When you enter the room, the pirate is alerted but not moving. Instead, it checks its surroundings every 16 frames to see whether a Samus is nearby or not, and starts moving if it finds one. This means, that its exact movement is based on Samus position when the pirate notices her. Meaning, to manipulate the pirate reliably, one would have to enter the room, and then move consistenly (say: frame perfectly) all the way up to the pirate. While the actual movement is not that difficult, this requires at least two frame-perfect bomb placements. Unlikely to happen and save time. It would be a lot easier to set this up by exiting through the door on the left and re-entering, since frame-perfect movement from there is actually quite doable. But that door locks behind you, making returning to the previous room impossible, so nope.
Timing ends on the last meaningful input, hence the slightly strange ending. By having the last pirate shoot you into the ship, it is possible to stop giving inputs a lot earlier (215 frames earlier, to be exact).
Things that did not make it
- Skipping the text after picking up a major upgrade
- When a Power Bomb (or its explosion) is on screen, ledges can not be grabbed, and the game can not be paused. This includes the mandatory opening of the item menu after you pick up a major upgrade. In theory, you can set a power bomb, grab the upgrade, and then leave the room while the bomb goes off, essentially skipping all the text. Plasma room and Ice Suit room are candidates for this trick, as they are relatively small and allow for a quick escape. Unfortunately, the frame windows are still too tight in both cases.
- Moving during the Varia Suit cutscene
- After picking up the varia suit, the game usually plays a small cutscene. It is possible to move during that cutscene if you perform a refill glitch while picking up the item. This can be done when picking up the "Gravity Suit" in this game since it is actually the Varia Suit. However, in this game, the time you get from being able to move during the cutscene does not make up for the time lost from doing the refill glitch. Sadly, that way, we also miss out on some slightly jumbled graphics that would have resulted from moving during the cutscene. You can get an idea of the glitch here.
Resources
RAM watches were used to get crucial information about things such as enemy behavior, RNG-patterns and Samus' exact position. For this, the page datacrystal has a very useful RAM-map of the original zero Mission that did help a lot. A small ROM map can be found here.
Biospark's MAGE was also of great help for figuring out specific values.
Dragonfangs kindly shared some of his knowledge on RNG-behaviour, which eventually allowed me to write a decent lua script for drop predictions (it is high-maintenance, but it does the job).
For getting into lua scripting, I found CoolHandMike's tutorial video quite comprehensible.
TASVideos has a comprehensive list of LUA functions and Bizhawk's LUAconsole also contains its own list.
local p_RNG1 = 0x0C77
local p_RNG21 = 0x0002
local p_RNG22 = 0x0003
local p_RNG23 = 0x0004
local p_RNG24 = 0x0005
--Enemy Index 1 Infos:
--Position
local p_XPOS11 = 0x01B0
local p_XPOS12 = 0x01B1
local p_XPOS13 = 0x01B2
local p_XPOS14 = 0x01B3
local p_YPOS11 = 0x01AE
local p_YPOS12 = 0x01AF
local p_YPOS13 = 0x01B0
local p_YPOS14 = 0x01B1
--Health
local p_Health1 = 0x01C0
local p_Health2 = 0x01C1
--Sprite ID
local p_spriteid = 0x01C9
--ROM Enemy Stats
local p_rom = 0x2B0D68
--background 1
local p_XPOSB1 = 0x00E8
local p_XPOSB2 = 0x00E9
local p_XPOSB3 = 0x00EA
local p_XPOSB4 = 0x00EB
local p_YPOSB1 = 0x00E6
local p_YPOSB2 = 0x00E7
local p_YPOSB3 = 0x00E8
local p_YPOSB4 = 0x00EA
--you have to manually insert the index number of the enemy you want to observe here
local p_INDEX = 2
--RNG calculator
local function RNGFORM()
local RNG1 = mainmemory.readbyte(p_RNG1)
local RNG21 = mainmemory.readbyte(p_RNG21)
local RNG22 = mainmemory.readbyte(p_RNG22)
local RNG23 = mainmemory.readbyte(p_RNG23)
local RNG24 = mainmemory.readbyte(p_RNG24)
local RNG2 = RNG21+(RNG22*256)+(RNG23*256^2)+(RNG24*256^3)
local XPOSB1 = mainmemory.readbyte(p_XPOSB1)
local XPOSB2 = mainmemory.readbyte(p_XPOSB2)
local XPOSB3 = mainmemory.readbyte(p_XPOSB3)
local XPOSB4 = mainmemory.readbyte(p_XPOSB4)
local XPOSB = XPOSB1+(XPOSB2*256)+(XPOSB3*256^2)+(XPOSB4*256^3)
local YPOSB1 = mainmemory.readbyte(p_YPOSB1)
local YPOSB2 = mainmemory.readbyte(p_YPOSB2)
local YPOSB3 = mainmemory.readbyte(p_YPOSB3)
local YPOSB4 = mainmemory.readbyte(p_YPOSB4)
local YPOSB = YPOSB1+(YPOSB2*256)+(YPOSB3*256^2)+(YPOSB4*256^3)
--gui.drawText(235, 148, ".")
--for i = 0, 31, 1 do
for i = p_INDEX-1, p_INDEX-1, 1 do
local XPOS11 = mainmemory.readbyte(p_XPOS11+0x0038*i)
local XPOS12 = mainmemory.readbyte(p_XPOS12+0x0038*i)
local XPOS13 = mainmemory.readbyte(p_XPOS13+0x0038*i)
local XPOS14 = mainmemory.readbyte(p_XPOS14+0x0038*i)
local XPOS1 = XPOS11+(XPOS12*256)+(XPOS13*256^2)+(XPOS14*256^3)
local YPOS11 = mainmemory.readbyte(p_YPOS11+0x0038*i)
local YPOS12 = mainmemory.readbyte(p_YPOS12+0x0038*i)
local YPOS13 = mainmemory.readbyte(p_YPOS13+0x0038*i)
local YPOS14 = mainmemory.readbyte(p_YPOS14+0x0038*i)
local YPOS1 = YPOS11+(YPOS12*256)+(YPOS13*256^2)+(YPOS14*256^3)
local SID = mainmemory.readbyte(p_spriteid + 0x0038 * i)
memory.usememorydomain("ROM")
local HL1 = memory.readbyte(p_rom + 0x000000 + (0x000012*(SID)))
local HL2 = memory.readbyte(p_rom + 0x000001 + (0x000012*(SID)))
local MAXH = HL1 + HL2 * 256
local NOD1 = memory.readbyte(p_rom + 0x000006 + (0x000012*(SID)))
local NOD2 = memory.readbyte(p_rom + 0x000007 + (0x000012*(SID)))
local PROPNOD = NOD1 + NOD2 * 256
local SH1 = memory.readbyte(p_rom + 0x000008 + (0x000012*(SID)))
local SH2 = memory.readbyte(p_rom + 0x000009 + (0x000012*(SID)))
local PROPSH = SH1 + SH2 * 256
local LH1 = memory.readbyte(p_rom + 0x00000A + (0x000012*(SID)))
local LH2 = memory.readbyte(p_rom + 0x00000B + (0x000012*(SID)))
local PROPLH = LH1 + LH2 * 256
local MS1 = memory.readbyte(p_rom + 0x00000C + (0x000012*(SID)))
local MS2 = memory.readbyte(p_rom + 0x00000D + (0x000012*(SID)))
local PROPMS = MS1 + MS2 * 256
local SM1 = memory.readbyte(p_rom + 0x00000E + (0x000012*(SID)))
local SM2 = memory.readbyte(p_rom + 0x00000F + (0x000012*(SID)))
local PROPSM = SM1 + SM2 * 256
local PB1 = memory.readbyte(p_rom + 0x000010 + (0x000012*(SID)))
local PB2 = memory.readbyte(p_rom + 0x000011 + (0x000012*(SID)))
local PROPPB = PB1 + PB2 * 256
--local TEST = memory.getcurrentmemorydomain()
memory.usememorydomain("IWRAM")
--print(SID, PROPPB)
--print(i+1, XPOS1)
local FORM1 = (math.floor((((RNG1 + 1)%256) + ((RNG2+1)/16) + p_INDEX + XPOS1 + YPOS1)))%32 + 1
RNG = {10, 13, 2, 6, 8, 7, 9, 14, 10, 2, 4, 14, 4, 12, 15, 13, 12, 11, 1, 3, 15, 0, 6, 7, 8, 11, 5, 0, 3, 5, 1, 9}
local FORM2 = ((RNG1+1)%256 + RNG2 + 1 + ((RNG[FORM1])*256)) % 1024
--print(RNG1, i+1, FORM2)
--print("FORM2", FORM2)
--print("PROP", PROPNOD)
--for PB Flame Head
if ( FORM2 <= PROPNOD ) then
print (RNG1, "/")
else
if ( FORM2 <= PROPNOD + PROPSH ) then
print (RNG1, "Small Health")
else
if ( FORM2 <= PROPNOD + PROPSH + PROPLH ) then
print (RNG1, "Big Health")
else
if ( FORM2 <= PROPNOD + PROPSH + PROPLH + PROPMS ) then
print (RNG1, "Missile")
else
if ( FORM2 <= PROPNOD + PROPSH + PROPLH + PROPMS + PROPSM) then
print (RNG1, "Super Missile")
else
print (RNG1, "Power Bomb")
end
end
end
end
end
end
end
--Health:
local function HEALTH()
--for i = 0, 31, 1 do
for i = p_INDEX-1, p_INDEX-1, 1 do
local HEALTH1 = mainmemory.readbyte(p_Health1+0x0038*i)
local HEALTH2 = mainmemory.readbyte(p_Health2+0x0038*i)
local HEALTH = HEALTH1 + HEALTH2 * 256
local SID = mainmemory.readbyte(p_spriteid + 0x0038 * i)
memory.usememorydomain("ROM")
local HL1 = memory.readbyte(p_rom + 0x000000 + (0x000012*(SID)))
local HL2 = memory.readbyte(p_rom + 0x000001 + (0x000012*(SID)))
local MAXH = HL1 + HL2 * 256
memory.usememorydomain("IWRAM")
print(p_INDEX, HEALTH, "/", MAXH)
end
end
while true do
emu.frameadvance()
RNGFORM()
--HEALTH()
end
RAM address | Note |
---|---|
13E6 | Samus X-position |
13E8 | Samus Y-position |
13EA | Samus X-velocity |
13EC | Samus Y-velocity |
0150 | in-game hours |
0151 | in-game minutes |
0152 | in-game seconds |
0153 | in-game frames |
13DC | Speedbooster |
1418 | weapon cooldown |
0A34 | projectile Y-position |
0A36 | projectile X-position |
0C77 | RNG 1 |
0002 | RNG 2 |
01C0 | health enemy 1 |
01B0 | X-position enemy 1 |
01AE | Y-position enemy 1 |
01F8 | health enemy 2 |
01E8 | X-position enemy 2 |
01E6 | Y-position enemy 2 |
0220 | X-position enemy 3 |
021E | Y-position enemy 3 |
0258 | X-position enemy 4 |
0256 | Y-position enemy 4 |
0290 | X-position enemy 5 |
028E | Y-position enemy 5 |
Suggested frame: 12776
feos: Claiming for judging.
feos: This beats all existing records (RTA or TAS), so great job, and also perfect ending! Space pirates want to stop you so hard that they push you right into your shuttle. Accepting.
Spikestuff: Processing...