# FireRed's Script Interpreter

Lecture ID: lecture_w08_d01
Week 8, Day 1

## Objectives
- Identify the FireRed files, structs, functions, constants, data tables, and state variables that implement each major game system.
- Connect week 8 material to the overall course plan.

## Transcript
All right, let’s pick up from the handoff point we carefully built last week. We are not starting today with raw input. We already traced that. We know the A button, D-pad movement, object-event lookup, coordinate triggers, and signpost handling are not magical direct calls into story content. They are field-control decisions. The important previous sentence is: the field layer eventually calls ScriptContext_SetupScript. Today we step through that doorway.

So, write this on the board: “FireRed story progression is bytecode plus mutable world memory.” That is the center of Week 8. The player thinks, “I talked to Oak,” or “that guard moved,” or “the door is now unlocked.” The engine thinks, “a script pointer was installed, command handlers advanced an instruction stream, flags and variables were mutated, and later branches observed those mutations.” That is a colder way to say it, but it is the architecture.

The source focus for today is the script engine in pokefirered/src/script.c, especially chunk_1be14bbdaf5c3400 for the core interpreter structure, chunk_03258f0288d89251 for script control flow, and chunk_d424688149aa3bcb for execution support. For command implementations, we use pokefirered/src/scrcmd.c chunk_c2a54fb6031b5886. For flags and variables, we use pokefirered/src/event_data.c chunk_a13cdbcb283dbf3b. Those are the files that turn authored event scripts into persistent game-state changes.

Now, before we read the interpreter as code, I want you to feel the design problem. Imagine a locked door in a creature RPG. The door says, “Only trainers with the Boulder Badge may enter.” How could we implement that?

A naive implementation would hard-code that one door in C. If player has badge, warp. Else show message. That works once. It fails as an architecture. Every guard, badge gate, story blocker, rival encounter, gift item, cutscene, and NPC would become a new C function or a large hard-coded decision tree. FireRed does not do that. FireRed puts a tiny interpreter between authored world content and engine systems. The authored content says, in effect, compare this variable, check this flag, branch to this label, show this message, set this flag, start this battle, warp to this map. The engine provides the commands.

So the first major mental model is that FireRed scripts are not C source running directly. They are bytecode-like command streams. The interpreter owns a pointer into that command stream. In the ScriptContext struct, declared through the script interface and implemented by the script engine in pokefirered/src/script.c chunk_1be14bbdaf5c3400, the key state is scriptPtr, mode, comparisonResult, nativePtr, stack, stackDepth, cmdTable, cmdTableEnd, and a small data array. That is almost everything you need for a compact virtual machine.

Let’s pause there. If I erase the Pokémon-specific names and just leave those fields on the board, what do we have? We have an instruction pointer, a mode flag, a comparison register, a native callback slot, a call stack, a command dispatch table, and local scratch storage. That is a VM. It is not a general-purpose VM like Lua or JavaScript. It is intentionally narrow. But architecturally, it is still a virtual machine.

Now, what does RunScriptCommand do? In pokefirered/src/script.c chunk_1be14bbdaf5c3400, the interpreter checks the context mode. If the mode is stopped, it returns false. If the mode is native, it calls a native C function pointer and uses the result to decide whether to continue into bytecode. If the mode is bytecode, it repeatedly reads one command byte from scriptPtr, advances scriptPtr, indexes into cmdTable, and calls the command handler. That is the central loop.

Notice the subtle contract: a command handler returns true when the interpreter should yield back to the surrounding engine, and false when the interpreter may continue immediately to the next script command. This is one of the most important details today. Script execution is not “one command per frame.” It is “run commands until a command says we need to wait.” A setflag command can run instantly and continue. A compare can run instantly and continue. A goto can change the pointer and continue. But a wait state, message wait, movement wait, fade wait, or native state machine can yield.

This connects directly to the callback and task architecture from weeks 1 through 6. FireRed already has a per-frame shell. AgbMain and SetMainCallback2 gave us the outer runtime structure earlier in the course, and RunTasks gave us per-frame task dispatch. The script interpreter plugs into that rhythm. It can run a burst of synchronous story logic, then stop when animation, text, movement, battle setup, or field transition needs time. So the script VM is not a replacement for callbacks and tasks. It is another state machine inside the same overall stack.

Check for understanding. If a script says, “set flag, set variable, compare variable, branch, show message, wait for button,” which parts must happen across multiple frames? The state mutations and branch can happen immediately in one interpreter pass. The message display may be initiated immediately. The wait for button must yield, because the player has not pressed the button yet. That is why ScriptContext_Stop and ScriptContext_Enable exist. They let a script command pause the script context and resume it later.

Now let’s connect this to the global script context. The engine has a primary script context, commonly reached through functions named ScriptContext_Init, ScriptContext_SetupScript, ScriptContext_RunScript, ScriptContext_Stop, and ScriptContext_Enable in pokefirered/src/script.c chunk_d424688149aa3bcb. The field layer does not usually create an arbitrary interpreter object. It calls ScriptContext_SetupScript with a script pointer. That function clears some message and quest-log state, initializes the global context with gScriptCmdTable and gScriptCmdTableEnd, installs the bytecode pointer, locks player field controls, and marks the context running.

That lock matters. Remember prior lectures: we already saw that field input is gated by player state, object events, movement, and interaction checks. Today we add the next layer. Once a script has ownership, player field controls are locked. That prevents the player from walking away while a scripted conversation, movement, warp, or battle transition is trying to maintain a coherent scene. This is not just a convenience. It is how the engine preserves consistency between player-visible narrative and underlying state mutation.

Let’s do board work. I’m drawing a vertical stack. At the top: player-visible event. Under it: field control selected a script pointer. Under that: ScriptContext_SetupScript. Under that: ScriptContext with scriptPtr and command table. Under that: ScrCmd handlers. Under that: flags, variables, map state, object state, party, inventory, battle setup, UI. That is the path we care about today.

Now, the command table is where script byte values become behavior. In pokefirered/src/scrcmd.c chunk_c2a54fb6031b5886, command handlers are named ScrCmd_end, ScrCmd_goto, ScrCmd_call, ScrCmd_return, ScrCmd_goto_if, ScrCmd_setvar, ScrCmd_compare_var_to_value, ScrCmd_compare_var_to_var, ScrCmd_setflag, ScrCmd_clearflag, ScrCmd_checkflag, ScrCmd_warp, ScrCmd_message, ScrCmd_waitmessage, and many more. The pattern is simple but powerful. Each command reads its operands from the script stream using helpers such as ScriptReadByte, ScriptReadHalfword, or ScriptReadWord, then mutates the ScriptContext or game state.

A concrete example: ScrCmd_setvar reads a variable id and a value, gets a pointer through GetVarPointer, and writes the value. ScrCmd_compare_var_to_value reads a variable id and a literal value, compares them, and stores the result in ctx->comparisonResult. Then ScrCmd_goto_if reads a condition code and a destination pointer. It checks sScriptConditionTable against comparisonResult. If the condition matches, it calls ScriptJump. That means branching is split over two commands: one command sets the comparison register, another command consumes it.

That split is worth noticing because it resembles assembly language more than high-level scripting. You do not need a full expression evaluator for “if var equals value then branch.” You need a compare instruction and a conditional jump instruction. In an embedded game engine, that is a very reasonable tradeoff. It keeps the interpreter small, and it keeps authored scripts expressible.

Now let’s bring in the locked-door scenario from this week’s lab. Suppose the lab bytecode has commands for setflag, clearflag, setvar, compare, branch, warp, and message. You might write a scenario like this in plain English: compare BADGE_COUNT to 1. If less than 1, branch to locked message. Otherwise message “You may pass,” warp to the next room, end. Locked message says “Only trainers with the Boulder Badge may enter,” then end. FireRed’s real script system is richer, but the architectural shape is the same. The compare command records a result. The branch command uses that result. The message command enters a UI flow. The warp command delegates to the overworld transition machinery. The state of the world is not inside the door object. It is in flags and variables.

Now, where are those flags and variables? In pokefirered/src/event_data.c chunk_a13cdbcb283dbf3b, FireRed defines special variables such as gSpecialVar_0x8000 through gSpecialVar_0x800B, gSpecialVar_Result, gSpecialVar_LastTalked, gSpecialVar_Facing, and text-related special variables. It also initializes persistent flags and variables in gSaveBlock1Ptr->flags and gSaveBlock1Ptr->vars through InitEventData. That file gives us VarGet, VarSet, GetVarPointer, FlagSet, FlagClear, and FlagGet.

This is the second major mental model of the week: scripts are transient, but world memory is persistent. A script pointer runs for a moment. A flag may remain set for the rest of the save file. A variable may encode a map scene number, a chosen starter, a story phase, or a temporary trigger state. When the player sees the world “remember” that an item was collected, a rival was beaten, an NPC moved, a badge was earned, or a map scene advanced, that memory is usually a flag or variable.

Let’s be precise about storage. Flags are packed bits. FlagSet finds the byte for a flag and ORs in a bit using the low three bits of the flag id. FlagClear clears that bit. FlagGet tests it. Variables are 16-bit values stored in the save block, except special variable IDs map to global special variable pointers. VarGet has an important behavior: if the id is not a valid variable id, it returns the id itself. That allows script operands to use the same read path for “this operand might be a variable reference or it might be a literal.” Again, this is a compact VM design choice.

Check for understanding here. If a script command reads an operand with VarGet, and that operand is VAR_SOMETHING, what does it get? It gets the current contents of that variable. If the operand is just a literal value outside the variable range, VarGet can return the literal. That is why several command handlers can support both direct values and variable-driven values without a full type system.

Now, compare that to object events from last week. We learned that an NPC has a local id and a script pointer. We also learned not to reduce the NPC to “a sprite that talks.” The object-event record is a data-backed actor, and the script pointer is its behavior hook. But today we see that the script pointer is only the beginning. Once the script enters the VM, it can read gSpecialVar_LastTalked, set story flags, change variables, hide or show objects, start movement, run battle setup, open shops, or warp maps. That is how a local map actor participates in global progression.

This also explains why the research packet repeatedly frames the course around “data table to runtime globals to callback, task, or script to visible behavior.” For battle entry, for example, pokefirered/src/battle_setup.c chunk_494d132a59422c85 includes script.h and event_data.h alongside battle and overworld headers, which is a clue that battle setup is not isolated from field scripts. Scripts can be the field-side doorway into battle. Likewise, pokefirered/src/learn_move.c chunk_af3598df6f092723 includes script.h, task.h, and event_data.h in a move relearner flow, reminding us that menu-like subsystems often return control to scripts after their own state machines complete. The point is not that script.c contains every system. The point is that the script VM is a coordination layer between authored world events and specialized engine subsystems.

Let’s return to interpreter control flow. In pokefirered/src/script.c chunk_03258f0288d89251, the engine provides ScriptJump, ScriptCall, and ScriptReturn. ScriptJump simply sets scriptPtr. ScriptCall pushes the current scriptPtr onto the stack and jumps to the target. ScriptReturn pops the stack back into scriptPtr. This is basic subroutine machinery. It means scripts can share standard behavior. In scrcmd.c, there are also standard-script commands like gotostd and callstd that index into gStdScripts. This is the scripting equivalent of using shared library routines for common interactions.

