A Lock on the Save File (and Why It's Only a Lock)
the door was wide open
Oliver's Racers writes your progress to a config file in plain text. Money, cars, parts, all of it, right there in a format you could edit in Notepad. For ages that didn't bother me even slightly. It's a little arcade racer, not online banking. If someone wants to cheat themselves a million bucks in single player, who am I to stop them.
Then I added stuff you buy with actual money, and stuff you win off a wheel, and "just open the file and change the number" suddenly wasn't so cute anymore. A premium car shouldn't also be a thirty-second text edit. So the file needed a lock.
sign the stuff worth cheating
The lock is a hash. Whenever the game saves, I grab every field somebody would bother faking — money, owned cars, parts, tickets, the premium stuff — mash them into one string, glue a secret salt on the end, and hash the lot. That hash gets written into the save right next to the data.
func _econ_sig() -> String:
var payload := "|".join([
str(money), str(owned), str(owned_parts),
str(tickets), str(owned_gold), str(premium_spins),
device_id,
])
return (payload + SALT).sha256_text()On load, before I believe a single value, I rebuild that hash from whatever's actually in the file and check it against the stored one. Edit money to 9,999,999 but don't also cook up a matching hash — which you can't, not without the salt — and the two won't line up. Seal's broken. I know the file's been messed with.
a broken seal has to actually hurt
First version of this was way too polite. When the seal failed I reset the money and a couple of other bits and left everything else alone. Which, it turns out, is a loophole with a neon sign on it. You could edit some other signed field the reset didn't touch and it'd sail right through.
And it got dumber. One of the signed fields is the list of secret codes you've already redeemed. So: wipe that list, break the seal on purpose, eat the small money reset, then go re-redeem every code again for way more than you lost. The punishment was profitable. Cool. Great job me.
So now a broken seal means I stop trusting the file entirely. No salvaging the "good" bits. Whole economy back to a fresh start, and every secret code gets marked used so clearing that list buys you nothing:
if tampered:
money = STARTING_MONEY
owned = [STARTER_CAR]
owned_parts = stock_parts()
tickets = 0
premium_spins = 0
redeemed_codes = CODES.keys() # wiping the list gains you nothing now
...I tested it the way you should — opened the file, typed nine million into the money field, launched. Game caught it and dropped me back to the starting cash. The Notepad cheat is dead.
okay, here's the part I'm not going to lie about
I've read too many posts that add a check like this and then strut around like they've built Fort Knox, so let me not do that.
This stops the guy with Notepad. It does not stop someone who actually knows what they're doing, and it never will. The game runs on the player's computer. The salt is baked into the game. On the web build the code is sitting right there in the browser for anyone to read. Dig the salt out and you can generate a perfectly valid hash for whatever numbers you want and hand the game a "sealed" save full of lies, and it'll believe every word.
I can't patch that from the client, because it's not a bug, it's just how any of this works. You cannot trust a number that a machine you don't own is allowed to rewrite. The only real fix is to stop asking the client at all — put the balances on a server and have the server refuse to take the game's word for anything. I already run a little server for the multiplayer, and moving the economy behind it is the actual answer. It's also a chunk of work, so for now it's on the todo list, not in the build.
so why bother
Because almost nobody is that person. Most people who'd cheat are just curious, or a bit tempted, and a save that visibly bites back — snaps itself to zero the second you poke it — turns "free and easy" into "annoying, why'd I even try." That's the vast majority of cases handled for a tiny fraction of the effort a server rewrite would cost.
The only real mistake would be believing my own hype and calling it secure. It's a lock on a door. I just happen to know exactly how thin the door is, and I'd rather say that out loud than pretend otherwise.
Written by Oliver
Indie game developer building 3D web experiences in Godot and Next.js. I write about creative coding, procedural generation, and game feel.