- 📝 Posted:
- 🚚 Summary of:
- P0189
- ⌨ Commits:
22abdd1...b4876b6
- 💰 Funded by:
- Arandui, Lmocinemod
- 🏷 Tags:
(Before we start: Make sure you've read the current version of the FAQ section on a potential takedown of this project, updated in light of the recent DMCA claims against PC-98 Touhou game downloads.)
Slight change of plans, because we got instructions for
reliably reproducing the TH04 Kurumi Divide Error crash! Major thanks to
Colin Douglas Howell. With those, it also made sense to immediately look at
the crash in the Stage 4 Marisa fight as well. This way, I could release
both of the obligatory bugfix mods at the same time.
Especially since it turned out that I was wrong: Both crashes are entirely
unrelated to the custom entity structure that would have required PI-centric
progress. They are completely specific to Kurumi's and Marisa's
danmaku-pattern code, and really are two separate bugs
with no connection to each other. All of the necessary research nicely fit
into Arandui's 0.5 pushes, with no further deep understanding
required here.
But why were there still three weeks between Colin's message and this blog
post? DMCA distractions aside: There are no easy fixes this time, unlike
📝 back when I looked at the Stage 5 Yuuka crash.
Just like how division by zero is undefined in mathematics, it's also,
literally, undefined what should happen instead of these two
Divide error
crashes. This means that any possible "fix" can
only ever be a fanfiction interpretation of the intentions behind ZUN's
code. The gameplay community should be aware of this, and
might decide to handle these cases differently. And if we
have to go into fanfiction territory to work around crashes in the
canon games, we'd better document what exactly we're fixing here and how, as
comprehensible as possible.
With that out of the way, let's look at Kurumi's crash first, since it's way easier to grasp. This one is known to primarily happen to new players, and it's easy to see why:
- In one of the patterns in her third phase, Kurumi fires a series of 3 aimed rings from both edges of the playfield. By default (that is, on Normal and with regular rank), these are 6-way rings.
- 6 happens to be quite a peculiar number here, due to how rings are
(manually) tuned based on the current "rank" value (
playperf
) before being fired. The code, abbreviated for clarity:if(bullets_in_ring >= 5) { if(playperf <= 10) { bullets_in_ring -= 2; } if(playperf <= 4) { bullets_in_ring -= 4; } }
- Let's look at the range of possible
playperf
values per difficulty level:Easy Normal Hard Lunatic Extra playperf_min
4 11 20 22 16 playperf_max
16 24 32 34 20 Edit (2022-05-24): This blog post initially had 26 instead of 16 for
playperf_min
for the Extra Stage. Thanks to Popfan for pointing out that typo! - Reducing rank to its minimum on Easy mode will therefore result in a 0-ring after tuning.
- To calculate the individual angles of each bullet in a ring, ZUN divides
360° (or, more correctly,
📝
0x100
) by the total number of bullets… - Boom, division by zero.
So, what should the workaround look like? Obviously, we want to modify neither the default number of ring bullets nor the tuning algorithm – that would change all other non-crashing variations of this pattern on other difficulties and ranks, creating a fork of the original gameplay. Instead, I came up with four possible workarounds that all seemed somewhat logical to me:
- Firing no bullet, i.e., interpreting 0-ring literally. This would create the only constellation in which a call to the bullet group spawn functions would not spawn at least one new bullet.
- Firing a "1-ring", i.e., a single bullet. This would be consistent with how the bullet spawn functions behave for "0-way" stack and spread groups.
- Firing a "∞-ring", i.e., 200 bullets, which is as much as the game's cap on 16×16 bullets would allow. This would poke fun at the whole "division by zero" idea… but given that we're still talking about Easy Mode (and especially new players) here, it might be a tad too cruel. Certainly the most trollish interpretation.
- Triggering an immediate Game Over, exchanging the hard crash for a softer and more controlled shutdown. Certainly the option that would be closest to the behavior of the original games, and perhaps the only one to be accepted in Serious, High-Level Play™.
As I was writing this post, it felt increasingly wrong for me to make this
decision. So I once again went to Twitter, where 56.3%
voted in favor of the 1-bullet option. Good that I asked! I myself was
more leaning towards the 0-bullet interpretation, which only got 28.7% of
the vote. Also interesting are the 2.3% in favor of the Game Over option but
I get it, low-rank Easy Mode isn't exactly the most competitive mode of
playing TH04.
There are reports of Kurumi crashing on higher difficulties as well, but I
could verify none of them. If they aren't fixed by this workaround, they're
caused by an entirely different bug that we have yet to discover.
Onto the Stage 4 Marisa crash then, which does in fact apply to all difficulty levels. I was also wrong on this one – it's a hell of a lot more intricate than being just a division by the number of on-screen bits. Without having decompiled the entire fight, I can't give a completely accurate picture of what happens there yet, but here's the rough idea:
- Marisa uses different patterns, depending on whether at least one of her bits is still alive, or all of them have been destroyed.
- Destroying the last bit will immediately switch to the bit-less counterpart of the current pattern.
- The bits won't respawn before the pattern ended, which ensures that the bit-less version is always shown in its entirety after being started or switched into.
- In two of the bit-less patterns, Marisa gradually moves to the point reflection of her position at the start of the pattern across the playfield coordinate of (192, 112), or (224, 128) on screen.
- The velocity of this movement is determined by both her distance to that point and the total amount of frames that this instance of the bit-less pattern will last.
- Since this frame amount is directly tied to the frame the player destroyed the last bit on, it becomes a user-controlled variable. I think you can see where this is going…
- The last 12 frames of this duration, however, are always reserved for a "braking phase", where Marisa's velocity is halved on each frame.
- Putting it all together, we get this formula:
boss_velocity.x = ((192 - boss_position.x) / ((duration / 2) - (12 / 2))); boss_velocity.y = ((112 - boss_position.y) / ((duration / 2) - (12 / 2)));
- Set
duration
to 12 or 13, and boom,Divide error
. - This part of the code only runs every 4 frames though. This expands the time window for this crash to 4 frames, rather than just the two frames you would expect from looking at the division itself.
- Both of the broken patterns run for a maximum of 160 frames. Therefore,
the crash will occur when Marisa's last bit is destroyed between frame 152
and 155 inclusive. On these frames, the
last_frame_with_bits_alive
variable is set to 148, which is the crucial 12duration
frames away from the maximum of 160. - Interestingly enough, the calculated velocity is also only
applied every 4 frames, with Marisa actually staying still for the 3 frames
inbetween. As a result, she either moves
- too slowly to ever actually reach the yellow point if the last bit was destroyed early in the pattern (see destruction frames 68 or 112),
- or way too quickly, and almost in a jerky, teleporting way (see destruction frames 144 or 148).
- Finally, as you may have already gathered from the formula: Destroying
the last bit between frame 156 and 160 inclusive results in
duration
values of 8 or 4. These actually push Marisa away from the intended point, as the divisor becomes negative.
tl;dr: "Game crashes if last bit destroyed within 4-frame window near end of two patterns". For an informed decision on a new movement behavior for these last 8 frames, we definitely need to know all the details behind the crash though. Here's what I would interpret into the code:
- Not moving at all, i.e., interpreting 0 as the middle ground between
positive and negative movement. This would also make sense because a
12-frame
duration
implies 100% of the movement to consist of the braking phase – and Marisa wasn't moving before, after all. - Move at maximum speed, i.e., dividing by 1 rather than 0. Since the movement duration is still 12 in this case, Marisa will immediately start braking. In total, she will move exactly ¾ of the way from her initial position to (192, 112) within the 8 frames before the pattern ends.
- Directly warping to (192, 112) on frame 0, and to the point-reflected target on 4, respectively. This "emulates" the division by zero by moving Marisa at infinite speed to the exact two points indicated by the velocity formula. It also fits nicely into the 8 frames we have to fill here. Sure, Marisa can't reach these points at any other duration, but why shouldn't she be able to, with infinite speed? Then again, if Marisa is far away enough from (192, 112), this workaround would warp her across the entire playfield. Can Marisa teleport according to lore? I have no idea…
- Triggering an immediate Game O– hell no, this is the Stage 4 boss, people already hate losing runs to this bug!
Asking Twitter worked great for the Kurumi workaround, so let's do it again! Gotta attach a screenshot of an earlier draft of this blog post though, since this stuff is impossible to explain in tweets…
…and it went through the roof, becoming the most successful ReC98 tweet so far?! Apparently, y'all really like to just look at descriptions of overly complex bugs that I'd consider way beyond the typical attention span that can be expected from Twitter. Unfortunately, all those tweet impressions didn't quite translate into poll turnout. The results were pretty evenly split between 1) and 2), with option 1) just coming out slightly ahead at 49.1%, compared to 41.5% of option 2).
(And yes, I only noticed after creating the poll that warping to both the green and yellow points made more sense than warping to just one of the two. Let's hope that this additional variant wouldn't have shifted the results too much. Both warp options only got 9.4% of the vote after all, and no one else came up with the idea either. In the end, you can always merge together your preferred combination of workarounds from the Git branches linked below.)
So here you go: The new definitive version of TH04, containing not only the
community-chosen Kurumi and Stage 4 Marisa workaround variant, but also the
📝 No-EMS bugfix from last year.
Edit (2022-05-31): This package is outdated, 📝 the current version is here!
2022-04-18-community-choice-fixes.zip
Oh, and let's also add spaztron64's TH03 GDC clock fix
from 2019 because why not. This binary was built from the community_choice_fixes
branch, and you can find the code for all the individual workarounds on
these branches:
th04_0_ring_ignore
th04_0_ring_as_single_bullet
th04_0_ring_as_cap_bullets
th04_0_ring_as_gameover
th04_marisa4_crash_still
th04_marisa4_crash_move
th04_marisa4_crash_warp
Again, because it can't be stated often enough: These fixes are fanfiction. The gameplay community should be aware of this, and might decide to handle these cases differently.
With all of that taking way more time to evaluate and document, this
research really had to become part of a proper push, instead of just being
covered in the quick non-push blog post I initially intended. With ½ of a
push left at the end, TH05's Stage 1-5 boss background rendering functions
fit in perfectly there. If you wonder how these static backdrop images even
need any boss-specific code to begin with, you're right – it's basically the
same function copy-pasted 4 times, differing only in the backdrop image
coordinates and some other inconsequential details.
Only Sara receives a nice variation of the typical
📝 blocky entrance animation: The usually
opaque bitmap data from ST00.BB
is instead used as a transition
mask from stage tiles to the backdrop image, by making clever use of the
tile invalidation system:
TH04 uses the same effect a bit more frequently, for its first three bosses.
Next up: Shinki, for real this time! I've already managed to decompile 10 of her 11 danmaku patterns within a little more than one push – and yes, that one is included in there. Looks like I've slightly overestimated the amount of work required for TH04's and TH05's bosses…