⮜ Blog

⮜ List of tags

Showing all posts tagged
and

📝 Posted:
🚚 Summary of:
P0242, P0243
Commits:
08352a5...dfa758d, dfa758d...ac33bd2
💰 Funded by:
Yanga
🏷 Tags:

OK, let's decompile TH02's HUD code first, gain a solid understanding of how increasing the score works, and then look at the item system of this game. Should be no big deal, no surprises expected, let's go!

…Yeah, right, that's never how things end up in ReC98 land. :godzun: And so, we get the usual host of newly discovered oddities in addition to the expected insights into the item mechanics. Let's start with the latter:


Onto score tracking then, which only took a single commit to raise another big research question. It's widely known that TH02 grants extra lives upon reaching a score of 1, 2, 3, 5, or 8 million points. But what hasn't been documented is the fact that the game does not stop at the end of the hardcoded extend score array. ZUN merely ends it with a sentinel value of 999,999,990 points, but if the score ever increased beyond this value, the game will interpret adjacent memory as signed 32-bit score values and continue giving out extra lives based on whatever thresholds it ends up finding there. Since the following bytes happen to turn into a negative number, the next extra life would be awarded right after gaining another 10 points at exactly 1,000,000,000 points, and the threshold after that would be 11,114,905,600 points. Without an explicit counterstop, the number of score-based extra lives is theoretically unlimited, and would even continue after the signed 32-bit value overflowed into the negative range. Although we certainly have bigger problems once scores ever reach that point… :tannedcirno:
That said, it seems impossible that any of this could ever happen legitimately. The current high scores of 42,942,800 points on Lunatic and 42,603,800 points on Extra don't even reach 1/20 of ZUN's sentinel value. Without either a graze or a bullet cancel system, the scoring potential in this game is fairly limited, making it unlikely for high scores to ever increase by that additional order of magnitude to end up anywhere near the 1 billion mark.
But can we really be sure? Is this a landmine because it's impossible to ever reach such high scores, or is it a quirk because these extends could be observed under rare conditions, perhaps as the result of other quirks? And if it's the latter, how many of these adjacent bytes do we need to preserve in cleaned-up versions and ports? We'd pretty much need to know the upper bound of high scores within the original stage and boss scripts to tell. This value should be rather easy to calculate in a game with such a simple scoring system, but doing that only makes sense after we RE'd all scoring-related code and could efficiently run such simulations. It's definitely something we'd need to look at before working on this game's debloated version in the far future, which is when the difference between quirks and landmines will become relevant. Still, all that uncertainty just because ZUN didn't restrict a loop to the size of the extend threshold array…


TH02 marks a pivotal point in how the PC-98 Touhou games handle the current score. It's the last game to use a 32-bit variable before the later games would regrettably start using arrays of binary-coded decimals. More importantly though, TH02 is also the first game to introduce the delayed score counting animation, where the displayed score intentionally lags behind and gradually counts towards the real one over multiple frames. This could be implemented in one of two ways:

  1. Keep the displayed score as a separate variable inside the presentation layer, and let it gradually count up to the real score value passed in from the logic layer
  2. Burden the game logic with this presentation detail, and split the score into two variables: One for the displayed score, and another for the delta between that score and the actual one. Newly gained points are first added to the delta variable, and then gradually subtracted from there and added to the real score before being displayed.

And by now, we can all tell which option ZUN picked for the rest of the PC-98 games, even if you don't remember 📝 me mentioning this system last year. 📝 Once again, TH02 immortalized ZUN's initial attempt at the concept, which lacks the abstraction boundaries you'd want for managing this one piece of state across two variables, and messes up the abstractions it does have. In addition to the regular score transfer/render function, the codebase therefore has

And – you guessed it – I wouldn't have mentioned any of this if it didn't result in one bug and one quirk in TH02. The bug resulting from 1) is pretty minor: The function is called when losing a life, and simply stops any active score-counting animation at the value rendered on the frame where the player got hit. This one is only a rendering issue – no points are lost, and you just need to gain 10 more for the rendered value to jump back up to its actual value. You'll probably never notice this one because you're likely busy collecting the single 5-power spawned around Reimu when losing a life, which always awards at least 10 points.