Now, a question: why does ScriptContext have a stack of return pointers instead of relying on the C call stack? Because script calls are bytecode-level calls, not C calls. The interpreter remains one C loop, but the script language has its own call-return discipline. If an NPC calls a standard message routine, then that routine returns to the original script, the ScriptContext stack is what remembers where to go back. This distinction matters in your lab. Do not implement branch and call as the same operation unless your language truly has no return. A branch discards the old path. A call preserves it.

Let’s also talk about native mode. ScriptContext can run in SCRIPT_MODE_BYTECODE or SCRIPT_MODE_NATIVE, with SCRIPT_MODE_STOPPED as the inactive state. ScrCmd_gotonative and SetupNativeScript let a script hand control to a C function that may return true when it is ready to resume bytecode. That gives FireRed an escape hatch. If a behavior is too complex or too frame-dependent for bytecode, the script can defer to native code, then continue. This is a clean embedded-engine compromise: scripts express common sequencing, while C handles specialized operations.

Here is the advanced architectural point. FireRed’s scripting system is not “data-driven” in the simplistic sense that all behavior is data. It is data-driven at the boundaries where authoring benefits from data, and native-code-driven where engine complexity demands code. The interpreter is the seam between those worlds. You should not conclude, “Scripts are better than C,” or “C is better than scripts.” The actual design says: use script bytecode for sequencing, branching, flags, variables, messages, object movement requests, and subsystem entry points; use C for the subsystems and hard real-time state machines underneath.

Now, let me give you a quick recap before we go deeper. We have a ScriptContext. It stores the instruction pointer, mode, comparison result, call stack, command table, and scratch data. ScriptContext_SetupScript installs a script into the global context and locks field controls. ScriptContext_RunScript advances the interpreter until it finishes or yields. RunScriptCommand dispatches command bytes through gScriptCmdTable. Commands in scrcmd.c read operands, mutate context or game state, and decide whether execution continues immediately. Flags and variables in event_data.c provide persistent world memory. That is the backbone.

Now I want to ask a diagnostic question, because this is where advanced students often make one subtle mistake. Suppose a guard blocks a doorway until you beat a trainer. Where should the “trainer beaten” fact live? Not in the guard’s current sprite object. Not in the door. Not in the field input code. It should live in progression state, usually a flag or variable. The guard’s script reads that state and branches. The object may be hidden or moved based on that state. The map may run an on-frame or on-load script based on that state. But the memory itself belongs to the event data layer.

This is how story progression becomes state mutation. Story is not stored as paragraphs of narrative. It is stored as bits and integers that scripts interpret. A player says, “The world changed after I got the badge.” The engine says, “A flag was set, a variable changed, later scripts took different branches, and maybe object visibility or map scripts responded.” That is the machine-level translation of progression.

Now, as we continue, we are going to inspect two execution styles side by side: immediate scripts, such as certain map header scripts, and the global script context, which yields across frames. That distinction is going to matter when we talk about blockers, wait commands, movement scripts, and why some map setup can run to completion immediately while conversations and cutscenes must cooperate with the frame loop.

So, put two columns on the board now. In the left column, write immediate context. In the right column, write global context. The names are not just vocabulary. They are a scheduling decision.

In pokefirered/src/script.c, chunk_1be14bbdaf5c3400, the engine has two script context objects, a global script context and an immediate script context. The global one is the one that participates in the field loop. The immediate one is a temporary interpreter context used when the engine needs a script to run to completion as part of setup. So when you see RunScriptImmediately, do not read that as “a script that is more important.” Read it as “a script whose commands are expected to finish now, before control returns to the rest of the engine.” That is a very different execution contract.

Let me say that again in runtime terms. Immediate scripts are like setup routines. Global scripts are like cooperative processes. The global script can say, “I have started a fade, or a message box, or a door animation, and I need to yield until some other subsystem catches up.” The immediate script should not depend on that kind of wait unless the caller is deliberately prepared for it, because RunScriptImmediately loops on RunScriptCommand until the script stops. That loop appears in pokefirered/src/script.c, chunk_03258f0288d89251, and it is one of the cleanest places in the engine to see the boundary between data-driven script logic and frame-driven gameplay.

Now, what is inside a script context? This is worth slowing down for, because this is the mental model you will use in the lab. In include/script.h, the ScriptContext struct stores a mode, a scriptPtr, a stack depth, a small return stack, a comparison result, a nativePtr, a command table range, and a small data array. That is a bytecode VM in compact C form. It is not a general-purpose virtual machine with registers and arithmetic and memory protection. It is an event scripting VM specialized for overworld behavior. It has just enough machinery to read bytes, dispatch commands, call and return, compare values, branch, and suspend.

Point to scriptPtr first. scriptPtr is the program counter. Every time the interpreter reads an opcode or an operand, scriptPtr advances. ScriptReadByte is the smallest version of that idea. ScriptReadHalfword reads two bytes little-endian. ScriptReadWord reads four bytes. This matters because when a command handler executes, it is not being handed a parsed AST. It is handed a context whose scriptPtr is currently sitting after the opcode. The handler consumes exactly the bytes that belong to that command. If it consumes too many, the next command begins in the wrong place. If it consumes too few, the next operand gets mistaken for an opcode. So the discipline of the interpreter depends on every command handler agreeing with the script assembler’s encoding.

Here is the board trace. Suppose the byte stream says: compare variable X to value 1, then goto_if equal label Open, then message Locked, then end, then label Open, warp somewhere. The interpreter does not understand “locked door scenario” as a concept. It sees an opcode. It indexes the command table. It calls a C function. That C function reads a variable id and a literal value. It writes a tiny result into comparisonResult. Then the next opcode, goto_if, reads a condition byte and a destination pointer. It checks comparisonResult against the condition table, and maybe assigns scriptPtr to the destination. That is the entire branch. There is no hidden story engine under it.

That is why pokefirered/src/scrcmd.c, chunk_c2a54fb6031b5886, is so important this week. It is where the abstract command names become mutations and control-flow changes. ScrCmd_setvar calls GetVarPointer and writes a halfword. ScrCmd_setflag calls FlagSet. ScrCmd_clearflag calls FlagClear. ScrCmd_checkflag writes the result into ctx->comparisonResult. ScrCmd_compare_var_to_value also writes comparisonResult, but with the three-way comparison result: less than, equal, greater than. Then ScrCmd_goto_if and ScrCmd_call_if interpret that comparison result through a condition table. Notice the separation: comparison commands do not jump. Branch commands do not compare. That small split is what makes the scripting language composable.

Check for understanding here. If a script does checkflag FLAG_GOT_KEY and then goto_if TRUE, what piece of state does goto_if actually inspect? Not the flag directly. It inspects ctx->comparisonResult, which was set by checkflag. That means the branch command is dependent on the immediately prior comparison-style command. If some other command between them overwrites comparisonResult, the branch now responds to the wrong condition. That is the kind of low-level sequencing hazard you should expect in bytecode-style event systems.

Now let us connect that to the event data layer from the previous passage. In pokefirered/src/event_data.c, chunk_a13cdbcb283dbf3b, flags and variables live behind FlagSet, FlagClear, FlagGet, VarGet, VarSet, and GetVarPointer. A normal persistent flag goes into gSaveBlock1Ptr->flags, packed eight flags per byte. A normal persistent variable goes into gSaveBlock1Ptr->vars as a 16-bit value. Special variables are different; values at the special variable range resolve through gSpecialVars to globals such as gSpecialVar_Result, gSpecialVar_LastTalked, and gSpecialVar_Facing. That is the reason scripts can use a common variable-like interface for both durable save data and transient communication with engine subsystems.

So the locked door in your lab should not be implemented as a C if statement buried inside the door object. It should be implemented as script state. The player presses A, and from prior weeks you already know how we reach the object or sign script entry; we are not re-teaching that lookup. The important week eight piece begins after the script pointer is selected. The script says, in effect: check whether the key flag is set. If it is, open the door or warp. If it is not, show a message. Maybe set a variable to record that the player has examined the door. Maybe later an NPC checks that variable and gives a hint. That is progression as shared state, not progression as one giant mission controller.

Now look at waits. The global script context has a status variable, sGlobalScriptContextStatus, with running, waiting, and shutdown states. ScriptContext_RunScript checks that status. If the context is shut down, it returns false. If it is waiting, it returns false. If it is running, it locks player field controls and calls RunScriptCommand. That is in pokefirered/src/script.c, chunk_d424688149aa3bcb. The lock is not cosmetic. It enforces that while a global script is actively controlling a conversation or cutscene, the normal field controls do not freely advance the avatar state. Again, this builds on our input and movement lectures without redoing them: the field loop still exists, tasks still run, callbacks still frame the engine, but the script context can temporarily own the player interaction channel.

Inside RunScriptCommand, the mode determines what happens. If the context is in bytecode mode, it reads opcodes and dispatches handlers. If the context is in native mode, it calls a C function through nativePtr. If that native function returns true, the context switches back to bytecode. If it returns false, the context remains in native mode and RunScriptCommand returns to its caller. That pattern is the script VM’s yield mechanism. It lets a bytecode command start an asynchronous engine operation and then poll a native completion predicate across frames.

A concrete example is delay. In scrcmd.c, ScrCmd_delay reads a pause count, stores it in a pause counter, and calls SetupNativeScript with RunPauseTimer. Then it returns true. The script is no longer simply advancing through bytecode. On later runs, the native function decrements the counter and returns true only when the pause is done. Fade commands use the same idea with a palette fade completion check. Door animation waiting uses the same architectural shape: start the door animation with one command, then use a wait command that installs a native predicate until FieldIsDoorAnimationRunning reports completion. The command names differ, but the scheduling architecture is the same.

This is the key sophistication I want from you now. Do not describe wait commands as “pauses.” That is too vague. A wait command is a conversion from bytecode execution to native polling, often combined with ScriptContext_Stop or SetupNativeScript, depending on the command. Some waits put the global context into waiting status until another subsystem explicitly re-enables it. Some install a native predicate and keep the interpreter alive in native mode. The precise choice depends on who owns the continuation. A message box may wait for UI input and then re-enable the script. A palette fade can be polled. A battle transition may hand off to the battle system and return through a later script address. These are not all the same kind of delay.

Let us tie in battles for a moment, because the script interpreter is not isolated from the rest of the RPG. The research packet points to pokefirered/src/battle_setup.c, chunk_494d132a59422c85, as a source for battle transition and script integration. In scrcmd.c, the trainer battle commands call into battle setup functions: configure the trainer battle, start the battle, then later choose post-battle script addresses. So the overworld script VM becomes the front door to a different major subsystem. A trainer object is data-backed in the overworld, yes. But the moment its script reaches trainerbattle and dotrainerbattle, the system crosses into battle setup, battle transition, party state, and eventually returns to script-controlled overworld progression. That is the course thesis again: data table, runtime globals, callback or task or script, then visible behavior.

