Useful addresses

AddrSizeWhat
004C2 bytesCamera X (screen position)
004E2 bytesCamera Y
005A1 byteVolatile controller-based RNG
005B2 bytesBit shifting RNG (big endian)
005D2 bytesFrame timer
00C62 bytesFuel remaining
00C82 bytesWeapon power remaining
03741 bytePlayer facing
03361 bytePlayer X speed direction (0=right, 1=left)
029B(lo) 02BA(hi)2 bytesPlayer X speed
0200(lo) 021F(hi)2 bytesPlayer X pixel position
01971 bytePlayer X sub-pixel
03551 bytePlayer Y speed direction (0=up, 1=down)
02D9(lo) 02F8(hi)2 bytesPlayer Y speed
025D(lo) 027C(hi)2 bytesPlayer Y pixel position
023E1 bytePlayer Y sub-pixel

Movement

WhatMax p/fUpDownLeftRight
Thrust3.0Angles 24 to 31, 0 to 8Angles 8 to 24Angles 17 to 31Angles 1 to 15
Gravity3.0Inverted gravityNormal gravity
Object swing4.0
The pod's maximum speed is 4.0 pixels per frame. This can only be done by tethering an object and having it pull you along. If your thrusters or the gravity attempt to accelerate the pod in the same axis, it will instantly reduce the speed to a maximum of 3.0 if the pod is traveling faster.

Thrust

Thrusting enforces a maximum speed of 3.0 pixels/frame.
The default thrusters accelerate an unburdened pod at 16, when facing straight into some direction. Carrying an object will cut this acceleration by varying amounts depending on object mass.
Thrusting straight right (direction 8) or straight left (direction 24) will count some thrusting force upward, and will prevent reaching
Basic thrusting with default thrust or Double Strength Thrusters take 2 fuel per frame, or 16 fuel when using boosters. Efficient Engines reduce the costs to 1 or 8 fuel per frame, respectively.

Gravity

Gravity enforces a maximum downward (or upward, if reverse) speed of 3.0 pixels/frame. Even zero gravity is counted as downward with zero acceleration, and will enforce the downward maximum.
Gravity usually pulls the pod down at a constant rate, depending on the planet. There may be areas on a planet submerged under a fluid of sorts. The only affect of entering one such fluid is a change in the acceleration of gravity. There's still no friction.
Anti-Gravity completely shuts off gravity when active. Downward acceleration, speed maximum, and tethered object will not be affected by gravity with this item in use.

Angles

The pod can face 32 different directions.
Straight left (24) or straight right (8) is still counted as a thrust upwards of usually zero acceleration. If you desire to travel faster than 3.0 pixels/frame going up, avoid thrusting in these directions.
Straight down (16) or straight up (0) is not counted as thrusting either left or right. There will be no interference for horizontal momentum that exceeds 3.0 pixels/frame.

Tethered objects

