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

299 lines
11 KiB
JSON

{
"format": "cheatspecter-lesson",
"version": 1,
"id": "pointers",
"title": "Pointers: the way back to a value that moves",
"summary": "The value moves on every start, but the game always knows where it is. Follow the same trail.",
"minutes": 10,
"games": {
"vault": {
"name": "Specter Vault",
"objects": [
{
"id": "world",
"static": true,
"fields": [
{
"id": "level",
"type": "u32",
"value": 4,
"offset": 8
},
{
"id": "player",
"type": "pointer",
"to": "player",
"offset": 24
}
]
},
{
"id": "player",
"fields": [
{
"id": "gold",
"type": "u32",
"value": 120,
"offset": 64
},
{
"id": "keys",
"type": "u32",
"value": 2,
"offset": 68
}
]
}
],
"actions": [
{
"id": "chest",
"label": "Open a chest (+25 gold)",
"effects": [
{
"field": "player.gold",
"op": "add",
"by": 25
}
]
}
],
"hud": [
{
"label": "Gold",
"field": "player.gold",
"show": "number"
},
{
"label": "Keys",
"field": "player.keys",
"show": "number"
}
],
"decoys": [
{
"type": "u32",
"value": 120,
"count": 8
}
],
"copies": [
{
"of": "player.gold",
"count": 1,
"follow": "fade"
}
]
},
"tower": {
"name": "Specter Tower",
"objects": [
{
"id": "game",
"static": true,
"fields": [
{
"id": "session",
"type": "pointer",
"to": "session",
"offset": 16
}
]
},
{
"id": "session",
"fields": [
{
"id": "floor",
"type": "u32",
"value": 1,
"offset": 8
},
{
"id": "hero",
"type": "pointer",
"to": "hero",
"offset": 32
}
]
},
{
"id": "hero",
"fields": [
{
"id": "lives",
"type": "u32",
"value": 5,
"offset": 48
},
{
"id": "falls",
"type": "u32",
"value": 0,
"offset": 52
}
]
}
],
"actions": [
{
"id": "fall",
"label": "Fall off",
"effects": [
{
"field": "hero.lives",
"op": "sub",
"by": 1,
"min": 0,
"refill": 5
},
{
"field": "hero.falls",
"op": "add",
"by": 1
}
]
}
],
"hud": [
{
"label": "Lives",
"field": "hero.lives",
"show": "number"
}
],
"decoys": [
{
"type": "u32",
"value": 5,
"count": 30
},
{
"type": "u32",
"value": 4,
"count": 12
}
],
"copies": [
{
"of": "hero.lives",
"count": 1,
"follow": "fade"
}
]
}
},
"steps": [
{
"kind": "read",
"title": "The game has the same problem you do",
"text": "Your gold is somewhere new on every start. You have to search for it. But the game does not search: every time it draws the gold counter, it goes straight there. How?\n\nIt keeps a note. When the game creates the player, it writes down the player's address in a place it can always find. A value that holds the address of something else is called a **pointer**.\n\nUsually it is a short chain:\n\n1. At a **fixed spot** in the game's own data, always the same distance from the start of the game, there is a pointer to \"the world\".\n2. Inside the world, a fixed distance from its start, there is a pointer to \"the player\".\n3. Inside the player, a fixed distance from its start, there is the gold.\n\nThe addresses change on every start. The fixed spot and the distances do not, because they are part of how the game was written. Follow the chain and you arrive at the gold, wherever it is today.\n\nWritten down, a chain looks like `[[game+0x4040]+0x18]+0x40`. Read it from the inside out: take the pointer stored at game+0x4040, add 0x18, take the pointer stored there, add 0x40, and you are at the value."
},
{
"kind": "choice",
"title": "Check yourself",
"text": "",
"question": "In a pointer chain, which parts stay the same every time the game starts?",
"options": [
"The addresses of the player and the world.",
"The fixed starting spot in the game, and the distances added at each step.",
"Nothing. That is why you search again."
],
"answer": 1,
"explain": "The objects land somewhere new each time, so their addresses change. Where the game keeps its first note, and how far into each object the next thing sits, is baked into the game."
},
{
"kind": "practice",
"title": "Find a way back",
"text": "Finding a chain by hand means searching memory for anything that holds your value's address, then for anything that holds *that* address, and so on. The tool does it for you.\n\n1. Find your **Gold** until one match is left. Opening a chest changes it.\n2. On that row, open **…** and choose **Make it survive restarts…**\n3. Leave the depth as it is and press **Search**.\n4. The tool lists the chains it found, shortest first. Press **Use** on the first one.\n5. A form opens with the cheat. Give it a name, such as **Lots of gold**, set the value to **9999**, and press **Save**.",
"game": "vault",
"tool": "find",
"goal": {
"check": "saved",
"label": "Save your gold as a cheat that follows a pointer path",
"type": "value",
"field": "player.gold",
"location": "pointer"
},
"hints": [
"Find tab: search for the Gold number, open a chest, type the new number, Narrow down.",
"The … menu at the end of the row has \"Make it survive restarts…\". In the window that opens, press Search.",
"Press Use next to the first path in the list. In the form, the value box decides what number is held. Press Save at the bottom."
],
"done": "Your cheat no longer says \"this address\". It says \"start here in the game, follow this note, then that one\"."
},
{
"kind": "practice",
"title": "Restart and see it hold",
"text": "Press **Restart game**. The gold is back to what it was, at an address you have never seen.\n\nOpen **Your cheats** and switch your cheat on.",
"game": "vault",
"tool": "cheats",
"goal": {
"check": "all",
"of": [
{
"check": "restarted",
"label": "Restart the practice game"
},
{
"check": "value",
"label": "Have 9999 gold or more in the restarted game",
"field": "player.gold",
"is": "atLeast",
"value": 9999
}
]
},
"hints": [
"After the restart your cheat is still under Your cheats. Its switch is off; turn it on.",
"If the gold does not change, open … and Edit on the cheat and check that its value is 9999 and \"Hold it at this value\" is on."
],
"done": "No searching. The cheat followed the chain to wherever the gold lives in this run of the game."
},
{
"kind": "read",
"title": "Not every chain is real",
"text": "The tool finds chains by looking at memory as it is right now. Some of what it finds are coincidences: a leftover address in a spot the game has stopped using, that happens to lead to your value today and will not tomorrow.\n\nThere is one honest test: **restart the game and see whether the cheat still shows the right value.** Do it two or three times on a real game, ideally after loading a different level or save. A chain that keeps working is real.\n\n- **Shorter chains are more trustworthy.** One or two steps from the game's fixed data is typical. Five steps through unrelated objects is suspicious.\n- **An update can move the starting spot**, just as it moves code. A pointer cheat is tied to a game version in the way a pattern cheat is not.\n\n> tip: For \"infinite\" cheats, switching off the instruction (lesson 6) is usually the better tool. Pointers earn their place when you want to set a value to something: this much gold, that level, a position."
},
{
"kind": "practice",
"title": "On your own: a longer way round",
"text": "In this game the lives are two notes away from the fixed spot, and the search may offer you more than one path. Remember the last step: not every path is real. Make a cheat that holds **99 lives**, then restart the game and show that it still works.",
"game": "tower",
"tool": "find",
"goal": {
"check": "all",
"of": [
{
"check": "saved",
"label": "Save your lives as a cheat that follows a pointer path",
"type": "value",
"field": "hero.lives",
"location": "pointer"
},
{
"check": "restarted",
"label": "Restart the practice game"
},
{
"check": "value",
"label": "Have 99 lives or more after the restart",
"field": "hero.lives",
"is": "atLeast",
"value": 99
}
]
},
"hints": [
"Find the Lives number. Falling off changes it. There are many small numbers in memory, so it may take a few rounds.",
"… then \"Make it survive restarts…\" and Search. If more than one path comes back, do not take the shortest on trust: a path with 1 pointer here is a coincidence, something that happens to sit near the lives in this run. Press Use on the one with 2 pointers, set the value to 99 in the form and save.",
"Restart the game, then switch the cheat on in Your cheats.",
"If the lives do not change after the restart, the path you used was the coincidence. Find the lives again, make a new cheat from the other path, and switch that one on."
],
"done": "Two pointers deep, and it held. Between switching off instructions and following pointers, you can now make a cheat for almost anything that survives a restart.",
"fresh": true,
"challenge": true
}
]
}