Let me give you a second example, more advanced than the locked door. Imagine an NPC who blocks a hallway until you defeat a rival. Before the battle, the NPC’s script checks a trainer flag or story flag. If not complete, it displays a message and perhaps starts a forced movement or battle. After victory, the post-battle script sets a flag. The next time the map loads, an object visibility condition may hide the blocker, or the blocker’s script may branch differently. The player experiences “the rival moved out of the way.” The engine sees “a flag changed, and future object/script evaluation reads a different value.” That is why this week pairs the interpreter lecture with the next lecture on flags and variables as world memory. Today is the execution mechanism. Next time is the memory model in more detail.

Now I want to make the command dispatch itself very explicit. In RunScriptCommand, the interpreter reads cmdCode from *scriptPtr, increments scriptPtr, then obtains cmdFunc from ctx->cmdTable[cmdCode]. It also checks that the function pointer is before cmdTableEnd. Then it calls the handler. The handler returns a bool. If the handler returns true, RunScriptCommand returns true to its caller. If the handler returns false, the interpreter stays inside the while loop and immediately fetches the next opcode. That means many commands execute in a burst during a single frame. Setting a variable, comparing a value, branching, and setting a flag can all happen before the engine yields. Commands that start something visible or time-dependent tend to return true so the engine can come back later.

This is a good moment for a misconception check. When a script displays a message, does every bytecode command take one frame? No. That is not the model. The interpreter runs until it hits a command that yields, waits, switches to native polling, stops, or ends. Simple state mutation commands are effectively collapsed into the same interpreter run. So if your clone interpreter in lab executes exactly one command per frame, that is a valid teaching simplification only if you know it is a simplification. FireRed’s actual script loop is more aggressive: it keeps going until a handler tells it to return.

That has design consequences. If you create a custom command in a FireRed-like engine, you must decide whether the command is instantaneous or cooperative. Instantaneous commands return false and let the interpreter continue. Cooperative commands return true, or otherwise arrange for waiting, because the engine needs a frame boundary. Opening a door should not instantly mutate every visual state unless the animation system is deliberately bypassed. Setting a story flag should be instant. Warping likely initiates a transition. Message display yields to text UI. Battle start yields to battle setup. The return value is not an afterthought; it is part of the scheduling contract.

Let us also name the call stack. ScriptCall pushes the current scriptPtr and jumps to a subscript. ScriptReturn pops it. The stack is small, twenty entries in the struct. This is not recursion for clever programming exercises. It is a convenience for shared script fragments: common message patterns, standard scripts, repeated behavior. But because the stack is bounded and the bytecode has no rich runtime diagnostics, script authors have to treat calls conservatively. An infinite call chain or missing return is not a cute bug. It can corrupt the intended control flow or stop the context.

Now, one subtle point about immediate map scripts. Map headers contain script tables tagged by script type. In script.c, map header helpers search the gMapHeader.mapScripts pointer for a tag, then either run a script immediately or check a table of variable comparisons. RunOnLoadMapScript, RunOnTransitionMapScript, RunOnResumeMapScript, RunOnReturnToFieldMapScript, and RunOnDiveWarpMapScript all use the immediate style. TryRunOnFrameMapScript is different. It checks a map script table, and if a condition matches, it sets up the global script context. That distinction is exactly what our opening passage promised. Some map setup can complete immediately because it is configuring state. Frame scripts can become global scripts because they may need to interact with the player-visible world over time.

So if I write on the board, “on-load equals immediate, on-frame equals global,” what should you add underneath it? Add, “because of yielding.” Not because one is map-related and one is event-related. Both are map-related. The difference is whether execution is expected to complete now or cooperate with future frames.

Here is another check for understanding. Suppose a map transition script needs to set weather, adjust an object graphics variable, and initialize a scene variable. Immediate or global? Immediate is plausible, because those are setup mutations. Suppose a map frame script detects that VAR_SCENE equals 2, walks an NPC toward the player, opens a message box, fades the screen, and starts a battle. Immediate or global? Global, because movement, message, fade, and battle handoff require coordination with per-frame subsystems.

Now, how does this connect to the course plan? In weeks one through three, we treated the runtime shell as callbacks and tasks. In weeks six and seven, we treated overworld maps and object events as data that become actors and interactions. Week eight is where those pieces acquire memory and sequence. A map object script is not just “some code attached to an NPC.” It is an entry point into a small virtual machine. That virtual machine reads bytecode from event script data, dispatches C handlers through a command table, mutates save-backed flags and variables through event_data.c, and yields to the callback/task world when visible behavior needs time.

This is why I keep insisting that a creature RPG is not one loop. It is a stack of cooperating state machines. The main callback decides the broad mode. Tasks handle per-frame work. The overworld interprets movement and interaction. The script VM sequences story logic. The event data layer remembers what changed. Battle setup bridges into combat. Inventory commands bridge into bag state. UI commands bridge into text and menus. The script interpreter is one of the places where those systems meet without becoming a single tangled function.

Let me close this segment with the lab translation. Your lab interpreter needs commands for setflag, clearflag, setvar, compare, branch, warp, and message. Do not start by building a parser. Build the runtime contract first. Have a program counter. Have a command table or equivalent dispatch map. Have a comparison register. Have persistent flags and variables. Decide which commands continue immediately and which commands yield. Then implement the locked door as data: check key flag, branch to open path, otherwise message locked. If you do that, you will have captured the essential FireRed architecture at a small scale.

And as we move forward, keep this synthesis in your head: FireRed’s script interpreter is not magic narrative machinery. It is a byte reader, a function dispatcher, a tiny call stack, a comparison latch, a cooperative wait mechanism, and a gateway to world memory. The elegance is not that any one piece is complicated. The elegance is that these modest pieces let designers express badges, doors, NPC blockers, rival battles, tutorial gates, hidden items, and map scenes as stateful scripts sitting on top of the same engine. Next, we will keep the interpreter in view but zoom further into the memory it manipulates: which flags last, which variables are temporary, which special variables are communication registers, and how progression emerges from those choices.

Now, let’s keep going from that synthesis, because there is a subtle design point hiding inside it. If the interpreter is a byte reader and a dispatcher, then the story system is not located in one big “story manager.” And I want you to notice how different that is from the way many modern game projects are first imagined. A novice architecture often starts with a class called something like StoryManager, or QuestManager, and then everything narrative is routed through that one object. FireRed’s architecture is more distributed. The interpreter sits in pokefirered/src/script.c, especially in the script engine material identified in our packet as chunk_1be14bbdaf5c3400, chunk_03258f0288d89251, and chunk_d424688149aa3bcb. The command behavior sits in pokefirered/src/scrcmd.c, chunk_c2a54fb6031b5886. The persistent memory the commands mutate sits in pokefirered/src/event_data.c, chunk_a13cdbcb283dbf3b. So when the player says, “I beat Brock, so this NPC moved,” the engine does not remember that sentence. It remembers bits and small integers.

Let me say that again because it is central to week 8. FireRed does not store story as prose. It stores story as state. The prose, the NPC placement, the door blockers, the battle availability, the tutorial sequence, and the map transitions all consult that state.

On the board, I’m going to draw a very plain pipeline. First box: script pointer. Second box: opcode byte. Third box: command function. Fourth box: flag or variable mutation. Fifth box: visible world behavior. That is the flow we care about today. And if you compare it to the patterns we have already traced in earlier weeks, it should feel familiar. In week 1, we talked about the runtime shell: AgbMain, main callbacks, and the way SetMainCallback2 swaps the active high-level mode. In week 2, we talked about headers and generated data as contracts. In week 7, we traced object events and script labels as map-authored entry points. Today we are looking at what happens after one of those entry points is chosen. We are inside the little virtual machine that turns a data pointer into behavior.

Now, let’s take a concrete example, and I’ll deliberately keep it smaller than the actual game. Imagine an NPC standing in front of a door. The NPC should say, “Only trainers with the Boulder Badge may pass.” If the player has the badge flag, the NPC should step aside or the script should allow the transition. If not, the NPC blocks the player. Architecturally, the interesting question is not the sentence. The interesting question is this: where is the authority for “has the badge”? It is not in the NPC object itself. It is not in the map tile. It is not in the text box. It is in event data, exposed through the flag API in pokefirered/src/event_data.c, chunk_a13cdbcb283dbf3b, and manipulated by script commands in pokefirered/src/scrcmd.c, chunk_c2a54fb6031b5886.

So the script bytecode says, in effect, check a flag, compare the result, branch to one label if true, branch to another label if false. The command implementation reads the operands out of the script stream, calls the event-data layer, updates the comparison latch or branches the script pointer, and then returns control to the interpreter. Notice the separation of duties. The interpreter does not know what the Boulder Badge means. The event-data layer does not know what the NPC is saying. The map script does not know how bits are packed into save memory. Each piece has a narrow job.

Check for understanding here. If I ask, “Where should I look to understand how an opcode advances the current script pointer?” your answer should be pokefirered/src/script.c, especially the interpreter and control-flow chunks. If I ask, “Where should I look to understand what setflag actually does?” your answer should start in pokefirered/src/scrcmd.c, chunk_c2a54fb6031b5886, and then follow the call into pokefirered/src/event_data.c, chunk_a13cdbcb283dbf3b. If I ask, “Where should I look to understand why this NPC script was selected in the first place?” then we are back in week 7 material, object-event records, local IDs, script labels, and interaction dispatch. We are not reteaching that today, but we are using it as the entry ramp.

Now let’s refine the idea of the comparison latch. In a conventional C program, you might write if flag equals true, then goto this block. In the script VM, that often decomposes into multiple bytecode-level operations. One command performs a check or comparison and stores the result in interpreter state. Another command asks whether that stored comparison satisfies a condition and then changes the script pointer. That is why I called it a latch. It is not a general-purpose expression evaluator. It is a small remembered result that the next branch instruction can consume.

This is one of those places where advanced learners should resist overdesigning the clone. When you build the workshop interpreter, do not begin by designing a full expression language. Start with FireRed’s bias: small commands, explicit state, and branch commands that are easy to serialize. Your minimal interpreter can have a field called comparisonResult. Then compare var 0x4001 to value 3. Then branch_if equal to address or label DoorOpens. That is enough to model a surprising amount of RPG progression.

Let me put a miniature trace on the board. Suppose the player talks to a guard. The object-event system has already resolved the interaction and handed us a script pointer. The script context begins executing at byte zero. It reads opcode checkflag. The operand is FLAG_BOULDER_BADGE, or whatever symbolic constant your script assembler resolves to a numeric flag. The command function calls FlagGet. The result is placed somewhere the next condition can observe, either directly through a comparison result or through a variable such as VAR_RESULT depending on the command convention. Then the interpreter reads goto_if. The operand says, condition true, target DoorAllowed. If the condition succeeds, ScriptJump changes the script pointer. If not, execution simply continues into the refusal text.

The important part is that branch behavior is just pointer mutation. There is no cinematic story graph engine hidden underneath. There is a pointer to bytes. Some commands increment it by reading operands. Some commands replace it with another pointer. Some commands push the current pointer onto a tiny call stack and jump elsewhere. Some commands pop that stack and return. That is the whole control-flow vocabulary we need to explain call, goto, return, conditional branch, standard message routines, and many map scenes.