The quirk resulting from 2) is more intriguing though. Without a separate reset of the score delta, the function effectively awards the current delta value as a one-time point bonus, since the same delta will still be regularly transferred to the score on further game frames.
This function is called at the start of every dialog sequence. However, TH02 stops running the regular game loop between the post-boss dialog and the next stage where the delta is reset, so we can only observe this quirk for the pre-boss sequences and the dialog before Mima's form change. Unfortunately, it's not all too exploitable in either case: Each of the pre-boss dialog sequences is preceded by an ungrazeable pellet pattern and followed by multiple seconds of flying over an empty playfield with zero scoring opportunities. By the time the sequence starts, the game will have long transferred any big score delta from max-valued point items. It's slightly better with Mima since you can at least shoot her and use a bomb to keep the delta at a nonzero value, but without a health bar, there is little indication of when the dialog starts, and it'd be long after Mima gave out her last bonus items in any case.
But two of the bosses – that is, Rika, and the Five Magic Stones – are scrolled onto the playfield as part of the stage script, and can also be hit with player shots and bombs for a few seconds before their dialog starts. While I'll only get to cover shot types and bomb damage within the next few TH02 pushes, there is an obvious initial strategy for maximizing the effect of this quirk: Spreading out the A-Type / Wide / High Mobility shot to land as many hits as possible on all Five Magic Stones, while firing off a bomb.

Turns out that the infamous button-mashing mechanics of the player shot are also more complicated than simply pressing and releasing the Shot key at alternating frames. Even this result took way too many takes.

Wow, a grand total of 1,750 extra points! Totally worth wasting a bomb for… yeah, probably not. :onricdennat: But at the very least, it's something that a TAS score run would want to keep in mind. And all that just because ZUN "forgot" a single score_delta = 0; assignment at the end of one function…

And that brings TH02 over the 30% RE mark! Next up: 100% position independence for TH04. If anyone wants to grab the that have now been freed up in the cap: Any small Touhou-related task would be perfect to round out that upcoming TH04 PI delivery.

📝 Posted:
🚚 Summary of:
P0088, P0089
Commits:
97ce7b7...da6b856, da6b856...90252cc
💰 Funded by:
-Tom-, [Anonymous], Blue Bolt
🏷 Tags:

As expected, we've now got the TH04 and TH05 stage enemy structure, finishing position independence for all big entity types. This one was quite straightfoward, as the .STD scripting system is pretty simple.

Its most interesting aspect can be found in the way timing is handled. In Windows Touhou, all .ECL script instructions come with a frame field that defines when they are executed. In TH04's and TH05's .STD scripts, on the other hand, it's up to each individual instruction to add a frame time parameter, anywhere in its parameter list. This frame time defines for how long this instruction should be repeatedly executed, before it manually advances the instruction pointer to the next one. From what I've seen so far, these instruction typically apply their effect on the first frame they run on, and then do nothing for the remaining frames.
Oh, and you can't nest the LOOP instruction, since the enemy structure only stores one single counter for the current loop iteration.

Just from the structure, the only innovation introduced by TH05 seems to have been enemy subtypes. These can be used to parametrize scripts via conditional jumps based on this value, as a first attempt at cutting down the need to duplicate entire scripts for similar enemy behavior. And thanks to TH05's favorable segment layout, this game's version of the .STD enemy script interpreter is even immediately ready for decompilation, in one single future push.

As far as I can tell, that now only leaves

until TH05's MAIN.EXE is completely position-independent.
Which, however, won't be all it needs for that 100% PI rating on the front page. And with that many false positives, it's quite easy to get lost with immediately reverse-engineering everything around them. This time, the rendering of the text dissolve circles, used for the stage and BGM title popups, caught my eye… and since the high-level code to handle all of that was near the end of a segment in both TH04 and TH05, I just decided to immediately decompile it all. Like, how hard could it possibly be? Sure, it needed another segment split, which was a bit harder due to all the existing ASM referencing code in that segment, but certainly not impossible…

Oh wait, this code depends on 9 other sets of identifiers that haven't been declared in C land before, some of which require vast reorganizations to bring them up to current consistency standards. Whoops! Good thing that this is the part of the project I'm still offering for free…
Among the referenced functions was tiles_invalidate_around(), which marks the stage background tiles within a rectangular area to be redrawn this frame. And this one must have had the hardest function signature to figure out in all of PC-98 Touhou, because it actually seems impossible. Looking at all the ways the game passes the center coordinate to this function, we have

  1. X and Y as 16-bit integer literals, merged into a single PUSH of a 32-bit immediate
  2. X and Y calculated and pushed independently from each other
  3. by-value copies of entire Point instances

