Brightness, reading palette, text size, comments and reading sound.Adds page background, hover cards, button corners, code colours, width and layout.Everything, including diagram styling, motion, inline code, depth and fine details.
Reading sound Off
Sound starts off. Play when you want it.
Original sounds, released into the public domain under CC0.
Margin comments
Hide author and reader comments without deleting them.
Page brightness
Lesson text size
Reading palette
More options
Reader finish
Modern adds semantic colours and refined controls. Soft depth adds frost and shadows.
Page background
Hover cards
Definitions, examples, tips, and lesson summaries that open over words and links. Off also removes the underlines.
Chat button
Which corner the button for asking questions about the book sits in. Top corners stop short of the lesson list and contents; at the bottom right it sits under the Notes button. It takes the colour of the chapter you are reading.
Notes button
Where the 📝 Notes button sits on lessons. Hidden leaves a thin tab on the edge; Alt + N brings it back.
Settings panel
Popover opens under the button. Side sheet slides in from the right with a dimmed page behind it.
Sidebar and header contrast
Original keeps the dark lesson list and header. Unified tints them to match the page and palette.
Reading width
How wide the lesson text column is. Standard is about 70 characters a line.
Table of contents
Light or dark for the “On this page” panel on the right, whatever the page is.
Lesson spacing
Code brightness
Syntax colours
Moving effects
System respects reduced motion while still advancing steps. On Play enables moving effects when you start a diagram. Off keeps manual steps.
Fine-tuning
Depth
Flat is the original. Soft adds faint shadows to diagram boxes, cards and buttons and lifts them slightly on hover.
Inline code
Classic keeps the badge look. Soft quiets the background and border for paragraphs full of keywords.
Floating buttons
Docked shows the Notes and chat buttons normally. Minimal makes them small icons that fade until you point at them.
Gradients
Use flat colours for headings, cards and diagram backgrounds.
Diagram background
Changes diagram surfaces; labels and role colours remain readable.
Diagram boxes
Heading rectangles
Diagram label backgrounds
Diagram frames
Diagram size
Page grid
Time per step
Semantic colours
Ligatures
What the colours mean
Terms, inputs, and data
Code and processing
Results and tips
Cautions
These colours follow your theme; their meanings stay the same. Data tables and diagram explanations use the information colour; code panels use processing. The lesson header and navigation identify its chapter. Correct answers are green; errors are red.
Alt + T palette · Alt + D page · Alt + C code · Alt + S syntax · Alt + H semantic · Alt + L ligatures · Alt + N lesson list · Alt + O on this page
1. Computer, Game, and Code Foundations — print chapter
You do not need to prove that you already know something before you study it.
The next lesson starts with the parts of a computer and builds each idea from
there. You do not need to look up its missing foundations before you begin.
The lessons in this opening chapter connect four views of one game
action: the computer runs instructions, the game checks whether a purchase is affordable, Rust expresses that check
and subtraction, and memory holds the changing state. A Windows lab and
a gold scan then make those ideas visible. The chapter ends with that first
scan; the next chapter follows the value into the code that changes it.
Read the sequence as a tutorial; later, the concept lessons are also places
to return when an unfamiliar term appears.
Here is one study loop that works well for unfamiliar technical ideas. It is a
suggestion, not a requirement: the later lessons explain their subjects
directly and do not depend on it, so use it when an idea will not stick and
skip it when it does.
Look up the exact part you need.
Close or hide the answer.
Explain the idea in your own words.
Check and correct what you said.
That loop gives you fast feedback. It also prevents a common mistake: reading
the same paragraph several times and confusing recognition with understanding.
Give a study session a specific goal. “Understand game hacking” does not say
what you should be able to do afterward. “Explain what I observed after one
jump, and distinguish that observation from my guess about its cause” does.
You can finish that goal with a short explanation and a repeatable observation;
you do not need to understand the entire game first.
Choose a goal that matters to the next task. If you want to follow a gold
purchase, learning how to distinguish the stored amount from its display is
useful. Collecting unrelated facts about every game engine can wait. As the
book introduces code and tools, a goal can become more concrete: run one
example, explain its inputs and result, or reproduce one error.
Broad questions make it hard to tell whether an answer is complete.
❌ Too broad: “How do games work?”
✅ Better: “When I press the jump button, what changes on the screen?”
✅ Better: “If I press jump while the character is standing still, does the
character rise before landing again?”
A useful technical question usually names:
the object you are studying;
the relationship you want to understand;
the evidence or example you can check.
The jump question names the action you control, the change you want to observe,
and a way to check it: try the same button press while standing still and watch
the character. If nothing happens, you have a more useful next question: was
the button received, or does this situation prevent jumping? You can investigate
one possibility at a time.
objectthe jump button
relationshipwhat the character does after one press while standing still
evidencerepeat the press from the same starting position and watch the rise and landing
A checkable question names the action, the change to look for, and a repeatable observation.
Write down what you know, what you do not know, and what you are
assuming. For the jump question, you know which button you pressed and
whether the character visibly rose. You do not yet know whether the game
received the input or which grounded-state or cooldown check allowed the jump. “The character was standing
on the ground” may be an assumption until you check the starting position.
These distinctions keep an attractive explanation from silently becoming a
fact.
A source can be accurate and still answer a different question. Instructions
for another game version or another processor architecture may explain the
general mechanism while giving the wrong steps for your experiment. Check the
target, version, conditions, and actual claim before using the source.
Prefer evidence you can inspect: the relevant code, the tool’s documentation,
or a controlled observation you can repeat. Each has limits. Documentation
describes an intended contract; an experiment shows what happened under the
conditions you recorded. One successful jump does not establish that jumping
works from every position. Several sites repeating the same original claim
also do not provide several independent checks.
Two runs from the same save: what explains the difference?Each coin is worth 1 gold. Add a second change to run B and see whether the difference still points to one cause.
Run A, run B, and the difference
Two runs from the same save
Run A: no action
start 100
nothing happens
ends 100
Run B
start 100
+1 coin
ends 101
What the difference tells you
Run B − run A = 101 − 100
+1
one change, so one cause
Only the coins differ, so the +1 belongs to them: 1 coin × 1 gold. Repeat run B to check that it happens again.
Consider another explanation before settling on the first one. If a jump
fails, perhaps the input was missed, or perhaps the character was already
airborne. Reset to a known position and repeat the press, then vary one
condition. A useful test produces different predictions for the competing
explanations.
Occam’s razor is a useful preference for an explanation that needs fewer
unsupported assumptions while still accounting for the evidence. It helps you
choose what to investigate first. Simplicity does not prove an explanation
true; a more complicated cause may fit a later observation better.
Separate an observation from its explanation. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
record the visible motion
record whether the input arrived
keep unverified causes as possibilities
compare what each observation establishes
choose the next discriminating check
report the observed result and its limits
Result: a rise was observed; the cause still needs evidence.
Final values: motion = rose; input = unknown; input_arrival = unknown; possible_explanations = some path produced upward motion; observed = rise; next_check = input arrival.
Read every step of the lesson’s example
Step 1, line 1. Start with the selected inputs. Follow the highlighted operation and the changing state below.
motion = rose; input = unknown.
Step 2, line 2. The key press alone does not show whether the game received it.
First follow an example whose starting point, action, and result are explained.
Suppose a game shows 100 gold. Buying an item for 25 gold leaves 75: subtracting
25 from 100 gives 75. After seeing that worked through, you can predict what a
second 25-gold purchase would do, then check in the game. Later lessons use
this same visible change to ask where the value is kept and how code updates it.
When you reach a code example, read its explanation before predicting a result.
If you use an AI completion or copy a line, explain every accepted line. “It
compiles” is useful feedback, but it is not the same as understanding.
An error is information about this attempt. It does not establish that you
cannot learn the subject. Keep the exact message or unexpected result, return
to the last step you can explain, and reduce the task to one small example.
For the jump problem, check one press from one known position before studying
the whole movement system.
Change one thing and record what happened. If you keep making the same guess,
take a short break, explain the problem aloud, or ask another reader to walk
through the example with you.
“It does not work” gives another person almost nothing to inspect. A useful help
request includes:
what you expected;
what actually happened;
the smallest relevant code;
the exact error message;
the target version and architecture;
what you already tested.
That information turns a complaint into a question someone can reproduce.
Here is that list as a request you can fill in and send to a person or an
assistant:
PromptAsk for help someone can reproduce
I expected [what you expected]. Instead [what actually happened]. The smallest
code that shows it is [the code]. The exact error message is “[the message]”. I am
using [the tool or game, its version, and 32-bit or 64-bit]. I already tried
[what you tested]. What should I check next? Please say which part of my report
your answer depends on.
Ask for the missing observation or explanation directly: “I expected the
character to rise from this position; the input was recorded but the position
did not change. What should I check next?” When helping someone else, recognize
what they already tested and ask a useful question about the remaining gap.
Avoid treating a mistake as a judgment about the person.
AI can create practice questions, compare your explanation with supplied source
material, simplify a difficult paragraph, or suggest a small test. Give it the
actual source, version, and code when those details matter.
Always check important claims against the program, documentation, or source
code. If the answer cannot show where a fact came from, treat it as a lead to
verify—not as evidence.
The study loop at the start of this lesson is optional, and nothing later in
the book depends on it. If you want more detail on its steps, this reference
expands them: what to do before answering, how to mark a correction, and a
routine for one study session.
Optional study reference: answering, correcting, and a short session
Read the source before answering an unfamiliar question
If you encounter an unfamiliar idea, read the explanation and its example
before trying to answer a question about it. For a detail that depends on a
particular tool or version, consult that tool’s documentation too.
Then hide the source and answer without looking at it. This matters because
copying a sentence tests your eyes, while rebuilding its meaning tests your
understanding.
Suppose a lesson says:
A game’s state is the information that describes what is happening now.
A good answer in your own words could be:
State is what the game currently remembers, such as a character’s health or
position.
The wording changed, but the meaning stayed accurate. Do not add a claim the
lesson did not support, such as “state is only what I can see on the screen.”
After answering, compare your answer with the source or ask for a correction
that uses the source. Mark each important part as:
correct — the meaning matches;
missing — an important part was left out;
incorrect — the answer says something the source does not support;
unclear — the idea may be right, but the wording hides it.
Rewrite only the parts that need work. A correction should tell you why the
old answer failed, not merely show a replacement sentence.
If you miss the same question in the same study session, look it up again and
retry with different wording. This tests whether you can express the idea in
more than one way. If you retry on another day, answer as accurately as you can
after reviewing it again. Save repeated weak spots for spaced review.
Practice should include checking and correcting the result; repeatedly
rehearsing an incorrect explanation preserves the mistake. Over time, keep a
few worked examples and corrected errors that you can return to when the same
difficulty appears.
The four marks work as a request to a helper too:
PromptCheck my explanation against the lesson
Here is a paragraph from a lesson: “[paste the paragraph]”. Here is my explanation
in my own words: “[paste your answer]”. Using only the paragraph, mark each
important part of my answer as correct, missing, incorrect, or unclear, and say
why each part that is not correct fell short. Do not add facts the paragraph does
not contain.
Accuracy and flexible wording are different skills
choose a useful goal and separate known facts from assumptions;
look up an unfamiliar idea before answering;
check that evidence fits the target and consider another explanation;
explain it without copying the source;
compare, correct, and retry your answer;
give a helper enough detail to reproduce your problem.
Lesson 1.2
How a Computer Runs a Game
Suppose a game shows 100 gold. You collect one coin, and the display changes
to 101. What had to happen inside the computer for that small change to
appear? We can answer without knowing a programming language yet.
The game’s files stay on a storage drive when the game is closed. When
you start it, the computer puts the code and the information needed soon into
RAM (random-access memory). RAM is fast working storage; its contents are
temporary. The CPU (central processing unit) runs the code and changes
values such as your gold. A keyboard, mouse, or controller supplies input.
The GPU (graphics processing unit) helps turn the game’s drawing work
into the picture you see.
These parts have different jobs in our coin example. The stored game file
contains the instructions for collecting a coin. While the game runs, RAM
holds the current gold value. The CPU updates that value from 100 to 101, and
the game draws the new number on the screen. The parts communicate through
the computer’s hardware; none of them is the whole game by itself.
Changing gold and drawing it are separate workThe CPU changes the stored value first. The screen shows 101 only after a separate drawing step reads it.
1Before: RAM and the screen both say 100
idleCPU register
100RAM: gold
Gold: 100screen
The stored gold and the picture agree.
2Update: the CPU adds 1 and stores 101
100 + 1 = 101CPU register
101RAM: gold
Gold: 100screen: old picture
RAM now holds 101, but nothing has drawn a new picture yet, so the screen still shows 100.
3Draw: the next picture reads 101
reads 101drawing step
101RAM: gold
Gold: 101screen
The drawing step reads gold from RAM and builds a new picture. Now the screen agrees with RAM.
All 3 steps shown
The CPU changes the stored value first. The screen shows 101 only after a separate drawing step reads it.
The CPU is sometimes called the computer’s brain. That is a useful first
picture of its central job, but the CPU does not understand a game as a person
does. It executes instructions: small, specified operations such as
copying a number, adding two numbers, comparing them, or going to another
instruction.
The CPU has tiny working places called registers. Imagine one register
currently holds 100. An add-one instruction changes that register’s value to
101. It did one small job; it did not decide what a coin means or draw the
number. Later, the program can store the result in RAM for continued use.
This is a description of the change, not executable CPU code:
working value starts at 100
add 1 to the working value
working value is now 101
The exact CPU instructions and register names depend on the machine. For
now, follow the value: 100 went in, one was added, and 101 came out.
A register change reaches memory and the display. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
RAM.gold = starting_gold
working = RAM.gold
working = working + 1
RAM.gold = working
display = RAM.gold
draw the copied display value
Result: memory and the new display both contain 101; they changed at different steps.
Final values: gold = 100; stored_gold = 101; working = 101; display = 101.
Read every step of the lesson’s example
Step 1, line 1. Start with the selected inputs. Follow the highlighted operation and the changing state below.
gold = 100.
Step 2, line 1. RAM and the last drawn display start at 100.
gold = 100; stored_gold = 100; working = not loaded; display = 100.
Memory: RAM.gold = 100; working = not loaded; display = 100.
Step 3, line 2. Copy the stored amount into a working register.
A register holds a value named by an instruction. A cache holds memory
lines the processor may reuse; it is not another set of variables the game
names. Common CPUs have L1, L2, and L3 cache levels. A cache hit can serve a
memory request nearby; a miss may require a farther cache level or RAM.
Intel’s cache explanation
describes why those extra fetches can make the CPU wait.
RAM holds the running program’s resident code and data. An SSD or HDD
keeps persistent files; the operating system performs file I/O to bring their
contents into working memory. Their capacities and access times depend on the
machine, so this hierarchy gives roles rather than fixed speed numbers.
Working values and saved filesWorking storage above · persistent storage below
CPU registersValues named directly by instructionsSmallest working set
CPU caches · L1 / L2 / L3Copies of memory lines near the CPUA hit avoids a slower fetch
Main memory · RAMResident program code and dataA larger working set
↕Explicit file I/O or OS pagingStorage access is a separate operation
SSDPersistent files in flash storageHDDPersistent files on magnetic disks
A cache miss can require a RAM fetch; it does not automatically mean a disk read. Capacity and timing depend on the machine.
A program is a collection of instructions arranged to do a larger job.
Our example becomes a tiny program if we let a number come in and send the
new number out:
input: 100
copy the input into a working value
add 1 to the working value
output: 101
The input is the value supplied to this calculation. The output is
its result. A running program can also remember values, cause effects, and
keep working rather than stop after one output. In a game, the gold count is
part of the game’s changing state: information describing what is
happening now. The input might be the event “collect a coin,” and the new
state has 101 gold.
A larger program may need to add one in many places. We can give that small
behavior a name: add_one. A named piece of program behavior is a
function. Instead of describing its steps every time, another part of
the program can ask add_one to work on a number.
function add_one(number):
result = number + 1
give back result
add_one(100) gives back 101
This is explanatory pseudocode, not code you can run as written. The name
number stands for the input supplied on each use; result is the value
after adding one. A function can also do work without taking an input or
returning a useful value. Naming behavior lets the program reuse and
organize it.
What if a game can award a coin or charge one? We can name the second
operation subtract_one. Both operations change the same starting number by
one. Now the program needs another input: which action occurred?
starting number: 100
action "collect": add_one(100) gives back 101
action "spend": subtract_one(100) gives back 99
For “collect,” the program follows the add path and ends at 101. For
“spend,” it follows the subtract path and ends at 99. It must compare
the action with the choices and then follow one path. This choice is a
branch. A branch lets the same program respond differently to different
input. It does not run both outcomes for one action.
In a real game, spending may also need a rule such as “do not spend when
gold is zero.” That is another comparison and branch. Lesson 1.4 will turn
this small calculation into a program written in Rust.
We have treated 100 and 101 as ordinary numbers. To find them in a running
game, we also need to know how computers represent numbers.
In familiar decimal notation, each place is worth ten times the place
to its right. The number 101 means one hundred, zero tens, and one unit:
1 × 100 + 0 × 10 + 1. Binary uses only 0 and 1. Each place is worth
twice the place to its right. One binary digit, either 0 or 1, is a bit.
Eight bits make one byte. The eight places in a byte are worth 128, 64,
32, 16, 8, 4, 2, and 1. For example:
place value: 128 64 32 16 8 4 2 1
bits: 0 1 1 0 0 1 0 0
total: 64 + 32 + 4 = 100
Writing eight bits for every byte gets tiring. Hexadecimal, or hex,
uses sixteen digits: 0–9 followed by A–F for values ten through fifteen.
One hex digit represents four bits, so two hex digits represent one byte.
The byte above splits into 0110 (six) and 0100 (four), written
0x64. The prefix 0x tells you it is hex. Its value is still decimal
100: 6 × 16 + 4. Decimal, binary, and hex are different ways to write the
same number, not three different kinds of gold.
high four bits0110
low four bits0100
hex 6
hex 4
0x64 = 6 × 16 + 4 = 100
The same one-byte value, split into two groups of four bits and written in hexadecimal.
One byte can represent 0 through 255. Larger values use several bytes. The
memory lesson just before the first scan will show where those bytes sit and
how a program reads them together.
The CPU executes instructions represented by patterns of bits. Writing a
whole game as bit patterns would be difficult. Assembly language gives
human-readable names to operations such as moving and adding values (Example: In x86 assembly, add eax, 1 adds 1 to the register eax. It is the add-one step from earlier, written as one CPU instruction.). A
higher-level language lets us express larger ideas, such as functions
and branches, without spelling out every CPU operation.
Our add-one function and collect-or-spend branch can be written in a
language like Rust. A compiler translates source code into machine code
for a chosen target. The translation is not necessarily one source line to
one CPU instruction. Here is our same small function in Rust:
fnadd_one(number: u32) -> u32 {
returnnumber + 1;
}
fn names a function. number is its input; u32 means a non-negative
whole number stored in 32 bits. The arrow says the function gives back a
u32, and return sends back the sum. Calling add_one(100) therefore
gives back 101, just as the earlier pseudocode did. This is a function
fragment, not a whole program; Lesson 1.4 builds and runs the complete Rust
example. Lesson 2.2 follows the later steps that turn source into a program
file.
The operating system helps programs use the computer
Even our tiny add-one program needs more than arithmetic if a person must
type a number and see an answer. It needs a way to receive input and display
output. A game also needs files, sound, windows, and access to devices.
An operating system (OS) provides common services for those jobs and
coordinates programs that use the computer. This book uses Windows for its
practical examples. A program written for Windows can ask Windows to read
a file or show a window instead of controlling each device from scratch.
An application is a program made for a user to do something. A game is
an application: it uses the OS’s services, runs its own instructions, and
keeps its own changing state.
The Rust source a developer writes has already been translated into machine
instructions and linked with other code before the native game file exists.
Other games may load scripts that an interpreter runs while the game is open.
Either way, the CPU eventually carries out instructions. Lesson 2.2
follows those routes in detail; here we start with the native game file.
The game file on storage contains code and other saved information. Starting
it creates a process: one running instance of that program (Tip: To see processes on Windows, open Task Manager and choose the Details tab. Each row is one process, with the process ID (PID) that tools show when you pick a process to attach to.), with its own
memory and resources. Windows loads the code and needed data into memory and
starts a thread, a path that executes instructions in the process. A
process may have more than one thread, but one is enough to picture this
first example.
Many games repeat a broad game loop while you play:
receive input, such as the event “collect a coin”;
update state, changing gold from 100 to 101;
draw the current state, so the display shows 101;
repeat for the next action.
Actual games split this work among many systems and sometimes several
threads. This loop (Reference: Lesson 4.1 follows one pass of this loop inside a game engine, including the time step the engine uses.) is a first model for asking a useful question: did the
input arrive, did the stored value change, and did the display show the
changed value?
The CPU carries out individual instructions. A program arranges many of
them into useful behavior: receive a value, apply the add-one operation,
and produce a changed value. A function names that reusable behavior;
a branch chooses whether to collect or spend. Storage keeps the game
file, RAM holds the running program’s state, and the operating system
loads the file as a process the CPU can execute.
The gold value changed while the game was running. The next question is
what the game’s rules do with it and how several players can each have
their own value. Game Fundamentals starts there. We will
return to bytes and addresses just before the first memory scan in
Lesson 1.8.
Lesson 1.2 followed a game action through the CPU and a simple loop. To
understand why a displayed number changes, we need a model of the game
around it: what the player does, which check or calculation runs, what the game remembers,
and what it shows. The byte-level memory model comes just before our first
scan in Lesson 1.9.
Our small game has players, coins, and a shop. Call one player Ada. We will
follow her purchases and movement, then ask what the game must keep while
she plays and what it must save when she leaves. The examples describe
chosen checks and updates for this game; another game can model the same activity
differently.
A player supplies input, such as clicking the Buy button or pressing a
movement key. The game interprets that input as a requested action. It checks
its current state—the information it remembers about the world—and
calculates any accepted change. It then draws or plays an output that the
player can perceive. A common game loop repeats those steps as play
continues:
The example takes the successful purchase path. If the player has only 10
gold and the item costs 25, the affordability check fails and gold stays 10. The screen
reports what the game decided; it does not decide the purchase itself.
Later in this lesson we will see why the screen may even hold a separate
copy of that result.
The purchase checkChange the gold or the price. The comparison runs first; the subtraction happens only when it says the player can pay.
Compare first, then subtract only if allowed
The check
Stored values
gold 100
cost 25
↓ compare
Can the player pay?
100 ≥ 25: true
Accepted
Subtract
100 − 25 = 75
Gold afterward
75stored back in gold
100 ≥ 25 is true, so the branch runs: 100 − 25 = 75 is stored as the new gold.
Games can also update enemies, timers, motion, or music without a fresh
button press. On each update, the game can subtract time from a countdown, move a projectile
by its speed and elapsed time, or choose an enemy action. It then draws the
changed world or plays a sound. An engine is
reusable software for common game jobs (Example: Unity, Unreal Engine, and Godot are engines, and so is Bevy, an open-source engine written in Rust. Each supplies the loop, the drawing, and the loading; the game supplies its prices, movement calculations, and decisions.). It commonly supplies the repeating
loop, drawing, and loading resources such as images and sounds; the game
supplies particular behavior, such as looking up an item price and rejecting
a purchase when gold is below that price. Finding the right place to
change a game starts by asking which side owns the behavior. Lesson 4.1 develops that boundary.
A game remembers more than one number. It might track one player’s gold,
current health, and maximum health at the same time. How can it keep those
values together and apply the same calculation to many players?
Suppose Ada starts with 100 gold, 80 health, and a maximum of 100
health. These values are part of the changing game state.
player:
gold = 100
health = 80
max_health = 100
The labels tell us what each value means. If the player buys an item for 25
gold, gold becomes 75, while health stays 80. If the player then takes 20
damage, health becomes 60, while gold stays 75. The changes belong to one
player, but the purchase uses gold and the damage calculation uses health.
A group of related named values is a record. Each value inside it is
a field. The record lets the program talk about a player’s gold without
confusing it with that player’s health. It also gives later memory work a
question to ask: which nearby stored values belong to the same player?
gold75
health60
max_health100
one player's related fields
After the purchase and the hit, three different values still belong to one player record.
Gold and health describe Ada, but the game also needs to distinguish Ada
from another player, a coin, or the shop door. A thing the game treats as
having its own identity is an entity. Depending on the game, entities
can include characters, pickups, projectiles, doors, or even an invisible
object that controls a round.
The game can assign Ada an identifier, such as player ID 17. The ID
answers “which player?” Her gold and health answer “what is true about that
player now?” Buying an item changes her gold; it does not turn her into a
different player. A display name is another field and need not be unique.
Two players called Ada can still have different IDs.
An entity can also need several kinds of information. Ada has health,
money, a position, and a picture the game can draw. Games often group
related information into components, each describing one responsibility:
Part of Ada’s model
Information it needs
Work that uses it
Health
Current health and maximum health
Apply damage or healing
Wallet
Gold
Check a purchase
Position
Location in the world
Move Ada and test nearby objects
Appearance
Which image or model to use
Draw Ada
A component here means a part of the game’s model, not necessarily one
particular programming technique (Reference: Lesson 4.1 shows one such technique, called ECS: entities are identities, components are data, and systems are the code that runs over them.). One game might put all these fields in
a single player record; another might keep separate component records
associated with player ID 17. Both can implement the same gameplay. We
need this distinction because the information visible on one character
need not occupy one uninterrupted block of memory.
An entity also has a lifetime: the interval from its creation to its
removal. A coin entity stops being available after Ada picks it up. A later
coin can reuse its storage without being the same coin. Some games reuse
IDs too, so an ID is meaningful only within the identifier assignment and reuse policy of that
game. Later memory lessons develop how to recognize a currently valid
entity; the first question is already useful: does this information still
belong to the thing we intended to observe?
The game does not need a different image file for every coin. It can load
one coin image and use it to draw many coin entities. An asset is a
resource the game uses, such as an image, a three-dimensional model, a
sound, a map, or a set of starting values. An instance is one particular use of a
definition or resource in the running world.
Suppose two coins use the same image. One coin is beside Ada and one is
near the shop. Each has its own identity, position, and availability. If Ada
collects the nearby coin, that instance disappears and her gold rises
by one. The coin image remains loaded, and the coin near the shop remains
available. Changing a shared image can change how both coins look;
removing one coin instance affects that one coin.
The same distinction applies to player setup. A character definition might
specify a starting maximum health of 100. Ada’s current health of 60 is
state belonging to her running instance. A definition’s default and an
instance’s current value can be equal when the instance starts, then
diverge during play.
A class defines fields and methods; an instance holds values
In a language such as C++, a class describes a kind of object: which
fields it has and which operations can use those fields. A function called
through an object is a method. A player instance is one particular
object created from that definition. Ada and Bo can use the same damage
method while holding different health values.
This explanatory pseudocode names the receiver self: the specific instance
whose method was called. It is a sketch, not runnable Rust or C++:
class Player:
gold
health
damage(amount):
self.health = max(0, self.health - amount)
Ada = Player(gold=75, health=60)
Bo = Player(gold=40, health=50)
Ada.damage(20) # Ada's health becomes 40; Bo's stays 50.
max(0, value) selects whichever is larger, so damage cannot make health
negative in this model. Ada.damage(20) chooses Ada as self; it does not
subtract 20 from every Player. A class definition does not mean that all
instances share one set of field values. Games can also organize data using
components rather than one class per entity, as we saw above.
Choose a player or a damage amount below. Read the result first if useful,
then step through how the receiver selects the changed field.
One Player definition, two independent players. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
class Player: gold, health
damage(amount):
self.health = max(0, self.health - amount)
Ada = Player(gold=75, health=60)
Bo = Player(gold=40, health=50)
target.damage(amount)
Ada now has 40 health. Bo keeps 50 health, and both gold fields stay unchanged.
For the table below, add a second player called Bo.
Bo has 40 gold and 50 health. Copying the first player’s field names
into gold2, health2, and so on would work for two
players, but every new player would demand more special-case code. Instead,
the game can keep several player records in a collection:
Player
Gold
Health
Maximum health
Ada
75
60
100
Bo
40
50
80
The table is a picture of two records, not a claim that RAM stores a table
with headings. A compiled game stores values as bytes; the labels here tell
us what each value means. Lesson 1.8 will show how a type tells code how to
read those bytes.
Suppose both players collect one coin. We could write “Ada’s gold becomes
76; Bo’s gold becomes 41.” For many players, repeating one calculation is
simpler:
for each player in the collection:
add 1 to that player's gold
That repeated pass is a loop. One pass handles Ada (75 becomes 76);
the next handles Bo (40 becomes 41). The add-one calculation stays the same when the
collection grows. This is the reason to introduce a collection and a loop,
rather than a list of data-structure names to memorize.
Class cheatsheet: a class defines fields and operations; an instance holds one object’s current values; the receiver selects which instance a method changes.
Rust gives a record type a name with struct. We can describe the three
numeric fields from our example. This fragment declares a shape for
player values; it does not yet run a purchase or create a player:
structPlayer {
gold: u32, // Amount an accepted purchase can spend.
health: u32, // Current health after damage or healing.
max_health: u32, // Upper limit for a health update.
}
Player is the record type. Each line inside the braces names a field,
then gives its type after a colon. u32 means a non-negative whole number
held in 32 bits, or four of the eight-bit bytes from Lesson 1.2. The comma
separates fields. This
declaration describes the shape of a player; it does not create Ada or Bo.
The text after // is a comment for the reader. It explains a field’s
purpose and does not add another field or change the program’s behavior.
Lesson 1.4 will create values, change fields, and write a loop in Rust.
A game’s actual layout may differ from this source-level sketch. A memory
tool has to infer fields from the bytes and the instructions that use them.
Lessons 3.8–3.10 develop that recovery in detail, including
alternative layouts and objects that move or disappear.
Having fields together also lets the game check relationships between them.
Ada’s health of 60 fits her maximum of 100. A health value of 120 with a
maximum of 100 would exceed the permitted range
0 <= health <= max_health
Here, <= means “less than or equal to”: health must be at least zero and
no greater than the maximum. Our damage and healing calculations should keep it
inside that range.
We do not need a new data structure to express this condition. It is a
relationship the game should preserve when it applies damage or healing.
Lesson 1.4 keeps both fields in its working player record. Later chapters
call a condition that must remain true an invariant and show how to test one
when recovering a game’s data.
An address alone does not prove that a player record is still the same
player. The game can remove a player and use that memory for someone else.
For this first pass, remember that an address tells you where to look now;
it does not guarantee an object’s identity forever. Lesson 3.10 develops
ways to recognize identity and lifetime.
We have said “Ada buys an item,” but a computer first receives something
more mechanical: a mouse click, a key press, or a controller button. The
game maps that input to an action, such as “buy this item” or “move
right.” Keeping the action separate from its physical input allows the
same purchase handler to work with several devices and different key bindings.
An action is a request. The purchase handler still compares the price with Ada’s
gold before accepting it. Context matters too: pressing the right arrow
might move Ada during play but select the next item while a shop menu is
open. The same physical input can request different actions in different
parts of the game.
A game also distinguishes a button being pressed now from being
held down. In our game, one press of the buy button requests one
purchase. Holding the movement button requests continued movement. If the
purchase handler ran on every update while the button was held, one press
could accidentally buy many items. This makes the input’s timing part of
the behavior we need to understand.
To move Ada, the game needs a way to describe where she is. In a small
two-dimensional world, use two numbers: x for horizontal position and
y for vertical position. Together, (x, y) are coordinates. Our
example chooses positive x toward the right, positive y upward, and
one unit as one tile’s width. Another game can choose these conventions
differently.
Ada starts at (2, 3). Moving one unit right gives (3, 3): x changes
from 2 to 3 and y stays 3. These are world coordinates, describing
Ada’s location in the game. Her screen coordinates describe where her
picture appears on the display. If the camera follows Ada, she can move
through the world while staying near the middle of the screen. Moving the
camera changes the view without moving Ada in the world.
Movement also takes time. Suppose Ada’s speed is 4 world units per second
and she moves right for one quarter of a second. The distance is
4 × 0.25 = 1 unit. Starting at (2, 3), she ends at (3, 3) if nothing
blocks her. This pseudocode shows the calculation:
The # lines describe the inputs and result, and * means multiply. The
elapsed time tells the game how much movement to apply.
Lesson 4.4 adds a
third axis and turns the difference between two positions into a distance
and a direction; Lesson 7.1 follows a
world position onto the screen.
The work that advances movement, health, timers, and other changing state is the
simulation. The work that makes an image of the current world is
rendering. One displayed picture is a frame. A simulation update
is often called a tick. These names let us distinguish advancing the
world from drawing it.
Why does the distinction matter? Suppose a game moves Ada one unit each
time it draws a frame. A computer drawing 120 frames in one second would
move her twice as far as one drawing 60. Our speed calculation instead
asks how much game time passed. Sixty updates of 1/60 second each produce
the same four units of unobstructed movement as 120 updates of 1/120 second
each: both add up to one second.
A game can use the measured time since the last update, or advance the
simulation in fixed steps, such as 1/60 second. With fixed steps, it can
run zero, one, or several simulation updates while preparing the next
picture. These are two useful scheduling models; real games also decide
how to handle stalls and paused time. A frame count alone cannot tell us
how many gameplay updates occurred.
This also explains change without input. Once a projectile has a speed,
the simulation can move it on later ticks. A timer can count down, and an
enemy can act. The player does not need to press a new button for each
change. A pause menu might stop the simulation while still rendering the
menu, so a visible animation is not proof that every game timer is running.
Contact tests can block movement, award gold, or apply damage
Our movement calculation proposes a new position. It has not yet checked
whether Ada would pass through a wall or touch a coin. The game gives
objects simplified shapes for detecting contact. The test for whether
these shapes meet or overlap is collision detection.
For a simple worked example, treat Ada as a point starting at x = 2
and place a wall at x = 3. She requests movement to x = 4, keeping the
same y. Looking only at her destination could miss the wall: the point
at 4 is past it. Testing the movement from 2 to 4 finds the crossing. Our
game then stops Ada just before the wall. Actual characters have width,
but the point example shows why a path and a final position answer
different questions.
Detecting contact and deciding its consequence are separate jobs. A wall
blocks movement. A coin allows movement but awards gold and removes its
own entity. A trap allows movement but applies damage. All three can
begin with a contact test and then produce different changes to the world.
A physics system can calculate motion using speed, forces, gravity,
and contact responses. A game can also use simple movement calculations without
a general physics system. The drawn image and the collision shape may
have different sizes or levels of detail, so touching an image is not
always the same as touching the shape the contact test uses.
The same contact need not have the same outcome in every situation. Our
game gives Ada three modes: alive, defeated, and respawning. At
health zero, she changes from alive to defeated and movement requests are
ignored. After a two-second wait, she enters respawning. Placing her at
the spawn point and restoring her health makes her alive again.
A change between modes is a transition. A set of allowed modes and
conditions and actions for moving between them is a state machine. Here, “state” names
one behavior mode. Earlier, “game state” meant all the information the
game remembers, including gold, health, position, and this mode. The mode
is one part of that larger state. This game keeps Ada’s player identity
through the respawn; another could remove her character entity and create
a new one, and the visible respawn alone does not tell us which.
The game can also announce a particular occurrence: “Ada collected coin
42.” Such an occurrence is an event. An action requests something,
current state describes what is true, and an event reports what happened.
“Collect coin 42” is the request; “Ada has 76 gold” is the resulting
state; “coin 42 was collected” is the event. If the coin was already
removed, the game can reject a second request instead of awarding gold
again. Lesson 4.9
turns repeated readings of state into events and shows when to act on one.
After Ada collects a coin, the game may store 76 as her current gold and
also prepare the text "Gold: 76" for the screen. The text is derived
from the number, but it is not necessarily the number that the purchase
handler reads.
If someone changes only the display text to "Gold: 999", the screen may
show 999 while the purchase check still sees 76. Changing a displayed copy
can affect appearance without changing gameplay. This is why observing a
matching number in memory is only the start of an investigation. Later
lessons compare what changes after a purchase with what the game actually
uses to decide whether the purchase is allowed.
In our local example, the purchase check reads Ada’s gameplay gold. That
value is authoritative for the purchase: the check uses it as the
amount Ada actually has. The display copy reports the result but does
not decide it. Authority describes this role, not a special numeric type.
In a multiplayer game, a client is the program a player uses to take
part. A server is a program that receives player requests over a connection,
checks them against the shared game state it maintains, and sends accepted
results to the participating clients. In a common shop design, the client
sends the item ID Ada wants. The server looks up that item’s price, compares
it with the gold it stores for Ada, and either rejects the request or
subtracts the price and records the purchased item.
Suppose the server has 76 gold for Ada and the client’s display shows
999. The server accepts one 25-gold purchase and updates its value to 51.
It sends the accepted result back to the client, which can then show 51.
The display’s 999 did not give the server more gold. A network delay can
also leave a client temporarily showing an older result, even when
neither program has a bug. Different games divide responsibility
differently, so ask which side has authority over each particular
decision. Lesson 9.1 follows such
requests and replies, including a client that shows a predicted result
and then corrects it, and
Lesson 9.2
checks a purchase against server-owned state.
The running game holds live state: what it needs for play now. Closing
the game ends that running instance. To continue later, it writes selected
information to storage. Keeping information beyond the running process is
persistence; a save file is one way to provide it.
After Ada reaches 76 gold and 60 health, a save could keep those values,
her maximum health, position, and which coins she has collected. It need
not keep the pickup sound’s playback position or the prepared text
"Gold: 76"; the game can recreate the display from gold when it loads.
Saving selects state at a particular moment. If the file stores 76 gold
and Ada later spends 25 without saving again, live gold becomes 51 while
the file still records 76. A save file and live state can disagree simply
because they describe different moments.
Loading creates new running records. A saved player ID can help the game
reconnect related information, but a past RAM address does not give the
new process a valid player record.
Lesson 10.1 follows a
real save from running memory to a file and back.
We can now connect the meanings we have used. First, a designer chooses
an idea from a real or imagined world: a character owns coins and can
spend them. The game represents the information its calculations need as entities and
fields, such as player ID 17 with 76 gold. This chosen representation is
a game-data model. Ada’s health number likewise models how the game’s
damage and healing calculations use; it is not a complete description of a person’s body.
To save that information or send it to another program, the game chooses
a format: a specification of which fields are included and how to write
and read them. A save might include gold and collected coins. A movement
message might include only a player ID and the requested direction. They
serve different purposes, so they need not contain the same information
or resemble the live record’s layout.
An encoding assigns byte patterns to the chosen values. Here is a
small invented format for a player gold record. It accepts IDs and gold
amounts from 0 to 255, writes the ID first, and uses one byte for each
field. Given player ID 17 and gold 76, it produces two bytes:
# Toy format: one byte of player ID, followed by one byte of gold.
meaning: player_id = 17 gold = 76
field order: player_id gold
hex bytes: 11 4C
Lesson 1.2 taught hexadecimal: 0x11 is 1 × 16 + 1 = 17, and 0x4C
is 4 × 16 + 12 = 76. The reader of this toy format knows the first byte
means an ID and the second means gold because the format says so. The
two bytes alone do not label their own meanings. Writing the text digits
"76" or choosing a four-byte number would require a different encoding.
Real formats can include additional fields, larger values, and checks;
this example only shows how a declared format gives bytes meaning.
Watch it happenFrom an idea about coins to the two bytes 11 4C
A world idea, a character owning coins, becomes a game-data model with player ID 17 and gold 76. Each number is copied into a slot of a two-byte format, then converted to hexadecimal with the arithmetic shown, giving the saved bytes 11 4C.
Read the steps
A designer starts from an idea about a world: a character owns coins it can spend.
The game keeps only the parts its rules need. Here that is player ID 17 and 76 gold. This chosen representation is the game-data model.
To save or send this, the game picks a format: which fields to write, in what order. This toy format writes the player ID, then the gold, one byte each. Each number is copied into its slot.
An encoding turns each number into a byte pattern. In hexadecimal, 17 is 1 × 16 + 1 = 0x11, and 76 is 4 × 16 + 12 = 0x4C, because C stands for 12.
The file now holds the two bytes 11 4C. Nothing in the bytes says which one is the player ID; the format does.
Design chooses the model; a format and an encoding say how chosen information becomes saved or transmitted bytes.
The animation follows the meaning through those design choices. Live
fields in RAM are already represented by bytes too; a game does not have
to turn its whole world into a save file on every tick. In memory, code
uses a known layout and types. In a save or message, code uses that
format’s field order, widths, and encodings. Lesson 1.8 develops the memory representation in detail.
Keep two questions separate when investigating a value: how is it
represented? and what role does it have? A number stored as four
bytes could be current gameplay gold, a display copy, a saved value, or
a server result. Its byte representation alone does not establish its
authority or tell us when it was last updated.
Ada’s input requests an action. Purchase checks and damage calculations read
her current state and update
the relevant fields. Simulation advances that world through time;
rendering shows a view of it. Identity tells the game which entity a value
belongs to, components organize its responsibilities, and assets can be
shared by many separate instances. A collection lets one loop visit
several player records.
The gameplay amount, displayed text, server’s gold value, and saved value can
all describe Ada’s gold while serving different roles. Understanding those
roles comes before deciding what a matching number in memory means. The
game model explains the information; its memory layout or save/message
format explains how code represents it as bytes.
We have described the update in words. Lesson 1.4 turns the
same player’s values and calculations into a small Rust program.
We have already followed one value through a CPU instruction (Lesson 1.2)
and grouped it with one player’s other values (Lesson 1.3). Now we can write
a small program that uses those ideas. We will start with 100 gold and ask
what happens when an item costs 25. Lesson 1.8 will show the bytes this
program uses before we try to find them in a running game.
Call the two players in this lesson Ada and Bo. Ada starts with 100 gold
and Bo with 40. The short Rust fragments each explain one idea, and the
next lesson runs a separate complete program. Here we will also compare
program logic, collections, and ways to organize code.
Data is information stored or sent as bytes. A value is what those
bytes mean when interpreted, and a type tells the program how to
interpret and use them. For example, the four bytes 50 00 00 00 can
represent the number 80 when read as a little-endianu32. We will work
through the byte order in Lesson 1.8. For now, notice that “four bytes”
and “80 health” describe different levels of the same information.
50
00
00
00
four stored bytes
read as a u32: the value 80
A type tells code how to interpret stored data.
The number 100 is a literal: a value written directly in the code.
A variable is a name for a value the program can use. This first
fragment names the current gold and the cost:
letmutgold: u32 = 100;
letcost: u32 = 25;
let introduces a variable. gold and cost are names we chose.
The colon gives each variable a type. Game Fundamentals used u32 for
a non-negative whole number stored in four bytes. The equals sign sets
the starting value, and the semicolon ends the statement. mut means
the value named gold may change. We do not need to change cost,
so it has no mut.
The type keeps different kinds of value apart. 80 can be a u32 health
amount, "Ada" can be text, and true can be a bool answer to a question.
Adding two health amounts makes sense; adding the player name to health does
not. Later, Lesson 3.1 will show how types can even distinguish two numbers
that happen to use the same underlying bytes, such as a player ID and a
health amount.
At this point, gold is 100 and cost is 25. To calculate a purchase, the
program reads both values, subtracts cost, and stores the result back in
gold:
gold = gold - cost;
The right side is calculated first: 100 − 25 = 75. The equals sign then
assigns 75 to gold. The cost stays 25.
gold before100
cost25
gold after75
read 100 and 25, subtract, then store 75
The assignment changes gold. It does not change the cost.
The program’s state is the information it remembers at one moment.
Before the assignment, gold is 100; afterward, it is 75. The cost is an
input to that calculation. The subtraction result is a temporary value
until it is stored as the new gold. A screen might then show the derived
text "Gold: 75", but that text is not necessarily the stored amount the
next purchase checks.
This input → calculation → stored state → display path helps us ask which
value a later piece of code actually reads. Game Fundamentals first showed
that the display may hold a copy; Hacking Fundamentals will turn that
observation into an experiment.
What if gold is only 10 and the cost is 25? A purchase should not be
allowed. Because u32 cannot represent a negative number, blindly
subtracting would also be the wrong operation. We need the branch from
Lesson 1.2:
ifgold >= cost {
gold = gold - cost;
}
>= asks whether gold is greater than or equal to cost. The answer is
a boolean value, either true or false. if runs the
instructions between the braces only when the answer is true. Starting
with 100 gold, the comparison is true and gold becomes 75. Starting
with 10, it is false and gold remains 10. The braces mark the group of
statements controlled by the branch.
This gives us a repeatable algorithm, meaning a procedure with
ordered steps: read the current gold, compare it with the cost, and
subtract only if the player can pay. The same procedure works for 100 gold
and for 10 gold. An algorithm is the plan; Rust code is one exact way to
express it. Its three building blocks are already visible here: do steps
in sequence, use if to select a path, and later use a loop to
repeat work for several players.
Predict first
A player has exactly 25 gold, and the item costs 25. The code is the branch above: if gold >= cost { gold = gold - cost; }
What is gold after this code runs?
Answer: 0. 25 >= 25 is true, because >= also accepts equal values. The branch runs and stores 25 − 25 = 0, which is a valid u32.
if gold >= cost { gold = gold - cost; }Try the exact-payment case with both comparisons. They agree everywhere except when gold equals the cost.
Compare first, then subtract only if allowed
The check
Stored values
gold 25
cost 25
↓ compare
gold >= cost
25 >= 25: true
Accepted
Subtract
25 − 25 = 0
Gold afterward
0stored back in gold
25 >= 25 is true, so the branch runs: 25 − 25 = 0 is stored as the new gold.
You can first write the plan in pseudocode, ordinary steps arranged to
look like code without committing to a language’s exact syntax:
receive the current gold and the item cost
if the gold is at least the cost:
give back gold minus cost
otherwise:
give back the original gold
This is a sketch to discuss and trace, rather than a program Rust can run.
With 100 and 25, it gives back 75. With 10 and 25, it gives back 10. Naming
the inputs and both outcomes makes the next function easier to write.
One purchase algorithm, different inputs. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
A function gives a behavior a name so another part of the program
can use it. This one takes a gold amount and a cost, then gives back
the gold amount after a permitted purchase:
fn begins the function definition. The names inside parentheses
are its parameters: the two input values it will receive. Each
has a u32 type. The arrow -> u32 says the returned value is
also a u32. return sends the value back to whoever used the
function. If there is enough gold, it returns the subtraction. If
there is not, it returns the original amount. The final return
also shows that functions do not have to run every line: the first
return ends that call.
For example, spend_if_possible(100, 25) returns 75.
spend_if_possible(10, 25) returns 10. Those expressions call
the same function with different inputs. They do not change a
variable by themselves; the caller must store the returned value:
gold = spend_if_possible(gold, cost);
This simple result does not tell the caller whether a purchase happened
when the cost is zero. A later lesson introduces types that can
represent success or failure explicitly. We only need the resulting
gold amount here.
Gold and cost are inputs to spend_if_possible; its returned amount is
an output. The caller can store that output in its mutable gold
variable. cost stays fixed during the calculation. Input, output, and
mutable are therefore overlapping descriptions, not three separate kinds
of variable. They answer “where does the value flow?” and “may it change?”
Two other descriptions concern where a name is usable and how long its
storage exists. A local name belongs to a scope such as one function.
People often use global for a name available outside a single function,
although the language’s visibility controls which code can use it. A Rust static item has storage for the entire program run. That
storage duration does not by itself mean its contents can never change;
Rust separately checks how code obtains shared or editable access; a long
lifetime does not grant permission to mutate. See the
Rust Reference’s static-item explanation
for the complete access requirements. Our purchase calculation needs only its supplied
inputs, so it does not need a shared global gold value.
The ordinary 100-gold purchase is a useful baseline case, sometimes
informally called a base case. In recursive algorithms, “base case” has a more
specific meaning: a case solved directly without another recursive call.
For this affordability check, we are checking ordinary and unusual inputs. It is not
enough to check only that example. An edge case exercises an unusual
end of the allowed range, such as having zero gold. A boundary case
checks where a decision changes: just below, exactly at, and just above
the price. A corner case combines unusual conditions, such as zero
gold and a zero price. These labels can overlap; the important part is
writing down the inputs and expected result:
Case
Gold
Cost
Expected returned gold
Ordinary purchase
100
25
75
No gold
0
25
0
Just below the price
24
25
24
Exactly the price
25
25
0
Just above the price
26
25
1
No gold and a free item
0
0
0
The exact-price case checks that >= allows payment when gold equals cost.
Changing it to > would reject that purchase. The free-item case also
shows the limitation already mentioned: unchanged gold alone cannot tell
us whether a purchase happened. The Rust Primer demonstrates an automated
test for its stamina check; Hacking Fundamentals will check the conditions
that permit a memory write.
We can group gold, health, and maximum health into one record. For this
example, Ada starts with 100 gold and 80 health so the program can
perform the purchase itself.
That record is a data structure: a chosen way to organize related
values. Rust uses struct to describe its shape:
structPlayer {
gold: u32,
health: u32,
max_health: u32,
}
Each line inside the braces is a named field. Now we can create one
player value:
letada = Player {
gold: 100,
health: 80,
max_health: 100,
};
The braces supply the three field values. A comma separates fields;
the semicolon ends the let statement. ada.gold reads Ada’s
gold field. ada.health reads her health. The dot selects a field
from this particular record.
The pair health and max_health has a required relationship: a valid
player’s health must be between zero and the maximum. For Ada, 80 is within
0 to 100. If a mistaken update made health 120 while the maximum
stayed 100, the record would violate that relationship. We will call
such a condition an invariant below. For now, check the relationship
instead of trusting either field alone.
Suppose Ada has 100 gold and Bo has 40. We want to charge each of
them 25. A collection stores several values. The square brackets
below make a fixed-size array containing two Player records:
letmutplayers = [
Player { gold: 100, health: 80, max_health: 100 },
Player { gold: 40, health: 50, max_health: 80 },
];
We make players mutable because its records’ gold values will
change. A loop repeats one operation for each record:
forplayerin &mutplayers {
player.gold = spend_if_possible(player.gold, 25);
}
Read for player in ... as “take each player in turn.” The
&mut players part lets the loop access the existing records so
it can change them; Rust calls this a mutable borrow. We will study
borrowing more fully when a tool begins reading another process in
Chapter 3. Here, the important effect is that the loop changes the
records in the collection, rather than temporary copies.
On the first pass, Ada’s 100 becomes 75. On the second, Bo’s 40
becomes 15. If Bo had only 10, his gold would stay 10 because the
function’s comparison would reject that purchase. The same named
operation works for both players; adding a third record would not
require writing another affordability check and subtraction.
Ada before100
Ada after75
Bo before40
Bo after15
first loop pass
second loop pass
The same guarded 25-gold subtraction visits two records and produces two results.
The Player record groups fields belonging to one player; the array
groups several players. Both are data structures, chosen for different
questions. We will compare other useful collections after tracing this array.
The function, record, and loop fragments above show
both outcomes without seeing a longer program yet:
Player
Gold before
Cost
Gold after
Health afterward
Ada
100
25
75
80/100
Bo
40
25
15
50/80
The same purchase function visits each record. It changes gold, not health.
The Rust Primer
runs a separate stamina-based program with two runners and shows how Rust
lends each record to a function in turn.
Programming logic: sequence, selection, and repetition
The purchase program combines three simple ways for instructions to flow.
A sequence does one step and then the next. A selection chooses a
path based on a condition, as if did for the purchase. Repetition
runs steps again, as for did for the two players. These are not three
special kinds of program; they are building blocks you can combine.
ExampleThree prices in order
Suppose Ada meets three prices in order: 25, 90, and 10. Start with 100
gold and trace this short Rust fragment:
letmutgold = 100;
forcostin [25, 90, 10] {
ifgold >= cost {
gold = gold - cost;
}
}
The array supplies prices in sequence. The for loop repeats
the body once per price. The ifselects whether subtraction runs
on that pass. Follow the stored gold, not just the prices:
Pass
Price
Comparison
What happens to gold?
Start
—
—
100
1
25
100 ≥ 25 is true
100 → 75
2
90
75 ≥ 90 is false
remains 75
3
10
75 ≥ 10 is true
75 → 65
The final gold is 65. Order matters: if the 90-gold purchase came first,
it could succeed while the later 25-gold purchase would fail. This is why
reading unfamiliar logic means finding both which steps can run and
when they run. A while loop also repeats, but it keeps going while
its condition remains true; the for loop here has a known collection
of three prices. Later lessons combine these same forms into scanners,
parsers, and game bots.
Once values are in a collection, the question determines the work. After
the earlier two-player purchase, Ada has 75 gold and Bo has 15. For a new
item costing 25, we can ask several different questions:
Question
Kind of result
Answer for Ada and Bo
Can Bo afford the item?
Decision: a yes/no answer
No, because 15 is below 25.
How many players can afford it?
Count: the number that meet a condition
One: Ada.
What is the least or greatest gold amount?
Minimum or maximum: an extreme value
Minimum 15; maximum 75.
Counting requires checking each player’s gold against the price. Finding
a maximum requires comparing the gold amounts with one another. Neither
requires sorting all players first. If there are no players, the count
is zero, but there is no player’s minimum or maximum to return. We will
use Rust’s Option in the next lesson to represent a missing answer.
Clarifying the desired result helps you choose a collection and an
algorithm, instead of copying a solution to a different question.
The array above has two fixed Player slots. A vector can grow when the
number of values changes; this small history gains one new sample:
letmutrecent_gold = vec![100, 75];
recent_gold.push(65);
The values are now 100, 75, and 65. A hash map instead looks up a value by
key, and a queue takes waiting items in arrival order. T in Vec<T> names
the stored item type. A grid groups rows and columns; a graph records
connections. The operation you need determines the useful arrangement.
Lesson 3.3 compares these choices
with the full table and decision diagram before its bounded byte examples.
Programming paradigms: ways to organize a purchase
A programming paradigm organizes instructions and responsibilities.
The same guarded purchase still gives 75 for (100, 25) and keeps 10 for
(10, 25). These four styles decide where and when that work happens:
Check gold >= cost, then update gold only on the permitted path. Our
named function keeps that check in one place instead of copying it into
every caller.
A method such as ada.buy(25) groups the purchase behavior with Ada’s
record. Rust’s impl syntax in the next lesson shows how a method receives
the record it uses.
A Buy click can produce an event. Its handler asks the purchase function to
update that player’s gold. The event decides when the calculation runs;
the affordability check still decides whether spending is allowed.
Calling spend_if_possible(100, 25) lets another part of the program use
the purchase calculation without repeating its branch and subtraction. That is
an abstraction: a smaller interface hides useful steps (Example: vec.push(65) is an abstraction too. The caller asks for one more item; how the vector finds room, and moves its items if it must grow, stays hidden.). It still has
to keep its promise. If the caller and function disagree about the
inputs, the friendly name does not repair the mistake. Lesson 6.7 later explains source-level APIs and machine-level
ABIs when we begin calling compiled game functions.
An invariant is a condition valid state keeps true
For our u32 values, zero is already the smallest possible number. If
Ada has 80 health and a maximum of 100, the function returns true; for
120 and 100 it returns false. The purchase function has its own condition:
only subtract a cost when the player can pay. An invariant (Example: A growable list in memory keeps begin <= end <= capacity_end: its used part can never reach past the storage allocated for it. Lesson 3.10 checks those pointer bounds on recovered bytes.) gives you a
concrete check when unfamiliar state or a recovered field might be wrong.
Lesson 14.5
uses such conditions to distinguish valid and invalid game state.
Return to spend_if_possible rather than trying to hold the whole page
in your head. One useful route is to ask five questions:
What enters? Gold and cost, both u32 values.
What can stop or change the path?gold >= cost chooses the return.
What state changes inside? None; this function calculates a result.
What leaves? A u32: 75 for (100, 25), or 10 for (10, 25).
Who stores that result? The caller’s assignment to gold or
player.gold.
The same questions work for a new function or event handler: find the
inputs and types, checks, state changes, and output. A dense line can be
broken into smaller expressions to see which value each one produces.
mut permits gold to change while cost stays fixed. The 10-gold case
takes the failed branch of gold >= cost, so spend_if_possible returns
unchanged gold. One loop applies that same function to Ada and Bo. The
three-price trace combines ordered steps, a branch, and repetition.
A vector keeps a growing history; a hash map answers a lookup by key;
a queue keeps pending actions in arrival order. A visible “Gold: 75”
label can be separate from the gold field the calculation reads. Procedural,
object-oriented, functional, and event-driven styles can organize the
same check and subtraction in different places, while valid_health checks that player
health stays within its allowed range.
The Rust Primer uses a different game action to make the
Rust syntax executable. Hacking Fundamentals then turns
an observed purchase into a question about a running game: which value
actually controls it?
This lesson gives Rust syntax a purpose. A program runs steps in order,
chooses between possible steps, repeats a step, and keeps related state
together. Rust has familiar ways to express each of those ideas. We will
build one small program around a fictional runner named Mira, who has 12
stamina. A dash uses 4 stamina. A second runner named Sol will let us see
why the same dash function can update several records.
Programming question
Rust expression in this lesson
What happens first, next, and last?
Statements in main, function calls, and assignments.
Which path should run?
if, match, and if let.
What should repeat, and when should it stop?
for, while, loop, and collection iterators.
Which values belong together?
struct, collections, and enum.
Who can read or change a value?
&, &mut, ownership, and methods.
What if a value is missing or an operation fails?
Option, Result, and ?.
The traces below open with a worked example. Change a value or step through a branch to see why the result changes. Typing practice is optional: Practise typing this hides selected names, parameters and calculations, so you can recall them after reading the explanation. Hints and an answer reveal are always available. Copy visible code offers practice with the complete snippet instead.
Cargo is the tool normally used to build and run Rust projects. After
installing a Rust toolchain, a terminal can check that Cargo is available:
cargo --version
A version number means the command is ready. If it is not installed, the
official Rust installation guide
explains the supported setup. You can read this primer now and run it after
the Windows lab setup in Lesson 1.7 (Alternative: To try a short program sooner, the Rust Playground compiles and runs it in your browser with nothing to install. The Windows tools later in the book still need the lab setup from Lesson 1.7.).
Make a project in a folder you control:
cargo new rust_stamina_primer --vcs none
cd rust_stamina_primer
Cargo creates Cargo.toml, which describes the project, and
src/main.rs, which holds the starting program. Replace the contents of
src/main.rs with the complete program below. The shorter fragments before
it each isolate one idea; do not paste all of them into the same file.
cargo run compiles and
runs it. cargo check checks the code without producing a program you
can run. The official Cargo chapter
walks through these same commands.
Start with three values, each with a distinct job. The statements run from
top to bottom; later statements can use names created earlier:
letmutstamina: u32 = 12;
letcost = 4_u32;
letcan_dash = stamina >= cost;
let binds a name. mut permits a later assignment to stamina.
The : u32 is an explicit type; the _u32 on 4 gives the literal
that type too. Rust can infer that cost is a u32 and can_dash is a
bool, because >= produces true or false. A semicolon ends each
statement. To update state, assign a new value to the same name:
stamina = stamina - cost; // first calculate 12 - 4, then store 8
The assignment above assumes the runner has enough stamina (Caution: Without a check, a u32 that would go below zero does not become negative. A debug build stops with a subtract-with-overflow panic; a release build wraps around, so 2 − 4 gives 4,294,967,294.). To choose whether the dash runs or reports insufficient stamina, use if
and else:
ifcan_dash {
stamina = stamina - cost;
} else {
println!("Not enough stamina");
}
With 12 stamina, the first branch runs and stamina becomes 8. The else branch
would run for 2 stamina and a 4-stamina cost. The braces group the statements
belonging to each path. This is selection: test a condition, then run
the matching branch. Later, match and if let will select by the shape
of a result instead of a true/false condition.
A comparison chooses whether stamina changes. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
let mut stamina = start;
let cost = dash_cost;
if stamina >= cost {
stamina -= cost;
} else {
println!("Rest first");
}
The branch ends with 8 stamina. The guarded subtraction spent the cost.
A for loop repeats a body for each item in a collection. Here the loop
tries three dash costs in order, carrying the current stamina into the next
attempt:
letmutstamina = 12_u32;
forcostin [4, 10, 2] {
ifstamina >= cost {
stamina -= cost;
}
}
// stamina is 6: use 4, skip 10, use 2
stamina -= cost is short for stamina = stamina - cost. The trace is 12 → 8;
10 needs more stamina than remains, so stamina stays 8; using 2 leaves 6. The code
shows repetition without writing the dash check three times.
The next attempt uses the amount left by the previous one. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
let mut stamina = start;
for cost in [4_u32, 10, 2] {
if stamina >= cost {
stamina -= cost;
}
}
// the loop has finished
All three costs have been visited. The final stamina is 6.
The watched amount belongs to one runner throughout the loop. Change its starting value to 16: all three costs now fit and the final amount is zero.
Use for when there is a collection to visit. Use while when the
condition determines whether another pass is needed:
letmutattempts = 0;
whileattempts < 3 {
attempts += 1;
println!("Attempt {attempts}");
}
// prints Attempt 1, Attempt 2, Attempt 3
Use loop when the body decides exactly when to stop:
letmutremaining = 2;
loop {
ifremaining == 0 {
break;
}
remaining -= 1;
}
The important question is always what makes the repetition stop?
The for example ends after its three costs, the while example ends
when attempts reaches three, and the loop example reaches break
when remaining reaches zero.
So far stamina is a standalone number. A game also needs to know whose
stamina it is. A struct names a shape of related fields; Runner groups a
name with a stamina amount. A function names a repeatable behavior. Here
dash checks stamina before subtracting the cost; show_stamina displays the state:
structRunner {
name: String,
stamina: u32,
}
fnshow_stamina(runner: &Runner) {
println!("{} has {} stamina", runner.name, runner.stamina);
}
fndash(runner: &mutRunner, cost: u32) -> bool {
ifrunner.stamina < cost {
returnfalse;
}
runner.stamina = runner.stamina - cost;
returntrue;
}
fnmain() {
letmutmira = Runner {
name: String::from("Mira"),
stamina: 12,
};
show_stamina(&mira);
letdashed = dash(&mutmira, 4);
println!("Dash succeeded: {dashed}");
show_stamina(&mira);
}
Run it with cargo run. You should see:
Mira has 12 stamina
Dash succeeded: true
Mira has 8 stamina
The order of those lines matters. main starts, creates this sample
runner, prints the starting value, asks dash to check and spend stamina, and
prints the new value.
The first and last print calls read the same runner record, before
and after one controlled change.
Mira.stamina before12
dash cost4
Mira.stamina after8
dash checks the cost, then changes Mira's stored stamina
The dash call changes Mira's record; show_stamina only reads it.
let mut mira creates a mutable runner value inside main.
main owns that value for this example. The name field is a String:
owned text stored in Mira’s record. u32 is a non-negative whole-number
type, suitable for this simple stamina example.
show_stamina(&mira) lends the function a read-only reference to Mira.
The &Runner parameter says it may inspect a runner without taking over
ownership. dash(&mut mira, 4) lends a temporary editable reference;
&mut Runner says the function may change that runner. When the call
returns, main can use Mira again. Neither function needs to copy the
whole record just to read or update its fields.
Think of Mira’s record as one notebook. show_stamina may read it; dash
needs permission to write in it. The analogy stops at Rust’s borrowing checks:
references are checked by the compiler, and Rust will reject conflicting
access to the same value during an editable borrow. Lesson 3.1 develops ownership, borrowing, and error handling
when we build an external memory tool.
Remove mut from let mut mira and run cargo check: the
editable &mut mira call is rejected before the program runs.
Put mut back to make the intended change explicit. Compiler messages
are useful clues about which part of the code’s promise does not match.
Ownership (Reference: The Rust Book's chapter on ownership gives the rules with more examples: each value has an owner, there is one owner at a time, and the value is dropped when its owner goes out of scope.) becomes clearer when we compare two assignments. A u32
number is a small Copy value: assigning it to another variable copies
the number, so both names remain usable. A String owns growable text;
assigning it normally moves ownership to the new name:
letstamina = 12_u32;
letcopied_stamina = stamina; // both names can still be used
letname = String::from("Mira");
letmoved_name = name; // moved_name now owns the text
// println!("{name}"); // rejected: name no longer owns it
println!("{moved_name} has {copied_stamina} stamina");
A u32 is copied; a String is movedThe two assignments from the example, in order. The number is duplicated; the text on the heap is not.
1let copied_stamina = stamina;
Stack
12stamina
12copied_stamina: its own copy
A u32 is just four bytes, so the assignment copies them. Both names hold 12 and both stay usable.
2let moved_name = name;
Stack
movedname: cannot be used
pointer · length 4moved_name
↓ the pointer leads to the same text
Heap
M
i
r
a
Only the small String value is copied, and name is marked moved. "Mira" stays where it was with one owner, so it is freed exactly once.
All 2 steps shown
The two assignments from the example, in order. The number is duplicated; the text on the heap is not.
This does not make a second independent copy of the text. If both names
claimed to own the same growable allocation, each might try to release
it later. Rust makes the transfer visible to the compiler instead. A
borrow such as &mira is different: it lets a function use the value
temporarily while Mira remains owned by main.
This interactive ownership model needs JavaScript. The surrounding lesson still explains every step in text.
The interactive view separates the String value’s small bookkeeping
from the text it owns. It previews the stack and heap vocabulary that
Lesson 1.8 will explain. The important ownership consequence here is that
the old name cannot be used after its owned String moves; the new
owner can use it. A copyable u32 behaves differently.
Rust’s symbols are easier to read when each has a job:
In the example
Read it as
name: String
a field named name whose type is String
-> bool
this function returns true or false
runner.stamina
the stamina field in this runner
String::from("Mira")
call from associated with the String type
&mira / &mut mira
lend read access / lend editable access
println!
a printing macro; the ! marks macro syntax
The braces in println!("{} has {} stamina", ...) mark slots filled by
the values after the quoted text. The {dashed} form uses the local
variable by name. In a later collection example,
use std::collections::{HashMap, VecDeque}; brings two standard-library
names into scope; :: walks through their module path.
To see how one function updates several records, add Sol to the program. Mira
starts with 12 stamina and Sol with 7; each has a separate health amount.
Replace src/main.rs with this complete version;
do not paste a second main underneath the first one:
The first loop lends each existing runner to dash for an edit. That
loop finishes before the second loop lends each runner to show_runner
for reading. Mira’s stamina becomes 8 and Sol’s becomes 3; their health
fields do not change. cargo run should print:
Mira has 8 stamina and 70/100 health
Sol has 3 stamina and 45/60 health
The shape of each runner became larger, but the stamina check did not need a
special Mira version and a special Sol version. Add a third record with
2 stamina: the dash fails and that record keeps 2. The function’s
bool result could also be used to
display whether each dash succeeded.
An impl Runner block attaches a method to the record type. &mut self
gives that method editable access to its receiving runner, just as
dash receives &mut Runner. A short method can reuse the existing check:
implRunner {
fntry_dash(&mutself, cost: u32) -> bool {
dash(self, cost)
}
}
In the full program’s first loop, runner.try_dash(4) has the same result
as dash(runner, 4): Mira 8 and Sol 3. Rust methods organize behavior;
class inheritance is not required. Lesson 3.4 keeps the full direct-check
alternative and its original practice.
Use that alternative in place of this method, not as a second method with
the same name.
The bool from dash says whether the dash succeeded, but it does
not say why it failed or how much stamina is left. When an answer can take
one of a few distinct shapes, Rust uses an enum. This separate program
decides a dash without changing a runner record:
DashOutcome::TooTired { missing } => println!("Need {missing} more stamina"),
}
}
This prints Need 2 more stamina. The if chooses which outcome to construct.
match then selects the arm for that outcome and names its stored value.
The last expression in each branch has no semicolon, so it becomes the
function’s answer. If you later add a third kind of dash outcome,
Rust requires the match to handle it too. This is how the logic of
alternatives appears in a type, instead of being hidden in a special
number such as -1.
The two-runner program uses a fixed array. A vector can instead grow as
new values arrive:
letmutrecent_stamina = vec![12, 8];
recent_stamina.push(6);
The vector now holds 12, 8, and 6. A hash map finds a value by key; a queue
takes actions in arrival order. Lesson 3.3 gives their complete Rust
examples, including a missing
map entry or an empty queue.
An iterator repeatedly yields items. For stamina values 8, 3, and 6, a
query keeping values at least 4 counts two ready runners. The loop forms
above show how such repetition works. The full iterator/loop comparison
and original practice
follow the collection examples when we examine bounded sequences.
.get(1) returns an Option: Some(value) when that array position
exists, or None when it does not. if let runs its block only for the
matching shape and names the value inside it:
letdash_costs = [4, 10];
ifletSome(cost) = dash_costs.get(1) {
println!("Second dash cost: {cost}");
}
This prints 10. .get(5) would give None and skip the print, while direct
out-of-bounds indexing would fail at runtime. A missing answer differs from
a valid zero: 4_u32.checked_sub(4) gives Some(0), whereas
2_u32.checked_sub(4) gives None. No negative unsigned value is invented.
These calculations return an answer; they do not change a runner’s fields.
Option says whether a value exists. Result<T, E> says either
Ok(value) or Err(reason). Parsing the text "4" as a u32 succeeds;
"tired" is not a whole number. The caller handles both possibilities:
match"4".parse::<u32>() {
Ok(cost) => println!("Cost: {cost}"),
Err(error) => println!("Invalid cost: {error}"),
}
::<u32> selects the numeric type. For "4", the success arm prints
Cost: 4; for "tired", the error arm runs. Invalid input is not a valid
zero cost. T names the success type and E the error type.
The final expression without a semicolon supplies the returned value.
Here success is Ok(cost); a failed parse returns before that line.
The caller still decides how to handle the failure. Lesson 3.2 keeps the
complete two-input caller and original parser practice,
then contrasts that parse error with a domain-specific purchase failure.
The book’s memory tools also introduce syntax whose detailed requirements belong
beside the tool that needs them. You can still tell what role each plays:
Later syntax
Programming idea it expresses
Where it becomes concrete
trait MemoryReader { ... }
A promise that different readers provide the same behavior.
You can read a lab function as inputs → operation → Result → caller’s
decision even before knowing every Windows API name. The later lesson
defines the new boundary when you reach it.
Below the complete two-runner program, add this test. It creates a runner
with only 2 stamina, requests a 4-point dash, and checks both the answer and the
unchanged state:
#[test]
fninsufficient_stamina_keeps_state() {
letmutrunner = Runner {
name: String::from("Mira"),
stamina: 2,
health: 70,
max_health: 100,
};
letdashed = dash(&mutrunner, 4);
assert!(!dashed);
assert_eq!(runner.stamina, 2);
}
#[test] marks a function for Cargo’s test runner. assert! requires
the answer to be false here; assert_eq! requires the stamina to remain 2.
Run cargo test. If you change dash so it subtracts without
checking, this test catches the mistake. The test does not prove every
possible dash is correct, but it protects one important boundary.
The successful path can be checked with 12 stamina and cost 4. The Cargo Book’s testing
guide explains how
tests in a project are discovered and run.
In the two-runner program, change dash(runner, 4) to
dash(runner, 20). Neither runner has 20 stamina. The function returns
false before subtraction, so the final lines show 12 and 7 stamina.
The 4-point cost restores the original run.
You can now read the Rust code in the next lessons as a set of explicit
promises: a value has a type, a reference says what access a function
gets, and a return value says what came back. Hacking
Fundamentals applies the same programming concepts to a
different game’s observed behavior.
You have now seen names, branches, loops, ownership and fallible results.
The following lessons take one group of those ideas further with small
worked examples, adjustable traces and optional recall practice:
Cleanup and helpful errors pairs a local guard with
restoration, then distinguishes library causes from application context.
Use the link that answers your current question. Later memory and tool
lessons also point back to the relevant idea when it first appears.
Lesson 1.6
Hacking Fundamentals
Imagine an offline game whose display shows 100 gold. Buying an item for
25 changes the display to 75. The screen shows the result, but it does
not identify the affordability check, subtraction, or
stored value that caused it. Game hacking in this book means
investigating that behavior and making a deliberate, testable change to
the game or its data.
An observation is what you can actually see: after a 25-gold purchase,
the display changes from 100 to 75. An explanation is a hypothesis:
perhaps a four-byte value in the game’s memory stores the gold used by
the purchase handler. Other explanations are possible. The game may keep a
display copy, recalculate gold from other data, or let a server decide
the result. A matching number is therefore a candidate, not a conclusion.
Does editing a candidate change the next purchase?. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
start with gold 100; buy once
observe game and display at 75
write 500 into the selected candidate
repeat the same 25-gold purchase
redraw from the game’s resulting gold
compare the competing explanations
Result: 475 appears. In this model, that outcome separates a state candidate from a display copy; a real claim needs controlled, repeatable evidence.
Step 1, line 1. Start with the selected inputs and follow how the next operation changes the state.
candidate = state.
Step 2, line 2. The first purchase leaves 75 gold.
candidate = state; gold = 75; display = 75.
Step 3, line 3. The edit changes only the selected storage.
candidate = state; gold = 500; display = 75.
Step 4, line 4. The next purchase reads the game-state amount, then subtracts 25.
candidate = state; gold = 475; display = 75.
Step 5, line 5. Drawing copies the new state; a display-only edit is overwritten.
candidate = state; gold = 475; display = 475.
Step 6, line 6. Result: 475 appears. In this model, that outcome separates a state candidate from a display copy; a real claim needs controlled, repeatable evidence.
candidate = state; gold = 475; display = 475.
Watch it happenOne purchase, one candidate, one test
A game screen showing 100 gold beside a memory panel holding a candidate value of 100. A 25-gold purchase lowers both to 75. The candidate is overwritten with 500 and the purchase repeated: the screen shows 475, so the candidate is part of the rule rather than only the displayed text.
Read the steps
The screen shows 100 gold. One value in memory also holds 100: a candidate from a scan, not yet proof of anything.
Act: buy an item whose cost is known, 25 gold.
Observe: the screen falls from 100 to 75, and the candidate falls with it.
Hypothesize: perhaps this stored value governs the purchase. It might also be a display copy, so it is a candidate, not a conclusion.
Test one candidate: write 500 into it, then repeat the purchase. The screen still shows 75, because nothing has been recomputed yet.
Judge the evidence: the screen now shows 475, which is 500 − 25. The purchase used the changed value, so the candidate is part of the rule, not just the text on screen.
The visible change motivates a hypothesis; the second purchase tests whether the candidate affects the game’s state or only its display.
The fastest way to get completely lost is to open a memory scanner and
start searching before deciding what you are asking. You can end up with
hundreds of addresses—numbered locations in the game’s running memory—with
no way to tell which matters and no record of
which ones you have ruled out. A scanner finds values that match a
question; a debugger can later show the instruction that reads or writes
a candidate. Neither tool supplies the meaning by itself.
✅ “Find the value that decreases by exactly 25 when the local player buys one
known item.”
The behavior is only repeatable if it starts from the same conditions each
time:
exact game build;
map, save, or level;
starting value;
action you will perform;
result you expect to observe.
Good starting behaviors include spending gold, taking damage, moving along one
axis, or toggling one graphics setting. Avoid changing several things at once.
One coin, three runsEach run records the start, the single action, and what the display showed.
1Run 1: load the save, collect one coin
start 100
collect 1 coin
see 101
+1
Start 100, one coin, and the display shows 101: a change of +1.
2Run 2: load the same save and repeat
start 100
collect 1 coin
see 101
+1
Same start, same action, same +1. A result that repeats is evidence; a single run could have been a coincidence.
3Run 3: repeat without reloading
start 101
collect 1 coin
see 102
+1
The start drifted to 101, so the display shows 102. The +1 held, but the runs no longer compare directly: reload so every run starts at 100.
All 3 steps shown
Each run records the start, the single action, and what the display showed.
Before searching memory, predict the checks and calculation the game appears to use.
For this lesson’s 25-gold purchase, our first guess is:
if gold >= cost:
new_gold = gold - cost
else:
new_gold = gold
Try the same idea on another behavior. For health, a first guess might be:
new_health = max(0, old_health - damage)
A prediction can also be a condition valid state keeps, such as
0 <= health <= max_health.
Lesson 14.5
tests conditions like this against a running game’s state.
The health calculation, stored result, and visible bar are connected,
but they need not be one stored value:
These predictions help distinguish the health used for later damage and
defeat decisions from text,
animation state, or a cached display value.
This self-contained Rust fragment makes the distinction visible:
letmutgold = 100_u32;
letdisplayed_gold = gold; // Copy the amount before the purchase.
ifgold >= 25 {
gold -= 25; // Change the amount the next purchase will read.
}
Now displayed_gold is still 100, but gold is 75. Changing the copy
would not make a later purchase use 100 gold; the next affordability check reads gold.
A real game may refresh its display differently, so the
experiment must establish which value the purchase handler actually reads.
At this stage these checks and formulas are hypotheses. The game may apply armor, difficulty
scaling, or delayed damage, or a server may own the result, and any of these
would break the simple formula.
Before code touches another process, decide its limits in a small contract.
This is a preview of the external tools built later in the book; you do
not need to implement it for the first memory experiment:
Target: Wesnoth 1.14.9, 32-bit Windows build
Input: process name and expected build fingerprint
Read: one narrowed gold candidate in the current run
Change: one u32 field after confirming its current value
Stop: cancel key, version mismatch, invalid address, or failed read
Restore: write the saved original value back when the experiment ends
This contract prevents a small experiment from quietly turning into an
unbounded tool.
The steps also have dependencies: a successful write needs a successful
read of the intended field in the intended build. A failed read does not mean
the player has zero gold. Stop at that failed step and preserve its error,
rather than letting later code act on an invented value. The purchase itself
depends on the current gold and price; the display depends on an update of its
copy. Following those relationships tells you which earlier assumption to
check when a later result is wrong.
A later tool may replace the temporary address with a module-relative
pointer path: a sequence of lookups anchored to one exact game build.
Lesson 1.8 first explains a pointer, Lesson 2.7 explains why the
absolute address moves, and Lesson 2.8 builds the path. For
this experiment, the useful limit is simpler: verify the game build and
current value before changing anything.
Use the least complicated technique that can answer the question. A
breakpoint is a configured debugger stop that pauses execution at a chosen
instruction or memory access; we will use one only after learning the
debugger.
scan for the visible value;
make one controlled game change;
filter the candidate addresses;
repeat until the list is small;
later, set a breakpoint on the strongest candidate;
observe which instruction reads or writes it;
follow the stored address leading to the object and inspect nearby fields.
The first lab stops after scanning, filtering, and testing a candidate.
The breakpoint and object steps become practical after Chapters 2 and 3
teach the required tools and pointer model.
Inside one scan, the scanner follows an algorithm over readable regions:
ranges of numbered locations the operating system currently permits it to
read. It copies one limited chunk at a time:
An algorithm is a repeatable set of steps. The scanner does not know
what gold means; it compares bytes according to the value and type you
supplied. Even a small choice such as checking only every fourth byte
can miss a value starting at one of the positions between those checks.
Each controlled change throws away the candidates that did not change the same
way. The numbers below are only an example, but the shape is typical:
first scan for 1004,012
after spending 25 gold37
after a second purchase3
write breakpoint1 instruction
Scanning narrows the addresses; a write breakpoint then names the instruction that stores to the survivor, which ties the address to the game's own code rather than to a coincidence of values.
An address found once is only a candidate for that run; its object can even
disappear before the run ends. Restarting the game shows whether it is
repeatable; the module-relative path described above is what can carry over
to a new run of the same build.
Each kind of check rules out a different mistake. A second scan with a
different amount rules out an unrelated value that happened to match once. A
write breakpoint pauses the game when an instruction writes to a chosen
address. Chapter 2 teaches how to use one. It shows which instruction
changed the candidate; a restart shows whether the path survives a new
process.
If a scanner is slow, ask where its time goes. A bottleneck is the step
that limits the overall work. It might be reading another process, comparing
many values locally, sorting the remaining candidates, or writing a large log.
Finding a number and accessing its bytes are different operations, and neither
requires sorting the results unless the tool needs that order.
Measure a repeatable scan before changing its design. Record how many reads it
made, how many bytes it compared, how many matches it kept, and the time spent
in the expensive stages. Improving comparisons will do little if most time is
spent waiting for reads. Keep the first working experiment small enough to
understand; Sampling Game State Under Load later handles repeated
observations and slow log writes. Larger collections and more workers are
choices to evaluate against a measured need.
If this is the game-play gold field, changing 75 to 100 should allow a purchase
that costs more than 75, and the value should remain consistent after the next
game update.
Then:
save the original bytes or value so you can restore them;
confirm the value still matches what you expect;
apply one bounded change;
observe the immediate and next-update results;
restore the original state;
repeat from a known starting condition.
If only the text changes, you probably found a display copy. If the value snaps
back, another system owns or recomputes it. If unrelated fields break, your type,
width, or address may be wrong.
Compare the expected and observed result at the first step where they
diverge. Suppose you expected to read 75 before a write but actually read 74.
Investigate that mismatch before explaining the later failed purchase.
A log records events and values without requiring you to inspect them
immediately. A useful entry could include the operation, expected amount,
actual amount, and stage:
That entry explains why the tool should reject the write. For a failed process
read, record the reported error and how many bytes were read too. Keep the
target build and tool version with the run so the entry retains its meaning.
A debugger lets you pause at the suspicious step and inspect its values and
instructions. Pausing changes timing: other work may wait, or an input or
timeout may behave differently. Logging also adds work, especially when it
writes to disk. If a failure disappears when you add diagnostics, that is
evidence to investigate the timing change; it does not establish that the
underlying problem is fixed. What a Debugger Can Reveal explains
the pause, while the later sampling lesson shows how to limit recording delays.
Separate code that decides what should happen from code that performs a
Windows API call. In this toy decision function, a write is allowed only when the
current value still matches the value we expected and the replacement is
at most 10,000. That limit is an example policy for this exercise, not a
universal gold limit. Pure logic like this can be tested without
launching the game:
The first test passes because 74 is different from the expected 75, so
should_write returns false. ! reverses that boolean, making the
assertion true. The #[test] marker tells Rust’s test runner to call the
function, and assert! fails when its expression is false. The other tests
exercise a permitted write and both sides of the 10,000 limit. The
Rust test guide
explains these test features. The compiler checks types, tests check examples,
and the debugger checks the running game. Each one catches mistakes the other
two cannot. This function checks the supplied sample; the game may change
again after that sample was read.
Lesson 4.3 rechecks the
value immediately before a write and reads it back afterward.
Keep the smallest implementation to the contract above, with the decision
separate from process access so you can test it with ordinary values. Reuse
an existing tool or library when it supports your target and you can check
its behavior, version, error handling, and maintenance. Writing every
dependency yourself does not make the experiment more reliable.
As repeated code or new use cases appear, give each responsibility a small
interface. Refactoring changes the organization while preserving the
intended behavior; keep the existing examples and tests working as you do it.
Tool Architecture, Commands, and Cleanup develops this separation
when several features share observations and must coordinate changes.
Review small changes as you work. A diff compares edited source with its
previous version, making an accidentally changed field width or missing check
easier to see. A walkthrough follows one example through the complete read,
decision, write, and restoration path, often with another reader. Automated
checks and these reviews answer different questions: passing a logic test does
not prove that the live address is the game’s gold field.
Start documentation with the question, the tool’s purpose, and its limits.
Then show the route through the experiment before detailing each step. A
reader needs to know which file or command to begin with, what result to
expect, and what a failure means. Define names consistently: if gold means
the amount used by the purchase check, call the screen copy displayed_gold throughout.
Keep a compact experiment record:
Reference environment: game version and build identity, architecture,
operating system, tool versions, and the save or map used.
Reproduction: starting state, exact actions or commands, expected result,
observed result, and how to restore the original state.
Evidence: candidate values, relevant instructions or logs, failed checks,
and which explanations remain possible.
Decisions: why you chose this candidate, data type, limit, or dependency;
include the observation that would make you reconsider it.
For a tool you build, document one complete supported path from a fresh source
checkout to a runnable artifact, including the compiler, target, dependency
versions, required options, and tests. Keep that path consistent with the
instructions you actually verify. Reproducible steps let another person repeat
the experiment; they do not automatically guarantee an identical executable
byte for byte. How Source Becomes a Running Process explains the
build stages, and the next lab fixes the environment for this book’s first scan.
Read the record from a newcomer’s position. If an instruction says “use the
usual settings,” supply the settings. If a screenshot is the only record of a
result, write down the important value and condition too. Update the record
when the tool or target changes so an old address or command is not mistaken
for a current promise.
The record above holds statements you trust for different reasons, and the
reason decides what can be built on each one. Four kinds cover most of them:
Kind
What makes it that kind
From the purchase
seen
you watched it happen
the display went from 100 to 75
worked out
arithmetic on things you saw
the item cost 100 − 75 = 25
guessed
an explanation no test has checked yet
the address the scan kept holds the gold the purchase check reads
open
a question, with the test that would answer it
which instruction writes that address? A write breakpoint on it during one purchase shows each one
A guess becomes seen only when a test you ran shows it; an explanation that
merely sounds right leaves it a guess. A search that finds nothing is the claim
most often given the wrong kind, because its result covers only what it
searched.
Predict first
After the purchase you scan every readable region for the four-byte whole number 75 and get no matches.
Which line belongs in the record?
Answer: No readable region held 75 as a four-byte whole number when the scan ran. That is exactly what the scan checked, and nothing more. Every other way the game could store gold stays open until a search that looks for it runs.
The scan compared memory with the bytes 4B 00 00 00: 75 is 0x4B
(4 × 16 + 11), stored lowest byte first. Gold kept as a 32-bit floating-point
number (Rust’s f32) would be the bytes 00 00 96 42 instead, which
How Bytes Become Numbers and Pointers
works out, so a whole-number scan can never match it. A scan that checks only
every fourth position also misses a value that starts between them. Each of
those possibilities stays open until its own search runs: a scan for the f32
value 75.0, or one that checks every position.
A matching address is a lead. A repeatable action and a predicted check or calculation
give it meaning; controlled changes and a breakpoint narrow the lead.
A reversible edit tests whether that address changes the amount a purchase reads or
only a displayed copy. The compiler and tests check the tool’s logic, while the
debugger observes what the running game actually does.
Lesson 1.7 prepares a repeatable Windows lab. Lesson 1.8
shows what its scanner will read, and Lesson 1.9 walks
through the first gold scan with screenshots. Its 15-gold recruit is a
separate game action with its own starting value and observed result.
Lesson 1.7
A Windows Lab You Can Reset
Before using a scanner or debugger on the course game, set up a place
where the game, tools, and starting conditions can be repeated. This
lets you tell a real game change from a difference in the environment.
Use a separate Windows virtual machine for the book. A virtual machine (VM)
is a computer simulated inside your main computer. It can run its own copy of
Windows and can be reset without reinstalling your everyday system.
A VM gives the experiments a controlled environment:
old game builds and debugging tools stay separate from personal files;
a snapshot can restore a known state after a crash or bad configuration;
the Windows and tool versions stay fixed, so results repeat;
optional connections to the host can stay disabled until needed.
A snapshot is not a security guarantee and does not make every action harmless.
It is a recovery point. Test that you can restore it before depending on it.
The same caution covers what you bring into the VM. A program can do exactly
what it advertises and something else at the same time: a “free trainer” that
really does give you gold while it quietly copies the passwords saved in your
browser. That is called a trojan, after the wooden horse, and it is one of
the most common ways players lose accounts, because the cheat working is exactly
what makes people trust it. Every tool in this book comes from its official
project page or is one you build yourself.
VirtualBox (Recommended: This lesson’s screenshots and VBoxManage commands use VirtualBox, so its menus match what you see here. VMware and Hyper-V work too, but you then find the matching menu yourself.), VMware, or Hyper-V can work. A practical starting configuration is:
2 or more virtual CPU cores;
4–8 GB of RAM;
about 50 GB of storage;
graphics acceleration if a target game requires it.
Install Windows, apply updates, and create a normal user account. Run an
individual tool with additional rights only when its operation requires them.
Keep shared folders, clipboard sharing, drag-and-drop, and USB passthrough off at
first. Enable a connection only when you need it and know what crosses it.
rustup (Reference: The official Rust installer and version manager. It installs rustc and Cargo into your own user folder.) for the compiler and Cargo;
Visual Studio Build Tools (Reference: Microsoft’s own C++ compiler, linker, and Windows libraries, without the full Visual Studio editor. Rust on Windows uses its linker.) with “Desktop development with C++” so the MSVC
linker and Windows libraries are available;
x64dbg (Reference: An open-source debugger for Windows programs, with a 32-bit and a 64-bit version. Download it from the project’s own page.) for debugging 32-bit and 64-bit programs;
Cheat Engine (Reference: A memory scanner and debugger, used here for the first scans. Read each installer screen, and decline any offer of extra software.) for the first value-scanning lessons;
Wireshark (Reference: A free tool that records and displays network traffic. The networking lessons later in the book use it on your own computer.) for later local network captures;
Git (Reference: The version-control tool. The labs expect the course repository at one exact revision, so your results match the lessons.), so you can use the exact course source revision.
Open PowerShell and check the compiler tools:
Terminal window
rustc --version
cargo --version
Then verify that a tiny program builds:
Terminal window
cargo new first_lab
cd first_lab
cargo run
You should see Hello, world!. If this step fails, fix the toolchain before
adding debuggers, games, or course code to the problem.
The specific Wesnoth, AssaultCube, Urban Terror, Wyrmsun, and Flare builds used
in the historical lessons are 32-bit Windows programs.
Terminal window
rustup target add i686-pc-windows-msvc
Read the target name in parts:
i686 — 32-bit x86 instructions and plain pointers, which store
32-bit addresses;
pc-windows — Windows is the target operating system;
msvc — use Microsoft’s Windows ABI and linker toolchain.
Code loaded inside a process must match that process’s architecture. A 64-bit
DLL cannot be loaded into a 32-bit game. Later code changes, register choices,
stored-address widths, and calling conventions also depend on the architecture.
An external tool can sometimes have a different bitness because Windows copies
bytes across the process boundary. The course keeps its Windows labs 32-bit so
addresses and machine-code examples stay consistent. For another game build,
inspect its executable and choose the matching target instead of copying this
choice blindly.
cargo check parses and type-checks the programs without producing a final
optimized build. It is the fastest first check for a compiler or dependency
problem.
The workspace uses the windows crate for typed Windows API declarations and
anyhow to attach useful context to errors. Cargo reads those dependencies from
Cargo.toml; you do not need to copy a long feature list from the lesson.
When a lesson needs an executable or DLL, build the release version:
That command produces a 32-bit Windows build even if the development VM is
64-bit. If it fails, the first error tells you whether the target toolchain,
project code, or a Windows library needs attention; a successful build alone
does not prove the tool can attach to the course game.
Use a snapshot to return one VM to recorded files and settings. Use a
clone when you want a separate experiment copy beside the baseline.
VirtualBox offers two disk arrangements: a full clone copies the
dependent virtual disks; a linked clone writes into its own differencing
disks while relying on source base disks. Keep that base available. The
VirtualBox cloning guide
describes these choices and its network-adapter address policy.
Disk separation is one boundary. Before starting both copies, review their
network adapter MAC addresses, guest host names and fixed IP settings, and
host sharing. A MAC address identifies an adapter on its local network;
cloning disk content alone does not make all copied network settings unique.
Changes in a shared host folder are outside the guest disk snapshot.
The model below changes an invented save in an experiment copy, then resets
it. Choose how the copy was prepared and inspect the disk dependency.
A recovery point and a second VM solve different problems. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
baseline: save_gold = 100
choose snapshot, linked_clone, or full_clone
prepare a separate test copy, if cloning
test_copy.save_gold = changed_gold
check baseline and disk dependencies
restore the snapshot or reset the test copy
Reset returns the test save to 100. You have one VM at its recorded starting condition.
Final values: choice = snapshot; changed = 75; baseline = 100; test = 100; disk dependency = snapshot disk chain; second VM = false.
Read every step of the lesson’s example
Step 1, line 1. The known starting save records 100 gold. We want to compare a change against this starting condition.
choice = snapshot; changed = 75; baseline = 100; test = 100; disk dependency = snapshot disk chain; second VM = false.
Step 2, line 3. A snapshot records a recovery point for this VM. It does not create a second machine for simultaneous comparison.
choice = snapshot; changed = 75; baseline = 100; test = 100; disk dependency = snapshot disk chain; second VM = false.
Step 3, line 4. The same VM now saves 75. Its recovery point still describes 100.
choice = snapshot; changed = 75; baseline = 100; test = 75; disk dependency = snapshot disk chain; second VM = false.
Step 4, line 5. Restoring will rewind the same VM to the recovery point. It does not undo changes to an external shared folder.
choice = snapshot; changed = 75; baseline = 100; test = 75; disk dependency = snapshot disk chain; second VM = false.
Step 5, line 6. Reset returns the test save to 100. You have one VM at its recorded starting condition.
choice = snapshot; changed = 75; baseline = 100; test = 100; disk dependency = snapshot disk chain; second VM = false.
Keep the baseline stopped while preparing a named experiment copy. Start
from the recorded course build and your own resettable test program or
save. A separate VM supports comparison and recovery; it does not prove
that network peers or host files are isolated.
Recovery cheatsheet: a snapshot rewinds one VM; a full clone copies its virtual disks; a linked clone keeps a dependency on its base disks. Host sharing and networking need separate checks.
When you later record an address (a location in the running game), an
offset (a distance from a chosen start), or a byte pattern (a sequence
to search for), record the exact environment too. The next lesson develops
these ideas; their usefulness depends on:
the game name, exact build, and architecture, because a different build can
move every function and field;
the executable’s file hash (Example: In PowerShell, Get-FileHash .\wesnoth.exe prints the file’s SHA-256 hash. The same bytes always give the same hash, and changing a single byte gives an entirely different one.), a short fingerprint computed from its bytes:
matching hashes are strong evidence that two copies use the same build even
when their version labels are ambiguous;
the Windows version, because system DLLs change between updates;
the repository commit and the tool and debugger versions;
the starting save, map, or settings;
the VM snapshot the machine started from.
The snapshot restores files and installed tools, and source control restores
the exact code. The build and file hash tell you whether an address found last
week still applies to the game in front of you.
Before the first memory experiment, confirm that you can:
restore the “Ready for lessons” snapshot;
run cargo check in windows-labs;
launch the target game in the VM;
identify whether its executable is 32-bit or 64-bit;
name the exact build and starting state an experiment depends on.
The VM is ready. Lesson 1.8 now shows what a scanner means
by an address, a value type, and a pointer before you search the game.
Lesson 1.8
How Memory Actually Works
Before scanning a running game, we need to know what the scanner reads.
Suppose a health bar says 100. The player takes a hit, and the bar says
75. Somewhere inside the running game, a stored value changed.
This lesson gives you the first map of where that number lives and what it is
made of. You will use the same map when you find a value, follow a pointer, or
read another program’s memory later in the book.
We will distinguish locations, contents, types, and pointers here.
Lesson 3.6 later takes the bits apart one at a time,
including exactly how a fractional number like 10.0 is stored.
People use the word for several kinds of storage, and mixing them up causes a
lot of early confusion.
Layer
How big, how fast
What lives there
CPU registers and caches
tiny, extremely fast
the handful of values the CPU is working on right now
RAM
large, fast
the running game’s code, objects, and current state
storage drive
huge, slow
the game’s files, art, settings, and saved games
There are layers because no single technology is fast, large, cheap, and
permanent all at once. Fast memory costs a lot, so you get a little of it. Cheap
storage is slow, so it holds the things you are not using this instant.
From here on, when this book says “memory” it means RAM. That is where your
health value lives while you are playing. Turn the computer off and it is gone —
which is exactly why saving a game has to write a file to the drive.
Picture a process’s memory as an enormous row of boxes. Each box holds one
byte, and each box has a number called its address. The number tells
the process where to look during that run; it need not identify the same data
after the game restarts.
That really is most of the idea. Memory is boxes, one byte each, each with an
address. Here are four of them:
0x100064
0x100100
0x100200
0x100300
Four boxes of RAM. The small label is each box's address; the number inside is the byte stored there.
Lesson 1.2 introduced hexadecimal as another way to write numbers. The
0x prefix on an address announces it. Inside the boxes, each byte is shown
as two hex digits with the prefix left off, which is also how debuggers and
hex dumps display bytes. The next two sections show why the first byte,
64, represents decimal 100.
A bit is a single switch that is either 0 or 1. A byte is eight bits in a
row. The question is how eight 0s and 1s can mean a number like 100.
The answer is the same trick ordinary numbers use: position. In the decimal
number 100, the 1 is worth a hundred only because of where it sits. Each
position in decimal is worth ten times the position to its right — ones, tens,
hundreds, thousands.
Binary works the same way, except each position is worth two times the
position to its right. Starting from the right, the eight positions in a byte
are worth:
128 64 32 16 8 4 2 1
To read a byte, add up what the positions holding a 1 are worth. The positions
holding a 0 add nothing. As a formula, where b at position i is the bit
that sits i places from the right:
value=i=0∑7bi2i
1280
641
321
160
80
41
20
10
64 + 32 + 4 = 100
The byte 0x64, bit by bit. Each small label is what that position is worth. The three highlighted 1 bits add up to 100.
With every bit set, a byte holds 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255. With
none set, it holds 0. So a byte has exactly 256 possible values, 0 to 255.
Lesson 1.10 uses those positions to inspect flags, build masks,
and compare shifts with rotations.
Eight 0s and 1s are tiring to read, so people write bytes in hexadecimal,
which is base 16. It counts with sixteen digits: 0 to 9, then A to F
standing for 10 to 15.
Hex fits bits perfectly. Four bits have the place values 8, 4, 2, and 1, so they
can hold anything from 0 (all off) to 15 (all on) — exactly the range of one hex
digit. Split a byte into two halves of four bits and each half becomes one hex
digit:
80
41
21
10
80
41
20
10
4 + 2 = 6, hex 6
4 = 4, hex 4
The same byte split into two four-bit halves. Each half is one hex digit, so the byte is written 0x64.
That is the whole reason debuggers use hex: two hex digits are always exactly
one byte, so you can see where each byte starts and ends. The same number
written in decimal, 100, hides that. It gets more obvious with bigger numbers:
decimal 4096 looks arbitrary, while 0x1000 is clearly a round figure in memory
terms.
You do not need to convert hex in your head. Recognise the 0x, and use a
calculator until it becomes familiar. Converting by hand works like decimal
with sixteens instead of tens: 0x64 is six sixteens plus four, 6 × 16 + 4 =
100.
There are several ways to check a conversion, whichever tool is open. Each one
turns 0x64 into 100 and back:
Windows Calculator
Open the menu and choose Programmer. Select HEX, type 64, and the
DEC line shows 100. Select DEC, type 100, and HEX shows 64.
One box holds one byte, and one byte only goes up to 255. Anything bigger has to
spill into the boxes next door. A u32 — the type name for an unsigned 32-bit
whole number, explained properly below — uses four boxes in a row.
Once a value spans several boxes, the value needs a byte order: which box holds the low byte, then the next byte, and so on.
Think about the decimal number 1,234. The leading 1 is worth the most — it means
a thousand. The trailing 4 is worth the least. The bytes inside a multi-byte
value work the same way, and the byte worth the least is called the
least-significant byte.
Windows games on x86 and x86-64 put the least-significant byte in the
lowest-numbered box (Alternative: The opposite order, most-significant byte first, is called big endian. Internet protocols traditionally use it, and Lesson 9.4 reads Wesnoth's four-byte frame length that way.). That arrangement is called little endian.
Example1,000 in four boxes
Each box counts in units 256 times bigger than the box before it, because one
byte holds 256 different values — the same reason each decimal column counts in
units ten times bigger than the one to its right. So the four boxes of a u32
count ones, 256s, 256 × 256 = 65,536s, and 256 × 256 × 256 = 16,777,216s. Here
is the number 1,000, which is 0x3E8 in hex:
0x1000E8
0x100103
0x100200
0x100300
× 1
× 256
× 65,536
× 16,777,216
0xE8 is 232, and 232 × 1 + 3 × 256 = 1,000
Little endian: the byte worth the least sits in the lowest-numbered box.
Same number, two byte ordersThe value stays the same; its byte positions change.
1,000 decimal0x000003E8most significant → least significant
Little endian
Least significant byte at the lowest address
Addresses increase ↓
0x1000× 1
0x1001× 256
0x1002× 65,536
0x1003× 16,777,216
232 × 1 + 3 × 256 = 1,000
Big endian
Most significant byte at the lowest address
Addresses increase ↓
0x1000× 16,777,216
0x1001× 65,536
0x1002× 256
0x1003× 1
3 × 256 + 232 × 1 = 1,000
1,000 occupies four bytes. Little endian: E8 03 00 00. Big endian: 00 00 03 E8.
Point to or select a byte to find the same significance in both layouts. Byte order rearranges whole bytes; it does not reverse the bits inside each byte.
The practical effect is that reading a hex dump left to right shows the bytes in
the opposite order from how you would write the number: E8 03 00 00, not
00 00 03 E8.
This trips up nearly everyone at least once. Expect the reversal every time you
compare a number you worked out against bytes you actually saw.
Now the four boxes from the start of the lesson make sense. 64 00 00 00 is
0x64 ones plus nothing else, which is 100.
Look again at those four boxes. There are three separate things going on:
0x1000 is a location — which box;
64 00 00 00 is a pattern of bytes starting in that box;
100 is one interpretation of that pattern.
0x100064
0x100100
0x100200
0x100300
the byte pattern 64 00 00 00
one interpretation: the u32 value 100
The address names one box. The pattern spans four. The number 100 appears only when something reads those four boxes as a u32.
The third one does not follow automatically from the first two, and this is the
single most important idea in the lesson. Those bytes only become the number 100
once something decides to read four boxes, treat them as one whole number, and
use little-endian byte order. Read the same boxes with a different type or byte order
and the very same bytes mean something else, as the next section shows.
So never treat “address” and “value” as the same word. An address tells you
where to look. A value is what you get after you look and decide how to read
what is there.
A type answers two questions: how many bytes to read, and what the bits
inside them mean.
lethealth: u32 = 100;
letspeed: f32 = 3.5;
letalive: bool = true;
The names are for you. The types are for the compiler. These are the ones you
will meet most often:
Number types at a glance
Rust type
Size
What the bits mean
u8
1 byte
a whole number from 0 to 255
u16
2 bytes
a whole number from 0 to 65,535
u32
4 bytes
a whole number from 0 to 4,294,967,295
i32
4 bytes
a whole number from −2,147,483,648 to 2,147,483,647
f32
4 bytes
a number that can have a fractional part, such as 3.5
f64
8 bytes
the same idea as f32, with more precision
bool
1 byte
0 means false, 1 means true
The names follow a pattern. The number is the size in bits. u means
unsigned: the value can never be negative. i means a signed integer,
which can be. f means floating point, a number with a fractional part.
The ranges come from counting patterns. One bit has 2 patterns, and every extra
bit doubles the count: two bits have 4, three have 8, and eight have 256, which
is why a u8 stops at 255. An i32 has exactly as many patterns as a u32,
but it spends half of them on negative numbers, which is why its range is split
around zero. Lesson 3.6 counts the 16-
and 32-bit patterns and shows which patterns the negative numbers use.
u32 and f32 are both four bytes, but they use those 32 bits in completely
different ways. A u32 reads all 32 bits as one whole number. An f32 splits
them into three parts — a sign, a power of two, and the digits of the number —
much as scientific notation writes 1,500 as 1.5 × 10³.
So one four-byte pattern holds two totally different numbers, depending on which
type you ask for:
00
00
20
41
read as u32: 1,092,616,192
read as f32: 10.0
One four-byte pattern, read two ways. Both readings are correct; they answer different questions.
The u32 reading uses only what this lesson has covered: written most
significant first, the bytes are 0x41200000, and a calculator turns that into
1,092,616,192. The f32 reading decodes a sign, an exponent, and a fraction; those calculations are not covered here, so Lesson 3.6
works through them bit by bit, all the way to 10.0. What matters now is that
nothing in memory changed between the two readings. Only the type did.
Floats have one habit worth knowing from the start: most fractions cannot be
stored exactly. Ask an f32 to hold 10.1 and it actually holds 10.100000381…,
the nearest value its 32 bits can express;
Lesson 3.6 derives that value.
That is why float values in games are rarely round, and why memory scanners let
you search for a float within a small range instead of only an exact value: a
scan for exactly 10.1 would miss the stored value.
An array puts equal-sized values back to back. A 3D position is often three
f32 values in a row, one each for x, y, and z:
+0x0000
00
80
3F
+0x0400
00
00
40
+0x0800
00
60
C0
x = 1.0
y = 2.0
z = −3.5
A position stored as three f32 values. The labels are offsets from the start of the position.
Each value is four bytes, which is why the labels go up by 4 and not by 1.
Text and lists need additional layout information: where the data starts and how its end is found. A C string (Reference: Lesson 3.11 shows how text becomes bytes in UTF-8, UTF-16, and null-terminated C strings.) just runs until it hits a
zero byte. Other kinds of string store a pointer and a length. A growable list
usually stores a pointer to its contents plus a length and a capacity. The code
that reads a string or list is what tells you which layout a game uses.
When you inspect a compiled game without useful debug information, many source
names and type declarations are unavailable. The compiler used them to choose
instructions, and those instructions provide clues about how the game uses
the bytes:
how many bytes an instruction reads or writes at once: 1, 2, 4, or 8;
which kind of instruction does it — x86 has separate instructions for
floating-point values, so a float load means an f32 or f64;
what the code does with the result — compares it with a signed or unsigned
test, adds it to an address, passes it to a drawing function;
how the bytes change when you make the game change;
whether nearby bytes behave like parts of the same object.
This is also why a memory scanner asks what kind of value you are hunting before
it searches. The whole number 10 is stored as 0A 00 00 00. The float 10.0 is
stored as 00 00 20 41. The two patterns do not share a single byte, so a
search for one can never find the other.
This interactive lab needs JavaScript. The lesson text around it still explains the same idea.
A pointer is a number that happens to be an address
Every value so far has been data about the game: 100 gold, 10.0 metres per
second. A pointer is a value too — an ordinary number, stored in ordinary
bytes, in an ordinary box. What makes it a pointer is only what that number
means: it is the address of something else.
Nothing in the bytes marks them as a pointer. 00 50 00 00 could be the number
20,480 — 0x50 is 80, and 80 × 256 = 20,480 — or it could be a pointer to
address 0x5000, which is the same number written in hex. As always, the code
that reads them decides which.
Here is the whole idea. Four ordinary bytes at 0x2000:
0x200000
0x200150
0x200200
0x200300
read together as a u32: the number 0x5000, where the player object starts
Three different numbers are involved, and running them together is the most
common early mistake by a wide margin:
Number
What it is
Here
where the pointer lives
an address
0x2000
the pointer’s own value
also an address
0x5000
what it points at
the actual data
the player object
Read address 0x2000 and you get 0x5000. That is not the player. To reach the
player you read a second time, now from 0x5000. That second read is called
dereferencing the pointer.
Think of a library catalogue card. The card is not the book. It sits in a
drawer at its own location, and all it carries is a shelf number. Finding the
book is two separate steps: read the card, then walk to the shelf.
That comparison is worth keeping because it survives being pushed on. The
drawer slot is 0x2000. The shelf number written on the card is 0x5000. The
book is the player object. And here is the part that matters most: if a
librarian removes that book and shelves a different one in the gap, your card
still reads 0x5000 and still sends you confidently to a real shelf holding a
real book — the wrong one. Nothing about the card looks damaged.
That is not a flaw in the comparison. It is precisely what can go wrong with a
stale pointer. Lesson 3.10 shows the address-reuse example in
memory.
Because the object can move. It can be destroyed and rebuilt somewhere else.
When the game updates the pointer at 0x2000, code that reads it again can
follow the new address instead of holding on to the old one.
Following the pointer works only while the box at 0x2000 itself still holds
the game’s current pointer. Lesson 2.8
finds such a starting box that survives a restart and follows a chain of
pointers from it.
A pointer is not guaranteed to stay good, either. The object can be removed, the
memory reused, a module unloaded. An address that is not zero can still be
completely wrong.
In source code a pointer usually has a type attached. Rust writes a raw pointer
to a u32 as *const u32, read aloud as “a pointer to a u32”. That type
describes the data at the other end, not the pointer itself, and it is called
the pointee type. It tells the program how many bytes to read at the
destination and how to interpret them.
Follow one pointer value, 0x5000, with three different pointee types. The
bytes waiting at 0x5000 are the 00 00 20 41 pattern from the type section:
0x500000
0x500100
0x500220
0x500341
through a *const u32: 4 bytes, 1,092,616,192
through a *const f32: 4 bytes, 10.0
through a *const u8: 1 byte, 0
One address, three pointee types, three different reads. The pointer held the same number every time.
The pointer’s own size has nothing to do with its pointee type. For the plain
raw pointers to sized values used in this book’s x86 labs, it is set by the
process architecture: 4 bytes in a 32-bit game and 8 in a 64-bit game, whether
it leads to a single byte or a 2,000-byte player object.
Lesson 3.6 shows why, and
how the pointee type also decides how far a pointer moves when it steps to the
next item in a list.
Say a player object starts at 0x5000, and its health sits 48 bytes into it.
object base = 0x5000
field offset = 0x30 (48 in decimal: 3 × 16)
health address = 0x5000 + 0x30 = 0x5030
0x5000object starts
· · ·
0x5030health
the offset: 0x30, or 48 bytes along
The base says where the object starts. The offset is the distance to the field. Their sum, 0x5030, is where health is read.
Three different numbers, doing three different jobs. The base says where the
object starts. The offset says how far in the field is. The sum says where
to read.
An offset is not an address and it is not the health value. On its own it means
nothing at all — 0x30 is just “48 bytes along from something.”
Offsets matter because they may outlast one address. The object’s base address
may land somewhere new every time the game starts, while the health field can
remain 48 bytes into the same object layout for that build. Verify the offset
again when the game or layout changes.
Watch the pointer read and the field calculation together. Notice when
a number is being read from memory and when it is being used as an address:
Watch it happenFrom a stored pointer to a field of the object it points to
Four bytes at 0x2000, 00 50 00 00, are reversed and read as the little-endian address 0x5000, the base of a player object. An address marker then moves 0x30 bytes along the object from 0x5000 to 0x5030, and the health field is read at that destination.
Read the steps
The pointer lives at 0x2000, and its four bytes are 00 50 00 00. This is where the pointer is stored, not where the player object starts.
Read the four bytes as one little-endian number. The byte at the lowest address counts least, so reading from the highest address back gives 00 00 50 00, which is 0x5000: the start of the object.
Locate the field: add the verified health offset, 0x30. This only calculates an address: 0x5000 + 0x30 = 0x5030. Nothing has been read from there yet.
Read the health field at 0x5030 using its verified type. The value comes from the bytes there, not from the pointer value 0x5000 or the offset 0x30.
Two memory reads with an address calculation between them: first obtain the object base, then locate and read its health field.
The pointer and offset example gives you a way to find a field now. It does
not promise that the object will occupy the same address later. Two common ways
of handing out memory help explain why.
A function can use its thread’s stack for temporary values. Its stack
frame lasts through that call; later calls can reuse the same bytes. An
allocator can give a longer-lived game object space on the heap. That object
may survive many calls, but the game can eventually remove it and reuse its
address for a different object. A growing collection can also move its
contents. Lesson 3.8 draws a call frame in memory, and
Lesson 3.10 follows a heap address from one object to another.
Lesson 2.7 then builds pointer paths that can survive a moving
object base.
A debugger shows virtual addresses in a particular process. The game’s
0x5000 and your tool’s 0x5000 need not name the same bytes. An external tool
therefore opens a process handle and asks Windows to read an address in
that game’saddress space. Windows also divides the space into protected
pages; a number that looks like an address may not be readable at the moment
you try it. Lesson 3.7 works through a remote read, and
Lesson 6.5 examines the page map in detail.
A scanner copies bytes while the game continues to update them. Its result is
a snapshot gathered over a short stretch of time, not a promise that every
field describes one instant. This is a copy of selected memory, unlike the
whole virtual-machine recovery snapshot from Lesson 1.7. For example, if the
game removes one enemy after
you read its pointer and reuses that address before you read its health, both
reads can succeed while your combined answer describes neither enemy
correctly. Re-check a promising candidate and its object identity before
trusting it. Lesson 3.7 shows that sequence and the checks an
external reader needs.
A byte is the sum of the place values of its set bits. On x86, a hex
dump shows a multi-byte u32 least-significant byte first. An address,
the bytes at that address, and the value produced by interpreting those
bytes are different things; four identical bytes can mean a whole
number or a float when read with different types.
A pointer has a size set by the process, while its pointee type controls
the value read after following it. base + offset computes the address
of a field. Stack and heap allocations have different lifetime patterns,
and another process has its own virtual address space. A memory scan
reads state while the game can continue changing it; it is not an
instantaneous frozen copy.
Lesson 1.9 uses these distinctions to scan a real gold
value, narrow the candidates, and test one of them in the game.
An address names a location, while a type tells a program what its bytes
mean. We will use a memory scanner on Wesnoth: observe a gold value,
search for matching bytes, make one in-game change, and filter the results.
The goal is not merely to reach a final address. The goal is to understand why
each scan removes some candidates and why one test still does not prove that a
candidate is the main game-play value.
Value Type:4 Bytes (Explanation: Four bytes hold one 32-bit whole number, the same size as Rust’s i32 or u32. The scanner must read all four bytes at an address to see the number the screen shows.);
Scan Type:Exact Value (Alternative: If a game never shows the number, scan for an unknown initial value instead. After each change in the game, keep the addresses that changed, increased, decreased, or stayed the same.);
Value: the current gold amount.
Start the first scan.
Straight after the first scan, many addresses hold the same number.
The scan will probably find many addresses. That is expected. The number 100 can
appear in timers, object fields, capacities, interface data, and unrelated
allocations.
At this point, each result proves only this:
These four bytes were readable as the integer 100 when the scanner checked
them.
Return to Wesnoth and perform the planned action. Recruit the unit and read the
new gold value off the screen.
After recruiting, the new gold amount is the next value to search for.
If the value changed from 100 to 85, return to Cheat Engine, enter 85, and choose
Next Scan.
The scanner does not search the whole process from the beginning. It checks the
previous candidate addresses and keeps only those that now contain 85 (Example: Say the first scan found three addresses holding 100: A, B, and C. After the purchase only B and C hold 85, so A is dropped, and the next scan checks just B and C.). In set
notation:
You do not need the symbol memorized. It means “keep items that are in both
groups.”
What Next Scan does with each candidate. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
candidates = [A, B, C] # every address that held 100
# you recruit a unit; the screen now shows new_gold
kept = []
for addr in candidates:
now = read_4_bytes(addr)
if now == new_gold:
kept.append(addr) # keep it
# otherwise it is dropped
candidates = kept
Next Scan leaves B and C. Both followed the gold, so one may be the field and the other a copy for the screen. Repeat with a different change to tell them apart.
Final values: candidates = B, C; new_gold = 85; kept = B, C; addr = C; now = 85.
Read every step of the lesson’s example
Step 1, line 1. The first scan for 100 left three candidates. Each one only proved that its four bytes read as 100 at that moment.
candidates = A, B, C.
Memory: address A = 100; address B = 100; address C = 100.
Step 2, line 2. The screen now shows 85. B and C changed with it, but A, which only happened to be 100, did not.
candidates = A, B, C; new_gold = 85.
Memory: address A = 100; address B = 85; address C = 85.
Step 3, line 3. Start an empty list for the addresses that survive.
Memory: address A = 100; address B = 85; address C = 85.
Step 4, line 4. Take the next candidate: A.
candidates = A, B, C; new_gold = 85; kept = (empty); addr = A.
Memory: address A = 100 (reading now); address B = 85; address C = 85.
Step 5, line 5. Read the four bytes at A right now. They read as 100.
candidates = A, B, C; new_gold = 85; kept = (empty); addr = A; now = 100.
Memory: address A = 100 (reading now); address B = 85; address C = 85.
Step 6, line 8. 100 does not equal 85, so A is dropped. Its value did not follow the gold.
candidates = A, B, C; new_gold = 85; kept = (empty); addr = A; now = 100.
Memory: address A = 100 (reading now); address B = 85; address C = 85.
Step 7, line 4. Take the next candidate: B.
candidates = A, B, C; new_gold = 85; kept = (empty); addr = B; now = 100.
Memory: address A = 100; address B = 85 (reading now); address C = 85.
Step 8, line 5. Read the four bytes at B right now. They read as 85.
candidates = A, B, C; new_gold = 85; kept = (empty); addr = B; now = 85.
Memory: address A = 100; address B = 85 (reading now); address C = 85.
Step 9, line 7. 85 equals 85, so B stays in the list.
candidates = A, B, C; new_gold = 85; kept = B; addr = B; now = 85.
Memory: address A = 100; address B = 85 (reading now); address C = 85.
Step 10, line 4. Take the next candidate: C.
candidates = A, B, C; new_gold = 85; kept = B; addr = C; now = 85.
Memory: address A = 100; address B = 85; address C = 85 (reading now).
Step 11, line 5. Read the four bytes at C right now. They read as 85.
candidates = A, B, C; new_gold = 85; kept = B; addr = C; now = 85.
Memory: address A = 100; address B = 85; address C = 85 (reading now).
Step 12, line 7. 85 equals 85, so C stays in the list.
candidates = A, B, C; new_gold = 85; kept = B, C; addr = C; now = 85.
Memory: address A = 100; address B = 85; address C = 85 (reading now).
Step 13, line 9. Next Scan leaves B and C. Both followed the gold, so one may be the field and the other a copy for the screen. Repeat with a different change to tell them apart.
Make another controlled change with a different result. For example, recruit a
unit with another cost or end a turn if the scenario changes gold in a known
way. Enter the new gold value and perform another Next Scan.
After Next Scan the candidate list is much shorter.
Using different values is important. If you repeat only 100 → 85 → 100, an
unrelated value that happens to follow the same pattern can remain in the list.
Continue until you have a small number of candidates. Do not assume the first
remaining address is correct.
Add one candidate to the address list. Before changing it:
copy its current value so you can put it back;
confirm the game still shows the expected gold;
choose a small replacement value;
write the replacement once;
observe the screen and one real purchase;
restore the original value (Recommended: Do this before any other test, even if the change looked harmless. A leftover value can make the next test start from a different state, and then two results can no longer be compared.).
Wesnoth after one candidate was edited: the display shows the new value.
Different results support different explanations:
What happens
Likely meaning
the amount changes and the next purchase uses that amount
strong candidate for game-play state
only the text changes
likely a display copy
the value changes back on the next update
another system recomputes or owns it
nothing visible changes
unrelated value or a copy used elsewhere
nearby behavior breaks
wrong address, type, or field width may have been changed
A single run can mislead, because an unrelated value might change at the same
moment by coincidence. Getting the same result again from the same starting
state makes that explanation less likely.
The game can legitimately keep more than one gold-related value:
the current simulation value;
a previous value used by an animation;
text prepared for the interface;
a value in a replay, event, or network message;
a temporary calculation.
That is why value scanning is usually the start of reverse engineering. A write
breakpoint later shows which instruction changes the strongest candidate. The
surrounding instructions can reveal the object base and other fields.
The address you found belongs to this run of the game. After a restart, gold
may live somewhere else, because the process layout and the heap can change
between runs. A later lesson on building and loading a program
explains why some distances stay fixed within one build even when absolute
addresses move. First, a debugger lets us follow the candidate value to the
instruction that changes it.
The first scan returned readable candidates that matched while it ran;
it did not identify “the gold address.” Each controlled game change
filtered that candidate set. Changing one survivor and then making a
real purchase showed whether it affected gameplay, only the display,
or neither. Restoring the original value leaves the game in a known
state for another run.
The candidate address tells us where to look, but not why the value changes.
When a purchase subtracts gold, which instruction performs the subtraction,
and what decides whether the player can afford it? What a Debugger Can
Reveal begins Chapter 2 with that question. We will pause the
program at the change, then learn how source code becomes the instructions
the debugger shows.
Lesson 1.10
Bits, Masks, Shifts, and Rotations
Lesson 1.8 explained how bytes hold a health number.
Sometimes a program uses the positions inside one byte separately. One
position can record “door open,” another “lamp on,” and another “alarm active.”
To inspect or change just one of them, we need bitwise operations:
operations on corresponding bit positions rather than on whole decimal values.
A bit is either 0 or 1. An unsigned byte has eight positions, numbered
from 0 on the right to 7 on the left. Position i contributes 2^i when
its bit is 1. The byte 00000101 has positions 2 and 0 set, so it represents
4 + 1 = 5. Binary literals in Rust begin with 0b.
bit 7: 1280
bit 6: 640
bit 5: 320
bit 4: 160
bit 3: 80
bit 2: 41
bit 1: 20
bit 0: 11
00000101 has two set bits. Its decimal value is 5, not the count of its set bits.
The number 5 alone does not tell us whether it means five coins or two
enabled flags. That meaning comes from the code and format using the byte.
For our invented status format, bit 0 means door open, bit 1 means lamp on,
and bit 2 means alarm active. The other five positions are reserved and
start at zero. Status 5 therefore means door open and alarm active.
A mask is a bit pattern used to select positions. A mask of 00000100
selects bit 2, whose weight is 4. AND, written & in Rust, keeps a bit
at 1 only when both corresponding input bits are 1:
status: 00000101 5
mask: 00000100 4
AND: 00000100 4
For this format, status & 0b00000100 isolates the alarm bit. Testing whether
that result is nonzero tells us whether the alarm is active:
For a mask containing several 1 bits, “nonzero” means any selected bit
is set. To require all selected bits, compare the result with the mask.
With mask 00000011, status 5 has only the door bit, so status & mask
is 1: nonzero, but not equal to 3. A zero mask selects nothing.
The bitwise & above produces a number. Boolean && combines true/false
conditions, such as “enough gold and item in stock.” They answer different
questions and accept different operand types.
Four operations explain the common edits. A 0 in a mask preserves the
input position for OR and XOR. AND preserves positions where its mask is 1.
Operation
Rust expression
Effect on one selected bit
AND
value & mask
Keep only selected 1 bits
OR
value | mask
Set selected bits to 1
XOR
value ^ mask
Flip selected bits
NOT
!value
Flip every bit within the value’s type
For our status byte, setting the lamp uses OR with bit 1. Clearing the alarm
uses AND with a mask that has zero at bit 2 and ones everywhere else.
NOT constructs that clearing mask. XOR toggles the door bit:
letstatus: u8 = 0b00000101; // Door and alarm are on.
letlamp_on = status | 0b00000010; // 00000111: all three are on.
letalarm_off = status & !0b00000100; // 00000001: only door is on.
letdoor_flipped = status ^ 0b00000001; // 00000100: only alarm is on.
These are three separate calculations from the same starting status. They
do not successively update one variable. OR sets an already-set bit to 1
again; XOR would switch it off.
Predict first
A game hides gold 125 (01111101) by storing 125 XOR 90, where 90 is 01011010. The stored byte is 39 (00100111).
Which calculation gets 125 back from the stored 39?
Answer: 39 XOR 90. XOR flips the positions where the key has a 1: bits 6, 4, 3 and 1 of 90. The first pass flipped those four bits of 125; flipping the same four again restores them. 00100111 XOR 01011010 = 01111101 = 125.
XOR with a key, then XOR with it againClick bits to change the value or the key. A 1 in the key flips that position; a 0 leaves it alone.
Value, bit 7 to bit 0
Key, bit 7 to bit 0
Encode, then decode with the same key
Encode: value XOR key
Value 125
bit 7bit 0
0
1
1
1
1
1
0
1
Key 90: 1 means flip
0
1
0
1
1
0
1
0
↓ flip where the key has a 1
Stored 39
0
0
1
0
0
1
1
1
Decode: stored XOR the same key
↓ flip the same positions again
Back to 125
0
1
1
1
1
1
0
1
125 XOR 90 = 39, and 39 XOR 90 = 125. The key flips the same 4 positions on both passes, so the second pass undoes the first.
Width matters for NOT. Flipping 00000101 as a u8 gives 11111010, decimal
250. A 32-bit value has 32 positions to flip. A bit pattern must be interpreted
with its type and width, just as a memory scan must choose its field width.
The starting example isolates the alarm bit: 5 AND 4 gives 4. Change the
byte, mask, or operation below. Use “Show me the result” whenever you want
the endpoint, then step back to inspect the selected positions. Mask and
count controls remain visible for comparison; each explanation states which
inputs the operation uses.
Change one byte, mask, or bit operation. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
let byte: u8 = 0b00000101;
let mask: u8 = 0b00000100;
let count: u32 = 1; // 0..=7
let result: u8 = byte & mask;
AND keeps a position at 1 only when both input bits are 1. The result is 00000100 = 4.
Final values: byte = 5; byte bits = 00000101; mask = 4; mask bits = 00000100; count = 1; operation = and; result = 4; result bits = 00000100.
Read every step of the lesson’s example
Step 1, line 1. 5 is 00000101. Add the weights of its 1 positions to recover the decimal value.
A logical shift moves bits left or right, fills the newly opened positions
with zeros, and discards bits that leave the chosen width. In Rust, << shifts
left; >> on an unsigned integer shifts right. For an eight-bit value:
start: 00000101 5
shift left 1: 00001010 10
shift right 1: 00000010 2
A left shift by one doubles the unsigned numeric value when the result
fits. A right shift by one divides it by two and drops the fractional part.
But 10000001 shifted left by one becomes 00000010: the original high
bit was discarded. It does not become the wider number 258.
Our controls restrict the count to 0–7 for an eight-bit shift. A zero count
leaves the byte unchanged. Do not assume that a count of eight is a portable
way to produce zero: count validity and machine count handling depend on the
operation and language. Rust’s
checked_shl and checked_shr
return None for a count at least the type’s width. They check the count;
they do not report every bit discarded by a valid shift.
A rotation moves bits in one direction but carries the outgoing bits to
the opposite end of the same width. Rotate 10000001 left once and its
leading 1 reappears at the right:
A rotation preserves the number of set bits, though their positions and the
numeric value can change. Rotating a byte by eight positions returns the
original byte. These methods and their distinction from shifts are documented
in the Rust u8 reference.
Suppose an invented byte stores an item kind in its upper four bits and a
quantity in its lower four. 10100011 means kind 10 and quantity 3. A field
is packed when it occupies selected bit positions rather than a whole
byte of its own.
letkind = packed >> 4; // Move high four bits down: 10.
The mask selects; the shift moves selected positions to weights starting at
one. These operations explain a packed value only after evidence establishes
its field boundaries. “A byte equals 163” alone does not prove this format.
Bit order within the number and byte order across several memory addresses
are separate questions; Lesson 1.8 develops byte order.
When Lesson 2.3 introduces assembly, recognize the same ideas
in and, or, xor, shl, and shr. Track the operand width and the result,
then investigate any flags or branches that use it.