This is why the script control-flow chunk in pokefirered/src/script.c, chunk_03258f0288d89251, matters so much. It is the bridge between “a script is just data” and “a script has structure.” Calls and jumps are not abstract syntax tree operations. They are runtime pointer operations. And because commands read their own operands from the stream, each command is responsible for leaving the pointer at the correct next byte. If a command reads one byte too few or too many, every later command is misaligned. In a bytecode interpreter, pointer discipline is correctness.

Let’s pause there. Why is that more fragile than calling C functions directly? Because the compiler protects a C call boundary. If you pass the wrong number of arguments to a normal C function, the type system or calling convention may catch it, depending on the situation. But a script byte stream is just bytes. The meaning comes from the command table. If opcode 0x21 means “read a halfword operand,” then the command handler must read exactly that halfword. The script authoring tools and macros must emit exactly that halfword. The interpreter trusts the protocol.

Now this also explains why constants and headers become so important. Script commands, flag IDs, variable IDs, special variable IDs, and map script labels all need stable numeric meanings. A designer does not want to remember that a particular badge is bit number whatever. They want symbolic names. But at runtime, the interpreter does not carry those names. It carries numeric operands. That is the same data-driven lesson we have been repeating since the beginning of the course: nice authoring names collapse into compact runtime records.

Let’s bring in event variables now, because flags alone are too small. A flag answers a yes-or-no question. Did the player get this badge? Is this hidden item collected? Has this one-time NPC gift been received? A variable answers a small numeric question. Which starter did the player choose? Which step of a scene is active? Which fossil did the player take? Which rival battle variant should be used? In pokefirered/src/event_data.c, chunk_a13cdbcb283dbf3b, the variable functions give scripts a way to read and write that small world memory. In script command form, pokefirered/src/scrcmd.c, chunk_c2a54fb6031b5886, commands like setvar, copyvar, addvar, subvar, compare, and specialvar are the vocabulary that map scripts use to manipulate those values.

Here is the concrete door example upgraded from a flag to a variable. Suppose the door is not controlled by a badge, but by a puzzle state. Variable PUZZLE_STAGE starts at 0. When the player talks to statue A, the script sets it to 1. When the player talks to statue B, the script checks whether PUZZLE_STAGE equals 1. If yes, it sets it to 2. If no, it resets it to 0. The locked door script checks whether PUZZLE_STAGE equals 2. If yes, warp. If no, message. That is story progression as state mutation. No central quest object required.

And now I want you to connect this to blockers. A blocker is not necessarily a special engine type. Sometimes it is an NPC whose script refuses access. Sometimes it is a coordinate event that fires when you step onto a tile and runs a script. Sometimes it is a map connection or warp that is effectively unreachable until some other world state changes. We already covered the event lookup side in recent lectures, so today’s point is narrower: once the script starts, the blocker’s authority usually comes from flags and variables. The visible object is the presentation. The state is the truth.

This is also where FireRed’s architecture becomes broader than “dialogue scripting.” Look at the research packet’s high-value chunks outside script.c. pokefirered/src/battle_setup.c, chunk_494d132a59422c85, includes script and event-data related headers among the battle setup dependencies. That is a signal that battle entry and world progression are linked, not isolated. A trainer battle is not just a battle-system event. It is often launched from field context, guarded by flags, followed by script continuation, and then written back into progression state. Similarly, pokefirered/src/learn_move.c, chunk_af3598df6f092723, shows the move relearner as a state machine module that includes script.h and event_data.h. That does not mean the script interpreter implements the move relearner menu. It means specialized systems often need to communicate with the same field script and event memory layer.

That is an advanced architecture point, so let’s slow it down. The script system is not the only state machine in the game. We have seen task state machines, callback state machines, menu state machines, battle state machines, and field movement state machines. The script VM is one more state machine, but it is special because it is the one designers most often use to sequence world-facing behavior. When a specialized C system finishes, it may need to hand a result back to the script layer. That is what special variables are for. They act like registers at the boundary between native C functions and script bytecode.

Imagine a script calls a native special function to check whether the party has a Pokémon that can use a move, or to start a naming screen, or to choose a menu option. The C function may do complicated work the bytecode language should not express directly. But the script still needs the answer. So the result is placed in a known variable, commonly a special result variable, and then the bytecode branches based on it. That boundary is clean: native code handles complicated engine behavior; script code handles sequence and progression.

Check yourself on this distinction. If a script command says “call special function,” is the VM suddenly becoming a general C interpreter? No. It is invoking a registered native routine and then resuming bytecode-level control. If that native routine needs to communicate back, it writes into event variables or special variables. The script can then compare and branch. So the architecture is hybrid: compact bytecode for authored flow, native C for heavy engine operations, and event data as the shared memory vocabulary.

Now, let’s talk about waiting, because without waiting the interpreter would be unusable for field scenes. Many script commands can complete immediately. setflag is immediate. setvar is immediate. compare is immediate. A message box, a movement sequence, a fade, a battle, or a menu is not immediate from the player’s point of view. The engine cannot freeze the entire machine in a blocking while loop until the player presses A. Remember our runtime shell from week 1: the game advances frame by frame through callbacks and tasks. So the script system has to cooperate with that frame loop.

That is where the execution support material in pokefirered/src/script.c, chunk_d424688149aa3bcb, matters. The script context can enter a mode where bytecode execution pauses while some native condition is checked across frames. In design terms, this is a cooperative wait mechanism. The script says, “I have started something asynchronous from the perspective of the frame loop; do not advance to the next story command until that thing reports completion.” This is how field messages, fades, movement, and other time-spanning operations can still be authored as if they were sequential. The script reads naturally: show message, wait for button, close message, set flag. Underneath, that sequence is distributed across frames.

Let’s draw that as a timeline. Frame one: script command opens a message box and enters wait state. Frame two: main callback runs field logic, message box is still open, script does not advance. Frame three: player presses A, message system marks completion. Frame four: script resumes at the next byte. That is not a thread. That is not blocking I/O. That is a cooperative state machine synchronized through shared status.

And this is why the lab interpreter includes a message command even if your first version just prints to a console. If you want the architecture lesson, make message optionally blocking. Have the interpreter return a status like “still running” or “waiting.” Then your outer loop calls it again next frame or next tick. You will immediately feel why FireRed’s scripts are not just plain functions. A plain function wants to run to completion. A field script often wants to run until it reaches a wait, yield to the engine, and resume later.

Let’s do a quick recap before we add the next layer. A script begins from a pointer chosen by the overworld or event system. The script context reads bytes from that pointer. The opcode indexes a command table. The command handler reads operands, mutates state, jumps the pointer, calls native code, or asks the interpreter to wait. Flags and variables live in event data, with implementations centered in pokefirered/src/event_data.c, chunk_a13cdbcb283dbf3b. Higher-level commands live in pokefirered/src/scrcmd.c, chunk_c2a54fb6031b5886. The interpreter mechanics and control flow live in pokefirered/src/script.c, in the chunks named in the week packet. That division is the source-level map I expect you to be able to navigate.

Now, what should an advanced student notice here that a beginner might miss? First, the script VM is intentionally weak. That weakness is a feature. It prevents every map script from becoming arbitrary engine code. The script layer sequences operations and mutates progression state, but the expensive or delicate operations remain native. Second, the VM’s weakness means the surrounding APIs must be well chosen. If there is no command for a needed behavior, designers cannot express it cleanly. If there are too many commands, the bytecode surface becomes chaotic. FireRed sits in that old handheld design space where compact, purpose-built commands are preferable to a large embedded scripting language.

Third, the same flag and variable memory can be consulted by many systems. The battle setup path can care about flags. Menus and special functions can use variables. Map object visibility can depend on flags. Coordinate events can mutate variables. This creates expressive power, but it also creates coupling. If two scripts use the same variable for unrelated meanings, you get progression bugs. If a temporary flag is assumed to persist, you get a scene that works until a map reload. If a permanent flag is used for a temporary cutscene lock, you can accidentally alter the save file forever.

So when we move into the next lecture, “Flags and Variables as World Memory,” that is the problem we are going to sharpen. We are going to ask which pieces of state are persistent, which are temporary, which are special communication registers, and which are local conventions built by script authors. The interpreter itself does not answer those design questions. It merely provides the mechanism.

One last check for understanding before we continue. Suppose I show you a bug report: “After losing to a rival battle, the NPC no longer appears, but the follow-up dialogue did not play.” What are the first architectural suspects? Not graphics. Not input polling. Not the avatar movement attempt flow. We already know those systems can get the player to the interaction. The suspects are progression state and script continuation. Was a defeated flag set too early? Did the battle setup return through the expected script path? Did a variable record the wrong scene step? Did a wait-state or native callback fail to resume the script? That diagnostic habit is the point of this week.

And notice how the source packet supports that habit. We do not have to wave our hands and say “the game probably has some quest state somewhere.” We can name the actual source neighborhoods: script interpreter in pokefirered/src/script.c, script command implementation in pokefirered/src/scrcmd.c, event flags and variables in pokefirered/src/event_data.c, and cross-system users such as pokefirered/src/battle_setup.c, chunk_494d132a59422c85, and pokefirered/src/learn_move.c, chunk_af3598df6f092723. That is what I mean by architecture literacy. You can move from a player-visible symptom to the likely files, structs, functions, constants, tables, and globals that make the symptom possible.

So, as we continue, keep your mental model small and precise. The script interpreter is not a story brain. It is a disciplined bytecode runner. Its power comes from where it is placed: between map-authored entry points and the shared world memory that the rest of the game respects.

Now, let’s make that precise. If the A-button interaction code from last week gives us a script label, or if a coordinate event gives us a script pointer, what happens next is not magic. The script context receives a pointer into bytecode, reads one command byte, dispatches that byte through a command table, and then lets the command handler consume whatever arguments follow. That is the interpreter loop in plain language.

On the board, I’m going to write this as “pointer, opcode, handler, state mutation, yield or continue.” That five-part rhythm is the heart of today’s lecture. Pointer: where are we in the script stream? Opcode: what command are we about to execute? Handler: which C function implements that command? State mutation: did we change a flag, variable, map, object, message box, or callback? Yield or continue: can the interpreter keep running this frame, or must it wait for some other system?

That last question is where FireRed starts to look like an actual game engine rather than a toy interpreter. In a small lab interpreter, you might run the entire script in one call. You read setflag, then message, then compare, then branch, then warp, and you are done. FireRed cannot always do that. A message command may need to open a text box, let the text printer animate, wait for input, and then resume. A movement command may need an object event to walk several tiles over multiple frames. A warp command may transfer control into map loading and screen fade logic. So the interpreter must be able to execute bytecode and also cooperate with the callback and task shell we studied earlier.

