Files
Tiago Valente 08c9abb573 CheatSpecter 0.4.1
A quality pass after 0.4.0 was tried on a real console.

- Fix the Learn page crashing in current browsers when changing lesson steps
- Clear feedback for every search outcome; real progress with time left and Cancel
- Dialogs scroll inside the screen with Save and Cancel always in view
- Fix a console error on every closed connection to the payload
- Copy buttons work when the page is served from the PS5
- The practice game sits beside the lesson's tools and lasts between steps
- Damaged cheat files, vanishing error messages, unreadable memory shown as zeros
- Phone layout: one-line header, larger switches, readable rows
- Browser tests under test/ui and a guard against effects that return values
2026-10-10 15:19:56 -07:00
..
2026-10-10 15:19:56 -07:00
2026-10-10 12:06:50 -07:00
2026-10-10 15:19:56 -07:00

Lessons for CheatSpecter's Learn section

This folder is what the Learn page in CheatSpecter shows. The app does not ship with lessons: when someone opens Learn and presses "Load lessons", their browser downloads index.json and then each lesson from this folder on GitHub. Change a file here and everyone gets the new version the next time they open the page.

Lessons are data, never code. A lesson cannot contain HTML, scripts, links or images. The app checks every file it downloads against the format below and refuses the whole file if anything is off, so a typo here means a lesson that will not open. Run the checker before you commit:

node scripts/check-lessons.mjs

It prints what is wrong and where (lessons/find-a-number.json: lesson.steps[2].goal.field: …), or 12 lessons, 74 steps: all fine. The test suite (test/learn.test.mjs) runs the same check, and then has a scripted learner complete every practice step, so a goal that cannot be reached fails the tests.

To try your lessons in the app before they are on GitHub, run npm run demo (or npm run dev): with the demo console running, the app loads this folder from your computer instead.

Files

learn/index.json              the courses, and which lessons are in them, in order
learn/lessons/<id>.json       one lesson; <id> is the lesson's id

index.json

{
  "format": "cheatspecter-lessons",
  "version": 1,
  "edition": "2026-10",
  "courses": [
    {
      "id": "values",
      "title": "Finding and changing values",
      "summary": "One or two sentences shown under the course title.",
      "lessons": [
        { "id": "find-a-number", "title": "Find a number you can see", "summary": "…", "minutes": 8, "practice": true }
      ]
    }
  ]
}

title, summary and minutes must be the same as in the lesson's own file (the list of lessons is drawn from the index alone). practice is true when the lesson has at least one practice step. edition is shown at the bottom of the list; change it when you publish. Ids are lower-case letters, digits and dashes.

A lesson

{
  "format": "cheatspecter-lesson",
  "version": 1,
  "id": "find-a-number",
  "title": "Find a number you can see",
  "summary": "Search for your ammo, change it in the game, search again, and edit what is left.",
  "minutes": 8,
  "games": { "range": { … }, "shop": { … } },
  "steps": [ … ]
}

games holds the practice games the lesson's steps use, by a name of your choosing. steps are shown one at a time, in order.

Text

Wherever a step has text (and in done and explain), this small markup works. Anything else is shown as the characters you typed.

You write You get
a blank line a new paragraph
# Heading on its own a heading
lines starting with - a bulleted list
lines starting with 1. , 2. a numbered list
> note: …, > tip: …, > warning: … a callout
**bold** bold, for the names of buttons and for new words
`code` code, for instructions, addresses and file names

Write for someone who has never done this. Name buttons exactly as the app shows them, and say what the learner will see after each action.

Steps

Every step has a kind and a title.

read: something to read. text.

choice: a multiple-choice question. question, options (2 to 6 strings), answer (the position of the right one, counting from 0), explain (shown once they get it right), and optionally text above the question.

answer: a question answered by typing. question, accept (the answers that count; capitals, spacing and a final period are ignored), explain, optionally text.

practice: the learner does something in a practice game with the real tools.

{
  "kind": "practice",
  "title": "Fire, then narrow down",
  "text": "What to do, step by step.",
  "game": "range",
  "tool": "find",
  "goal": { "check": "narrowed", "label": "Narrow the list down to one match", "field": "player.ammo", "atMost": 1 },
  "hints": ["A nudge.", "A stronger nudge.", "Nearly the answer."],
  "done": "Shown when the goal is reached: what just happened, and why it matters."
}
  • game: which of the lesson's games. Steps in the same game carry on in it (the learner's search and cheats are still there), also when a reading or a question sits between them, unless a step has "fresh": true, which starts a new game with no cheats. Mark a step fresh when it does not use anything from the steps before it.
  • tool: the tab shown when the step opens: find, memory, code or cheats.
  • hints: one to six, revealed one at a time, weakest first.
  • "challenge": true marks a step the learner does alone. Give the goal and no steps. Every practical lesson should end with one, in a game with different numbers from the guided part.

The learner also gets a Show me button. It reaches the goal with the same tools and writes down what it did; you do not script it.

Writing practice steps that hold up

