Well, parserp asked me for this weeks ago, but apparently I forgot until he posted the question again today
What is SMC?You may have noticed that some assembly games (such as
Phoenix) can save a user's settings, high scores, and even paused game state without ever seeming to create appvars or other variables to hold that data. Instead, they store the information in the program itself.
This is called
self-modifying code, or SMC for short. With SMC it's actually possible to create a program that modifies its own code while it's running, but that's usually a tricky thing to do, and especially in a compiled language like Axe where the programmer doesn't have full control over the machine code produced. However, data in Axe is easy to control. At the low level Axe is on, everyone's dealing with individual bytes and pointers, and since they're your bytes and pointers, you know (or should know) what they mean.
It's just dataData inside a program is no different from data in another variable. We could keep a two-byte high score at GDB0:
Remember, any strings or hex data or data in a
Data( command are put inside the program, and whenever they're "stored" to a static variable (such as
GDB0, Pic0, and Str0, the static variable becomes a pointer like any other pointer (such as
L1). So it's easy enough to manipulate data in a program. All that's left is to figure out a way to make the changes stay until the next time the program is run.
But that's where things get messy, because how the user runs your program makes a big difference in whether or not changes made with SMC will remain. The simplest case is when the program is in RAM and the user is running it from a shell, such as Ion, MirageOS, or DoorsCS. In this case, there's only one copy of the program, and whatever changes you make to it (or it makes to itself) are permanent. If the program was in archive, it gets trickier. Because programs are never run directly from archive, the shell makes a temporary copy of it in RAM and runs that. Some shells, such as MirageOS and DoorsCS, can "write back" the program into archive—basically deleting the original program and putting the RAM copy, along with whatever changes you made, into the archive. If your user is using a shell that doesn't support writeback (or if they have it disabled, such as when the option is set in MirageOS), the changes are lost when the temporary RAM copy is deleted. Note that all this depends on how the user has his calculator set up; you can't force someone else's shell to write back your program to archive, in other words. (Well, you probably could, but you probably shouldn't. It gets messier still.)
The simple caseFor people using shells, this will do:
The first time the program is run, it will display the current high score as 0. If it's run from a shell, it'll show 65535 every time after that.
Unfortunately, not everyone uses a shell to run programs. This isn't a problem for you if you compile your program in MirageOS or DoorsCS format, since those programs can't be run from the homescreen, but if you want to be nice and compile your program as Ion (so that everyone can run it, with or without a shell), you get to deal with the quirks of the
Asm( command. That's because for whatever reason, the TI-OS makes a temporary copy of the program
even though it's in RAM before running it. That causes a few problems. First of all, it means it's impossible to run a program with
Asm( when the amount of free RAM is smaller than the size of the program (try it yourself). Second, it means if you want to use SMC, you'll have to deal with two copies of the program, one of which could be anywhere in RAM.
ComplicationsYou can use
GetCalc( to look for that "other copy," find the position of the two bytes of
GDB0 in that variable, and store the high score there. But
GetCalc( needs a string containing the variable name in order to work. You could assume it's the name that you gave the executable (in this case,
prgmAWESOMEG). As soon as your user renames your program, though, you're out of luck. And in today's calculator world of shells and other assembly utilities, renaming programs is a breeze.
Fortunately, when a program is run from the homescreen with
Asm(, the full name of the program is stored at a specific spot in memory. This particular "spot in memory" is called OP1, located at
E8478, and because it's used for a wide variety of things, from variable management to floating-point math, it can quickly be overwritten. However, if you don't create, (un)archive, or delete any variables in your program, that "other copy" will stay in the same place. So if you save its location at the very beginning, you'll be able to work with it later on:
That's fine. Now we know that
Q points to the "other copy" of the program. But how do we know
where in the program we should store the high score? If you store it in the wrong place, you could easily overwrite code in the program. That would be very bad.
What we do know is that at least in the running copy of the program, the high score is stored at an address called
GDB0. We also know that the
start of the program that's currently running is at address
E9D93, because that's where a program is always moved before it's executed. So we could subtract
E9D93 from
GDB0, and we'd get the offset into the program where we're storing our high score!
If you don't understand how that works, imagine you're trying to give someone directions to your house. You know what block it's on, but not how far it is from the end of the block. Yet (for whatever reason) you know the exact GPS coordinates of your house, down to the meter, and you also know the exact GPS coordinates at the end of the block, down to the meter. All you have to do is subtract one from the other, and you know how far your house is from the end of the block.
It's the same idea here—you know the
absolute location of the high score (it's
GDB1). You also know the absolute location of the start of the program (it's
E9D93). All you have to do is subtract, and now you know where the high scores are,
relative to the start of the program.
Now imagine the entire block your house is on just got blown off the face of the earth and lands in Siberia. The search team knows exactly where the block landed, and now you need to tell them which house you're in so they can rescue you. (In our case, the block landed at the address saved in
Q.) Add this to the end of your program:
More complicationsThe program should now work fine for people using
Asm(, but it can cause problems for people using a shell because shells don't put the name of the program in
E8478. We need to check for that, or else our little routine could write two bytes anywhere in RAM when a user runs the program from a shell. (Why can't we all just get along?)
One thing we could do is use a
magic number. Basically, we put a known value at a known position in the program, and before we save to
{Q+GDB0-E9D93}, we look at the corresponding position relative to the address held by
Q. If it's not the same value, then
Q doesn't point to the program we think it points to, so we don't try to save the high score there.
At the beginning of your program, add this line:
I chose the hex string
D1ECA510 because the bytes it holds are very rare in code, so there's a lower chance that it just so happens to appear somewhere else. You could use a different value; in fact, you probably should, or else we'd all be using the same magic number and it wouldn't be so uncommon anymore. Just try to pick something random. (A string of zeroes is a very, very poor choice.)
Now change the last line of your program to this:
(Remember, the bytes are flipped because the processor is
little-endian—that is, two-byte values are stored and read with the least-significant byte in front. You don't have to worry too much about it.)
The final programTo summarize, here's a table to organize all the possible cases:
| Program in RAM | Program in archive |
Shell with writeback | Modifications saved | Modifications saved |
Shell without writeback | Modifications saved | Modifications discarded |
Asm( | Save modifications manually | N/A |