The source packet points us directly at the relevant neighborhoods. The script engine itself is in pokefirered/src/script.c, especially chunk_1be14bbdaf5c3400 for the core script engine, chunk_03258f0288d89251 for script control flow, and chunk_d424688149aa3bcb for execution support. The command implementations live in pokefirered/src/scrcmd.c, chunk_c2a54fb6031b5886. The world memory that scripts tend to mutate lives in pokefirered/src/event_data.c, chunk_a13cdbcb283dbf3b. I want you to hear those as a map, not as trivia. If a script seems to run strangely, you look in script.c. If one command behaves strangely, you look in scrcmd.c. If the wrong story state is being remembered, you look in event_data.c.

Now, what is the central runtime object here? Conceptually, it is the script context. In FireRed terms, the relevant struct is the script execution context, and the relevant state variables include the active script pointer, the command table pointer, stack-like return information for script calls, comparison result or condition state, and native callback state used when script execution has to wait on C-side behavior. You will see the globals for script contexts in pokefirered/src/script.c, in the source focus chunks I just named. The important architectural point is that a script is not represented as a giant C function. It is represented as data plus a tiny execution state. That is why map authors, event labels, and command macros can build story behavior without each event becoming bespoke engine code.

Let me pause and check the model. Suppose I say, “The player talks to an NPC, and the NPC gives the player an item only once.” Where should your eyes go? Not to the battle engine. Not first to the bag UI. First, from last week, you know the object-event record gives us an NPC local ID and a script label. Then this week, you follow that label into the script interpreter. The script uses a flag to ask, “Have I already given this item?” If not, it gives the item, sets the flag, and changes future behavior. The one-time nature of the event is not stored in the NPC object itself as some private little memory. It is usually stored as shared event data, through flags or variables, in the event data system represented by pokefirered/src/event_data.c, chunk_a13cdbcb283dbf3b.

Good. Now let’s make the bytecode side concrete.

Imagine a locked door. The player presses A facing the door. We are not going to re-teach the interaction lookup; just assume the earlier field control code has found the door’s script pointer. The script begins something like this in conceptual bytecode: check whether FLAG_GOT_KEY is set. If it is not set, show “The door is locked.” End. If it is set, show “You used the key.” Warp the player into the room. End.

In the lab version, you will implement that with commands named compare, branch, message, warp, and maybe setflag. FireRed has richer and more numerous commands, but the structure is the same. A check command reads an argument from the script stream, perhaps a flag ID or variable ID. The command handler calls into event_data.c functions, conceptually FlagGet or VarGet, to read the saved state. Then it updates comparison state inside the script context or writes a result into a special variable. A branch command reads a destination pointer or offset and decides whether to replace the script pointer with that destination. A message command reads a text pointer and hands it to the field message system. A warp command reads map identity and coordinates, then passes control into map transition machinery.

Notice the separation. The interpreter does not know what a “locked door” means. The flag system does not know what a “door” means. The map system does not know why this warp was allowed. The authored script composes those pieces. That composition is the story logic.

This is exactly why I keep calling progression “state mutation” rather than “plot.” Plot is what the player experiences. State mutation is what the engine can actually execute. The engine can set a bit. It can write a halfword variable. It can branch based on comparison state. It can change an object’s visibility. It can give an item. It can queue a message. From those small operations, you get “the guard moves after you deliver the parcel,” “the gym door opens after you defeat the leader,” and “the rival appears after you step on this coordinate.”

And this connects back to the whole course plan. In week 1, we decomposed the game into runtime shell, overworld maps, event scripting, creature data, battle, inventory, UI, save, and link systems. In week 2, we treated headers and generated data as contracts. In weeks 6 and 7, we followed the player through input, movement attempts, interaction checks, object events, and coordinate triggers. Week 8 is the hinge. The script VM is where those map-authored entry points become durable changes in the world. It is not the whole game, but it touches almost everything.

You can see that cross-system role in the packet’s supporting chunks. pokefirered/src/battle_setup.c, chunk_494d132a59422c85, includes script.h and event_data.h alongside battle, overworld, field control, and transition headers. That is a strong architectural clue: starting a battle is not isolated from scripting and event state. A trainer battle can be gated by flags, can set flags afterward, can return to field control, and can trigger follow-up scripts. pokefirered/src/learn_move.c, chunk_af3598df6f092723, also includes script.h and event_data.h while implementing the move relearner state machine. That tells us the same lesson in a different domain: even a specialized menu-like feature may need to cooperate with script entry and event variables. The script system is not merely “NPC dialogue.” It is a bridge between authored progression and engine subsystems.

Now, on the board, I’m going to draw the dispatch table. Picture an array indexed by command byte. Command 0 maps to a no-op or terminator-style command. Command 1 maps to another handler. Somewhere in that table are handlers for message commands, branch commands, variable commands, and so on. In pokefirered/src/scrcmd.c, chunk_c2a54fb6031b5886, the command implementation layer is where these handlers live. The interpreter in script.c does not need a giant switch with every story command’s behavior hard-coded directly into the loop. It can read the opcode and call the corresponding handler from the table. That design keeps the bytecode runner small and makes the command set extensible in a disciplined way.

But there is an important subtlety. “Extensible” does not mean “unbounded freedom.” Every command handler is still compiled C code. If you want a new primitive command, you add engine code and put it into the command table. Scripts are flexible because they can arrange existing commands in many combinations. They are not arbitrary user programs with memory allocation and recursion and all the features you would expect from a general-purpose language. That restriction is a virtue on the Game Boy Advance. You get predictable memory behavior, compact data, and a simple interpreter that can cooperate with frame-based systems.

Let’s do a check for understanding. If a script line says, conceptually, “set variable VAR_STORY_STAGE to 3,” where is the story stage stored afterward? Is it stored in the script pointer? No. The script pointer just moves forward. Is it stored in the command handler? No, the handler returns and may be called again later for some other event. The durable value belongs in event data, through the variable system in pokefirered/src/event_data.c, chunk_a13cdbcb283dbf3b. That is why another script, on another map, can later check the same variable and branch differently. Shared world memory is what lets local scripts produce global progression.

Now compare flags and variables. A flag is ideal for a binary fact: did this event happen, is this object hidden, has this trainer been defeated, has this one-time gift been claimed. A variable is better for a multi-valued state: which stage of a quest are we in, what trigger value should this map respond to, what menu result did the player choose, what temporary value should be communicated between a command and a special function. In a clean architecture review, if you see ten flags encoding one linear quest, you might ask whether a variable would express that progression more clearly. If you see one variable being used as a pile of unrelated booleans, you might ask whether flags would reduce accidental coupling. FireRed uses both because it needs both shapes of memory.

And now we can talk about blockers. A blocker is any piece of world behavior that prevents player progress until state changes. A locked door is a blocker. An NPC standing in the way is a blocker. A road guard demanding tea, a gym guide waiting for a badge, a rival battle before a route opens, all of those are blockers. Architecturally, the blocker is almost always an ordinary object, coordinate, or map script that checks shared event state. The “blocked” feeling comes from collision, dialogue, or a branch. The “unblocked” feeling comes from a later mutation to flags, variables, object visibility, or map transition availability.

That matters because it keeps us from inventing a separate “quest engine” in our mental model. There may be helper commands and special functions for famous progression moments, yes. But the underlying pattern is still script plus event data plus existing overworld systems. In pokefirered/src/battle_setup.c, chunk_494d132a59422c85, battle setup is adjacent to script and event data because a battle can be the event that removes a blocker. In pokefirered/src/daycare.c, chunk_119931105f546346, the include list also pulls in event_data.h and script.h among many other gameplay headers, which is another reminder that special systems often expose script-facing behavior rather than living in isolation. The game is full of these small bridges.

Let’s return to the interpreter loop itself. When the context is in bytecode mode, the runner reads from the script pointer. Reading a byte advances the pointer by one. Reading a halfword advances it by two. Reading a word or pointer advances it by the appropriate amount. So every command handler must obey a contract: consume exactly the arguments for that command, and leave the script pointer positioned at the next command unless it intentionally branches or calls somewhere else. This is the same discipline you used in the homework when you traced data records through globals, callbacks, tasks, scripts, structs, and functions. If a handler consumes the wrong number of bytes, every following command is misaligned. The interpreter will still read something, but it will be reading nonsense.

That is one of the debugging lessons of bytecode systems. A script pointer bug often looks like a totally unrelated command bug. You thought you were reading a message pointer, but the previous command consumed one byte too few, so now the message pointer is assembled from the wrong bytes. In a high-level language, the parser might catch syntax problems early. In a compact bytecode stream, the runtime discipline is stricter. The command table, command macros, and script assembler conventions all exist to preserve that alignment contract.

Now, if I were to ask you to implement the lab interpreter, I would not start by asking for messages or warps. I would start by asking for the script context. Give me a struct with an instruction pointer, a comparison result, a flag array, a variable array, and maybe a halted flag. Then give me a dispatch table from opcode to function. Then implement setflag and clearflag. Then implement setvar. Then compare. Then branch. Only after that should message and warp become interesting, because now message and warp can be gated by real state.

Why that order? Because it mirrors FireRed’s architecture. The flashy behavior is not the hard part. Printing “The door is locked” is easy. The architectural value is that the message appears only when the state says it should, and that the state can be changed by some other event elsewhere. That is what makes the world feel persistent.

Let’s test that with a concrete sequence. The player enters a house and talks to an old man. His script checks FLAG_GOT_KEY. If false, he displays a message and gives the key. Then he sets FLAG_GOT_KEY. Later, the player returns to the locked door. The door script checks FLAG_GOT_KEY. This time the branch goes to the open path, and the warp command runs. The old man and the door do not call each other. They share no direct reference. They coordinate through event data. That is the architectural move.

This is also why save persistence becomes relevant. If a flag or variable is meant to survive leaving the map, resetting the room, or saving and loading, it must live in the correct event data storage. Temporary values are different. FireRed has special variables and temporary script communication paths for results that do not mean permanent story progression. The source focus on pokefirered/src/event_data.c, chunk_a13cdbcb283dbf3b, is where you should look when distinguishing persistent flags, variables, and special variables. In your own engine, this is the difference between “player has obtained the key” and “the last menu choice was yes.” One belongs in save-backed progression state. The other may only need to exist until the current script resumes.

Now, notice how this raises the sophistication level compared with earlier weeks. In week 6, it was enough to say, “input leads to movement or interaction.” In week 7, it was enough to say, “object events and coordinate events identify scripts.” Here in week 8, that is not enough. You must ask what state is read, what state is written, whether the state is persistent, whether execution yields, and which subsystem resumes control. That is the advanced version of the trace.

When you read pokefirered/src/script.c, chunk_d424688149aa3bcb, pay attention to the support functions for reading script arguments and manipulating the script pointer. Those functions are easy to underestimate because they look small. But they enforce the interpreter’s basic mechanics. When you read chunk_03258f0288d89251, focus on control flow: branches, calls, returns, waiting, and mode changes. When you read pokefirered/src/scrcmd.c, chunk_c2a54fb6031b5886, focus on the command handlers as adapters. Each handler translates one bytecode instruction into a call against another part of the engine or against event memory.

And here is a useful mental distinction. script.c answers, “How does bytecode run?” scrcmd.c answers, “What does this bytecode command mean?” event_data.c answers, “Where does shared progression state live?” If you keep those three questions separate, you will debug faster and design cleaner systems.

