Given that I still have your attention before things get technical again, let's start this post with a hilarious discovery:
Point items with negative values?
I only discovered this quirk by accident, as another result of 📝 my 2024 code cleanup. Back then, pbg's original formatting code for these point item popups contained one of the numerous code analysis warnings I chose to address rather than ignore:
e->point is unsigned, which surely means that you can never collect point items with negative values, right? Well, I was just testing a newly changed part of the architecture when this happened:
Nice underflow.
Surprise, the game does in fact not clamp point item values to 0! Looking at the collection value formula immediately tells us what happens there:
(((469 - Viv.y) + (Viv.evade × 4)) × 160)
This works fine for regular gameplay where VIVIT's Y position is clamped to exactly 469. It also explains the 0 points you get for collecting point items while bottom-hugging with an empty Evade chain. But once VIVIT collides with anything and loses a life, her Y position is immediately set to 659, before she moves up at a speed of 1.5 pixels per frame. This creates a 127-frame window where the point formula will produce negative values. While items do despawn once their center Y coordinate is >487, it makes little sense to attempt to narrow this window down to fewer frames:
Items only despawn afterthe collision check against VIVIT, giving them one additional frame below 487 pixels where VIVIT can collect them.
Items accelerate at a rate of 3/64 or 0.046875 pixels per frame, up to a maximum speed of 6 pixels per second. Since VIVIT will collect any item whose center Y position is 23.984375 pixels higher than her own, this seems to result in (487 + 6 + 23.984375) = 516.984375 as the earliest Y position where a respawning VIVIT could collect items, reducing the window to just the 32 frames where VIVIT is between 516.5 and 470 pixels.
But even with that limit in mind, stage scripts can (and do) spawn enemies far outside the bounds of the playfield, and shots and bombs can freely destroy them there.
Clearly though, we have to do something about this issue. But what, and how?
First of all, the 5×7-pixel font used for these popups does not define a minus sign sprite. The original %d format specifier did add a minus sign into the string, but the popup renderer simply skipped over any non-numeric character. These negative point item values have always been part of the game, but the lack of a visible minus sign made their popups blend in with regular positive ones. If it weren't for code analysis convincing me to change %d to %u, we might have never spotted this entire issue.
Secondly, the score displayed in the top-left corner of the playfield can only increase and never decrease. Negative score points therefore only really "exist" as part of the score delta, where they soak up points from other score sources until the delta gets positive again.
We could immediately reflect any negative deltas in the displayed total score:
However, that number isn't just some interim value, but represents the actual internal and official score that will be recorded in the High Score list, and consequently also in the future replay format. If there's any code path where the delta doesn't get fully added to the score before the game ends – such as 📝 at the end of TH04's Stage 6 and Extra – we'd end up with a different score than the same gameplay would have gotten on pbg's original build. And sure enough, Shuusou Gyoku has the same issue. The delta is only ever fully added to the official score on a Game Over, and otherwise only gradually added as part of VIVIT's update function, in increments of at most 100,000 points per frame. If we immediately committed any negative delta and a player quits after just having collected a point item with a negative value, the official score of that partial run would be lower than it would have been in the original game. This doesn't matter right now because Shuusou Gyoku doesn't enter scores into the High Score list if you quit a round, but it will matter once we start saving such partial runs as replays, which we obviously want to do. But even apart from that, it's ultimately just too minor of an issue to justify any change to the game's well-defined scoring aspects, or to newly introduce an inconsistency between displayed score and recorded score.
Hence, we're left with fixing just the rendering bug of the missing minus signs, which is easily fixed by repurposing one of the various 5×1 pixel lines in #DCDCFE from the system sprite sheet.
So, let's split Shuusou Gyoku's game logic into a library! Step 1, obviously:
Defining the scope of "game logic"
On the one hand, the answer seems simple: The GameMove() function neatly lists all subsystems that are involved in a frame of gameplay. So let's take all of them, move them into a DLL, and then we're done, right? Well, there are several good reasons for not just taking this list of subsystems, and instead identifying and splitting off the actual minimum of gameplay-related code:
Performance. We'd ideally use this isolated game logic core for mass validation of every new release against every replay ever created. The few unneeded subsystems might only add a couple of milliseconds per replay, but that will quickly add up at this scale.
Thread safety, parallelism, and the associated refactoring effort. pbg stored most gameplay state in global variables, so we'd like to bundle all of it into a single C++ class that we can instantiate more than once. After all, 📝 process startup costs 30-40 ms on Windows, so it's much more preferable to multi-thread the replay validator instead. Minimizing the number of affected subsystems minimizes the amount of necessary refactoring work. Also, how about backwards seeking for replays? 👀 Said refactoring work would also lay the groundwork for the several "keyframe" snapshots of game data required for that feature to work at snappy frame rates. Might as well do it correctly now.
If we place all logic code in the new ssg/ directory and leave GIAN07/ with only the presentational aspects, anyone working with this codebase can immediately tell that modifying code in ssg/ runs the risk of forking gameplay and breaking existing replays. But the guidance and implicit documentation we gain from this physical separation can only be actually meaningful if we do make an effort to minimize the amount of code in ssg/ and maximize the amount of freely moddable code and data in GIAN07/.
While the game logic DLL will identify itself with the ReC98 release version number, I've also planned to hardcode a hash of all whitespace-stripped game logic source code. This hash will then be added to every replay as a second layer that can identify the game build a replay was recorded with. Hence, minimizing the amount of hashed code minimizes the chance for this identifying hash to change across versions or forks.
Right at the top of GameMove(), you can see a call to MWinMove(), which processes the game's dialog boxes. Due to my 📝 new lazily-updated immediate-mode approach to text rendering, each text box currently requires a dynamically allocated std::string to hold its exact contents. However, dialog also happens to be the only subsystem referenced in GameMove() that dynamically allocates during gameplay at all. If we can prove that this whole system doesn't affect game logic even with our planned support for showing dialog in the new replay format, we now have a good reason for keeping it in the frontend. And since we can prove it, this goes most of the way towards the pretty bonus goal of reducing C/C++ standard library bloat (and thus, OS API dependencies) in this future DLL.
And if we already don't want to include one subsystem, we might as well not include the correct number of subsystems.
Map tiles are probably the most notable case of decorative data that remains exclusive to the frontend. Since VIVIT never cares what she's flying over, the new logic layer not only doesn't have to process them and their scrolling, but doesn't even have to bother loading and decompressing any per-stage map data. All it needs are the ECL and SCL scripts from ENEMY.DAT.
This leaves us with a pretty concise list of subsystems that comprise Shuusou Gyoku's game logic:
Yup. Shuusou Gyoku uses a single RNG for everything from apparent gameplay aspects to decorative particles. The very first thing you learn about the kind of pseudo-random number generators that are used in replayable games is that they're stateful: Initializing them to a given seed will give you the same sequence of values, and every call to the rand() equivalent will move the internal state to the next number in the sequence. Hence, we end up with a clearly defined order:
…
31305
22709
32031
30508
339
32731
29375
30469
14300
…
Respawned rocks
Enemy initialization
ECL script
Evade particles
A representative sequence of random numbers pulled during a frame near the beginning of Stage 4.
Therefore, every single subsystem that can possibly draw a number from the RNG's sequence must count as game logic by definition. If any of these subsystems are
processed in a different order (which would be a natural side effect of cleanly splitting game logic and apparent presentation), or
not processed at all (which would be the case for a CLI tool that runs just the game logic layer),
the resulting sequence of random numbers no longer aligns with the sequence that existing replays were recorded with, causing a more or less immediate desync. This, by the way, is the same reason that would have delayed TH03 netplay for much longer than originally anticipated. Since TH03 also only uses a single RNG for both gameplay and visual elements and rollback is a non-negotiable basic feature of netplay, I would have had to spend some RE time on isolating RNG calls in rendering functions and moving them to their respective update functions.
And suddenly, this list grows by a few decorative aspects after all:
At least we can breathe a sigh of relief that pbg added no randomness to the render calls for the main bits of translatable text. Imagine, for example, the BGM title animation randomizing some rendered property of each grapheme cluster. That would have thrown a giant wrench into any translation plans.
That said, the game still throws a small wrench into my headless library plans. How does Shuusou Gyoku turn the Bonus and Evade points into these strings of individual letters I mentioned initially? Well, of course, by taking the most natural path offered by the C programming language:
Note how this also establishes an upper character limit for translations of these terms.
And since every character (and even space) in the resulting string moves and draws from the RNG independently, this means that Shuusou Gyoku relies on a sprintf() implementation with standards-compliant length modifier support for replays to not desync. As mentioned above, I would have really wanted game logic to not overly rely on C standard library functions, and then it depends on the one function that has long been a theoretical thorn in my side anyway, due to all the unsightly locale bloat it pulls into the Windows binary. Can't we get rid of sprintf() here, then? It's just two calls, after all. For the Evade string, we could easily get by without it, as the Evade counter and Evade score can't exceed the 3-digit 999 and the 7-digit 9,990,000. So we could simply provide a fixed-size static string and divide the individual digits into fixed offsets within this array. The temp variable in the Bonus string, however, can theoretically go up to…
Small bullets
((10,000 + (999 × 100)) × 600) +
Large bullets
((15,000 + (999 × 100)) × 1803)
…273,104,700 points, which is 9 digits rather than the 7 allotted in the format string, and makes a manual approach considerably more awkward. Not to mention that the frontend also still uses sprintf() for all dynamic menu text, so it would still get linked into the main binary as long as I don't fully throw out MSVC's implementation. Is all this locale bloat really that bad, though? Maybe my pursuit of purity isn't even worth it. Let's trace the chain of function calls down to the system APIs that Microsoft's sprintf() relies on:
sprintf()
__stdio_common_vsprintf()
__crt_cached_ptd_host::__crt_cached_ptd_host(_locale_t) with a nullptr,
which then initializes the PTD's locale pointer from __acrt_initial_locale_pointers.
These locale pointers are set on C runtime initialization via the following call chain:
On its own, this really doesn't look too bad or bloated. The 4 🚨-marked API calls aren't available on Windows 9x, but 📝 I already figured out how to deal with that last year. The bigger problem, however, is what this means for my plans of building a beautiful freestanding game logic library with low Win32 API dependencies. C runtime initialization is all-or-nothing, and if I throw it out, I can no longer even use sprintf() because those CRT locale pointers remain uninitialized. But now I'm curious. What does all of this ANSI codepage retrieval code actually get us as far as sprintf() is concerned?
Locale-accurate decimal separators when formatting floating-point numbers
Returning with an error code if the format string ends in the middle of a two-byte codepoint in the locale's current codepage. (Except that this stopped working in SDK version 10.0.17763.0, released in October 2018, which introduced a nasty sign extension bug in this line. I'll leave the specifics as an exercise to the reader; I certainly had a good laugh.)
So, one feature where locale usage 📝 has, infamously, long been a notorious time bomb more than anything, one broken feature that only ever mattered for the very unsafe use case of non-hardcoded format strings (because MSVC's /execution-charset already checks the validity of hardcoded format strings at compile time), and a third feature that looks like mere non-portable syntactic sugar around a WideCharToMultiByte() call with that initial locale's codepage. That third feature might even have been useful in times when you had to support Windows 9x and just didn't want to type out that fairly lengthy WideCharToMultiByte() call. But even then, the syntactic brevity comes at the cost of representing one and only one conversion behavior, which happens to yield some pretty ambivalent results:
On the one hand, I wouldn't have expected such a crusty old API to correctly decode UTF-16 surrogate pairs and convert them into a valid sequence of UTF-8 bytes without falling into the CESU-8 trap. But was it really the best choice to make this conversion always fail with errno == EILSEQ on the first non-representable codepoint, instead of opting for any replacement character? And what's up with this codepage-dependent ASCII transliteration, where ä and ö get turned into a and o across the board, but Ÿ only gets turned into Y on CP1251 and is treated as invalid on CP932? But it gets worse. Even these results require you to either use setlocale() with a locale string that specifies or implies a global codepage for the C runtime, or sprintf_l() with a dedicated locale object created from the same kind of locale string. That's right – all that C runtime startup code we've traced above doesn't even set any specific codepage for the wide→narrow conversions done by %S and %C. Instead, you're forced to link in an additional ~34.5 KiB of C standard library bloat to get any sort of useful result out of these conversions at all. If you don't, these conversions use the dedicated logic for the default C locale, which is worse than useless:
Under the C locale, these format specifiers simply take the first 256 Unicode codepoints, truncate them to 8 bits, and fail on everything else, producing a byte string in an encoding that is neither valid ASCII nor valid UTF-8. The resulting bytes do happen to match the original string when interpreted as CP1252 in this case, but only because the last 75% of Unicode's Latin-1 Supplement block happen to match CP1252 and the first two characters fall into exactly that range. This is also why the Ÿ vanishes – as we've seen above, Ÿ is 0x9F in CP1252, but U+0178 in Unicode, so it fails the conversion. And if the UTF-16 string had contained any of the C1 control characters between U+0080 and U+009F inclusive, the resulting string wouldn't have been valid CP1252 either.
This ridiculous non-encoding is not just a recent fallback path, but traces way back to Microsoft C/C++ 7.0 from 1992, whose C runtime featured Microsoft's very first and preliminary implementation of the underlying wctomb() function. Back then, Microsoft hadn't yet implemented locales into the CRT either, so this protected cast from wchar_t to char was the only thing the function did. And in a tragic turn of events, POSIX lawyers can now claim that this preliminary hack from the Windows 3.0 days implements section XBD §6.2 of the POSIX standard:
ISO C specifies that wctomb() should use the current locale, but leaves the locale's encoding unspecified.
This encoding is not specified beyond the stipulation that the ASCII character set must appear somewhere. Implementations are free to map ASCII to any arbitrary codepoints, and then choose whatever other 128 characters they like to support.
And after all, this behavior is useful because it guarantees lossless round-trip conversions between narrow and wide representations as long as wide strings only use these specific 256 characters!!1! 🤓
Which leaves us with a state where both Microsoft and the POSIX police agree that this ancient code had better be kept around rather than removed. Backwards compatibility! But I've seen enough. No modern C++ (and even C) program needs any of this. Even the pure optics of a freestanding game logic DLL are much more meaningful, and more than justify nuking Microsoft's sprintf() implementation across all of Shuusou Gyoku's code. The replacement can come in one of three forms:
SDL offers a snprintf(), and we could abstract away the difference between it and the unsafe sprintf() used all over pbg's code. However, SDL only chooses its own no-nonsense implementation based on build-time availability of vsnprintf() – which is probably a given for most pre-compiled SDL packages on Linux and gets us right back to the system-specific behavior we were trying to avoid for a core piece of game logic. Not to mention that the logic DLL should explicitly not depend on SDL.
Type-safe, modern C++ alternatives like nanofmt are nice in theory, but highly disruptive in how they require a rewrite for every sprintf() call site. Also, code generation is highly disappointing, and we'd still need a solution for miniaudio's logger, which uses sprintf() as well.
Fortunately, there are plenty of minimal and no-nonsense sprintf() implementations out there that can work as drop-in replacements. eyalroz/printf ticks all the boxes, is actively maintained, and comes in at a very pretty 3.5 KiB of compiled code size in its most minimal configuration. Redirecting miniaudio to that implementation still requires silly header inclusion hacks though… Sigh, I want this library gone from this codebase so much.
With MSVC's sprintf() implementation gone, we should now be able to safely bypass the C runtime initialization before DllMain(), avoid the bloat it would pull into the binary, and end up with a completely freestanding ssg.v1.005.dll without any system dependencies:
Yes, that's all the magic. /NODEFAULTLIB would make things a lot harder.
That only leaves us with two more linker errors:
error LNK2001: unresolved external symbol ___CxxFrameHandler3
error LNK2001: unresolved external symbol ___std_terminate
Sigh again. Time to also take a close look at…
C++ exception handling on MSVC
…and finally learn what all the related compiler warnings and flags actually mean. It starts with one thing:
warning C4530: C++ exception handler used, but unwind semantics are not enabled. Specify /EHsc
This warning originates from the few hilarious instances where pbg used exceptions for control flow. After rewriting them, we should be able to drop /EHsc again and be good… …except for the fact that 📝 we've migrated to C++ Standard Library Modules two years ago. Creating these modules requires compiling the entire C++ standard library upfront, and Microsoft's implementation is free to throw and catch internally wherever it wants. If we remove /EHsc from the module compilation command line, we don't have to wait long before we encounter the first case of C4530 in the alphabet, emitted for <chrono>'s get_tzdb_list().
OK, but the fact that some standard library functions might throw and catch exceptions should not have any impact on our code. So let's try a mixed approach: Compiling the standard library with /EHsc, but removing the flag for our own code. Well…
warning C5050: Possible incompatible environment while importing module 'std.compat': _CPPUNWIND is defined in module command line and not in current command line
Which we now get for every translation unit in the codebase. This warning is much worse than the other one: C4530 always points at a specific exception handler, but C5050 just points at the import and complains about a macro mismatch, breaking down the difference between activated and deactivated unwind semantics to a single preprocessor macro. Does this macro matter? How can we possibly say whether it does or doesn't?
So let's take a step back to C4530 and that initial module compilation. On the one hand, we'd fully expect to run into some restrictions if we wanted to code completely without bothering about C++ exceptions. But if we simply don't use any of these functions where MSVC emits C4530, we should be good, right? After all, C4530 defaults to just being a warning, not an error. On the other hand, I do use plenty of std::vectors across the frontend, and those certainly do throw in quite a few of their methods. The game logic DLL obviously shouldn't use std::vector, but I definitely don't want to have two compiled standard library modules with different exception handler settings in my build tree. But wouldn't ignoring that warning just introduce yet another bit of uncertainty?
Well, not quite! As the diagnostic says, /EHsc is all about unwinding the stack in case of an exception – i.e., running the destructors of any local objects in scope while moving up the call stack to find a handler for that specific exception. And nothing else – you can still throw or catch exceptions just fine, but you just might leak memory or resources if you do. But that also means that /EHsc doesn't matter as long as you never catch anywhere. In that case, your program will terminate whenever it throws an exception because there's no handler. You might expect one final round of stack unwinding before termination, but the C++ standard also leaves that up to implementations, justifying it by claiming that enforcing it makes life harder for debugger writers.
That answers the question for our own code. But aren't we getting C4530 for standard library code that does catch an exception? Let's experiment with a feature where catching exceptions is crucial to its intended behavior, and where we can trigger it more easily than by occupying all memory:
import std;
#include <stdio.h> // `std::cout` could also throw, lol
struct Inner {
~Inner() {
puts("Inner destroyed");
}
};
int main()
{
// `std::packaged_task` stores the return value or exception of the callable
// it's initialized with. This obviously requires an exception handler
// inside the standard library, and therefore makes a good test case for
// `/EHsc`.
std::packaged_task<void()> task([] {
Inner instance;
throw "from task";
});
task(); // Run the task
// `#define try` can skip exception handling here
try {
puts("Rethrowing exception");
task.get_future().get();
}
#ifndef try
catch (const char *error) {
printf("Caught exception %s\n", error);
}
#endif
puts("Returning");
return 0;
}
And compile it with every permutation of /EHsc at std module level and source level, as well as with the final handler removed:
/EHsc for module
/EHsc for source file
try/catch block enabled
Warnings for source file
Exception behavior
❌
❌
Yes
C4530
💣 Caught, but not unwound
❌
✔️
Yes
C5050
⭐ Caught and unwound
✔️
✔️
Yes
⭐ Caught and unwound
✔️
❌
Yes
C5050, C4530
💀 Internal Compiler Error
❌
❌
No
C4530
✔️ Terminates
❌
✔️
No
C5050
✔️ Terminates
✔️
✔️
No
✔️ Terminates
✔️
❌
No
C5050, C4530
💀 Internal Compiler Error
Amazing how C++ modules are still bleeding-edge enough to crash compilers, a full two C++ standard versions after they were introduced! Those weren't the only Internal Compiler Errors I've seen in this development cycle either, not by a long shot… Regardless, these results are even better than I would have expected:
/EHsc and, by extension, C4530, don't seem to matter at all during module compilation. Any /EHsc setting on the module-importing source file seems to re-activate stack unwinding for any standard library code.
Why is that the case? Because neither std.ifc (the Built Module Interface file) nor std.obj (the compiled object file you're supposed to add to your link command line) actually contain any pre-compiled code. While it's clear that the IFC contains some sort of binary representation of the parsed and analyzed code, you might think that at least some pre-compiled code ends up in the .obj. But no, it only contains all static data referenced by the standard library. In fact, it can only ever contain data because all code in the std module comes from headers and therefore has to either be templated or declared as inline to maintain the One Definition Rule. Thus, the binary representations from the IFC file only get instantiated and specialized in each translation unit they're imported into, under that unit's exception handling rules. MSVC's frontend probably emits C4530 in a much too early analysis stage, way before the absence of /EHsc would actually matter. (And yes, the IFC does reference plenty of precompiled code, but all of that code is part of MSVC's runtime libraries.)
The only critical case is the very first one, where we do catch without /EHsc, and therefore skip running destructors you would expect to be running. In this case, however, MSVC emits C4530 precisely to protect us from this exact issue. This specificity shoots C4530 right up there towards GCC's 📝 -Wformat-overflow as one of the best diagnostics ever emitted by a C++ compiler.
Because we can now derive a rock-solid strategy: Disable C4530 during module compilation, but treat it as an error on all other build command lines. This allows us to safely compile everything without /EHsc, because we'd now get a compile error as soon as we write code where the absence of /EHsc would matter.
And as a nice bonus, MSVC can now properly optimize DLL-exported class constructors as it no longer has to accommodate exceptions flying across DLL boundaries.
So, we got pretty far, but in the end, did all this bloat reduction really matter? Absolutely, if you ask me:
And all of these three imports come from our own memory management code. Not removing C runtime initialization would have bloated the DLL to 157,184 bytes – more than twice the size it ended up being.
We've learned something about two unfortunate aspects of the C and C++ standard libraries, got rewarded with a beautiful, small, and freestanding binary that natively runs on every Windows version since 95, and we didn't even have to screw around that much to make it happen, despite still compiling with current Visual Studio. When I went into this, I assumed that we'd have to use /NODEFAULTLIB and link to at least SDL's C runtime polyfills, but we didn't even have to do that. Instead, the DLL still gets to use security cookies, MSVC's built-in functions for 64-bit math on 32-bit x86, and a wide range of modern C++ standard library features, including std::unique_ptr with adopted raw pointers and custom deleters, ranges and views, spans, and std::invoke.
Alright, I'm happy with the build quality, so on we go to Step 2 of the actual mission:
Completing the separation of game logic
Which certainly was the most grueling part of this development cycle. Even round initialization required a good part of a full push just to clean up the code and end up with a consistent API that works for regular 6-stage rounds, Extra Stage rounds, replays, and the pre-recorded demos. This codebase is truly cursed: You take a look at a new piece of code, notice that it's a mess of procedural state mutations, and just have to streamline it before you even want to touch anything. 😩
So, how do we get to our single C_SSG class that bundles all game state and offers a central method to run a frame of gameplay based on player input? If we want to keep code changes to a minimum and not basically rearchitect the entire game from scratch, there are two realistic paths from the code's original bunch of globals and functions:
Throw all gameplay state and functions into a single class. This would remove quite a few header files, but the .CPP files would stay largely identical, mostly only requiring changes to the function definition headers from free-function to class method syntax:
On the other hand, we'd then have a god object, and every change to its class declaration would trigger a recompilation of every .CPP file in the logic DLL.
Retain the code's original subsystem separation, turn each subsystem into its own class, and restrict the possibilities for rampant state mutation by only giving each subsystem the references to the other subsystems it actually needs. A few of these references can then even be const, providing additional safety and documentation of intent. C_SSG then bundles all of these smaller classes and acts as a giant switchboard connecting all the subclasses to each other. This maintains the game's existing code structure at the cost of hundreds of lines of reference management boilerplate. We do get to make good use of forward declarations to keep header dependencies to a minimum, though.
Initially, I gravitated towards option 1), but immediately disliked it mere minutes after I started typing. So, one class per subsystem it is. This is slightly more disruptive to the .CPP files in how we get an extra change to each function call between subsystems, but those are trivial to review. In most cases, it's just an additional dot accessor between two camel-cased words. But yeah, this only retains the original file layout in spirit as well. Once I was done, I immediately regretted that solution too. We might have avoided unneeded recompilations of logic translation units, but the whole nature of this split still required moving all game state rendering code to another place. And since it didn't really make sense to have one tiny source file per rendered subsystem, I went with a single giant one…
And just in case you're wondering how large C_SSG ended up being in the end: 403,616 bytes on 32-bit systems, and 405,984 bytes on 64-bit systems.
I did have to make a few more interesting implementation decisions as part of this whole logic/presentation split and the resulting API:
You might have already noticed it from the KERNEL32.DLL import list above, but yes, I decided to not add any file I/O to the game logic DLL. It made much more sense to leave that to whatever language the DLL is used from: The high-level file I/O functions of such languages are typically more convenient to use, and often provide far superior error handling than whatever I could cobble together across a FFI boundary. However, the DLL does take care of transparently decompressing packfiles and allocating the ECL, SCL, and replay buffers managed as part of each gameplay instance. In fact, this is exactly where the three Win32 heap management functions come in. This breaks down replay initialization to a short sequence of steps that involve passing the raw contents of ENEMY.DAT and the replay file as borrowed buffers into the C_SSG instance and then loading the necessary scripts by passing the stage number. But since jwilins' raw asset loading fork also exists and I do in fact endorse it for experimental use cases, I've also added a variant that bypasses the packfile and takes raw ECL and SCL buffers.
Speaking of FFI usage, this was another key design goal of this library. After all, we'd ultimately like this code to perform replay validation on Silent Selene's server, which is written in Python. It also stores replays in a PostgreSQL database, so it will certainly appreciate not having to write out replays to the file system before being able to pass them to game logic code. Without a specific developed use case, it didn't make all too much sense to spend time on full-fledged feature parity for the FFI just yet. And so, running and inspecting a replay remains C++-exclusive for now. But I at least started by ensuring, at build time, that all intentionally public headers and all of pbg's original gameplay structures are valid C code. This way, you can already feed them to all kinds of automated binding generators.
For the most part, the separation of logic and presentation continues to work as it did with the original code's split between GameMove() and GameDraw(): The logic layer runs one frame of gameplay, and the frontend's renderer can then take the resulting state and draw it. However, there are still several events that can happen in the middle of a frame, triggered by conditions and with parameters you couldn't pull from game state at the end of the frame. Sound effects are the most obvious example, but these kinds of events would also need to occur for the majority of SCL instructions, including all kinds of background effects and the entirety of the in-game dialog system. I could have put all of these events into some kind of queue to be processed at the end of a frame, but that would have required allocations and introduced a whole new problem of needing to snapshot a whole lot of data at the time of the event, just to ensure that we fully match the original game's behavior. So, classic hooks via C function pointers it is. Since we also need to cover three specific ECL instructions for certain visual effects and, ideally, pbg's existing debug tracer, I went for full-on per-instruction ECL and SCL hooks. In theory, you could use these hooks to even write a full debugger on top of the game logic library. For ECL, though, you'd have to explicitly opt into receiving hooks for every single instruction. The Extra Stage processes roughly 200,000 ECL instructions throughout the 7 minutes of a full run, which doesn't bode well for certain languages where C function pointers would have to cross expensive FFI boundaries. SCL instructions are hooked unconditionally, as they are far less numerous.
And since it doesn't really fit anywhere else: Obviously, the Linux version also gets a libssg.so.1.v1.005, together with the version-less linker-name and versioned SONAME variants as per the usual shared library conventions. Usually, the latter two would form a symlink chain that ultimately points to the real shared object containing the actual code. Unfortunately though, this naive approach would really degrade build performance under Tup in particular. Despite Tup's Unix-centric origin, Tupfiles really prefer the Windows approach of a separate import .lib file that defines the API surface of a DLL. If we then use the ^o^ flag in the DLL's link rule and build a new version of that DLL, Tup can then skip needlessly re-linking any dependent binaries if the newly created import .lib is exactly identical to the old one. On Linux, though, the final target behind libssg.so (which is what linkers read when you specify -lssg) is essentially different on every code change, triggering a rebuild of every dependent binary and thus kind of defeating a big point of shared libraries. Unix linkers don't produce a direct equivalent to Windows .lib files, but it's still possible to derive a .lib-equivalent code-less .so dummy from a regular full .so file. All it needs is some nm and sed magic to produce a stable piece of ASM source that we can then link into a regular, non-symlink libssg.so. 🧙 Then, linkers (and only linkers) will never actually look at the full .so file containing the actual code, and we get to also use the ^o^ flag to reproduce the same developer experience on Linux. Check out the respective tupblocks commit if you're interested in the specifics.
Since our library should be able to process multiple game states in parallel, this should no longer be a global flag. CLI and logic API users should not have to split replays into two batches based on whether the flag was set or not. Thus, this must be another round parameter.
However, the fact that this wasn't a proper round parameter also means that the flag wasn't propagated into pbg's old replay format at all. Hence, it's already impossible to identify existing replays recorded with the Hit option turned [O F F] from their header data alone. The CLI will help identify such replays (since running them with collision detection enabled is likely to cause a desync at some point), but it can't definitively tell either.
But if the replays don't contain this flag, who sets it? Well, it would continue to be passed from the frontend – i.e., the game and the replay CLI, introducing one more command-line option to the latter. And then we're right back to where we started, forcing users to manually track a replay-specific property as part of the simulation environment.
We could continue limiting this feature to debug builds. But do we really want to continue having separate behavior between Debug and Release builds, and add separate API contracts on top? 😵
That's way too many questions without easy answers for a minor debug feature that non-developers won't ever even encounter. Just removing it seems to be the easiest way forward, but on the other hand, this kind of feature is vital for debugging. So I chose to replace it with a different approach to invincibility that doesn't affect game logic: Simply starting each round with 252 lives. Why 252, and not the 255 you'd expect from the unsigned 8-bit integer we obviously assign to? Because the game spawns 3 extend items across its 6 stages and pbg doesn't clamp lives to any maximum value. 252 lives are plenty for clearing both a regular 6-stage run and the Extra Stage without moving, and allow you to still pull off a no-miss run that collects all 3 extends without the life counter rolling over to 0. Since this new feature merely overrides the initial PlayerStock value, which is directly stored in replays, we end up with a much more sustainable debugging feature. New debug replays recorded with this feature enabled remain compatible with even pbg's original release of Shuusou Gyoku, and replay hosters can identify them just as easily as if I had picked some previously unused bit in the header to indicate the old flag. Maybe they don't even have to check at all, since the PlayerStock number is a regular configuration option that the UI can just prominently display.
Maybe, we can even restore the original feature once we have that new extensible replay format and have implemented all the required bureaucracy for the highly requested quirk toggle. That would already create at least one replay-incompatible gameplay fork, after all. But for now, this is what we're going with.
Note the flipped menu logic: With the original Hit feature, the [O F F] choice opted into nonstandard gameplay, while the 252Lives feature only overrides the PlayerStock when turned [O N]. The negation here is purely visual, and existing 秋霜DBG.DAT files will automatically use the matching behavior.
Now that the codebase has to produce two more binaries than it did before, I also had to reconsider the physical layout of the repo, and how to best share any code that needs to be shared between any of these binaries. Ever since I started work on Shuusou Gyoku, I made a conscious effort to physically separate generic engine and platform code from game-specific code. Initially, I went for the most logical and terse layout from the point of view of my build system: The various subdirectories of the platform/ directory contained all platform-specific backend code, while game/ contained everything else that applied to all platforms. This kept the Tupfiles simple, as I only needed a few specific globs to list all files that should go into a platform's respective binary. A few cracks and exceptions started to emerge over the years, but the overall layout idea generally worked out well… …except that it didn't really matter what I did as long as all code went into just one binary. But now that we add two more, we really don't want to link all that code into all three binaries and rely on the linker to eliminate whatever isn't needed. All these unneeded dependencies would end up forming a somewhat triangular-looking build graph that reduces Tup's throughput and triggers unnecessary rebuilds during development. And we especially don't want to force replay servers to install all these development packages if all they need to compile is the game logic .so binary.
And so, the completed feature ended up calling for the game-independent code to be modularized along a total of four different axes:
A logic layer that provides a few small bits of code on top of the C++ standard library module, provides eyalroz's printf library, and adds no further dependencies. Since it mostly defines types, classes, and templates, and comprises very few translation units on its own, it's small enough to get statically linked into all three binaries.
An app layer that contains all the helpers on top of SDL that we want to use in both the game and the replay CLI, as well as the SDL dependency itself.
An API layer. This one currently only contains functions to retrieve game logic and ReC98 build version strings. Only exists because I needed an additional game-independent layer that gets linked into the game logic DLL but not into the CLI or game binary, since they should always pull these versions from the DLL.
And finally, an engine layer that gets all the remaining code required for the main audio-visual gaming experience. This layer also bundles all remaining third-party dependencies such as libogg/libvorbis, dr_flac, libwebp, and miniaudio.
Or, in a table format:
Layer
Logic DLL
Replay CLI
Game binary
logic/
✔️
✔️
✔️
app/
✔️
✔️
api/
✔️
engine/
✔️
And if we now take a step back and look at what I've actually written there, we notice that most of this code is indeed not specific to Shuusou Gyoku, but will also apply to Kioh Gyoku and even the eventual ports of PC-98 Touhou:
Windowed and exclusive/borderless fullscreen modes, freely switchable from inside the game? ✅ Check. I do not want to write this logic ever again in my life.
Software rendering with hardware scaling? ✅ 📝 Check.
Line, triangle, and box drawing? ✅ Obviously relevant for Kioh Gyoku, but this API could very well turn into our master.lib replacement for the native ports of PC-98 Touhou.
Sprite management and blitting? ✅ Same.
TTF/OTF text rendering? We may or may not actually want the embedded 16-pixel bitmaps of MS Gothic, IPAMonaGothic, or Unifont in place of a font ROM dump for PC-98 Touhou, but ✅ Kioh Gyoku will definitely need it. In fact, 📝 I built this entire current architecture with Kioh Gyoku in mind. Conditionally deactivating the code and its dependencies makes more sense than a further split into another reusable module.
Screenshots? ✅ 📝 Lossless WebP will still be the right choice for PC-98 Touhou.
BGM modding with waveform tracks? ✅ Someone's gonna ask for it sooner or later.
Replay CLI? ✅ At least as far as generic CLI code is concerned.
All the small bits of logic code that deal with things like buffer ownership, 📝 integer type casting, endianness, and type-level distinction between Shift-JIS and UTF-8 strings? ✅ Wouldn't have written them if I wasn't convinced about them being the right approach for legacy codebases, and they all apply to this entire set of games.
By now, the sum of these features has pretty much become an entire library on its own, providing proven solutions for modern-system ports of Japanese retro shmups with quality-of-life features. And given the very specific scope of the 7 games we want to port here, what better name to give to this new library than Hatoyama. Covers both Seihou and PC-98 Touhou, and evokes the honest and, for once, unironic doujin spirit of the original era. 🕊️ … OK, and Torte le Magic, if that ever gets open-sourced. Still a fitting name.
This newly separated library allowed me to trick a little as far as funding is concerned: Current work on Hatoyama may be needed for Seihou, but isn't specific to Seihou, as it will benefit the eventual modern-system ports of PC-98 Touhou just as much if not more so. Thus, Hatoyama can consume the Anything budget even if its backers didn't explicitly opt into Seihou-related work. In fact, this split already turned out to be highly necessary to fund roughly 1/4 of the work that went into this delivery. By extension, this also means that I've delivered about worth of PC-98 Touhou portability work. Just because some people were still thinking that PC-98 Touhou porting hasn't even started yet.
And a few hundred lines of CLI argument parsing code later, we finally get…
A replay validation CLI
Usage: ssg_cli [OPTIONS] replay_file [replay_file...]
Options:
-d, --dat <ENEMY.DAT> Custom path to the game data archive
-h, --help Print this usage text
-q, --quiet Only report replays that don't simulate completely
-v, --version Print version info
`replay_file` can contain `?` and `*` wildcards.
If `-d`/`--dat` is omitted, game data will be taken from `C:\path\to\ssg\ENEMY.DAT`.
Yes, that's single-level globbing for all operating systems. Works with relative paths, absolute paths, and UNC network paths!
Correctly initializing replays in such a CLI posed a final slight challenge. Since pbg's single-stage replay format doesn't actually store the stage in its header, we can only possibly derive a replay's stage from its filename. We could just enforce the same 秋霜りぷ*.DAT filename scheme that the original game's replay menu expects, but that idea quickly fell apart in the face of even my current existing Shuusou Gyoku replay collection, which features multiple descriptively named replays for every stage. I'd obviously prefer to analyze the whole set with a single * glob. Keeping in mind that you'd have to trim any descriptive text if you want to view these replays in-game, it made sense to go for a small algorithm that considered only the one part of such filenames that you'd keep unchanged for easy renaming. And so, the resulting algorithm simply looks for the 秋霜りぷ prefix and then detects the stage from a subsequent 1-6 or Ex string. This way, any descriptive extra text can freely include 1-6 or Ex, and can be placed before the .DAT extension, after the extension, or even at the beginning of the file name. The algorithm can then also support several mojibake versions of that prefix, which currently include the ‘š‚è‚Õ string you'd get on CP1252, and the four underscores (____) you'd get on Western Windows 98.
This need to handle Unicode filenames also brings us to what might seem like one of those typical strange ReC98 engineering decisions. The app/ layer's dependency on SDL might have already given away that I chose to write this CLI entirely on top of SDL's standard library and logging functions. Typically, you wouldn't consider using a low-level game library for a CLI tool, but there are several good reasons for SDL in particular:
Unicode command-line arguments just work without me having to write a single line of platform-specific code. That feature is obviously crucial given the filenames this tool usually needs to process. It sure is nice that I don't have to solve the same problems all over again.
Same for Unicode console output on Windows. The only platform-specific code we actually need stems from SDL's unfortunate choice of outputting to stderr rather than stdout, requiring a few slightly ugly lines of code.
SDL is part of all Windows and Linux packages anyway.
Ultimately, this CLI is meant to serve two goals: A general demonstration of the fact that Shuusou Gyoku's game logic now has a headless API, and a tool that will ultimately facilitate mass validation of every replay ever recorded, as part of my release pipeline. Depending on how I'm going to approach validation, I may or may not add support for a more machine-readable output format. Regardless, the CLI is meant to demonstrate the API, not the other way round.
If you remove all the argument parsing and globbing code, you're left with roughly 60 lines of C++ code that use the DLL's API for loading ENEMY.DAT and the replay, and looping over every frame. And that's with error handling at every step of the way. Shelling out to the CLI and parsing its output probably takes more code than just calling the same C API of the game logic DLL from your programming language of choice. And given the months of work that went into boiling down this API into only its most elementary steps, it's likely to remain stable for quite a while. (There will certainly be more hooks, though. Make sure to use proper binding generators if necessary for your language of choice!)
Since almost everything in the CLI is a call to either an SDL or logic DLL function, I can remove the C++ runtime initialization on Windows at, once again, little additional cost, and end up with a 9 KiB binary! I might assign way too much importance to these optics, but I love how this clearly communicates that the CLI contains no additional magic whatsoever, even if you don't read its source code.
Alright, we've kept everything nice and minimal, but how fast can we actually simulate gameplay if we don't have to render anything?
WARNING: `秋霜りぷ3 (Full clear, Hit OFF).DAT`: simulation stopped after 2035/15556 frames (13.1%)
Score: 6200
Misses: 5
Bombs: 0
Seed: 1f0a25b1
Load time: 0.7 ms
Simulation time: 6.1 ms
Simulation speed: 333607 FPS
As usual, the performance corresponds to the 32-bit Windows build running on my aging Intel Core i5-8400T. No one else gave me performance numbers.
Yup. That's 7 minutes of Extra Stage gameplay simulated in less than 200 ms on an 8-year-old midrange CPU. Running single-threaded 32-bit code that suffers from MSVC's horrid codegen and still has several untapped optimization opportunities. I would have been happy if game logic ran 20% as fast as this. But with those numbers, I can confidently cut multithreading from these 6 very packed pushes and delay it until we actually have masses of recorded replays to validate.
But it gets even better. The ssg.v1.005.dll I've been talking about is not only used by the regular modern build, but also by the Windows 98 one. Hence, we've even achieved this level of performance by sticking exclusively to i586-era x86 instructions. You might object to the mere idea of giving up performance in exchange for a cleaner game directory, but:
Replay
32-bit, i586
32-bit, SSE2
64-bit, -march=x86_64
Stage 4 (Laser)
46.2 ms
47.4 ms
50.2 ms
43.6 ms
44.6 ms
46.5 ms
43.0 ms
44.4 ms
45.9 ms
Stage 6 (Laser)
73.1 ms
75.1 ms
78.2 ms
69.6 ms
71.5 ms
75.1 ms
66.9 ms
68.7 ms
72.9 ms
Extra Stage (Wide)
145.5 ms
149.3 ms
153.0 ms
138.5 ms
143.1 ms
148.5 ms
133.9 ms
136.8 ms
146.6 ms
Extra Stage (Homing)
178.6 ms
182.5 ms
189.1 ms
171.5 ms
174.6 ms
181.3 ms
157.0 ms
161.8 ms
168.5 ms
Fastest, median, and slowest simulation speed among 30 runs on the same system, with warmed-up caches. Yes, -march=native exists, but happens to be 2% slower on my aging CPU. Computers are weird sometimes.
Why complicate the build and add another file for a ~5% speed improvement when a regular 64-bit Linux build, targeting the baseline x86-64 ISA from 2003, will already run somewhere between ~7% and ~13% faster? And if this ever turns out to be too slow after all, I'd rather explore algorithmic optimizations instead of staring at individual instructions.
Swappable game logic layers. Ideally, modders should be able to ship gameplay mods by just sharing logic DLLs with unique names that players should then be able to select in a menu or by specifying a CLI parameter. You shouldn't have to re-link either the game or the CLI just to use a different logic DLL.
If you just want to do me a favor: It really is time we got away from miniaudio. 😩 SDL 3's audio API would remove bloat and compilation pain for the modern build, and moving the Windows 98 build back to raw DirectSound with backported support for waveform BGM would significantly reduce that build's audio latency, especially on VMs.
Finally: This would be the best point to start working on Kioh Gyoku. After all the cross-platform engine work I did for Shuusou Gyoku over the past years and all the code splitting I did for this release, applying it all to a second game could be done pretty quickly. Really, the more I look into this game, the more hyped I am to tackle it. It has so many pieces of code that either are identical to their Shuusou Gyoku counterparts I already ported, that I know exactly how to port because I've already answered the underlying architecture questions, or that I don't have to port at all because they're unused. Of course it will have a few challenges of its own, but its codebase looks much cleaner at first glance, and doesn't suffer from the awful 8-bit rendering and replay preservation constraints that Shuusou Gyoku suffers from. I'm confident that I can now port this game in a fraction of the time it took me to port Shuusou Gyoku.
Next up: Spending several pushes on TH02 decompilation! I think I'll go for player shots: It's a sizable chunk of code that shouldn't take too long and will push both RE% and PI% by quite a bit, and we sure are due for some big gains there. Most notably though, it will solve a certain mystery that the gameplay community has been wondering about for a while. Then again, I really shouldn't do any favors for the gameplay community, given that they just endlessly meme about commissioning ReC98 but don't actually do it anymore…
…wait a moment. You might have noticed that this release did, in fact, not get out the door within anything resembling a reasonable timeframe. 📝 Last year's big subproject was almost twice as complex and got done much faster. So what happened?
in late March, then nothing, despite the store still being wide open. We've sure come a long way from the store getting sold out shortly after a new delivery. I slow down development accordingly, to a level that matches the current subscription volume, taking a much-needed break after 📝 the previous release.
No new transactions in April or May either, suggesting general economic worries. Still fine, cap system working as designed. I continue working at this slow pace, only speeding up slightly in May. Shuusou Gyoku's unique ✨ constraints ✨ and the resulting forward-thinking would still strangle any hope of fast progress during that month, let alone a release.
Store finally closes naturally after the subscription volume, unchanged since March, fills the cap in early June. People would later use this sold-out state to shoot down any suggestions of low funding having been a problem, claiming that I've always been sold out, while completely missing the fact that I only ended up sold out because development has been slow for a while.
TH06 New Classic gets announced out of nowhere. Heavily suggests that someone in the very Japanese business hierarchy involved in this release is actually cool with community decompilations and would use them for official releases in at least some capacity. Enough people knew about it and could have told me way in advance, so that I could have maybe hurried up and freed up some capacity to be ready for the surging community interest in old Touhou. Did they do that? No, of course not, who do I think I am? A potentially important person in the grand scheme of things? Yeah, no, 16 years of infrastructure work does of course only classify me as some fringe weirdo at best. Much more important that game announcement in Nintendo® Direct™ out of nowhere has psychologically intended hype effect on the cattle of paying fans. NDAs for a "doujin" game series that prides itself on being community-driven, imagine that. How utterly disgusting. Oh well, got to hurry up now then.
I notice that some developer forked my Shuusou Gyoku codebase and vibecoded on top of master earlier in June. The fork wildly mixes valuable QoL features with gameplay-related changes that clearly break replay compatibility. On the surface, it all sums up to exactly the kind of slop you'd typically associate with vibecode, and hardly presents a competition to the meticulous work I'm doing with the game. Heck, the slop is apparent from the very first sentence in its README:
Source code for Shuusou Gyoku, the first Touhou Project-adjacent game by Project Shrine Maiden (pbg).
Still, I can't help but worry about the speed these vibecoders operate at. If they prompted with just a bit more discipline, and focused on specific things…
Superplayer who also happens to be an AI person contacts me, asks for help in setting up the ReC98 build environment, then asks for the list of contribution-ideas. Implements two of them within a day like it's nothing. The sheer amount of code heavily suggests AI. I would now have to invest time into reviewing and potentially unslopping this work. Not now, though, my brain is in a different place, I want to at least finish this Shuusou Gyoku feature first.
A few days later, the same person's ReC98 fork has a branch with hundreds of decompilation commits. Person starts using the insights to figure out previously unknown gameplay details at a level of meticulousness that used to be exclusive to my work. Gameplay community understandably celebrates. I realize that I'm only one public announcement away from a serious competitor – or from being out of business entirely, given that this person does it all for free.
Feature development finally concludes a few days later. Tweet about the game logic DLL is averagely well-received, or perhaps even surprisingly well given its dry and technical content. Probably explainable by younger Gen Z's romanticization of Windows 9x screenshots or something. But then, practically no one cares about the actually useful consequence of this split. And it's not a social media algorithm issue either – the views aren't too far away from the other tweet, and every single like came from someone I know to be a developer as well. Gamers did not care about a feature that was specifically made to gain and secure their trust.
Yup. No additional performance measurements on different hardware, and no substantial reply to the topic at hand. No, FreeBSD. The gameplay community is breathing down my neck, and some BSDtard turns my attention to the stupidest tinkerer bullshit imaginable, while simultaneously sounding as if he has never read any of the blog posts that explain exactly what I think about CMake. Because we now just have to support an OS with approximately 0% market share on the desktop, and whose maintainers can't get their act together, play dumb, and pretend that a feature that C++ standardized over 2½ years ago does not matter. I have not seen any Linux distribution being so willfully obtuse. You literally have to regularly build libc++ normally and maybe move a few files to a distro-specific location. I did exactly this within a fucking Flatpak and it was no big deal. Is "common sense" also a "Linuxism" now? Note that this came from the same person who previously named Windowsisms as computing's biggest problem. I don't know, I enjoy the art of crafting and shipping beautiful binaries targeting the single most stable ABI in the world, but I can see a point there. Only that now, not even two years later, they shifted the goalposts to a point where we can't even agree on the Linux kernel anymore. How about you get your fonts right instead of distro-hopping and creating problems where there wouldn't have been any before you showed up?! What is wrong with the priorities of people?! Thinking that Windows is the devil while behaving exactly in the way that proves why Windows has endured for the past 30 years… Oh, and sound is broken too, because FreeBSD isn't a prime target for miniaudio either? I wonder why. 🙄
I would have loved to port this thing to FreeBSD as part of a regular push or two. Addressing and solving the actual supply chain packaging issues that prevent my system from just working without issues, and making FreeBSD better for everyone. Properly investigating why certain minor changes to the build script were necessary, and why the BSD approach might even make more sense in places, because it sure isn't documented anywhere else I could find. Heck, if someone else did that work and sent a well-argued PR that was easy to review and gave useful context on the changes, that would have been fine too. But throwing a half-baked PR for something this globally irrelevant my way after wagging your finger in the most entitled way imaginable, and doing so in the middle of QA? This is zealotry for the sake of it. I never want to see you ever again near anything I do. Questions of global OS adoption are not fought on the backs of the maintainers of silly niche indie games. The mere suggestion that low BSD adoption on desktops is even a problem worth solving by anyone makes me want to quit computers entirely and spend the rest of my life doing agriculture or running a bakery or something to that extent. Yeah, that would provide something of value to the world. Go join spaztron64 in his circlejerk over operating systems that nobody uses, I'm trying to run a business here. He already applauded this bullshit after all.
Isn't it lovely how I can finally say what I think out loud again, because I don't care which bridges I burn anymore? ❤️
Oh well, back to QA. In parallel, the 📝 shifting life priorities take a turn for the worse, and would completely revert by early August. QA work has been ever more painful for the past few years as the scope escalated, and now I have to do this work against a backdrop of intense stress, day job boreout, community disinterest in the current feature, a looming threat of vibecoders that is only somewhat kept in check by their own distractions, and needing to refactor the new repo layout for the sake of some fucking kid having discovered fucking FreeBSD as their new favorite plaything. As a consequence, I fail to get much if anything done for the rest of the month.
Someone else from the gameplay community's Discord servers forks my Shuusou Gyoku fork and implements better replays. Wait, what?! On top of the master branch, when even the BSDtard spotted the preview branch with this new and just completed feature? Without taking part in the existing discussion on the subject? Without even an attempt to coordinate with my plans? Result doesn't look vibecoded at a glance, but I don't care to look deeper. Maybe it's just a recreational exercise and not meant to set a community-wide standard. Probably not though, developer sounds serious. Gameplay community cheers as if they have completely forgotten what I was preparing all the time, and what they helped to prepare. No one bothers to ask any questions. That now makes three people who tried to bypass and undermine my business model in a single month.
Clearly, I can't continue working like this. I have to eliminate at least one source of stress. Surely I could capitalize on this renewed interest in old Touhou, right? Advertise again, talk to people, negotiate. Revert life priorities back to ReC98 with newfound determination, given how those other priorities ended up not working out after all. Secure a subscription volume up to about or , backed by several people this time. Then, I could quit my day job for just one year to see how this works out; a colleague of mine has arranged something very similar with my boss as well. I'd still make an overall financial loss, but I have enough savings to be years away from starving, and the increased speed and focus might actually be a worthwhile investment into both my health and the long-term goals of this project. Heck, I might even attract more support and not suffer as much of a loss. Gotta take that risk to find out.
I get invited into that one big PC-98 Touhou Discord server I never would have joined on my own because their application form used to have some character-related questions and I'm at least 17 years too old for that shit. Turns out that the initial impression was accurate and that the server does primarily consist of hyperactive kids. There, I just see more of the same. I'm your biggest fan said by people who have never commissioned a single thing. You'd think that your biggest fan would have expressed their fandom by sneaking in even just a small subscription during the 2½-month window when it was still possible… What even is my support base at this point, anymore? Gameplay people only give lip service and are getting increasingly impatient that no one else is funding their dream features. These kids obviously prefer to spend the little money they have on something else. The few remaining subscribers probably forgot that they're still sending money to me each month if spaztron64's typical slander of crowdfunding business models is to be believed. The silent majority that just enjoys my blog posts might as well not exist, and would just move on to another blog if I were to retire.
Another vibecoder appears on GitHub, takes the anniversary branch, and lets Claude vomit 186,268 lines of code in a day to produce what looks like an SDL-powered TH01 port to modern systems. Heavy emphasis on planning and tests suggests that this specific vibecoder is very serious. As of today, they haven't continued working on this port nor promoted it anywhere, which does suggest that it was just a failed experiment. If they don't position themselves as serious competition, everything would be fine. But I didn't know that at the time. Also, what about any vibecoding attempts that are hidden from view? By people who didn't hit the Fork button and instead quietly worked on closed-source ports?
Gameplay AI person doubles down and now claims to have implemented TH03 replays with test suites, along with 100% PI, and wants to investigate ports in the future. Again, lots of pog and much applause from fellow gameplay people. spaztron64 raises a single question about the legitimacy of it all, gets an answer that suspiciously hints at vibecoding but doesn't outright admit it, then joins in with the applause. After all, they are going to deliver and it benefits the scene, so what's not to like? After watching this unfold for weeks, this is the point where the writing is on the wall. The gameplay community would have been the only remaining group whose constant support ReC98 chants could have turned into clearly motivated (and hence, probably safe) funding. But now that someone from there just started to do it all themselves, there's nothing meaningful left for me to do anymore, and no funding in sight either.
Across all responses I got for my retirement tweets, there was exactly one person who managed to connect significant parts of this on their own from only publicly available information, and that was touhou-memories. Not spaztron64, who failed to think much further than oh no, a key pillar of my scene clout is under threat, better talk him out of it(archive link), as if he was in any position of swaying me with words after cheering on the vibecoders just as much as everyone else. And surely not /jp/; the usual opening posts full of uninformed sludge are par for the course in that terrain, but then derailing about half of the thread by talking about spaztron64 as if he was a key contributor just shows how much clout he gained by merely standing close to this project. Well, guess that confusion is settled now.
From that point on, I saw no way of meaningfully continuing ReC98 in such an environment. Once these vibecoders start talking to each other, collaborating, and releasing their stuff, ReC98 is increasingly going to look like a grift that no one will pay for anymore.
> You clearly are burnt out and not doing well. Perhaps just take a longer break?
That's the whole point: I can't take any break in this environment anymore. I should be able to take a break during times when low subscription volume plus lack of one-time contributions leaves the funding situation far away from the cap, like in April and May. But in this day and age, every break is just another reason for some vibecoder out there to become impatient, blaze away, and present players with yet another feature from my roadmap in exchange for motivating pog reactions, until nothing is left. Community fracturing or consequences be damned. Me taking a break is exactly what caused this! Maybe none of this would have escalated to this level if I managed to release these pushes in late May or early June.
> Why are you even telling people about them, then? Maybe they prefer to keep their work obscure?
Yeah, right. Replays aren't just a random playful mod feature you build because you are bored. During all this time, there also was a static English-only translation patch for Shuusou Gyoku, built on top of jwilins' raw asset loading fork. Did I consider that as a threat? No, because it doesn't hurt anyone, and has nothing to do with the more professional multilingual translation feature I planned to build. Heck, I even told that guy how to do it without desyncing replays. But if you build replays, it's because you want to set a standard and build the entire infrastructure along with it. Enjoy the responsibility, now it's yours.
> Looks awfully familiar, right?
Absolutely. This is the same kind of disruptive energy that I brought in 2013 when I released the first build of thcrap for patching TH14's trial version. The old guard back then immediately retired, and now it's my turn.
> Hello, this is Bluesky calling. We would never cheer for vibecoders! Why would anyone, really?
Yes, I know, you represent the political left, AI is basically the 2020s' version of labor class struggle against capitalist forces, and you are good friends with artists who are immediately affected by AI and have very well sensitized you to their struggles. Also, the AI bubble is about to burst any day now, and what will even be left of these vibecoders once their powerful cloud-based models become inaccessible or prohibitively expensive? Right? The reality, unfortunately, is much more nuanced and uncomfortable:
Local LLMs seem to defeat any notion of cloud dependence. I don't know how good they actually are, but a good rig to run them efficiently is still cheaper than my time. You could even play games on such a rig, on the highest GPU settings. As marketed to you in flashy trailers pumped out by your favorite giant industry.
If you say we would never cheer for vibecoders, most of you actually mean something very specific and much more conditional: If we see an AI usage disclosure, then we throw a tantrum and get out the pitchforks. Being skeptical before that point and wondering how certain overly impressive results can come from people with no prior experience is, apparently, much harder – especially if we're talking about software where you don't have the clear visual signs that artists have taught you to keep looking out for. In fact, here's a Twitter person arguing that expecting this kind of hype literacy of regular fans is completely unreasonable. (archive link)Can't we let people be hyped anymore, man? 🙄 Well, sure, but then this is no longer a scene worth working for, and I'm out. 🤷
It is easy to clown on vibecoders and not recognize them as legitimate actors when they're nobodies without any prior credentials in the scene. But it's much harder when they're established and respected community members and claim to solve real pain points experienced by the community. That's where the cheering is coming from. Once I made clear that these vibecoders may actually be people you consider your friends and might actually know what they're doing, all the people who jumped in with apparent counter-arguments suddenly became very quiet. It is just a tool after all, as some drunk man famously slurred into a microphone last year.
Remember that the gameplay community has shown such overwhelming demand for these features that they do not care whether they were vibecoded or not. But OK, you might still be opposed to vibecode on an ideological level, to the point where you would break a friendship over AI usage. And so, you would continue to fund ReC98 for an alternate implementation at my usual level of care, and continue to pretend that no vibecoder can possibly be a legitimate actor in this space. But why should I let you book me to write what's essentially duplicated code, and shift my role towards being a bastion against vibecode in a culture war? And fracture the already fractured Touhou community along yet another axis?
Because, and this might shock you, I am not actually fundamentally opposed to any and all kinds of AI usage in programming. The increasing decay of traditional search engines has long proved that this is once again a technology transition controlled by big money where individual resistance will increasingly feel like an uphill battle. What I hate much more is the growing trend of people shooting down open-source projects at the first sight of Anita Sarkeesian in the credits I mean an AGENTS.md in the repo. That's not a factual argument pointing out technical shortcomings, but a purely ideological one. It's lazy and lacks even just the attempt to understand context or nuance.
Or this kind of behavior. If there's any developer who has earned the right to set up an AI bot doing pre-reviews of random PRs against random and long obsolete code he stopped maintaining 31 years ago, it's a veteran like Koizuka. He's shipped more than enough code without AI at every level of abstraction to prove that he's a skilled developer. And guess what, steadily adopting the new technology of the era is the only way he could have gotten to where he is now. And most certainly, he has better things to do these days than to tinker with long obsolete e-waste. The anti-AI argument is actually much stronger if you focus on and dismantle the actual content that Copilot slopped here:
Talking about MASM/TLINK-style toolchains conflates assemblers and linkers. These are separate types of tools and handle this piece of code very differently, not to mention that they come from two different vendors.
TLINK only sees assembled .OBJ files, and the EXTDEF record of the OMF format does not even store the type of the pointed-to symbol. Thus, there can't be any symbol-type mismatch error at link time.
If there are other combinations of object file formats and linkers that would throw errors, you must name them, specifically. Things maybe existing and maybe causing a problem in potential but not fully defined scenarios is one of the most paralyzing trains of thought a developer can follow. If there's anything you should develop an allergic reaction to, it's this line of thinking. This is what makes BSDtards so insufferable.
Redeclaring an array of 8-bit values as 16-bit is completely valid if your optimized usage code naturally accesses the array values in 16-bit chunks. This is ASM, typical code smell rules that might apply to C code do not apply at this level of abstraction.
But aside from that: The bot already realized that the code only uses the address of the symbol via seg and offset, and no instruction dereferences the values behind the pointed-to symbol. Hence, the pedantically correct suggestion would be EXTRN _SolidTile, without any indicated pointed-to type, which is the equivalent of a void * in C.
But that only means that this suggestion doesn't matter. If you enjoy minimal diffs as I do, you'd reject it, but applying it doesn't hurt either.
Old man can still do it, apparently. That's how you argue against AI with facts and logic. And it only enhances your existing argument of AI being bad for the planet and theft and making us dumber and taking our jobs by adding the fact that all these crimes against humanity didn't even produce anything of value in this particular instance. But then again, that takes time and effort, and people have 30 other Discord servers to check for news and either spread pog reactions or pitchforks. And maybe I've just argued against the Lite effort level.
In any case, this shows how fractured the community already is, and I fear that it's only getting worse. What if I had been the one to set up that Copilot bot to defend myself against PRs that should not steal my time? How much community support would I have lost for the mere attempt at evaluating whether this technology helps or hurts me? And, perhaps most importantly: Why do I even have to ask this question now, in addition to the hundreds of questions that already form each release cycle?! (Plus one, because of fucking FreeBSD.)
Finally, they have test suites. You know, the thing I would have to charge extra for, because my whole funding model is geared toward not making features any more expensive than they have to be. The thing these 6 pushes were merely preparing, and that only received funding after I messed up twice. And sure, what is a test suite nowadays other than another thing to prompt for. But the fact that they do prompt for test suites is enough to conclude that these people aren't toying around and do want to set the standard.
At its core, this really isn't about vibecoders. This is about a disruptive assault, and a business model that failed because it had practically no resilience to it. It only ever worked because it rested on three very specific pillars:
The usual Touhou paradox where it's been simultaneously huge and culturally relevant enough for at least the past 18 years to come with a potentially big audience, yet small enough that there somehow isn't a huge crowd of devs competing to build key infrastructure. Y'know, that feeling that has made me go how can I possibly be the only one who both has these ideas and actually goes through and implements them constantly, for the past 16 years.
Maybe because the barriers to entry for building said infrastructure used to be legitimately high.
There may have been better devs than me, but they were either put off by the legal uncertainty of it all, or quickly moved on to higher-profile or simply more interesting projects outside of this legally gray shithole. Y'know, the thing that usually happens on its own when people aren't fixated on one thing that seems cooler than everything else in sight, and instead manage to live their lives in a way where these superior chances just naturally present themselves.
Topple pillar #2 by introducing LLMs and agents that finally are powerful enough for the job, shake pillar #3 by bringing in new, young, and energetic devs with no preconceived notions about tools and who believe they can start fresh by ignoring all of the constraints that have made things increasingly difficult, and things suddenly look a lot more different. Then, the resulting competition smashes pillar #1, and my entire business model falls apart.
> But clearly, they would have wanted to work with you?
So after a month of not even trying to involve me in anything, these gameplay people would now suddenly even have the money to pay me? After spending 4 years watching my capacity get increasingly consumed by Seihou and doing fuck all about it? Let's not kid ourselves. Any imaginable kind of cooperation would obviously be on their terms, and not on mine – whipped in front of them, unpaid, and probably entirely consisting of unslopping their LLM output. And I haven't even received any offers suggesting that truly humiliating job. What happened after my announcement on July 10 is far more telling, though. Sure, I blocked four key people after the tweets because that's the healthy thing to do when feeling assaulted. But it's not like anyone else who knew both sides tried to work out a solution with me either. It very much appeared that no single gameplay person had any interest in clearing up any misunderstanding or settling anything, and especially not in public. The old man had long become a nuisance anyway, it would seem. Easier to let him appear like a deranged madman to the uninitiated masses and cowardly stay in the shadows.
It seems like I was even doing the vibecoders a favor by not publicly naming them. I honestly thought this was unnecessary – if you really wanted to know, you could just comb through the GitHub fork network and discover them easily. Well, judging from the amount of what is even happening I have gotten in response, this turned out to be yet another unreasonable expectation. I have not seen a single person actually connecting my tweets to the specific and very public forks in question. At least touhou-memories figured it out on their own after I explained the whole timeline.
Maybe, all of this could have been sorted out if any one person communicated better. Maybe, these vibecoders then would have stopped pressing the button. But continuing to run a business that can only meaningfully exist at the mercy of every Touhou-adjacent vibecoder not pressing the button is an insane proposition.
> Just vibecode yourself, then! Beat them at their own game!
This has to be the most tone-deaf suggestion so far. Have you ever read any blog post from the last couple of years, or looked at the code? They exploded in detail precisely because the "right way" to do things becomes ever more complex as you accumulate more and more experience and begin to see new angles that could be perfected and understood. As such, asking questions about smaller and smaller details and building this deep understanding started to become the entire point of this project – and the entire reason why it continued to be worth doing after all these years, long after I lost interest in Touhou as a subject matter. It feels great to point to these posts and say yes, I built all of this, and now deeply understand everything I wrote here, and the money I made proves that this content is actually sought after. There is a place for vibecoding, but ReC98 was never going to be that place. 📝 The microtransaction concept should have brought quicker actual progress, but the market utterly rejected it, selling only in 4 years. Instead, every backer fully agreed with the slower and more careful plan. I could have blindly and quickly decompiled the games years ago if I wanted to, but no one ever approached me with a convincing offer that would have made this a worthwhile cause of action. Touhou fans in general don't play the games as the infamous saying goes, but they sure love reading about the games, and this is what they increasingly paid for. It sure seemed like a great and rare fit. Say what you want about secondaries or tourists, but at least they don't pose an existential threat.
So why should I now vibecode if other people vibecode just fine? Ever since the Discord days of crowdfunding in 2018, there hasn't been any room in my brain to bother about AI. It was full of plans for every actual feature that I still wanted to understand, solve, and implement with as few compromises to quality as possible. This silly arms race of ever larger LLMs seemed like the dumbest and most decadent thing ever to follow, entirely unrelated to any reason I work in this field to begin with. Thanks for showing me the utter error of my life choices, I guess.
> But you could still write these blog posts! I loved them very much.
As valuable as they were in the end, writing them has always been the most painful aspect of this project, by far. So painful that I would have outsourced the writing to the very first employee I would have hired. If you take away the coding work because machines now do it faster, you take away the one thing of ReC98 I mostly enjoyed. The reward for writing the previous blog post, so to speak. The bitter irony is that these posts were originally supposed to merely advertise the actual coding work by demonstrating quality, care, and lots of forethought for future plans. But now, this entire blog is just prompt context that vibecoders can feed to LLMs in order to improve their results. No deep engine or hardware knowledge necessary if some retard made it his marketing strategy to hand out all of that on a silver platter. Thus, I can't help but detect a hint of sadistic malice in this suggestion. As if I should continue slaving away for probably even less money (since I wouldn't code anymore), providing fresh human-made context fueled with my many years of experience, just so that some vibecoders can ingest it all into their LLMs, turn it into code, and get all the fame and opportunities for the playable result. I sure don't get anything out of this anymore once the excitement of having done great work starts to wear off.
Because even then, the economics don't make sense:
Year
Blog post count
Text bytes
Average
Asset count
Average
Relative complexity
2020
40
169,371
4,234
35
0.9
1.0
2021
23
146,709
6,379
22
1.0
1.3
2022
19
342,317
18,017
136
7.2
5.9
2023
13
389,428
29,956
117
9.0
8.5
2024
8
457,106
57,138
183
22.9
18.8
2025
11
603,909
54,901
191
17.4
16.0
Excluding the three policy-bugfix posts (📝 1, 📝 2, 📝 3), and the 📝 order advice post. Complexity is calculated as the geometric mean of the average text byte increase and the average asset count increase since 2020, i.e., √((Average text bytes / 4,234) × (Average asset count / 0.9)).
Even by my own funding standards, the increased complexity of the blog posts from the past two years alone would have long justified at least per push, compared to the from 2020. But once I went up to , we saw the store regularly staying open for months, so this was as much as the community was willing to pay. Forget making even an entry-level German IT salary, which is already half of an entry-level US IT salary because we don't need that many freedoms here. Let alone hiring employees at anything resembling humane, non-sweatshop conditions.
> OK, calm down. I've looked at the forks in question, and they are indeed slop. You are above this, even if and especially because you take your time. I'd still continue to invest in this project, even more so than before.
OK, now we're getting somewhere because this instantly reframes all the previous arguments:
I should have never bothered with all these people in their Discords who chanted support ReC98 and $10,000 to Nmlgc without ever actually doing that, and without ever even just coordinating a plan to do so. Or the people who took every opportunity to bypass and undermine the crowdfunding system. Or the silent readers who would have never come out of hiding if I hadn't announced my retirement. In fact, this project hasn't been carried by the core of any specific Touhou subcommunity for quite some time. Those communities may have been loud and visible and dragged all my attention onto them, but they sure weren't helping. Instead, the final subscribers all either came from the fringes of the Touhou fandom, or had long moved on to something else. It was their support that not only kept this all running, but also allowed me to plan ahead. And no, spaztron64, they did not forget what it was they were funding at any point, as all of them would later tell me.
I would still produce duplicated work and form a bastion against vibecode, but doing so would certainly be more tolerable if it was funded by a crowd that I know to still be enthusiastic about what ReC98 actually is.
If funding really is secured, I no longer have to bend over backwards to accommodate everyone possible in the hope of eventually being rewarded with their money one day, and can set firm boundaries. No unpaid interactions with gameplay people. Immediately closing bad PRs if they take too long to review. That sort of thing.
I sure don't know where to find more of you, but maybe someone could do community management to give me a stronger sense of those people actually being present. Paid, of course.
But now that I've had plenty of time to re-evaluate my life choices, do I really want to rely on a few wealthy patrons again? If I ever pulled a Miyazaki and came out of retirement for one more thing, I'd prefer it to be safely carried by an actual crowd for once. Maybe we could get to that point if all that vibecode indeed turns out to be slop. Not fulfilling even the most basic quality requirements, the tests don't help either, and the devs fail to fix any of it because they actually have no clue. In reality though, they'll probably just find another bored and highly competent student who will fix their stuff for free and for the love of the games, as it perhaps should have always been. I heard thcrap turned out alright too. (Actually, last time I looked at it in 2024, it looked even uglier in certain places than it did when I left, but who except me gives a shit. Just proves my point.) After all, being just good enough has always been the essence of Touhou. It's about time that the one guy who spent 16 years attempting to fight this trend finally gave up.
And really, is the pain of doing this work at this high level even worth it anymore, in such an environment? Thanks to AI, every community around every game can now get ports and full-blown QoL features at the press of a button. So much of the value of implementing these things thoroughly lies precisely in the fact that they are still an innovation rather than an expectation in this environment. But once that line is crossed, nothing is special anymore, and everyone only needs to wait for one person to pay for the inference, press a button, and maybe minimally supervise the result. So why even invest time and human touch at all. Just tells me I should go innovate somewhere else if there is no more innovation to be had.
> It's now almost three months later. No new vibecoded forks have appeared. 2 of the 4 forks you mentioned never went anywhere. One of them was unrecoverable slop to begin with. You were massively overreacting.
How could I have known that at the time? How can we rule out that this lack of development didn't at least indirectly result from my retirement tweets? And most importantly: How much do these three other forks really matter if the single most threatening fork is still alive, and increasingly justifies that flight response from July as its development continues?
> Please, just keep going! I have connections to Shanghai Alice Reprise and we'll surely get you on board once they remaster the PC-98 games. You're obviously a better fit for that job than any vibecoder will ever be.
Not that anyone ever said this (yay, NDAs…), but the fact that this sounds like a valid thought highlights another misunderstanding. ReC98, as hard as it became during the past few years, was exactly what I wanted to do and work on. Continued work on thcrap became morally untenable in early 2019 given how the community continued to throw money onto worse-than-worthless DRM-crippled re-releases. In case you forgot or weren't around in 2017-2019, and simply because it can't be repeated often enough:
If you buy the Steam version of a main series game by Team Shanghai Alice and expect to use thcrap on top of it, you are relying on thcrap shipping a literal DRM crack for this combination to work at all. Sure, these cracks aren't hard to make, and this basic kind of Steam DRM is trivially removed or bypassed without leaving any trace of it ever existing. But that only makes it less defensible: Pirates gonna pirate, and a quick automated cracking step makes no qualitative difference in the amount of copyrights they violate. But for modders, it does, because they now have to ship cracks and cross a different kind of moral boundary they did not have to cross before. Denuvo is fully defensible because its sheer strength and deep integration into a game clearly and unmistakably serve an understandable economic purpose. Even more so if the publisher graciously removes it after the launch window. But Steam DRM on top of the exact CD releases isn't. It serves zero purpose beyond pushing mods into legally even more questionable territory. ZUN could have gestured respect and appreciation for the people who translation-patch his games, but he instead chose to slap them straight in the face. He might have said that fan translations are cool, but this one action of flipping the DRM switch immediately invalidated all of his previous words. And before someone brings the but encryption is the default, and he's just an old man argument: He was fully aware of what he was doing.
And because I haven't burned the adjacent bridge yet, I'll speak out the logical conclusion as well:
As a Westerner with no ties to Japan and who doesn't speak the language, it is not only morally correct to pirate Windows Touhou, but in fact morally wrong to pay for it. These games are not marketed to us. Japan funds these games well enough for what they are. More Western money to ZUN only supports stagnation and his aforementioned reprehensible behavior while signaling entitlement to the fan labor that built the Western community to begin with. Literally every other link in this chain deserves your money more than ZUN does. Wiki authors, translators, journalists, website operators of any kind, artists, even just mere social media shitposters, you name it. The vast majority of you would have never wanted to support ZUN if it weren't for the collective efforts of these people. Submitting to some dumb idea of community peace like a little puppy and implementing Steam DRM cracks into thcrap was the single biggest mistake I made during the development of that project. To this day, I keep wondering about the alternate timeline where I put my foot down, refused to implement DRM cracks into thcrap, and blew up the issue. Clearly, the resulting confrontation would have gotten me somewhere, as opposed to the actual timeline that got me nowhere.
In 2019, this was the conclusion that left ReC98 as the only remaining imaginable project in this scene that I could personally stand behind. By focusing on the clearly abandoned part of this media empire and opening it up for the community, my work would never benefit any currently marketed aspect of Touhou and not make ZUN even richer, or at least not without me getting paid my fair share. Hence, I have no interest in doing anything else in this scene, and especially no interest in working on closed-source games for a media empire in a humble doujin trenchcoat.
If the community at large wanted this kind of project, they would have funded it much more heavily than they did, and I would have long been able to raise the cap to meet the higher demand and quit my day job along with it. But no. What the community at large actually wants is to enjoy le funny beer man's latest manufactured hype cycle, culminating in a superficially polished but technically rotten game to play on their original Nintendo® Switch 2™ electronic video game system. I have nothing but contempt for this entire industry and especially Touhou's place within it, and don't want anything to do with it. And sure, the Shanghai Alice Reprise releases will mark the first time where main-series Touhou actually will be worth Western money, and where piracy will be morally wrong, by my own logic. But I still have zero interest in working with Shanghai Alice Reprise if it means agreeing to any terms other than the exact ones I can reconcile with my conscience. And I guess "decent pay" and "no NDAs" are dealbreakers in the eyes of this money-printing machine. After 16 years of setting ever-increasing standards, 13 years of Nothing Ever Happening as a result, and 9 years after getting slapped in the face with Steam DRM, I deserve better than being treated as an unpaid volunteer, like they did with their translators. Also, how about owning up to the true nature of your RE efforts? This only proves official Touhou PR to be as insufferable as it has always been, continuing to sweep every actually interesting question under the rug. IceFairy would have been the one person in the position to most effectively call them out and keep them accountable, but he instead chose to build a brand on being an uncritical parrot of said official PR, as if that was what the world needed most. I feel intense fury every time I have to read any of his posts.
And if I can't stand ZUN and Shanghai Alice Reprise and IceFairy and Vanyk and the gameplay community and their dumb ending blockers and the hater community and uncritical kids and the video game industry and CRTs and CRT filters and people who push CRTs and CRT filters and call it visual literacyand alcohol and haven't cared about lore and characters for at least 15 years, why am I even still here? What door is there even supposed to open that would justify spending any more of my life on this? And even if a door were to open, won't it then open for the vibecoders, now that all eyes will be on them? In some way, I should thank them for finally breaking these chains. ⛓️💥
> Wow, you're an idiot after all. The scene is indeed better off without you attempting to block progress. Productivity is all that matters, and you haven't been productive for a while. Good riddance, get filtered 🔫
I fully agree.
*crickets*
Almost three months have passed since then. Why should I care about releasing this at any reasonable speed anymore. Every single other thing in my life has been more important than getting this release out.
Twitter keeps showing me how pretty much every game is ported to pretty much every platform now, expecting me to care. Are these ports slop or made by caring humans and just happen to coincide with the latest generation of LLMs and agents? Who cares, everything with a cool headline gets equally liked, retweeted, and algorithmically boosted. People are getting tired, but their quote tweets only give further attention to the thing.
In early August, a poll by swmprt14 showed that vibecoding acceptance rates are at 71% if you are a cool and reliable community person. As we've seen, this is the correct metric for our case, because this kind of poll directly influences creator motivation and thus controls what actually gets made. AI opponents had every chance to sway this poll in their favor after the issue got publicized, and look where it ended up. As usual for political squabbles, each side wants to convince the other that they are on the wrong side of history. But what are the 29% going to do? Build a competing product that is transparently hand-coded from the start, ends up maybe 10% better in the details, but costs at least 1000% more? Nah, they're just going to spam AI bad on social media to build up the mere impression that their opinion is more prevalent than it actually is. Talk is just as easy as prompting, after all. Whatever, video games are over, and the only winning move is not to play. I don't belong here anymore.
I feel at peace. No more hard problems to solve in exchange for 20 likes and pocket money. No more quality innovations to come up with, only for me to be ahead of everyone's time and have others reap the benefits a decade later. Imagine setting standards for 16 years and still getting introduced as someone on GitHub in one of the few mentions on the public Internet. No more expectations to manage. No more superplayers to correct on their wild misunderstandings of hardware, only for them to not read 📝 the one blog post I had to write solely to counter their imprecise language and the misinformation they kept peddling across the Internet with all the confidence in the world. How obvious that it's exactly those people who now turn to AI, it really is a perfect match. Imagine bending over backwards for a group of people that collectively provided a mere 1.84% of ReC98's total funding, and maybe a few minor corrections to blog posts. No more listening to inane ramblings about input lag while the ramblers are unable to diagnose the actual cause of whatever they're imagining. No more reasons to stay in a space I should have left 15 years ago. No more pretending that this is a worthwhile project to spend all of my life on, just because these games are "culturally relevant". Why do "one more feature" when there is no way to go from this local optimum? No more slimy, sycophantic, and sometimes just plain factually wrong IceFairy posts in my feeds, or getting sent to me in any other way. And finally, no more listening to leechers complaining about only one game being decompiled when the only one they have to blame for this state is themselves. There were several sad that hand-coded ReC98 is no longer economically viable, oh well responses to my retirement tweets, but guess who was the only person who admitted personal guilt and responsibility for the overall funding situation? touhou-memories, of course. They told me that they'd actually also be writing giant posts if it weren't for Tumblr's post size limitations. They definitely have my blessing as a spiritual successor more than whoever will fork the website repo in the future and pretend to be me.
The only thing I'm genuinely sad about not implementing is Arabic text rendering on PC-98. How telling that it's nothing related to gameplay, these people don't deserve any more of my time. But guess how much funding that ever got. And maybe even that is just 10 prompts away these days, so what even is the point. There is no better time to touch grass than right now.
Alright, everyone equally unhappy and potentially furious now? Good. I'll wrap up this project with PC-98 text RAM JIS trail word rendering; it's been in my drafts for 5 years, so it would be a waste not to finish and release it. I've heard that Ember2528 will also fund a few farewell gifts entirely unrelated to anything threatened by AI, allowing this project to go out in style.