Yet another unused function. No idea what it could have been intended
for – no boss sprite has animated sections that small.
But come on, out of all places that could have required the macro form
of a VRAM offset function, it had to be a unused one…
Anyway, that completes the low-level blitting functions for boss
entities!
Part of P0108, funded by Yanga.
Another unused function! This might indicate that Elis could originally
split herself into two sprites, similar to TH04 Stage 6 Yuuka?
Or it might just have been some other kind of animation effect, who
knows.
Part of P0108, funded by Yanga.
Used for Elis' entrance animation. Including an unused unblitting +
blitting function – apparently, ZUN wrote that one before the sloppy
EGC-accelerated wavy unblitting function that I decompiled way back in
6d2fa9f.
Oh well, the latter one is fine in context, since there are no other
sprites on screen during the animation.
Part of P0108, funded by Yanga.
… except that those single-plane functions provide basically no
useful abstraction in the context of a 4-plane image format. Should
have just gone all the way and directly take something like our
Planar<dots16_t> type to operate on all 4 planes, so that I wouldn't
have had to add that layer of abstraction myself.
Apparently, early ZUN was allergic to passing structures by reference?
Part of P0107, funded by Yanga.
Yes, functions! Those would have been nice earlier!
Also documented another overly specific Borland C++ code generation
quirk with regard to assigning expression results to elements of 16-bit
arrays…
Part of P0107, funded by Yanga.
"Oh wait, actually, I *do* want to blit 8 pixels at a time in some
cases" :zunpet:
First time in a long while that the VRAM access macros couldn't do the
job, because code generation wants *both* pointer arithmetic *and*
subscripts within the same expression.
But *come on*, just blit the 16 pixels for the byte-aligned case!
Part of P0107, funded by Yanga.
So if the boss entity class stores all non-pixel metadata for its
associated .BOS file, how can YuugenMagan have 5 independent eye
sprites, or Kikuri have more than one soul and tear sprite?
By duplicating that data, of course!
Those structure copies to local variables seen in the load functions
of these bosses still remain pointless, though.
Completes P0106, funded by Yanga.
"Hm, I have this format that specifies sprite width in multiples of 8,
but *actually*, I'd like to blit 16 pixels at a time… well, time to
pepper the code with divisions and casts, I guess :zunpet:"
Which becomes even more hilarious once you realize that the `operator
new` function does require bytes after all. Which leads
new dots16_t[image_size / 2];
to compile to
operator new((image_size / 2) * 2);
:tannedcirno:
Part of P0106, funded by Yanga.
What else to call something a boss can have multiple of, that may or
may not be part of a larger boss sprite, may or may not be animated,
and that may or may not have a orb hitbox?
Needed to be RE'd before the .BOS loading functions, because those are
actually methods of this class. And because renaming all these
variables in ASM land is tedious anyway, we might as well name each
individual entity of every boss.
Still, what's with the pointless local variable copies in the
YuugenMagan and Kikuri functions?
Part of P0106, funded by Yanga.
And that's all? No unblitting or non-aligned functions? Oh well, at
least this one correctly clips the sprite at all 4 edges of VRAM, for
once.
Part of P0105, funded by Yanga.
One fewer magic number. And one more deliberate dependency on a
PC-98-specific hardware constant, to further drive home just how
unportable these games are, even once decompilation will be complete.
Part of P0105, funded by Yanga.
Leading to slight complications in TH02's Music Room and shot type
selection menus. Thought about leaving those in C for a while, but I
still think it's worth it for the consistency we get with the VRAM
offset functions. Also, we'll have similar code for the main menus of
later games, and I'll surely won't be using C++ when starting out with
these.
Part of P0105, funded by Yanga.
We not only actually still need the (pointer, size) function to fill
heap-allocated memory, but we also need to differentiate between near
and far pointers, to move those pesky `PUSH DS` instructions to their
original places?!
Part of P0105, funded by Yanga.
"Hey, let's have separate functions for uint16_t and int16_t… and then
just *not* support negative numbers in the latter" :zunpet:
Part of P0104, funded by Ember2528.
If you've ever cheated more than 6 lives in TH01, you might have
noticed that those additional lives appear in additional rows in the
HUD. And well, that had to be coded somewhere…
Part of P0104, funded by Ember2528.
Both inlined and non-inlined page switching within the same function,
together with an approach that doesn't correspond to our other planar
access macros? That code must have been written during a very
experimental phase very early in the development of this game.
Part of P0104, funded by Ember2528.
Hey look, this one *does* only render to a single page, and then blits
the area to the other one! Using an unnecessary new function that isn't
even EGC-accelerated…
Completes P0103, funded by Ember2528.
The scores are rendered to *both* VRAM pages…? Which means that we
need a separate set of sprites to store the background behind the
numbers. This does not bode well for animated backgrounds…
Part of P0103, funded by Ember2528.
How optimized is TH01? Well, since the player Y is constant, checking
for Y ≥ 368 first would rule out a collision for the majority of
entities during gameplay after just 1 check.
TH01 still checks X first.
Part of P0102, funded by Yanga.
Or, in more relevant news: That's the function that forced TH01's
pellet sprites to be defined in C land. First sprite to make that jump.
Part of P0102, funded by Yanga.
… pellet unblitting uses a *doubly* sloppy 16×8 rectangle. 🤦
The resulting terrible flickering is probably why the Stage 15 and 20
battles enable this weird "interlace" mode that only renders and
hit-tests half of the pellets each frame… except that player shots
are still hit-tested every frame?
So yeah, your eyes aren't deceiving you, the game does effectively drop
its perceived frame rate in the Elis, Kikuri, Sariel, and Konngara
fights, and it does so deliberately.
And *then* you realize that those weird hit tests are actually a
futile attempt to mitigate the disastrous effects of a way too large
unblitting rectangle. Congratulations, you've found the most stupid
piece of code in this game.
Part of P0102, funded by Yanga.
ZUN might have gotten the impression that the EGC can *only* work with
multiples of 16 pixels per load or store? Which might explain why…
Part of P0102, funded by Yanga.
Note how Turbo C++ auto-generates that call to `operator new`, which
you don't see in the decompilation anymore. So yeah, as soon as you add
a constructor, Turbo C++ enforces heap allocation for any instance of
that class, even function-local ones that would otherwise be
stack-allocated.
That's where the bad reputation of C++ comes from, I guess?
Part of P0102, funded by Yanga.
First off: 👏 Just 👏 pass 👏 a 👏 reference 👏 to 👏 the 👏
currently 👏 iterated 👏 pellet 👏 if 👏 you 👏 need 👏 to 👏
modify 👏 it 👏
Second: Don't we hit-test pellets vs. shots already, in the CShots
class? So what is this garbage…?!
Completes P0101, funded by Yanga and Ember2528.
Actually quite cute with their hardcoded limitations, since the created
patterns do follow a clear logic.
But yeah, no 6-way spreads for you, we only support 2-, 3-, 4-, and
5-way ones here :zunpet:
Part of P0101, funded by Yanga and Ember2528.
Look, an unused pellet motion type! Which doesn't work as intended
thanks to no fewer than 3 ZUN bugs in one single if() expression.
Part of P0100, funded by Yanga.
A 4227-byte structure, with 100 pellet_t instances followed by a few
other data members. A textbook example of why you can't just quickly
get full position independence by parsing individual lines of ASM.
Part of P0100, funded by Yanga.
Also great news for those people who want to remove any and all C++ in
their mods, because this forces us to spell out subpixel literals as
actual floats, every time. And with that, you're back to being able to
simply search-replace for all the instances you'll have to change.
Part of P0099, funded by Ember2528.
And immediately, we discover another two hardcoded sprites, with, of
course, another set of functions for blitting and unblitting them…
Part of P0099, funded by Ember2528.
1 wrong byte of ASM that slipped through due to a off-by-one error in
an experimental mzdiff branch. Having MOV rather than LES here was
ultimately a harmless mistake though. In context, it's even covered by
the "identical instruction encoding" exception 🙂
A future sprite converter (documented in #8) could then convert these
to C or ASM arrays.
(Except for the piano sprites for TH05's Music Room, which are stored
and used in such a compressed way that it defeats the purpose of
storing them as bitmaps.
Which works in both Borland C++, Open Watcom, and Visual C++.
Not that we're about to port any of the games to these compilers, just
something I noticed while evaluating 32-bit compilers for ReC98's own
32-bit pipeline tools. Modders might want to look into that though,
since 100% position independence also makes it easier to change
compilers.
And that's exactly where TH01's sprite flickering comes from. You
aren't supposed to unblit all entities of one type immediately before
rendering them, since sprites of different entity types are free to
overlap each other. And you might have already blitted one of those
before!
Not to mention that ZUN *still* uses the sloppy unblitting method here.
🙄
Part of P0098, funded by Yanga.
Why the sloppyness of unblitting a whole 16×16 rectangle *if you have
a dedicated function to precisely unblit a .PTN sprite using its alpha
mask*???
Oh well, it's not the regular function called in the main loop, so who
cares…?
Part of P0098, funded by Yanga.
"Physics". Not only did ZUN restrict the X velocity to the 5 discrete
states of -8, -4, 0, 4, and 8 (because hey, unaligned blitting is slow
anyway?), but gravity is also only applied every 5 frames.
We're still missing quite a bit of usage code, but these are the core
functions. One of which turned out to be undecompilable, due to… a
rigorously defined instruction order when performing arithmetic between
`double`s and `float`s?! Still, spelling out all this stuff in ASM
seems much better than somehow splitting the data segment, just so that
we can immediately use literals there.
Part of P0097, funded by Ember2528.
… Wow, the orb is *actually* only ever displayed at byte-aligned X
coordinates, divisible by 8. It's only thanks to the constant spinning
that its movement appears at least *somewhat* smooth.
(And yeah, this is purely a rendering issue; internally, its position
*is* tracked at pixel precision.)
Part of P0097, funded by Ember2528.
So even TH01 wasn't 100% C++ after all. Turns out that this function
was the only instance in all of REIIDEN.EXE where ReC98 previously had
different encodings for identical x86 instructions.
Part of P0096, funded by Ember2528.
Surprise, it's the (terribly suboptimal) setgsta() example function
from the PC-9801 Programmers' Bible! 100% identical, so ZUN must have
read that book as well.
Used for screen shaking effects, as well as the scrolling backgrounds
at the start of the Final Boss stages.
Completes P0095, funded by Yanga.
No idea why ZUN split the 6 slots into 4 with a regular alpha plane,
and 2 with a negated alpha plane. The latter are only used for Sariel's
animations.
Part of P0095, funded by Yanga.
> "Sure, this is a PI push, but this second-to-last set of PI false
positives so small, let's immediately decompile it ^.^"
> Time taken: 22 minutes
Nice when this works out!
Part of P0095, funded by Yanga.
Too bad we still have to bother with renaming the function via #define
in the few cases where an executable contains two or more copies of
this function. We can't just make it `static`, since TH02 MAINE.EXE
actually does far-call it from a different segment (and thus,
translation unit).
Part of P0094, funded by Yanga.
The REIIDEN.EXE version (which is only shown when game-overing) has a
completely invisible timeout that force-enters a high score name after
1000... *keyboard inputs*? Not frames? Why. Like, how do even you
realistically to such a number.
(Best guess: It's a hidden easter egg to amuse players who place
drinking glasses on cursor keys. Or beer bottles.)
And hey, that initialization of the name variable with ASCII spaces.
The only actually meaningful byte-wise access… except that it's not,
because ZUN could have just used the Shift-JIS ideographic space for
the exact same effect.
Completes P0093, funded by Ember2528.
Now with POSIX file I/O in both executables. And the confirmation that
the name array is indeed exclusively accessed per-byte.
Part of P0093, funded by Ember2528.
It's exactly as terrible as you would have expected after hearing
"alphabet cursor actually stored as on-screen position". Nothing gained
in reducing redundancy any further here, any meaningful change would
pretty much have to rewrite the entire thing.
Part of P0093, funded by Ember2528.
In which inline functions are apparently the only way to trick Turbo
C++ into not actually optimizing away the useless `AND AL, 0FFh`.
But come on. Just accessing the high score name characters in chunks of
16-bit Shift-JIS codepoints (which is what they are!) instead of
breaking them down to bytes *everywhere* would have been both more
readable (no swapping of Shift-JIS literals, no bitshifts), more
efficient (this isn't running on an 8-bit CPU after all), and less of a
waste of my time…
Completes P0092, funded by Yanga.
Lol, the internal alphabet cursor position is actually stored as the
on-screen top-left position of the selected character, which this
function then maps back to a character index. At least that gets rid of
quite a bunch of PI false positives now.
Part of P0092, funded by Yanga.
With ternary operator expressions straight out of Jigoku.
(TL note: Jigoku means hell.)
Nice to see that Turbo C++ apparently has no nesting limit on function
call inlining in general, though!
Part of P0092, funded by Yanga.
The only TH01 executable not supporting the numpad?
And that's the point where you just rewrite the distinct input_prev_*
values as a single array, because they're local to input_sense()
anyway.
Part of P0091, funded by Ember2528.
Yes, TH01's memory info screen will recurse into itself for every 3
frames the PgUp key is held, requiring one additional PgDown press per
recursion to actually get out of it.
You can, of course, also crash the system via a stack overflow this
way, if that's your thing.
Part of P0091, funded by Ember2528.
Of which we can express precisely *nothing* as an inline function,
because Turbo C++ always emits a useless `JMP SHORT $+2` at the end of
such an inlined function if it contains nested `if` statements. This is
also what forced some of the functions in 90252cc to be expressed as
macros. By now, this is clear enough to be documented separately.
And to warrant this separate commit.
Completes P0090, funded by Yanga.
Starting with the odd one out, the one that doesn't use master.lib and
has two input sense functions: one for the main menu, and one for the
option window.
Both of which also immediately perform the ring arithmetic on the menu
cursor variable… because there's nothing else to be done with these
inputs in OP.EXE? Separating input sensing from processing apparently
wasn't all too obvious of a thought, and it's only truly done in TH02
and later.
Part of P0090, funded by Yanga.
That's… pretty specific. The only thing on the main menu with this
color is the "1996 ZUN" text at the bottom… probably part of an
effect that we never got to see. Every other idea would be baseless
speculation, given that the snapped buffer isn't used anywhere else.
Part of P0090, funded by Yanga.
Adding op/, main/, and end/ directories does nicely cover a great
majority of the "not really further classifiable slices" implied in
d56bd45.
Part of P0086, funded by [Anonymous] and Blue Bolt.
Again, 11 necessary workarounds, vs. forcing byte aligment in at least
18 places, and that number would have significantly grown in the
future.
Part of P0085, funded by -Tom-.
5 enums where code generation wants an `int`, vs. 11 cases where using
the minimum size is exactly the right default. So it's way more
idiomatic to force those 5 to 16 bits via a dummy element… except that
we can't give it a single, consistent name, because you can't redeclare
the same element in a different enum later.
Oh well, let's have this ugly naming convention instead, which makes it
totally clear that the force element not, in fact, a valid value of
that enum.
Part of P0085, funded by -Tom-.
Which repurpose the .PTN image slots to store the background of
frequently updated VRAM sections, like all the numbers in the HUD.
Future games would simply use the text RAM and gaiji for numbers. Which
would have worked just fine for TH01 as well (especially since all the
functions we've seen so far are aligned to the 8-pixel byte grid), but
it looks as if ZUN simply wasn't aware of gaiji during the development
of TH01.
Part of P0083, funded by Yanga.
Mangled C++ function names would *not* have been a mistake if I hadn't
made the other mistake of restricting parts of the code to C…
Part of P0083, funded by Yanga.
Which actually does inline… in C++, because Turbo C++ doesn't support
the `inline` keyword in C mode. So much for the superiority of that
language, even in 1994…
Part of P0083, funded by Yanga.
All the weird double returns in FUUIN.EXE just magically appear with
-O-! 😮
And yeah, it's a bowl of global state spaghetti once again. 🍝 Named
the functions in a way that would make sense to a user of the API, who
should be aware of typical side effects, like, y'know, a changed
hardware palette… That's how you end up with the supposed "main"
function getting a "_palette_show" suffix.
Completes P0082, funded by Ember2528.
They do make everything so much simpler, after all. Especially now that
th01/formats/grz.cpp should compile as a freestanding translation unit,
as part of the .GRZ viewer.
Part of P0082, funded by Ember2528.
Since we not only have the .PTN sub-image count array in the middle of
all those .GRP flags, but the .PTN loading code also reusing the
palette set flag…
Part of P0082, funded by Ember2528.
Turns out that the .GRP file functions are, of course, also present in
FUUIN.EXE… but use different palette fade functions there? This is the
first function in the way.
Part of P0082, funded by Ember2528.
Yet another run-length encoded graphics format, this one being
exclusively used to wastefully store Konngara's sword slash and kuji-in
kill "animation".
But for once, the terrible code generated by inline functions with
non-literal parameters perfectly matches what ZUN wrote here.
Part of P0081, funded by Ember2528.
I tried `brge` for the latter, but that had *the* most horrible
ergonomics, and I misspelled it as `bgre` 100% of the times I typed it
manually. Turns out that `dots` is also consistent with master.lib's
naming scheme, leaving `planar` to *actually* refer to types storing
multiple planes worth of pixels. These types are showing up more and
more, and deserve something better than their previous long-winded and
misleading name.
Part of P0081, funded by Ember2528.
Just like with the z_text_*() functions, master.lib doesn't already
have graph_init() and graph_exit() either, and once again, ZUN's code
here doesn't fully correspond to any master.lib function. Unlike the
z_text_*() functions though, those names aren't really the best
descriptions for these rather random combinations of BIOS calls and I/O
port writes…
Anyway, that's the entire segment!
Part of P0080, funded by Ember2528 and Splashman.
No macros for the port numbers here! Anyone who will try to read and
understand this code will probably want to look those up in PC-98
hardware documentation, and macros would just be an annoying layer of
indirection then.
Part of P0080, funded by Ember2528 and Splashman.
Page… 2? On a system with only page 0 and 1? Had to get out my real
PC-98 to double-check that I wasn't missing anything here, since
every emulator only looks at the bottom bit of the page number. But
real hardware seems to do the same, and there really is nothing special
to it semantically, being equivalent to page 0. 🤷
Part of P0080, funded by Ember2528 and Splashman.
Allowing us to then retrieve it using a function call with no run-time
cost, although we do have to be careful with the types here.
Also, is that another solution to decompilation puzzles that involve
types of number literals?
Part of P0080, funded by Ember2528 and Splashman.
And with all possible .COM executables decompiled, this set of changes
reaches an acceptable scope, allowing us to *finally*…
Part of P0077, funded by Splashman and -Tom-.
In which our typedefs mercilessly reveal ZUN's original sloppiness, and
the unncessary sign extension taking place here. Also, ❎ unused…
Completes P0069, funded by [Anonymous] and Yanga.
Yes, when clipping the start and end points to the screen area, ZUN
uses an integer division to calculate the line slopes, rather than a
floating-point one. Doesn't seem like it actually causes any incorrect
lines to be drawn, though; that case is only hit in the Mima boss
fight, which draws a few lines with a bottom coordinate of 400 rather
than 399. It *might* also restore the wrong pixels at parts of the
YuugenMagan fight, causing weird flickering, but seriously, that's an
issue everywhere you look in this game.
Part of P0069, funded by [Anonymous] and Yanga.
Templates would have been nicer, but as soon as you add just one
non-immediate parameter, Turbo C++ generates a useless store to a new
local variable, ruining the generated code.
Part of P0069, funded by [Anonymous] and Yanga.
TH01's (original) version also replicates the PC-98 text RAM's reverse
and underline attributes. Which was removed in later games,
interestingly and inconsistently enough.
Part of P0068, funded by Yanga.
I did consider not doing this, because "well, can't anyone who's
*actually* interested just diff the TH01 and TH02 implementations to
figure out the differences themselves", but that duplication ended up
feeling too filthy after all.
And hey, it's a nice excuse to update TH02's version to current naming
standards! 😛
Part of P0068, funded by Yanga.
Including the longest function present in more than one game among all
of PC-98 Touhou, and #23 on the list of longest functions overall,
which draws a 1-pixel line between two arbitrary pixels.
Completes P0067, funded by Splashman.
Semi-unused, that is, the one use of this function doesn't actually
move the rectangle to a different position. Ironically, the non-moving
back-to-front function immediately above *is* unused…
Also, too bad that stack order is the only reason we can't use structs
to combine all plane variables into a single object.
Part of P0067, funded by Splashman.
Previously sloppily mis-RE'd as "some page variable, idk", back in
2015…
Now also with a page number typedef. And yeah, restricting bool to C++
has now proven to be stupid after all.
Part of P0067, funded by Splashman.