Adding a Redemption Code
Redemption codes grant tools, materials, or cosmetics when a player types them into the Settings panel. data/codes.json stores only the SHA-256 hash of each code, never the plaintext, so the shipped .pck cannot be datamined for working codes. Adding one means hashing the code, adding its reward block, and knowing how one-time redemption is tracked.
Never commit a plaintext code — not in
codes.json, not in a commit message, not in a PR description. The file stores hashes only; the code-to-hash mapping lives in the private repo's pre-hashing git history and the publisher's records. This page deliberately uses no example plaintext.
How lookup works
Player input is normalized (trimmed, uppercased) and hashed before lookup. DataRegistry.get_code() does the hashing, so codes.json is keyed by the digest, not the code:
func get_code(code_id: String) -> Dictionary:
return codes.get(code_id.strip_edges().to_upper().sha256_text(), {})
CodeManager.redeem() normalizes the same way before calling it:
var code := raw_code.strip_edges().to_upper()
...
var def := DataRegistry.get_code(code)
Step 1 — Hash the uppercase code
Compute the SHA-256 hex digest of the uppercase code string. The file's _comment documents both methods; use either:
In Godot:
"YOURCODE".sha256_text()
On the command line:
python -c "import hashlib;print(hashlib.sha256('YOURCODE'.encode()).hexdigest())"
Both yield a 64-character lowercase hex string. Replace YOURCODE with the actual uppercase code. The result is the JSON key.
Step 2 — Add the hashed entry
One entry in data/codes.json, keyed by the digest from step 1. An existing entry's shape (the digest here is illustrative):
"1ac05da52cdab0440527ec1cd116f8e277b5feebc5c79f4fd1fa7b53e9ccd06a": {
"repeatable": false,
"rewards": {
"tools": { "quartzite_pickaxe": 1 },
"cosmetics": ["form_axel"]
}
}
Entry fields:
| Field | Meaning |
|---|---|
repeatable |
false (default) = one redemption per account; true = unlimited. |
rewards.tools |
{tool_id: count} — granted via ToolState.add_tool (once per redemption). |
rewards.materials |
{material_id: count} — granted via Inventory.add. |
rewards.cosmetics |
Array of cosmetic ids — granted via CosmeticManager.unlock. |
Do not add plaintext codes or any hinting label alongside the entry — the validator rejects a _-prefixed comment key only if it is not a hex digest, but the standing rule is no plaintext and no hints anywhere in this file.
Step 3 — One-time tracking
For a non-repeatable code, CodeManager records redemption in config.json (via SaveManager), so it survives slot deletion and cannot be farmed:
var summary := _grant_rewards(def.get("rewards", {}))
if not repeatable:
SaveManager.mark_code_redeemed(code)
A code the player already redeemed can still back-fill a newly added cosmetic. Add a cosmetic to an existing code later, and re-entering it grants only the un-owned cosmetics, never the farmable tools/materials again, because cosmetics are account-unique and unlock() is a no-op once owned:
if not repeatable and SaveManager.is_code_redeemed(code):
var backfilled := _grant_new_cosmetics(def.get("rewards", {}))
if backfilled.is_empty():
... # "Code already redeemed."
...
So you can safely extend an existing code with a new cosmetic reward; you cannot hand out its tools or materials a second time.
Validate
After editing data/codes.json, run the data validator. It confirms every key is a lowercase 64-char SHA-256 hex digest (guarding against an accidentally-pasted plaintext code), and that every reward tool, material, and cosmetic id exists:
<godot console exe> --headless --path . --script res://tools/validate_data.gd
The relevant check:
if hex_re.search(key) == null:
_fail("codes.json: key '%s' is not a lowercase SHA-256 hex digest — plaintext codes must never ship" % key)
Expect DATA OK. Then the smoke test for the config round-trip (redeemed-code tracking rides config.json):
printf '\n[autoload]\n\nZZSmokeTest="*res://tools/smoke_test.gd"\n' >> project.godot
<godot console exe> --headless --path . # expect SMOKE PASS
git checkout -- project.godot
Finally, verify the redemption end-to-end by typing the real code into the Settings panel in the editor — the validator can only confirm the hash is well-formed and the rewards resolve, not that the digest matches your intended code.
Key files
| File | Role |
|---|---|
data/codes.json |
Redeemable codes, keyed by SHA-256 of the uppercase code. |
scripts/core/CodeManager.gd |
redeem(), _grant_rewards, _grant_new_cosmetics back-fill. |
scripts/resources/DataRegistry.gd |
get_code() — hashes normalized input before lookup. |
scripts/core/SaveManager.gd |
is_code_redeemed / mark_code_redeemed (config.json). |
tools/validate_data.gd |
Enforces hex-digest keys; checks reward ids exist. |