Let me give you a final check before we move on. A player reports, “After I beat the trainer, the NPC still blocks the bridge.” What are three plausible architectural causes? First, the battle completion script may never set the expected flag. That sends us toward scrcmd.c and the relevant event script. Second, the NPC’s visibility or movement condition may be checking the wrong flag or variable. That sends us toward map event data and event_data.c semantics. Third, the battle might finish but not resume the follow-up script correctly, which sends us toward script.c control flow and battle setup integration, with pokefirered/src/battle_setup.c, chunk_494d132a59422c85, as a useful cross-system anchor. That is a much stronger answer than “the NPC bugged out.”

So, as we continue, I want you to keep following the pointer. Literally, the script pointer, but also the investigative pointer. From map event entry to script context. From opcode to handler. From handler to flag or variable mutation. From mutation to later branch. That chain is the backbone of story progression in FireRed, and in the next stretch we are going to look more closely at the control-flow commands that make scripts feel conditional rather than merely sequential.

Right, so let’s make that control flow concrete. Up to this point, we have been treating the script pointer as the thing that advances through a script, and that is true, but it is only half the story. The other half is that FireRed’s script VM keeps a small amount of interpreter state around that pointer, and the control-flow commands mutate that interpreter state just as surely as setflag mutates world state.

Put ScriptContext on the board. I want you to picture it as the little runtime record that answers, “Where am I in the script, what mode am I in, what was the last comparison result, and where do I return if this script calls a subroutine?” The header exposes struct ScriptContext with stackDepth, mode, comparisonResult, nativePtr, scriptPtr, a stack of script return addresses, a command table range, and a small data array. In the source focus for today, pokefirered/src/script.c, chunk_1be14bbdaf5c3400, is our anchor for the script engine side of that. The key idea is not merely “there is a pointer.” The key idea is “there is an interpreter context whose pointer, mode, comparison latch, native callback, and stack all cooperate.”

Now watch what happens when RunScriptCommand runs. The engine reads the byte at scriptPtr as an opcode, increments scriptPtr, indexes into the command table, and calls the corresponding ScrCmd function. That handler then consumes its own operands using ScriptReadByte, ScriptReadHalfword, or ScriptReadWord. So if the command is setvar, the opcode byte gets consumed by the dispatch loop, and then ScrCmd_setvar consumes a halfword variable ID and a halfword value. If the command is goto, the handler consumes a word-sized pointer and then changes scriptPtr to that destination. If the command is compare_var_to_value, the handler consumes a variable ID and an immediate value, compares them, and stores the result in ctx->comparisonResult.

This is why bytecode interpreters are such a useful architectural pattern in old handheld RPGs. The engine does not need a C function for “the Viridian old man has finished his coffee, the player may now pass.” It needs general opcodes for setting variables, checking flags, comparing values, branching, showing messages, locking control, moving actors, warping, and starting battles. The story designer’s specific sequence becomes data, but the interpreter remains stable.

Let me say that again in a more precise way. FireRed’s script layer is not “data-driven” because there is a JSON file somewhere saying questStage equals three. It is data-driven because story behavior is encoded as command streams, and those command streams are interpreted by a C runtime. The handler table lives on the C side. The story order lives in script bytes. The persistent memory lives in flags and variables. Those three pieces together are the progression system.

Now, if you look at pokefirered/src/scrcmd.c, chunk_c2a54fb6031b5886, you see the concrete command implementations we care about for today. ScrCmd_setvar uses GetVarPointer and writes a value. ScrCmd_setflag calls FlagSet. ScrCmd_clearflag calls FlagClear. ScrCmd_checkflag stores FlagGet into comparisonResult. ScrCmd_compare_var_to_value and ScrCmd_compare_var_to_var set comparisonResult from a three-way comparison: less than, equal, or greater than. Then ScrCmd_goto_if reads a condition and a destination pointer, consults the condition table, and either jumps or falls through.

That last phrase, “falls through,” is important. A branch command does not need to explicitly say “continue.” The pointer already moved past the branch’s operands. If the branch condition fails, scriptPtr is already sitting at the next command. If the branch condition succeeds, ScriptJump overwrites scriptPtr with the destination. That is the entire conditional story. Compare stores a latch. Conditional branch reads the latch. Jump mutates the pointer.

Check for understanding. If I compare VAR_STORY_STAGE to 2, and then I run a message command before goto_if, what value does goto_if use? It uses whatever comparisonResult currently holds unless the message command changes it. Most commands do not care about comparisonResult, but the design pattern is still “comparison, then nearby conditional branch.” If you separate those too casually, you create scripts that are legal but hard to reason about. In your lab interpreter, I want you to preserve that discipline: compare, then branch, with comparisonResult as explicit VM state.

Now, let’s use the locked-door lab scenario, because it is small enough to hold in our heads and rich enough to expose the architecture. Imagine a door to a gym back room. The door should open only after the player has spoken to an NPC and received permission. A simplistic engine might put an if statement directly inside the door object: if talkedToGuard, open; else show blocked message. That works for one door, but it does not teach the FireRed architecture. The FireRed-style approach is to make the door interaction enter a script. The script checks a flag or variable. If permission is absent, it displays a message and ends. If permission is present, it may clear a blocking flag, set a story variable, play a door animation, and warp the player or update the map.

So, on the board, I’ll write it in spoken pseudocode. Checkflag FLAG_GUARD_GRANTED_ACCESS. Goto_if true, DoorOpens. Message “The door is locked.” End. DoorOpens: setvar VAR_ROOM_STATE, 1. Warp to the back room. End. That is not exact FireRed script syntax, but it is the VM shape. The checkflag command touches event_data.c. The goto_if command touches script control flow. The message and warp commands touch field UI and overworld transition systems. The story change is just state mutation.

Now connect that to event_data.c. In pokefirered/src/event_data.c, chunk_a13cdbcb283dbf3b, the event memory layer is very explicit. InitEventData clears the save block’s flags and vars and clears special flags. GetVarPointer distinguishes normal saved variables from special variables. VarGet returns the variable’s value, but there is a subtle design convenience: if the index is not a variable index, VarGet returns the index itself. That means commands can accept operands that may be either literal constants or variable references in certain contexts. VarSet writes through a variable pointer. Flags are bit-packed: FlagSet, FlagClear, and FlagGet locate the byte and manipulate the bit for the flag index.

That is the storage truth under the story illusion. When the guard “remembers” that you have permission, the NPC does not remember. The map does not remember in a narrative sense. A flag or variable has changed. Later, a script command reads it. Later still, a coordinate trigger or map script may branch differently because of it.

Let’s pause there. What is the difference between a blocker and a lock in this discussion? A lock, in script.c terms, can mean field control locking: LockPlayerFieldControls, sLockFieldControls, and the ScriptContext_RunScript behavior that prevents normal player input from racing ahead while a script owns the field. A blocker, in progression-design terms, is a story gate: an NPC, door, cutscene tile, rival battle, badge requirement, or map transition that checks flags and variables. They are related because scripts often lock controls while resolving a blocker, but they are not the same thing. One is runtime control ownership. The other is game-state policy.

This is where pokefirered/src/script.c, chunk_d424688149aa3bcb, matters for execution support. ScriptContext_SetupScript initializes the global context, sets up bytecode, locks field controls, and marks the context running. ScriptContext_RunScript locks controls while running and unlocks them when the command stream finishes. ScriptContext_Stop moves the global context into a waiting status. That means a script can begin, run a few immediate commands, hit a command that needs asynchronous waiting, and yield back to the overworld without letting the player freely walk away from the middle of the transaction.

Now look at the handlers in scrcmd.c through that lens. ScrCmd_waitstate calls ScriptContext_Stop and returns true. ScrCmd_lock and ScrCmd_lockall freeze object events and set up a native wait function, so the bytecode script can pause until the field-side operation completes. ScrCmd_message shows text, and ScrCmd_waitmessage sets up a native wait until the message box is hidden. ScrCmd_warp sets a destination, calls the warp operation, resets initial player avatar state, and returns true. Those true return values are not decorative. In RunScriptCommand, a handler returning true causes the interpreter to return to its caller for now. That is how the VM cooperates with frame-based field behavior instead of blocking the entire engine in a tight story loop.

This is a major design point, so let me check the room. Why can’t every script command just run to completion immediately? Because not every game action is instantaneous. Text boxes require player input or timed UI state. Object movement unfolds over frames. Warps involve transitions and map loading. Battles hand control to another major state machine. If the script VM tried to finish all of that synchronously in one C call, it would fight the callback and task architecture we studied earlier. Instead, the script VM can yield through native waits and resume when the external operation reports completion.

You can see why battle setup becomes a cross-system anchor here. The packet points us to pokefirered/src/battle_setup.c, chunk_494d132a59422c85, which includes script.h and event_data.h alongside overworld, field control, event object movement, metatile behavior, and battle transition headers. That include pattern is not an accident. A trainer battle is often entered by script or overworld event logic, consumes battle-specific systems, and then must return to field progression with flags, variables, and follow-up scripts in a coherent state. When a trainer “is defeated,” that is not just an animation result. It becomes a flag, a post-battle script path, and a changed future branch.

Now, back to control flow. The phrase “branching and blockers” can tempt you to imagine big nested trees. FireRed’s script commands are lower level than that. The source gives you compare commands, a comparisonResult latch, and goto or call variants. You build the tree out of labels and jumps. This is closer to assembly than to a modern quest graph editor. It is simple, compact, and fast, but it puts responsibility on the script author and tools to keep labels and state transitions sane.

Suppose we have a rival encounter at a bridge. The map coordinate trigger starts the script. The script locks controls, turns the rival toward the player, shows a line of dialogue, starts a trainer battle, and after the battle sets a defeated flag. Then a later interaction checks that defeated flag and either blocks the player or allows the route to proceed. If the defeated flag is set too early, the player may lose and still have the world think the rival was cleared. If it is set too late, the battle may finish but the coordinate trigger may fire again. If the post-battle return pointer is wrong, the battle can end and the follow-up script never runs. So debugging this class of issue means following the script pointer and following the state mutation.

That debugging method should sound familiar from the end of the previous stretch. We do not say “the NPC bugged out.” We ask: what script entered? What command last ran? What was scriptPtr? Did comparisonResult contain the value we expected? Did a handler return true and yield? Was ScriptContext_Enable called afterward? Which flag or variable changed in event_data.c? Did a later branch read the same state slot, or a different one?

Let’s make this even more concrete for your workshop interpreter. You are building commands for setflag, clearflag, setvar, compare, branch, warp, and message. Do not start by inventing a quest manager. Start with a ScriptContext-like record. It needs at minimum a program counter, a comparison result, a running or waiting mode, and access to a flags-and-vars store. Then implement command handlers as small functions. Setflag writes to the boolean store. Setvar writes to the integer store. Compare reads values and records less, equal, or greater. Branch inspects that recorded result and either assigns the program counter or leaves it alone. Message emits UI work and may yield. Warp requests a map transition and should probably yield. That is the core FireRed lesson translated to a small modern lab.

