Genre: Strategy, Turn-based strategy (TBS), Indie, Card & Board Game
100% Orange Juice
504403158265497090 Points, or A Day in the Life of a Save File - Devlog
We're happy to share with you another devlog by Rapha, this time about the recently implemented big save rework, why it was necessary and what went into it! Hope you enjoy reading it.
The Perils of Binary
In the realm of video game development, preserving a player's progress is paramount. This is where game saves come in, capturing the state of a game so it can be resumed later. However, the methods used to store these save games can significantly impact their susceptibility to corruption, be it by external or internal factors.
The problem
100% Orange Juice since time immemorial used binary storage, it used to be the go to for this kind of data when processing power and storage space weren't plenty available. It's more efficient to write, to read, and uses less space in disk. But this efficiency comes at a cost, it's easy to mess up. It's why we've added more and more ways the game backups your saves at various points over time, to make sure players have some backup to return to if their save with years of progress gets corrupted.
Now let's imagine a theoretical game, Poppo Saves The World! Let's imagine it as a beat-em-up, where you go around fighting enemies, one after the other, until you win and save the world. Lets supposed you have the following section of data that needs to be saved:
- Wins
- Points
- Last stage cleared
Lets supposed you have 10 victories, 393939 points and the last stage cleared as stage 7. Let's imagine you used 64 bit values for everything. So:
|Data | Explanation|
|--|--|
|0A 00 00 00 00 00 00 00 | 10 in hex padded to 64 bits|
|D3 02 06 00 00 00 00 00 | 393939 in hex padded to 64 bits |
|07 00 00 00 00 00 00 00 | 07 in hex padded to 64 bits |
But then you introduce abilities to the game, you decide to store it as a single byte, that will keep if you have each of the 8 abilities available unlocked, one in each bit.
|8|7|6|5|4|3|2|1|
|--|--|--|--|--|--|--|--|
|0|0|0|0|0|0|0|0|
Let's suppose your player unlocked only the first one so it's a 1, so in binary:
|8|7|6|5|4|3|2|1|
|--|--|--|--|--|--|--|--|
|0|0|0|0|0|0|0|1|
You inadvertently decided to write it between wins and points, so you would have the new binary:
|Data | Explanation|
|--|--|
|0A 00 00 00 00 00 00 00 | 10 in hex padded to 64 bits|
|01 | 01 in hex, it's a single byte |
|D3 02 06 00 00 00 00 00 | 393939 in hex padded to 64 bits |
|07 00 00 00 00 00 00 00 | 07 in hex padded to 64 bits |
And as logically implied, you would change your reading code to expect that too! But then comes the problem! Your old save doesn't agree. Let's see what your game would read from an old save.
It expects 8 bytes for the Wins, so it reads 8 bytes:
0A 00 00 00 00 00 00 00
That reads to 10! Seems ok! Up until now everything have gone correctly.
But then it tries to read the byte for abilities...
D3
D3 is byte, your game doesn't know if it is valid or not, it's binary, it believes in the binary. Let's look at it for a bit, in binary:
|8|7|6|5|4|3|2|1|
|--|--|--|--|--|--|--|--|
|1|1|0|1|0|0|1|1|
So not only the first, but the second, fifth, seventh and eighth abilities are unlocked?!? Wow! That's a problem.
Let's continue, more 8 bytes:
02 06 00 00 00 00 00 07
That's 504403158265497090 points... That's definitely not what was written originally. By reading that byte that wasn't there, you misaligned the whole thing, and now your player has more points than literally the universe has seconds of life. And then you continue, last 8 bytes:
00 00 00 00 00 00 00
But that's not 8, you only got 7! If you really try to read that, you will get an unexpected behavior. What happens depends on what system, what processor and what contingencies are in place. In some systems you may read a byte from memory that has nothing to do with your save, in others you may read some leftover on the memory bus, in some you will crash... The ideal behavior would be the crashing.
But lets supposed it didn't. It loaded.
Now you have:
10 victories
5 abilities unlocked
504403158265497090 points
And never cleared a single stage
If this happens to your user, they won't understand a single bit of what's happening, will they?
The conclusion
You must be thinking now, "Well, that's easy, just don't mess up". Yeah, but that's easier said than done, with less than a dozen of information to save, and total control over it you can't commit this mistake.
But what about having 200000 different bits of data that need to be read and written correctly, by code written over 15 years, written by different people, with different designs in mind, written by functions in different files, some keeping proper spacing between data, with spare data for future use, some not. That was our situation! Years of updates adding numerous incongruities that we didn't account for, despite fighting against it all day.
Yeah, that's a design flaw, and that's why we are fixing it, starting with this step! 100% Orange Juice is a bigger game than one might imagine, and such huge code base being kept alive by less than 10 people is nothing less than a miracle driven by passion and willpower over the years.
The solution
We won't enter into detail, because our new save system not only is 100% reliable but has a good number of protection layers against people editing it. But firstly, if your save was changed externally, it won't load.
This is to avoid the game crashing if your storage is faulty and some bit flipped. That's why the backups are now more important than ever, if your main save corrupted, it won't load, no one will be able to restore it, its gone forever. The game won't even try to read it.
But not only that, now generally speaking, each bit of data inside the save games has a proper name to address them.
Let's bring the example from before:
Wins
Points
Last stage cleared
Now we build a database in memory, naming each value in a proper structure before writing it to the save. So it would be something like this:
wins: 10, points: 393939, last_stage_cleared: 7
For reading, you simply search for the value name and loads the value attributed to it: wins → 10, points → 393939, and it goes on.
It doesn't matter, you aren't reading in sequence, you have a proper system to encode and decode the values and search them by name. If abilities doesn't exist on the save, you load the default value and go on.
This basically clears the future from unwanted data changes, but nothing in life is a perfect solution. We still need to load the old save, and if that's corrupted, the new one will be too, as the old save system trusted above everything else what was written, in the exact order it was.
The game also may after some update write something wrong, that's a bug, it can happen, it doesn't mean the save is corrupted stricto sensu, but in lato sensu the data was handled wrongly, so that's still a bug, that's still a thing that needs to be reported.
So if after the save conversion on the last big update, you got more dice than the universe has seconds of life, the best we can suggest is contacting us! We will do the best in our power to fix your old corrupted save. But be aware there may be some times that it's impossible (mainly if you didn't play for the last 7~10 years, the amount of uncounted problems can be beyond any repair), but even in this horrific situation, talk with us, say what you hold dear about your old save, and we might be able to find a path that will make you happy.
Streamer Mode Added!
By popular demand, new options have been added for the comfort of streamers!
Two new options are available under System -> Streamer Options
Usernames
Setting this option to "Hide" will replace other player's names with "NET-A" etc, to avoid disruptive usernames while streaming.
Lobby Names
Setting this option to "Hide" will replace lobby names on the lobby list with the host's SteamID, to avoid disruptive lobby names while streaming.
Steam friends' lobby names will still show up normally.
Thank you for the suggestions on how to keep improving the game!
Versioned Hotfix Live!
Fixed Minions of the Master's ending cutscene looping for some players.
Fixes Deal's player select prompt bugging out if certain conditions are met.
Updated Dealer CPU behavior so that they can deal to players that don't have full hands yet.
Version 3.20 Adds New Options, Many Fixes!
Added a new config option to show or hide player stats in the lobby: Wins, Level, or both.
Added checks for loading the save when Steam is offline.
Fixed an issue where players cannot unlock cardbacks and the anniversary cutscene won't play on a new save.
Fixed an issue where brand new saves that possess the Mei & Natsumi Character Pack would have the Red & Blue pet assigned as a default pet for all cosmetic slots.
Fixed the movement modifier sound effect not playing for Live Mode, and playing for Bounty Hunt shoes.
Fixed a redundant instance of menu BGM while getting to the title screen.
Fixed Game of Origin and Hyper text on the unit cards being incredibly small.
Fixed Reproduction of Records replicated cards not being truly free of cost, and deducting stars (even if you lack the cost).
Fixed Kae's passive applying twice when playing a card.
Fixed Co-op cannonballs having a hard time following flying Big the Jonathan - they now use hitscan instead.
Improved cannonball animation when the target is directly above.
Fixed an issue that prevented some users from progressing towards unlocking Mio.
Fixed Contest of Cuteness crash in co-op when playing the card versus a wild unit in an online game with at least 2 human players.
10th Steam Anniversary Video!
We're proud to share with you a premiere for a special 10th Steam Anniversary video, releasing on May 31, 10 AM PST! ːqpː
ː100ojːJoin us here with your party hat on:
This video is the result of months of planning and weeks of editing. We hope you'll enjoy it, and maybe it'll bring up some fun memories for our long time players!
Unversioned hotfix live!
- Really fixed Grain & Bourbon's cute bunny ear accessories not being rendered.
Versioned hotfix now live!
Fixed QP (Dangerous) getting double bonuses from pudding.
Fixed broken gallery cutsceens.
Fixed an issue where Suguri and Suguri (46 Billion Years) pins required both to be completed in order to unlock the pins.
Fixed menu BGM resetting when coming back from credits.
Roll modifier SFX now only plays for card effects (and not say, Grain's passive).
Fixed double dice effects not working in battle after first turn.
Fixed Pigformation skins missing for characters that had them unlocked.
Fixed crash when Alte's unit effect was triggered.
Fixed Grain & Bourbon's cute bunny ear accessories not being rendered.
Fixed desyncing after having joined a Co-op game in progress that has an active summoned unit.
(beta) Fixed custom languages not loading
(beta) Fixed Hime Moonlight issue
(beta) Fixed Fish a Fish astral projection issue.
(beta) Fixed resolution list
(beta) Uses ANGLE to render OpenGLES2.0 (So 100% Orange Juice uses Chromium Technology now.) This will translate GL to DX9/11/12/Vulkan depending on your system, so old systems should be OK again (Modern Windows Systems will run using DirectX11)
Unversioned hotfix now live!
Reverted the graphics engine upgrade for the time being.
Dev note: in hindsight when we planned this update, we didn't expect to have this many active players. While we expected the change to generate a lot of reports, it turned out a bit overwhelming and we also want to make sure all the new players can play without issue while we figure them out. That's why we reverted the graphics engine change. The downside is that the new accessibility options are also gone because they rely on the OpenGL build. However, we've updated a new "Public OpenGL Beta" branch with the compatible OpenGL .exes, so anyone that wants to keep playing on/testing the new engine can just swap into that. We'll keep it up to date with future updates too so there's no problem playing online.
Fixed an issue with the 10 year anniversary cutscene not getting saved after seeing it once.
Unversioned hotfix live!
- Fixed Play of the Gods crashing the game client when played. - Fixed the display version to correct 3.19.
The Old Guard Crashes Into School!
The old guard has entered the (school) building! The two veterans from Flying Red Barrel, Bourbon and Grain, are now available to play for owners of the Bourbon & Grain Character Pack! The School Crashers event has also returned!
The soulful pilot longs for a time when the skies are free again. Leader of the Resistance from Flying Red Barrel, Bourbon brings unique tactical skills into play. His gruff voice is perfected by Mamiya Yasuhiro (Hugo Kupka in Final Fantasy XVI, Gigantomachia in Boku no Hero Academia, Viviano Westwood in JoJo no Kimyou na Bouken Part 6: Stone Ocean Part 2)!
+0/+1/-1, 7 HP, REC 6 Every time you take damage in battle, lose 1 max HP (down to 3) until KO'd.
Hyper: Heart of Resistance (BOOST) Lvl 2, Cost: 20 Target another active player. Next time they are challenged or challenge a unit while you are active, take their place in the battle. Gain +3 ATK, DEF and EVD at the start of the battle. You may only have this effect on one player at a time.
Character Design: Junpyon Character Art: Coffgirl Hyper Illustration: Junpyon
Grain
Malt's right hand man at the Guild, Grain will give your performance a cool evaluation, but he can also bring out some serious firepower! Grain is voiced by Yamazaki Takumi (Kawakami Bansai in Gintama, Isamu in Macross Plus, Nano in Togainu no Chi, and many more!)
+1/+2/-2, 5 HP, REC: 6 -1 MOV. Gain a Cannonball Counter upon playing a card.
Hyper: All Guns at the Ready (Event) Lvl 3, Cost: 30 Pay all stocked Cannonball Counters. Deal 1 damage to a random enemy. If the enemy is KO'd, gain 1 Win and Stars equal to 10x their level. Repeat this once for every Cannonball Counter paid.
Character Design: Junpyon Character Art: Coffgirl Hyper Illustration: Junpyon
School is Back in Session!
The anniversary fun continues with a much requested re-run of the School Crashers event! Collect pieces of NoName in fun Playground minigames to clear the event!
The event runs until June 16!
School Crashers has no new rewards this time, and is automatically finished if you have done so previously.
New Save System!
While this update may not be flashy, it'll hopefully make the game more robust to play for a long, long time! We've overhauled the game's save system with the new system we trialed in the second Bounty Hunt beta.
The save files are now found in a "save" folder in the game's installation folder. They are further separated into folders by your Steam user ID, and all 3 user profiles have a separate save file. The big benefit of this is that family sharing and similar scenarios will no longer overwrite your save file as it's based on your Steam ID. The new format of the save files should also make them way more safe against save corruption, keeping your progress more secure.
Graphics Engine Upgrade
The graphics engine in 100% Orange Juice has been overhauled to use OpenGL 3.3! It's a big change which completes the removal of any windows-only code from the game, but you shouldn't notice any immediate difference from it.
In the future, this makes new effects and features possible, possibly even new kinds of mods. Eventually this will enable us to release the game on other platforms besides Windows as well!
Full patch notes for the update can be found on the Steam forums, as usual.