# 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:

```gdscript
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:

```gdscript
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:

```text
"YOURCODE".sha256_text()
```

On the command line:

```text
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):

```json
"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:

```gdscript
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:

```gdscript
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:

```text
<godot console exe> --headless --path . --script res://tools/validate_data.gd
```

The relevant check:

```gdscript
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`):

```text
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. |

## Related

- [Codes and Redemption](/docs/underroot/codes-and-redemption)
- [Adding a Cosmetic](/docs/underroot/adding-a-cosmetic)
- [Save System and Migration](/docs/underroot/save-system-and-migration)
- [Verification and CI](/docs/underroot/verification-and-ci)