A subtle point: do not make compare return true or false directly into branch as a function call. Store the comparison result in the VM context. That may feel less elegant in a modern language, but it matches the source architecture and teaches you why commands can be sequenced independently. The branch is not “if compare then.” It is “use the interpreter’s most recent comparison result.” That distinction matters for understanding ScrCmd_goto_if in pokefirered/src/scrcmd.c, chunk_c2a54fb6031b5886.

Another subtle point: model flags and variables differently. A flag is a bit of world memory. A variable is a small integer world memory slot. If you treat every flag as a variable with 0 and 1, your lab can still function, but you will miss why FireRed’s event_data.c has FlagSet, FlagClear, FlagGet, and bit storage separate from VarGet and VarSet. The distinction is architectural documentation. When a script uses a flag, it is usually recording a boolean fact. When it uses a variable, it is often recording a phase, selection, counter, or scene state.

Now let me connect this back to map scripts, because control flow is not only inside NPC scripts. In pokefirered/src/script.c, chunk_03258f0288d89251, the map script support includes routines that find map script tables and run scripts for map lifecycle moments: on load, on transition, on resume, on return to field, and frame-table checks. The frame-table case is particularly relevant to state gating, because the map header table can compare variables and choose a script when values match. That means branching can happen before you are even inside a normal NPC conversation. The map itself can say, “given this state, run this script at this lifecycle point.”

So when we say story progression is state mutation, we mean it at multiple entry levels. An NPC script can mutate a flag. A coordinate event can be gated by a variable. A map script table can compare variables and run a script. A battle setup flow can return to a post-battle script that mutates trainer flags. The progression system is not one monolithic quest object. It is a distributed set of entry points all reading and writing shared event memory.

Here is a quick oral exam check. If a player enters a map and an automatic cutscene does not play, where do you look first? Not at the text box system. First, inspect the map’s script table and the variables it compares. Then inspect event_data.c semantics for whether the variable slot is saved, temporary, or special. Then inspect script.c map script dispatch. Only after that do you inspect the specific cutscene command sequence. If the cutscene starts but stops at a message, then you look at message and wait commands. If it starts the battle and fails afterward, then battle setup and post-battle script return become relevant.

This approach also keeps week 8 tied to the larger course plan. In earlier weeks, we traced data tables into callbacks and tasks. In weeks 6 and 7, we used field input polling, avatar movement, interaction checks, object-event records, local IDs, and trigger variables as prior foundations. Today we are putting the interpreter behind those foundations. Next, when we study flags and variables as world memory in more detail, you should already have the execution model in mind: state is not abstract. It is read by commands, written by commands, and consulted by later event dispatch.

One final synthesis for this segment. The script pointer is the immediate mechanism of execution, but it lives inside ScriptContext. RunScriptCommand dispatches bytes through a command table. scrcmd.c implements the verbs: compare, branch, set state, show UI, lock actors, warp, start larger systems. event_data.c stores the memory those verbs read and write. script.c coordinates global and immediate contexts, yielding, waiting, map lifecycle scripts, and field-control locking. The visible result is what players call story progression, but architecturally it is controlled mutation of shared event state plus controlled movement of scriptPtr.

Hold onto that model, because in the next stretch we are going to take flags and variables apart more carefully: where temporary state lives, where persistent story facts live, why special variables like the 0x8000 range exist, and how those choices affect save files, map reloads, and bugs that only appear after a warp or battle return.

Now let’s take that apart a little more slowly, because this is where many bugs in a creature RPG stop looking mysterious. If a story event behaves correctly the first time, breaks after a warp, then works again after a save reload, you are usually not looking at a rendering bug. You are usually looking at the wrong kind of state. The script VM is only the mover. The question is, what memory did the script mutate, and how long is that memory supposed to live?

On the board, I’m going to write three boxes. The first box is persistent event state. The second is temporary event state. The third is special scratch state. These are not philosophical categories. They map very directly to FireRed’s implementation. In pokefirered/src/event_data.c, chunk_a13cdbcb283dbf3b, the ordinary flags and variables are reached through gSaveBlock1Ptr, while special variables such as gSpecialVar_0x8000 through gSpecialVar_0x800B and gSpecialVar_Result are EWRAM globals accessed through the special-variable table. That one distinction explains a great deal of FireRed’s story architecture.

Persistent event state is the world’s memory. Has the player obtained a badge? Has a trainer been defeated? Has a story object disappeared? Has the National Pokédex been enabled? That state must survive leaving the map, entering battle, saving, resetting, and returning later. So it belongs in save-backed flags or save-backed variables. Temporary event state is more like map-session bookkeeping. It is allowed to disappear at field transitions or cleanup points. Special scratch state is even more immediate. It is the argument and return-value lane that lets script bytecode talk to C helpers without every command needing a custom calling convention.

Pause there and check your understanding. If I ask, “Should the fact that a one-time rival battle has been cleared go in gSpecialVar_Result?” the answer is no. gSpecialVar_Result is a return lane, not world memory. If I ask, “Should the menu choice the player just selected go in a permanent story flag?” again, no, unless that menu choice itself becomes a durable story fact. If I ask, “Should the fact that the player owns the Thunder Badge be represented by something save-backed?” yes, absolutely. That is the difference between state used during a transaction and state that defines the player’s world.

Now, let’s connect this to the interpreter mechanics we just finished. In scrcmd.c, chunk_c2a54fb6031b5886, commands like ScrCmd_setvar, ScrCmd_compare_var_to_value, ScrCmd_setflag, ScrCmd_clearflag, and ScrCmd_checkflag are not doing large amounts of narrative work by themselves. They are short adapters. They read operands from scriptPtr using the script-reading helpers, then they call event-data helpers such as GetVarPointer, VarGet, FlagSet, FlagClear, and FlagGet. The meaning of “open the world” is distributed. The bytecode command says “set this bit” or “compare this variable.” The constants tell us which bit or variable. The later map scripts, object visibility checks, trainer logic, and blockers interpret that bit as world progress.

This is why I keep pushing you toward the data table to global state to callback or script to visible behavior trace. We used that trace for maps. We used it for object events. We used it for A-button interaction entry last week. Now the same method applies to story. Do not start from the line of dialogue. Start from the state mutation. What variable changed? What flag changed? Who reads it later? Is that reader a map script, an object event, a trainer battle check, a special function, or a field-control routine?

Let’s use a concrete example that looks like the lab you are going to build: a locked door. The player presses A on a door object. We are not going to re-teach the A-button lookup path; that was last week. We will assume, from our prior foundation, that interaction dispatch has found the object event’s script pointer and loaded it into the global ScriptContext. The script begins. First it might check a flag, say a made-up FLAG_GOT_BASEMENT_KEY. Architecturally, ScrCmd_checkflag reads the flag ID from scriptPtr, calls FlagGet, and stores the result as the context comparison result. Then ScrCmd_goto_if reads a condition and a destination pointer. If the comparison says the flag is set, scriptPtr jumps to the open-door branch. If not, it falls through to the locked message branch.

Notice something subtle here. The branch did not ask the door whether it is locked. The branch asked shared event memory whether the world says this door may be opened. That lets the key be obtained somewhere else. It lets a cutscene clear or set the same blocker. It lets the door script remain a simple consumer of global progression state. That is data-driven story architecture in its plainest form.

Now suppose the player has the key. The script may show a message, play a sound, call an open-door animation command, wait for the animation, then warp. In scrcmd.c, chunk_c2a54fb6031b5886, the warp-family commands such as ScrCmd_warp, ScrCmd_warpdoor, and related variants read map group, map number, warp ID, x, and y operands, call SetWarpDestination, then trigger the appropriate warp behavior. ScrCmd_warp also resets the initial player avatar state and returns TRUE, which matters because returning TRUE from a command is one of the interpreter’s ways of yielding back to the surrounding field engine. So even in this tiny door example, we see the full chain: interaction entry gives us a script pointer, command dispatch interprets bytes, event_data answers progression questions, scrcmd initiates a field transition, and the overworld shell resumes around that transition.

Now, on the board, I’m going to write the word “variable” and underline it twice, because variables in this engine are a little more overloaded than flags. A flag is a bit. It is excellent for “yes or no.” A variable is a 16-bit value. It is better for scene phases, counters, selected choices, map-scene states, and compact enumerations. FireRed’s GetVarPointer in pokefirered/src/event_data.c, chunk_a13cdbcb283dbf3b, is the gatekeeper. If the index is below the variable range, VarGet treats it as an immediate value. If it is in the ordinary variable range, it resolves into gSaveBlock1Ptr->vars. If it is in the special-variable range, beginning at the 0x8000 family, it resolves through gSpecialVars to EWRAM special variables.

That design gives scripts a very compact calling style. A script command can read a halfword and then call VarGet. If the halfword is a literal like 5, VarGet can effectively yield 5. If the halfword is VAR_SOMETHING, VarGet yields the saved variable value. If the halfword is one of the special variables, VarGet yields the scratch value. So a single command implementation can accept either an immediate constant or a variable reference. That is a small implementation convenience, but across hundreds of script commands, it becomes a major architecture pattern.

Here is the advanced point: this convenience also creates an aliasing discipline that the script author must respect. If you pass a special variable where a persistent variable was intended, the command will work today and vanish tomorrow. If you store a durable scene phase in a temporary variable, the map may behave correctly until ClearTempFieldEventData runs. In event_data.c, chunk_a13cdbcb283dbf3b, ClearTempFieldEventData explicitly clears ranges of temporary flags and temporary variables and also clears certain system flags such as flute effects and Strength usage. So if a blocker depends on that range, it is intentionally session-scoped. That is not an accident; it is a lifecycle contract.

Let’s do a quick check. Imagine a cave boulder moved by Strength. Should “Strength is currently active in this field session” be permanent? No. FireRed treats that kind of state as field-session state, because when you leave and come back the current-use state should be recomputed or re-requested. Should “player has obtained the badge that allows Strength outside battle” be temporary? No. That is player progression. So even when two facts are related in game design language, they belong to different storage lifetimes.

This becomes especially important when larger systems are launched from scripts. Look at how broad scrcmd.c is in chunk_c2a54fb6031b5886. It does not only contain compare and branch. It touches item operations, object movement, lock and release, messages, Pokémon-giving helpers, trainer battles, wild battles, marts, field effects, door commands, money, coins, and more. That breadth is not messy by itself. It is a sign that script commands are the integration layer. The script VM is the place where story logic says, “Now call into inventory,” “now call into battle setup,” “now call into field effects,” “now change object visibility,” “now wait.” You should think of script commands as ports into subsystems.

That is why the source packet also points us to modules outside the script files. For example, pokefirered/src/battle_setup.c, chunk_494d132a59422c85, includes script.h and event_data.h alongside battle, overworld, event object movement, field control, and battle transition headers. That is exactly what we expect from a battle entry module in this architecture: battle setup is not isolated from progression. A trainer battle must know which trainer flag has been set, where to return afterward, what script continues after victory, and how the field state is restored. Similarly, pokefirered/src/daycare.c, chunk_119931105f546346, includes script.h and event_data.h because a facility like the daycare is not just a menu; it is a story-accessible service with persistent state, messages, party interactions, and script-facing behavior. And pokefirered/src/learn_move.c, chunk_af3598df6f092723, includes script.h and event_data.h as well, which makes sense for an NPC-driven move-relearner style interaction that must enter UI state machines and return results to the script layer.