Tethered objects are impacted by gravity in an intuitive way. By accelerating in the opposite direction of the motion of a tethered object, player actions can also help increase the tethering speed. The faster the tethering speed and the heavier the object, the more it influences/accelerates the player's speed. Generally speaking, this is again done in an intuitive way. But please note that there are oddities in how the game calculates the resulting pod speed. For example, continuously thrusting in the opposite direction of the movement of the tethered object will not necessarily result in generating the most speed. Some thrusts have more impact for the speed than others. And some thrusts even have a negative impact on the speed, even though they intuitively should instead be contributing. The underlying mechanics have not been investigated in further details, but are important to take into account when TASing, even if it's only in the form of playing around with inputs by "trial-and-error".
So for example, when travelling to the right, a tethered object will accelerate the player when moving in front of the pod (doesn't matter if the object is moving clockwise or anti-clockwise around the pod), up to a speed of 4p/f. Once the tethered object crosses the vertical line of the pod to the left side, it will start to decelerate the player. If the object comes to rest, it will no longer impact the pod's speed, even if it's off-center with respect to the pod. This speed-up mechanic is significantly more efficient when traveling to the left than when traveling to the right. The reason for this has not been investigated.
Vertical motion is impacted by tethered objects in a similar way as horizontal motion. The main difference being that the gravity fields of the planets prevent the pod from speeding up above 3p/f (not applicable to large wormholes!). This limitation can be bypassed by activating the "anti-gravity" upgrade. To speed up for example downward movement, place the tethered object above the horizontal centerline (or below in an inverted gravity field), the higher the better, and let it freely fall down. When it's about to pass the centerline, ensure the pod's y-speed is already 3p/f and activate anti-gravity. While the object swings below the pod, it will accelerate the pod up to a vertical speed of 4p/f.

The RNG

005A1 byteVolatile controller-based RNG
005B2 bytesBit shifting RNG (big endian)
The value at address 005A is affected by the following:
0018 Player 1 immediate - Exact mirror of controller state
0019 Player 1 pressed - Bits are set if pressed now, but not held before
001A Player 2 immediate
001B Player 2 pressed
005D Frame timer low-byte
005E Frame timer high-byte
005F Always zero?
0060 Looks like address 005D MOD 8
The thing that matters is the fact 005A is affected by direct controller input. This includes Player 2, which has no effect anywhere else in the game. By using Player 2, it is possible to gain very strong control over how the RNG works.
The values at address 005B and 005C are left-shifted every frame. The bit shifted into 005C is an XOR of bits 0x40 and 0x08 from 005A, although it has been observed to, on rare occasion, shift in an opposite of this bit. The bit shifted out of 005C is shifted into 005B, and the bit shifted out of 005B is thrown away.
Since 005B and 005C are affected by what happens with 005A, and 005A is affected by controller input, this means you should have complete control over the RNG.
During game play, you have control every other frame, but the RNG runs every frame. As the RNG "ticks" twice for every frame you have control, this makes it somewhat difficult to set the RNG perfectly.
All three RNG addresses are used for various random events.

Final boss

038B-038F Eye aperture states
0419-041D Eye timers
0439-043D Eye opening sequences
0458-045C Eye health
0460-0464 Eye states
The top eye can be despawned during the auto-scroller, well before the actual boss fight begins. This can be seen by monitoring the eye state address $0461. At the time of writing, the best description for what happens is found here: Forum/Posts/367718 . There have been no reports of additional eyes being despawned (or other eyes than the one at the top). The bot 'Jaffar', created by TASer 'eien86', was able to quickly find solutions that despawned the top eye, but did not find any other variants.
When the window gets "close enough" to the boss, the eye timers get initialized one-by-one to 0 and will then start ticking up every 8 frames. This appears to be programmed in a set pattern, meaning the start order of the eye openings is always the same. Note that small variations have been observed, so there appears to be a little room for manipulation in the timing of the first eye openings, but this has not been investigated in detail.
When an eye timer reaches 0x20, the game will branch at instruction $C08C to the piece of code for the eye opening sequence. The sequence is split in 8 segments, each consisting of 8 frames. The first and last consist of the eye being half-open (and not vulnerable). When the eye opening sequence begins, the game will also re-initialize the timer for the next phase. This is done by comparing one of the RNG-addresses and 0x1F. The result is written to the timer and becomes its new start value. The eyes with index 0, 2 and 4 use $5B for this and the eyes with index 1 and 3 use $5A. Since the RNG-addresses can be manipulated (see section above), the player has control over all eye openings at any time, except for the first cycle as explained above.

Wall Clipping

Various wall clips have been discovered with a wide range of difficulties and requirements. Below are the currently known types of wall clips.

CPU Overloading

CPU overloading is a strategy that players can implement to cause unintended behavior by the game code. The CPU on an NTCS NES has a maximum of 29780.67 cycles per frame, with each code instruction consuming a handful of these cycles. Solar Jetman is a very CPU intensive game at times, and this limit gets maxed out quite easily. When that happens, a CPU interrupt occurs that immediately stops what the code is doing and gets ready to start the next frame from the top. This allows the player to stop the code at a crucial time to intentionally cause a glitch to occur due to RAM not being in an intended state after the interrupt. While this actually has practical applications for RTA runners, this can be abused heavily in a TAS. The number of active entities is the key tool that players can leverage to control CPU usage, such as bullets and enemies. The debugger in FCEUX is a great tool for this because you can set a breakpoint at the start of the window, the end of the window, and at the interrupt handler and use the CPU cycle counter to know how far away you are from hitting the interrupt at the right time. This allows Currently, all use cases for this glitch lie in the collision handling portion of the game logic since that consumes most of the CPU time. Below outlines the known applications for CPU overloading.

Warp Duplication

On planet 1, there is a warp that is intended to take the player to planet 7. This is triggered by killing a special enemy within a specific time frame which spawns the warp. It has been known for a while that there is a glitch that can cause this warp to be duplicated, but it was later revealed that CPU overloading was the root cause of this glitch.
When a bullet makes contact with the warp enemy, a code branch executes to handle the collision. Instruction $89E6 is responsible for initializing the warp in the persistent entity array in RAM. Instruction $897B is responsible for clearing the warp enemy from the active entity array. If an interrupt is triggered between these two instructions then the warp will have spawned in and the enemy will continue to be spawned in, thus allowing the enemy to interact with the bullet a second time so that the warp is duplicated.
The planet that the glitched warp transports the player to is determined by its index in the persistent entity array. Planet 1 starts with 6 persistent entities, the intended warp is 7, meaning the glitched warp will transport the player to planet 8. If this glitch is repeated enough times in a row, then a warp directly to planet 12 will be created.
Notes:

MDR Clip

MDR clipping is a special variation of wall clipping, first seen in a Twitch stream by MickeyDadRad where he managed to get pushed out of bounds through a wall by a sphere enemy. This occurred by complete accident and an insane amount of luck. If this footage had never been captured, this glitch may never have been demystified.
The sphere enemies are unique in that they can push the player around without bouncing away like other enemies. If a player is near a wall when this happens, then the sphere enemy can push the player several frames in a row. Normally this causes the enemy to die very quickly, since it takes damage every time it comes into contact with the player. However, abusing CPU overloading allows us to get around this. Instruction $8902 is responsible for updating the sphere enemy's health when hit by a bullet. Instruction $8936 is responsible for clearing the sphere enemy from the active entity array if it's health is zero. By performing an interrupt when its health reaches zero, the enemy's health underflows to 255. This allows the sphere enemy to survive long enough to push the player all the way through a wall.
The player can freely move once out of bounds and can go back in bounds easily because wall collision is only one-way, although this usually cause the player to bump into the wall which deals some damage.
Notes

Boss Eye Despawn

One of the boss eyes can be despawned if an interrupt occurs during a meteor being destroyed by a bullet. This definitely needs more investigation as it only works with certain types of meteors, but essentially the local entity for the meteor has a index of it's persistent object, which is responsible for knowing when and where to spawn it. When the meteor is killed, it also clears out its persistent object at that index. But if an interrupt occurs that prevents the local entity from despawning, it can be "killed" again and clear out the persistent object at whatever index is there now, which can be a value of 01 where the top boss eyes is stored. This index has been observed to be 02 at times, which represents another boss eye. However, there is not a known CPU interrupt that can be triggered to despawn a second eye at this time.

The Tether Glitch

This glitch allows the pod to tether an empty entity and entities that are not intended to be carried. It is comparable to the popular stale reference manipulation glitch in Ocarina of Time.

Setup

The trigger for this glitch is a CPU interrupt during the collision handler for the pod and a dropped item from destroying a saucer forcer or a space snake enemy. The interrupt must occur after the entity data that represents the score value of the item is cleared from memory, but before the entity itself is cleared from memory. In this state, the item is now treated as a item that can be carried. One quirk of this glitch is that it requires at least 1 player bullet entity slot to be open out of the 8 available, because the score indicator takes up this slot and the code branch we are targeting for this interrupt only occurs when the score indicator is displayed. A fairly consistent setup has 7 player bullets spawned in and 2-3 enemies spawned in, but as with other glitches involving CPU interrupts there is unpredictable variance that may need to be accounted for. The next stage occurs when the dropped item despawns, or is collected a second time from shooting it with a bullet. The tether does not know that the item has been cleared from memory and remains tethering nothing. The next entity that loads into the same slot in memory that the item took up will then be teleported into the tether and be carried by the pod.

Applications

By loading an entity into the empty slot that the tether points to, important objects can be reached sooner than usual or even through walls because it will warp into the tether as soon as the object is loaded off screen.
Dropping off the item dropped from a saucer forcer can grant a large sum of money. This is known as the lottery glitch. The entity data in memory that is used for other dropped off items to determine the point value is normally used as a timer for the dropped item to keep track of when it should despawn. This means that a different value is rewarded for each frame it could be dropped off. There is not a direct correlation between value and frame count, as the read value is intended to be an index into a table to lookup the value and out of bounds values lead to unpredictable results.
In a pinch, this glitch can be used to gain item tether momentum in place of another item that may be more out of the way than spawning in a saucer forcer or space snake enemy.
On some levels, attempting to perform the tether glitch on a star dropped by a space snake enemy instead treats the star like a wormhole to the part room. It does still require the player to have the necessary fuel cells collected.

Arbitrary Code Execution

ACE has been solved in Solar Jetman and can be utilized in a TAS. This section outlines all the details necessary to set it up.

Corrupted Warp State

The key functionality that we are exploiting here has to do with how the game saves and loads state as warps are performed. There are different types of warps that update the game state in slightly different ways. Usually when warping to or from a warpship part room, the map ID of the previous area is saved along with the enemy states for that planet. This type of warp has a transition sequence that plays where a bunch of stars appear on the screen. This does not happen when taking a special warp, such as the warp from planet 1 to 7, or the warp from planet 4 to 11. In this case, the map ID of the previous area is not updated. The map loading sequences differ depending on the map ID of the current area and the map ID of the previous area. These IDs are compared to #0C to check if they are normal planet areas or warpship part room areas. If somehow both the current ID and the previous ID represent warpship part rooms, then corruption occurs due to a part of RAM being used in inconsistent ways. To be specific, the location in RAM where part of the persistent enemy states for the previous area are erroneously read and saved into the persistent object table, which is then spawned into the map and attempted to be updated.

Enemy Handler Function Address Tables

The function that updates a game object depends on the ID of the object. This ID is used as an offset to look up the high ($0E:AA4C) and low ($0E:AA17) bytes of the address of the code to update the object. If the ID of the object is larger than the lookup table, then addresses can be jumped to that were not at all intended. This gives us a set of addresses that could potentially be executed, some of them even occurring in RAM. The value that the object ID is set to comes from another lookup table for objects ($04:86CD), with the offset used being a byte of saved enemy state located in RAM at $0636. This is a value we can control.

Persistent Enemy Compression

When warping, the enemy states of the current map are compressed by a factor of 8. The code iterates through the persistent enemy table in RAM starting at $0460, and compares each byte in the table to #01. This is because when an enemy is killed, its value in this table gets updated from its ID to #00. A value of #00 gets converted to a 0 bit while any other value becomes a 1 bit, and the few bytes that are generated from the table are stored in RAM starting at $0633. This table is supposed to be used when warping back to the normal planet area from a warpship part room to not initialize any enemies that have already been defeated. So to control the critical value at $0636, all we need to do is defeat a specific set of enemies stored in addresses $0478 to $047F to produce any byte we want.

RAM Execution

To summarize:
This gives us a real but limited way to control where this jump ends up. The restrictions are the addresses available outside of the normal table range for object handler addresses and the object IDs available outside normal range in the object table.
Out of the limited options, $00C6 is the best because it is actually the only option we have any further control over without risk of corruption.

Health Bytes

$C6-$C7 represent the current health, while $C8-$C9 represent the max health. We have nearly full control over the 2 bytes of health as using boost for one frame subtracts one from the health value. The only restriction is that the high byte cannot exceed #E0. We do not have control over the max health bytes, but since $C8 is always #00 we are able to setup a jump instruction to another location in the zero page that we have more than 2 bytes of control. The best option turns out to be the 3 bytes of RNG, located at $5A-$5C. This means we need our health bytes set to #4C and #5A create instruction JMP $005A (4C 5A 00).

RNG Bytes

Effectively we have full control over RNG bytes $5A-$5C. $5A is affected by player 1 and 2 inputs and the frame count. Player 2 is especially important because those inputs do not affect other gameplay at all. Each frame, a bit from $5A is left shifted through $5C and into $5B giving us full control over these 3 bytes by performing input manipulations for 16 frames leading up to the critical frame. With 3 bytes fully at our disposal, we can jump to anywhere in RAM that we have even more control over. There are two good options, each with 8 straight bytes of complete control over. We will use $0241-$0248 which represent the y-subpixel positions of the player bullets. The RNG values we manipulate will produce instruction JMP $0241 (4C 41 2A).

Bullet Position Bytes

The RAM table that represents local objects has 8 slots for player bullets, which comes with position tables. Even when a bullet despawns, it's position remains stored in RAM. Additionally, subpixel positions are not cleared when a new bullet is spawned. This is actually a very critical detail because the stars that appear when warping actually consume the same bullet slots in RAM, but do not change the subpixel positions at all. The pixel positions are affected, however, which is why subpixel positions are being utilized instead.
While both x-axis and y-axis subpixel positions are controllable, the y-axis is chosen because it is easier to implement in a run. This is due to the player's x-axis velocity typically being maxed out. When at max speed, the subpixel position of that axis will not change as a bullet is shot and moves. Rather than adjust x-axis velocity and potentially slowing down the player we will use the y-axis subpixel positions of the player bullets.
These RAM values are only viable for ACE because this game actually represents position to the 1/256th of a pixel, meaning any byte in the #00-#FF range can be set as the subpixel value. This means we have absolute control of 8 straight bytes worth of instructions.

Initiating the Credits

If initiating the credits was as simple as jumping to somewhere, then this could have been done during RNG manipulation. There are two factors that make this not possible.
This ROM has multiple PRG banks. The one that is currently loaded does not contain the code to initiate the credits, which is located in bank #00 at address $AC20.
Jumping straight to the code that initiates the credits does not work immediately. There is one state byte that needs set first, otherwise the game will get stuck in a loop. Address $0075 needs to be set to #00.
Luckily, the accumulator at this stage is set to #00, meaning we can perfectly fit the instructions to initiate the credits in 8 bytes as such:
 STA $75 (85 75)
 STA $FFCB (8D CB FF)
 JMP $AC20 (4C 20 AC)
Executing these instructions immediately starts the credits, surprisingly with no corrupted visuals at all.

The Route

To set up the corrupted warp state, we must perform the following steps:
One other note here - While warping to planet 1 part room is possible as well and could be a lot faster due to the size of planet 1, this is not viable because there are not enough enemies on planet 1 and thus have no control over the corrupted byte.

GameResources/NES/SolarJetman last edited by ChucJo 16 days ago
Page History Latest diff List referrers View Source