A learner does not always arrive at a step the way you wrote it: they skip, come back the next day (the game starts over), restart the game when not asked, or type the wrong number. The app helps: when a step builds on earlier ones that were not done in the running game, it says so and offers the way back; and when a search has dropped the value a goal is about, it says that too. The text has to do its part:

  • Call things what the app calls them. Bold in a practice step is for what the learner presses or looks at, and the checker refuses a bold name that is neither in the app nor in the game. (Examples to type, such as a cheat's name, go in EXAMPLES in the checker.)
  • Do not say where on the screen something is. The practice game is at the bottom of a phone and beside the lesson in a wide window; a table has column headings on one and not on the other. Say "the practice game's panel", "the box on the row".
  • Do not assume a button that only exists in one state. "Press New search" is wrong when no search is going; write "if the Find tab shows a search, press New search".
  • Follow your own hints in the app, literally, in the game as it starts. A game is laid out the same way for every learner, so what you see is what they get: if the pointer search offers two paths, the hint has to say which one.
  • Order a goal's parts so that only the real thing passes. "Make a cheat that survives the update" is off, then updated, then off again; with updated first, a cheat made after the update would pass.
  • An older CheatSpecter reads these same files. Stay within the format, and keep text true for it as well: in 0.4.0 the game started over after every reading or question.

soon: text explaining practice that the app cannot offer yet.

Goals

A goal is what the app checks for. Each has a label: the sentence the learner sees with a circle or a check mark next to it. field means a value in the game, written object.field; action means one of the game's actions.

check Reached when Also needs
narrowed the learner's search results contain the value and are at most atMost long field, atMost
value the value is atLeast, atMost or exactly a number field, is, value
held the value is unchanged after each of times uses of an action in a row (frozen, or its code is switched off) field, action, times
steady a value the game changes by itself has not moved for seconds field, seconds
pressed an action has been used times times action, times
restarted the game has been restarted
updated the game's update has been installed (the game needs "update": true)
caught the debugger has reported the code that writes the value ("Find what writes to this") field
off the code behind the action's first effect is replaced with no-ops action
saved "Your cheats" has a cheat of type value (for a field) or patch (for an action). Add "named": true to require a name other than the automatic one, "grouped": true to require a group, "location": "pointer" to require a pointer path type, and field or action

To ask for several things, use { "check": "all", "of": [ … ] } with two to five goals. They count in order: a goal is only looked at once the ones before it are reached, and what it counts (restarts, uses of an action) is counted from that moment. So "restart, then switch the cheat on again" is restarted followed by off.

Practice games

A practice game is a description; the app builds a small pretend game from it, with real memory to search and real machine code behind every action.

{
  "name": "Specter Range",
  "objects": [
    { "id": "player", "fields": [
      { "id": "ammo", "type": "u32", "value": 30, "offset": 40 },
      { "id": "shots", "type": "u32", "value": 0, "offset": 64 }
    ] }
  ],
  "actions": [
    { "id": "fire", "label": "Fire", "effects": [
      { "field": "player.ammo", "op": "sub", "by": 1, "min": 0, "refill": 30 },
      { "field": "player.shots", "op": "add", "by": 1 }
    ] }
  ],
  "hud": [ { "label": "Ammo", "field": "player.ammo", "show": "number" } ],
  "decoys": [ { "type": "u32", "value": 30, "count": 12 } ],
  "copies": [ { "of": "player.ammo", "count": 3, "follow": "fade" } ],
  "drifting": 0,
  "update": false
}
  • objects (1 to 8) are the things the game keeps in memory. Each field has a type (u8, u16, u32, float, double, or pointer), a starting value, and optionally an offset from the start of the object (a multiple of the type's size; left out, fields follow each other from 16). A pointer has to (another object's id) instead of a value. Objects land somewhere new every time the game starts. One with "static": true lives in the game's own data, at the same place every time: give it a pointer to another object and there is a pointer path to find.
  • actions (up to 8) become buttons. Each effect adds to or subtracts from a u32, float or double field. A subtraction that would go below min sets the value to refill (or stops at min); an addition stops at max. An action with "every": 1000 runs by itself every that many milliseconds and has no button ("hidden": true hides one without a timer). Each effect is its own few bytes of code, which "Find what writes to this" finds and no-ops switch off. Effects that name the same "site" share one piece of code, as "take damage" for the player and for an enemy would; they must do the same thing to the same field of their objects.
  • hud (up to 6) is what the game shows: a number (with digits decimals), a bar (filled at max; the number is not shown, which is the point), or a time (seconds shown as m:ss).
  • decoys put the same number in unrelated places, so a first search finds more than one.
  • copies follow a value for a few changes and then stop, as a game's own copies of a number do; count 3 means narrowing down takes about four rounds.
  • drifting is how many values change on their own, which gives "stayed the same" a job in searches that start from "Unknown".
  • update true gives the game an "Install game update" button that moves its code.

If someone overwrites a game's code with anything other than whole instructions of no-ops, the game "crashes" the next time that code runs, and "Restart game" brings it back.

Changing the format

version is 1. The app refuses files with a higher version and tells the user to update, so new kinds of steps or goals mean a new version number and a new app release first. The types and the checker are in src/lib/learn.ts; the practice game is in src/lib/practice/.