So when you see script.h and event_data.h included all over the codebase, do not read that as accidental coupling only. Some coupling is real, yes, and in a modern engine you might put a cleaner service boundary around it. But architecturally, it tells us the same thing the gameplay tells us: story scripts are the glue that launch subsystems, and event data is the shared memory those subsystems use to report back.

Now let’s return to ScriptContext for a moment, but only to deepen the point. In pokefirered/src/script.c, chunk_1be14bbdaf5c3400, RunScriptCommand supports bytecode mode and native mode. In native mode, a C function can run until it returns in a way that lets the context resume bytecode. In bytecode mode, the interpreter loops command by command until a command returns TRUE or the script stops. That means a script can run several simple commands in one frame, then yield when it starts a message, menu, movement, battle, warp, or other asynchronous operation. The global context status functions in pokefirered/src/script.c, chunk_03258f0288d89251, make that lifecycle explicit: ScriptContext_RunScript locks player field controls, runs commands, shuts down and unlocks when complete, and waiting commands can place the context into a waiting state until some other field or UI code re-enables it.

This is an important correction to a common beginner model. Beginners often imagine a script as a blocking function: talk to NPC, run the whole script, finish. FireRed’s model is more like a resumable transaction. It runs until it must wait for the player, wait for animation, wait for movement, wait for a battle, or wait for a field transition. Then the engine continues its frame loop. Later, some subsystem marks the script context runnable again. That is why scriptPtr and event state must both be valid across waits. The pointer says where the transaction resumes. The flags and variables say what the world means when it resumes.

Now here is a concrete failure mode. Suppose a script gives an item, then sets a flag afterward, but the item-giving flow opens a message or menu and yields before the flag is set. If the player can somehow interrupt, transition, or trigger a save at the wrong boundary, the durable state may say “item obtained” in the bag but not “event consumed” in the flags. FireRed’s actual scripts and command sequencing are designed around those hazards, but your lab interpreter will be small enough that you can reproduce the bug easily. In your locked-door exercise, try putting “warp” before “setflag door opened,” and then ask what happens if the new map’s load script expects the flag already to be true. That is not just a toy problem. That is the basic class of progression bugs: side effects happen in the wrong order relative to yields.

Let me ask a check-for-understanding question, and answer it in the way I expect you to answer on the quiz. A script compares VAR_MAP_SCENE_PALLET_TOWN_OAK against a phase value, branches to a cutscene, runs the cutscene, and then increments the variable. Why use a variable instead of five flags? Because the scene is mutually exclusive phase state. The player is at phase zero, one, two, three, and so on. A variable represents that directly and prevents contradictory combinations like “phase one flag and phase three flag are both set.” Flags are still useful for independent facts. Variables are better for ordered or enumerated progression.

Another question. A script calls a C special that returns whether the party has a Pokémon with a required move. Where should that boolean return go? Most likely gSpecialVar_Result or a requested special variable. That is exactly the pattern shown by many scrcmd.c commands in chunk_c2a54fb6031b5886: commands call subsystem helpers and place success, failure, indexes, item pockets, party positions, or other short-lived answers in special variables. Then the next script command compares or branches on that result. It is a register-like style of scripting, not unlike a tiny machine with a few shared registers and a larger save-backed memory.

Now, do not overextend the metaphor. This is not a general-purpose CPU emulator. The script VM has a command table of high-level verbs. One command may set a bit. Another may open a shop. Another may start a battle. Another may warp the player. The bytecode is compact, but the command handlers are semantically rich. That is the right tradeoff for a content-heavy RPG on constrained hardware. You want designers and map scripts to express story flow compactly, while C code handles the expensive, stateful, engine-specific operations.

Let’s tie this back to the week plan. Week 6 gave us the runtime shell: main callbacks, task dispatch, and the habit of asking who owns the frame. Week 7 gave us object events, local IDs, script labels, coordinate triggers, and interaction entry. Week 8 is where those prior systems become progression. The player presses A, steps on a coordinate, enters a map, wins a battle, or returns from a UI. Those events do not become “story” until script code mutates shared state and later code reads it. Story progression is not a separate layer floating above the engine. It is the engine’s ordinary control flow plus durable state mutation.

So for the workshop interpreter, I want your implementation to preserve this architecture even if the command set is tiny. Give yourself a script pointer. Give yourself a command dispatch table. Give yourself flags and variables with different intended lifetimes, even if you use small arrays. Implement setflag, clearflag, setvar, compare, branch, warp, and message. Then build the locked-door scenario so the door does not own a private boolean hidden in the door object. Make the door read event state. Make the key script write event state. Make warp depend on the branch. That is how you practice the FireRed architecture rather than merely imitating its syntax.

And as we move into the next part, keep one question in your mind: when FireRed stores a fact, who is allowed to forget it? If the answer is “nothing until the save is deleted,” you are in persistent event data. If the answer is “the field lifecycle may clear it,” you are in temporary event data. If the answer is “the next command or subsystem handoff may overwrite it,” you are in special scratch state. That question will guide us through the remaining details of flags, variables, special variables, quest-log recording, and the nasty edge cases that appear when map reloads and battle returns interact with script continuation.

So, let me make that question practical. If I write “the door is unlocked” into a flag, I am saying the save file may remember it. If I write “the player chose yes in this menu” into a special variable, I am saying the next few script commands may care, and then the value can be overwritten without ceremony. That distinction is the difference between progression state and conversation scratch space.

Put this on the board: script execution is not the story. Script execution is the mechanism that mutates story state. That is why the same bytecode machinery in pokefirered/src/script.c, especially the ScriptContext fields and the RunScriptCommand path in chunk_1be14bbdaf5c3400, can support a sign, a rival battle, a warp, a gift Pokémon, a shop, or a cutscene. The interpreter itself does not know that one command is emotionally important and another is ordinary. It advances a pointer, reads operands, calls a handler, stores comparison results, jumps, calls, returns, waits, resumes. Meaning comes from which flags, variables, objects, maps, and subsystem handoffs the command touches.

Now, look at the handler layer. In pokefirered/src/scrcmd.c, chunk_c2a54fb6031b5886, the command implementations expose the vocabulary of the world: setvar, compare_var_to_value, setflag, clearflag, checkflag, warp, trainerbattle, special, specialvar, waitstate, fadescreen, opendoor, closedoor, and so on. That file is where abstract bytecode becomes concrete engine action. When ScrCmd_checkflag writes into comparisonResult, and a later conditional goto consults that result, you are seeing story branching expressed as plain interpreter state. No mystery. Just one command producing a small result, and another command consuming it.

Quick check for understanding. If a locked door script says, “check flag has key,” then “goto_if true open door,” where is the story memory? Not in the branch command. Not in the door animation. The memory is in the event flag storage. The branch is only a reader. The animation is only presentation. The flag is the durable fact.

And FireRed makes that durability explicit in pokefirered/src/event_data.c, chunk_a13cdbcb283dbf3b. That module gives us the flag and variable operations: FlagSet, FlagClear, FlagGet, VarSet, VarGet, and the pointer access behind script variables. This is one of those architectural modules that looks boring until you realize every route blocker, badge gate, defeated trainer, obtained item, opened shortcut, and story milestone eventually reduces to a small mutation in this event-data layer. The overworld changes because data changed first.

Now, contrast that with the script context. In pokefirered/include/script.h, the ScriptContext has scriptPtr, stackDepth, comparisonResult, nativePtr, a small stack, a command table, and a four-entry data array. That is execution memory. It is the VM’s working desk. If you use it to decide whether a command should continue this frame, fine. If you use it to remember whether the player has beaten a gym leader across a save load, that is the wrong level of memory. This is the design rule I want you to carry into the lab: the lifetime of the data must match the lifetime of the fact.

Let’s walk a concrete edge case. Suppose an NPC says, “I will battle you, and after the battle I will move aside.” The script may start in an object-event interaction, which we covered last week through NPC local IDs and script labels, but once battle begins, the field system hands control to the battle setup path. The research packet points us to pokefirered/src/battle_setup.c, chunk_494d132a59422c85, which includes both script.h and event_data.h. That pairing is not incidental. Battle setup needs to cooperate with script continuation and persistent event facts. If the NPC moving aside depends only on a temporary animation state, then returning from battle can break the scene. If it depends on a trainer flag or progression variable, then the map can reload, the player can return, and the world still knows the trainer was beaten.

This is also why wait commands matter. A script command that starts a palette fade, door animation, field effect, shop, or battle often cannot simply keep interpreting bytecode until the end. It has to yield. In script.c, chunk_03258f0288d89251 and chunk_d424688149aa3bcb support that control-flow picture: the context can stop, wait, and later be enabled again. In scrcmd.c, commands such as waitstate or commands that install a native wait function show the interpreter cooperating with other state machines. The VM is not isolated. It is a scheduler participant inside the larger callback, task, overworld, and subsystem architecture we have been building since week one.

Let me pause there. Does everyone see why a script interpreter in a creature RPG is not just “dialogue code”? It is a coordination language. It coordinates field control, messages, battles, item changes, map transitions, object visibility, and save-backed progression. That is why our week 8 lab asks you to build a small bytecode-style interpreter with setflag, clearflag, setvar, compare, branch, warp, and message. Those commands are not random. They are the minimum set needed to experience the real architectural pressure.

One more example before we close. The move relearner is not part of today’s script VM source focus, but pokefirered/src/learn_move.c, chunk_af3598df6f092723, is useful evidence for the course-wide pattern. It includes script.h and event_data.h while running its own menu state machine. That tells us something important: FireRed often enters a specialized subsystem, lets that subsystem run through callbacks or states, and then reconnects the result to script-visible or event-visible state. The script interpreter is the glue, not the only machine in the cartridge.

So here is the recap. From weeks 6 and 7, we already know how field input and object-event lookup can select a script. Today we followed what happens after that selection. The script context holds the current instruction pointer and execution scratch. The command table maps bytes to C handlers. scrcmd.c implements the world-facing verbs. event_data.c stores the durable facts. Control-flow commands read comparisonResult and redirect scriptPtr. Waiting commands let other systems finish before the VM continues. And story progression, at the engine level, is state mutation plus later reads of that mutated state.

To close today’s lecture, connect this back to the whole course plan. FireRed is not one giant story program. It is a network of data-backed systems that agree on shared state. Map data gives places structure. Object events give actors identity. The input and interaction path chooses an entry point. The script VM interprets that entry point. Flags and variables remember what changed. Battle, inventory, UI, and field effects temporarily take control, then return with results. When you can trace that chain from data record to global state to callback or task to script command to visible behavior, you are no longer guessing at the architecture. You are reading the game as an engineered system.

## Source References
- pokefirered/src/daycare.c
- pokefirered/src/battle_setup.c
- pokefirered/src/battle_util.c
- pokefirered/src/learn_move.c
- pokemon-showdown/data/abilities.ts
- pokemon-showdown/data/mods/gen9ssb/moves.ts