Any single declaration would only lead to at most two of the three cases generating the original instructions. No way around separately declaring the function in every translation unit then, with the correct parameter list for the respective calls. That's how ZUN must have also written it.

Oh well, we would have needed to do all of this some time. At least there were quite a bit of insights to be gained from the actual decompilation, where using const references actually made it possible to turn quite a number of potentially ugly macros into wholesome inline functions.

But still, TH04 and TH05 will come out of ReC98's decompilation as one big mess. A lot of further manual decompilation and refactoring, beyond the limits of the original binary, would be needed to make these games portable to any non-PC-98, non-x86 architecture.
And yes, that includes IBM-compatible DOS – which, for some reason, a number of people see as the obvious choice for a first system to port PC-98 Touhou to. This will barely be easier. Sure, you'll save the effort of decompiling all the remaining original ASM. But even with master.lib's MASTER_DOSV setting, these games still very much rely on PC-98 hardware, with corresponding assumptions all over ZUN's code. You will need to provide abstractions for the PC-98's superimposed text mode, the gaiji, and planar 4-bit color access in general, exchanging the use of the PC-98's GRCG and EGC blitter chips with something else. At that point, you might as well port the game to one generic 640×400 framebuffer and away from the constraints of DOS, resulting in that Doom source code-like situation which made that game easily portable to every architecture to begin with. But ZUN just wasn't a John Carmack, sorry.

Or what do I know. I've never programmed for IBM-compatible DOS, but maybe ReC98's audience does include someone who is intimately familiar with IBM-compatible DOS so that the constraints aren't much of an issue for them? But even then, 16-bit Windows would make much more sense as a first porting target if you don't want to bother with that undecompilable ASM.

At least I won't have to look at TH04 and TH05 for quite a while now. :tannedcirno: The delivery delays have made it obvious that my life has become pretty busy again, probably until September. With a total of 9 TH01 pushes from monthly subscriptions now waiting in the backlog, the shop will stay closed until I've caught up with most of these. Which I'm quite hyped for!

📝 Posted:
🚚 Summary of:
P0086, P0087
Commits:
54ee99b...24b96cd, 24b96cd...97ce7b7
💰 Funded by:
[Anonymous], Blue Bolt, -Tom-
🏷 Tags:

Alright, the score popup numbers shown when collecting items or defeating (mid)bosses. The second-to-last remaining big entity type in TH05… with quite some PI false positives in the memory range occupied by its data. Good thing I still got some outstanding generic RE pushes that haven't been claimed for anything more specific in over a month! These conveniently allowed me to RE most of these functions right away, the right way.

Most of the false positives were boss HP values, passed to a "boss phase end" function which sets the HP value at which the next phase should end. Stage 6 Yuuka, Mugetsu, and EX-Alice have their own copies of this function, in which they also reset certain boss-specific global variables. Since I always like to cover all varieties of such duplicated functions at once, it made sense to reverse-engineer all the involved variables while I was at it… and that's why this was exactly the right time to cover the implementation details of Stage 6 Yuuka's parasol and vanishing animations in TH04. :zunpet:

With still a bit of time left in that RE push afterwards, I could also start looking into some of the smaller functions that didn't quite fit into other pushes. The most notable one there was a simple function that aims from any point to the current player position. Which actually only became a separate function in TH05, probably since it's called 27 times in total. That's 27 places no longer being blocked from further RE progress.

WindowsTiger already did most of the work for the score popup numbers in January, which meant that I only had to review it and bring it up to ReC98's current coding styles and standards. This one turned out to be one of those rare features whose TH05 implementation is significantly less insane than the TH04 one. Both games lazily redraw only the tiles of the stage background that were drawn over in the previous frame, and try their best to minimize the amount of tiles to be redrawn in this way. For these popup numbers, this involves calculating the on-screen width, based on the exact number of digits in the point value. TH04 calculates this width every frame during the rendering function, and even resorts to setting that field through the digit iteration pointer via self-modifying code… yup. TH05, on the other hand, simply calculates the width once when spawning a new popup number, during the conversion of the point value to binary-coded decimal. The "×2" multiplier suffix being removed in TH05 certainly also helped in simplifying that feature in this game.

And that's ⅓ of TH05 reverse-engineered! Next up, one more TH05 PI push, in which the stage enemies hopefully finish all the big entity types. Maybe it will also be accompanied by another RE push? In any case, that will be the last piece of TH05 progress for quite some time. The next TH01 stretch will consist of 6 pushes at the very least, and I currently have no idea of how much time I can spend on ReC98 a month from now…