Skip to content
HN On Hacker News ↗

ReC98

▲ 70 points • 53 comments • by mimasama • 2w ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is human-written.

0 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 1 of 1
SEGMENTS · AI 0 of 1
WORD COUNT 1,669
PEAK AI % 0% · §1
Analyzed
Oct 25
backend: pangram/v3.3
Segments scanned
1 windows
avg 1669 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,669 words · 1 segments analyzed

Human AI-generated
§1 Human · 0%

📝 Posted: 2026-10-05 23:52 UTC 🚚 Summary of: P0336 Portability (Isolating Shuusou Gyoku engine/backend code into a shared port library) P0337 Seihou / Shuusou Gyoku (Game logic library, part 1/4: Code cleanup + low-dependency Win32 memory management) P0338 Seihou / Shuusou Gyoku (Game logic library, part 2/4: Frontend and initialization cleanup) P0339 Seihou / Shuusou Gyoku (Game logic library, part 3/4: Game logic cleanup + Instance classes) P0340 Seihou / Shuusou Gyoku (Game logic library, part 4/4: Physical game logic split / Windows and Linux DLL build) P0341 Replays (Shared CLI runner / Basic implementation for Shuusou Gyoku) 💰 Funded by: [Anonymous], Root, Ember2528, WishMakers, Arandui 🏷 Tags: No intro; by now, you probably all know what happened in the end. Welcome to the one feature that broke me, and the whole project along with it… Point items with negative values? Defining the scope of "game logic" The unexpectedly big scope of RNG Researching locale bloat in MSVC's sprintf() Safely bypassing MSVC's C++ exception handling Completing the separation of game logic Implementation details Changing debug invincibility The birth of a shared ReC98 source port engine A replay validation CLI Validation performance Future work Retirement announcement Epilogue 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: - sprintf(buf, "%d", e->point); + sprintf(buf, "%u", e->point); 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: Video demonstrating the incorrect rendering of item value popups that represent negative points in the original version 1.005 of Shuusou Gyoku. Recorded by shooting the first enemies at the beginning of Stage 1 while staying in the center of the playfield and then running into adjacent enemies to immediately lose a life; this causes VIVIT to collect the point items during her respawn animation while she's below the bottom edge of the playfield. DownloadVideo demonstrating the incorrect rendering of item value popups that represent negative points in the ReC98 P0295 build of Shuusou Gyoku, which accidentally interpreted these values as unsigned. Recorded by shooting the first enemies at the beginning of Stage 1 while staying in the center of the playfield and then running into adjacent enemies to immediately lose a life; this causes VIVIT to collect the point items during her respawn animation while she's below the bottom edge of the playfield. Download 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 after the 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: Video demonstrating the incorrect rendering of item value popups that represent negative points in the original version 1.005 of Shuusou Gyoku. Recorded by shooting the first enemies at the beginning of Stage 1 while staying in the center of the playfield and then running into adjacent enemies to immediately lose a life; this causes VIVIT to collect the point items during her respawn animation while she's below the bottom edge of the playfield. DownloadVideo demonstrating how negative item value popups would look like with a defined sprite for minus signs and immediate addition of negative deltas to the official score. Recorded by shooting the first enemies at the beginning of Stage 1 while staying in the center of the playfield and then running into adjacent enemies to immediately lose a life; this causes VIVIT to collect the point items during her respawn animation while she's below the bottom edge of the playfield. Download 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.