Life was like one of those many-storied houses of dreams where the dreamer, with a slow or sudden rush of understanding like a wash of cool water, knows himself to have been merely asleep and dreaming, to have merely invented the pointless task; the dreamer awakes relieved in his own bed and rises yawning, and has odd adventures, which go on until (with a slow sudden rush of understanding) he awakes in this palace antechamber; and so on and on.
Savefiles are used to store information on the disk. There are two reasons why this is usually done. One is to store information about the world so that it can be shut down and rebooted (possibly with new code). The saved information can then be loaded and the relevant aspects of the world restored to their previous state.
Another reason for using savefiles is to store information about players. Such player savefiles may be used to re-create their mobs when they log in or could even be transferred from one world to another along with the player. In this way, a world could be distributed across several servers running in parallel.
|
If a file with the specified name already exists, it will be used by the savefile. Otherwise, a new one is created. If no name is specified, a unique temporary file will be created.
When the savefile object is deleted the specified file will remain. It can be accessed at a later time by creating another savefile object with the same name. Notice the difference between creating the object and creating the file. Several savefile objects may be created to access the same physical file. That file is only created once and lasts until it is explicitly removed.
The only case in which this is not true is when no name was given and a temporary file was used. In that case, the file is deleted along with the savefile object. Such temporary savefiles may be useful when loading information from another world, as will be described in section 12.6.
|
By default, the Write procedure stores the value of each object variable. You will see later how to add additional information or how to control which variables are saved.
For now, let's just use the default Read and Write procs to save a player. It can be done like this:
mob/Logout() var/savefile/F = new(ckey) Write(F) del(src) mob/Login() var/savefile/F = new(ckey) Read(F) return ..()
In this case, each player is saved in a separate file with the same name as their key. To make sure it is a valid file name, we used ckey, the canonical form of the key, which is stripped of punctuation. This is still guaranteed to be unique, so there is no fear of conflict.
You might be wondering what happens when a new player logs in. Since they don't have a savefile, a new one will be created. The Read proc, of course, will find this file to be empty and will therefore return without making any changes.
When an existing player logs in, on the other hand, each variable will be restored to the value it had when last saved. Some of those variables may just be numbers or text. Some, like the contents list, may be more complicated. Those are handled too. (Gasp. Or at least inhale with slight exaggeration. Otherwise, you won't fully appreciate the sentence you just read.)
A few things are not saved. For example, the mob's location is not
restored. That is because the location is a reference to an external object
(like a turf). If that value were written to the savefile, it would be
treated just like an item in the contents list--the whole object would be
written to the savefile. That is certainly not what you want in the case of
the mob's location.
Variables like loc are called temporary or transient variables,
because their value is computed at run-time (when the mob is added to the
contents list of a turf). In some cases, temporary variables would just be
wasting space in the savefile, but in others (like loc), it would
be incorrect to save and restore them as though they were objects "belonging"
to the player.
The Write proc already knows about the built-in temporary
variables. If you define any of your own, however, you need to mark them as
such. You do that with the tmp variable type modifier. The syntax
is just like global and const.
The following example defines some temporary variables that reference
external objects. We don't happen to want those objects (in this case other
mobs) to be saved along with the player. Since there is no way for the
compiler to know that, we have to tell it by using the tmp flag.
In some cases, you might want to restore temporary variables when the player
is loaded. Take the case of the player's location. You may not want it
saved as an object but rather as a coordinate that can be restored when the
player returns.
In other words, you want to save it by reference rather than by
value. The trick is, you have to find some sort of reference that
will still be valid in the future--possibly even after you have recompiled
and rebooted the world. That is why the compiler leaves this sort of thing
up to you: only you can decide how best to do it.
In this case, we will simply save the player's map coordinates. The
following example gets the job done, but you will see an even better way to
do it later.
All we did was copy the player's coordinates into non-temporary variables
before calling the default Write proc. Then, when loading the
player, we simply moved back to the same spot.
You are probably thinking it would be more efficient if you could just write
the coordinates to the file yourself rather than making dummy variables for
the purpose. You are right. Working directly with savefiles is next.
The interior of a savefile is structured like a tree. Each node in the tree
has a data buffer and may also contain additional sub-nodes. (A
buffer is simply a sequence of stored values.) Since the file-system
is such a familiar analogy, we shall call the nodes directories and
the top node in the savefile shall be called the root
directory. (Yes, savefiles are upside-down trees just like the DM
object tree.)
Do not be confused between savefile directories and file-system
directories. The savefile itself is just a single file. The
savefile object simply presents the contents of that file in the
form of a hierarchical directory structure. To distinguish between the two,
one calls these data directories as opposed to file
directories.
The purpose of all this is to allow you to organize information in a logical
format. For example, you could have a master savefile with a directory for
each player. Or different aspects of the world could be stored in different
directories. The freedom to organize things in such a manner is not merely
esthetic; it determines what data can be saved and retrieved as a unit. If
everything were simply written in one continuous stream of data, the entire
file would have to be read from the beginning in order to find, say, the
information about a particular player.
The savefile variable cd contains the path of the current data
directory within the savefile. These paths are like DM object type paths
except they are stored in a text string. The root is a slash
By assigning cd, the current directory can be changed. An absolute
path may be specified (beginning with the root
It is possible to change to a directory that does not exist. If data is
written to it, the new directory will be automatically created in the
savefile. Valid directory names are the same as node names in DM code;
letters, digits, and the underscore may be used to any length. They are case
sensitive, so "KingDan" is different from "kingdan".
The dir savefile variable is a list of directory names inside the
current directory. An entire directory can be deleted by removing it from
this list. It is also commonly used to test for the existence of a
directory.
In the following example, a check is made when players log in to see if they
already exist in a master savefile.
Note that we are using ckey as the unique data directory name for
each player.
Information in a savefile is stored in buffers. These are sequential
records of one or more values. The term sequential or serial
implies that values which are written in a particular order must be read
back in that same order. This is different from random access data
which may be retrieved in any order (like items in a DM list object). In a
savefile, the directories are randomly accessed and the data within buffers
is sequentially accessed. The two methods each serve their own purpose.
2.1 tmp Variables
mob
var/tmp
mob/leader
followers[]
2.2 Overriding Write and Read
mob
var
saved_x
saved_y
saved_z
Write(savefile/F)
saved_x = x
saved_y = y
saved_z = z
..() //store variables
Read(savefile/F)
..() //restore variables
Move(locate(saved_x,saved_y,saved_z))
3. The Structure of a Save File
3.1 cd variable
"/"
,
which also serves as the delimiter between directory names like this:
"/dir1/dir2"
.
"/"
) or a path
relative to the current directory may be used. The special path name
".."
may be used to represent the parent of the current directory.
No matter how you assign it, the new value of cd will always be the
resulting absolute path.
3.2 dir variable
var/savefile/SaveFile = new("players.sav")
mob/Login()
SaveFile.cd = "/" //make sure we are at the root
if(ckey in SaveFile.dir)
SaveFile.cd = ckey
Read(SaveFile)
usr << "Welcome back, [name]!"
else
usr << "Welcome, [name]!"
..()
4. Data Buffers
|
When a directory is not specified, the current position in the buffer is accessed. This position always starts at the beginning when the directory is entered (by setting cd). Subsequent values are appended to the buffer as they are written.
When a directory is specified, the value written replaces any previous contents of the given directory. This is equivalent to entering the directory, writing to it, returning to the previous directory, and restoring the position in the buffer. An absolute or relative path may be used to specify the location of the directory.
We can now return to the problem of saving the player's coordinates. Before, this had to be done using dummy variables. Now it can be done directly.
mob/Write(savefile/F) //store coordinates F << x F << y F << z //store variables ..() mob/Read(savefile/F) var {saved_x; saved_y; saved_z} //load coordinates F >> saved_x F >> saved_y F >> saved_z //restore variables ..() //restore coordinates Move(locate(saved_x,saved_y,saved_z))
Ok, so we still had to define some dummy variables, but at least they are hidden in the Read proc rather than cluttering up the object definition. That saves memory and, as it happens, it also saves space in the savefile, because we chose to write the coordinates sequentially into the same buffer rather than into three separately named buffers.
Output Code | Contents of players.sav |
mob/Write(savefile/F) F["name"] << "Dan" F["gender"] << "male" F["icon"] << 'peasant.dmi' |
/dan name "Dan" gender "male" icon 'peasant.dmi' |
mob/Write(savefile/F) F << "Dan" F << "male" F << 'peasant.dmi' |
/dan "Dan", "male", 'peasant.dmi' |
The notation introduced above for reading and writing to a specified
directory works in general--not just on the left-hand side of the
input/output operators << and
>>. The syntax is a savefile followed by a
directory path in square brackets. This accesses the first value in the
specified buffer and may be used in any expression, including assignments.
It therefore behaves like a sort of permanent variable, which is a
convenient device.
You may be wondering why we didn't use the <<
operator to write players to a savefile. Instead we have been calling the
Write proc. With minor modifications, we could have used
<<. In fact, << internally
calls Write when saving an object, so it would almost be the same.
The difference between directly calling Write verses using
<< to save an object is in how the object will be
recreated. When >> is used, a new
object is returned. Obviously when you call the object's Read proc
directly, the object must already exist.
The << operator works by first recording the type of
the object being saved. It then moves into a fresh temporary directory and
calls the object's Write proc. When this returns, the entire
contents of the temporary directory (sub-directories and all) are packed up
and written as a single value after the object type information which was
already saved. This allows you to treat an object like any other value when
it is written to a serial buffer. (Programmers refer to this
process as serialization of the object.)
The >> operator simply reverses the sequence of
operations. It reads the stored type, creates a new object, and calls its
Read proc.
The terminology used to distinguish between the two different cases is a
property save verses an instance save. When you call
Write directly, you are doing a property save, because you are only
saving the properties of the object. When you instead use
<<, you are doing an instance save, because you are
saving a full instance of the object (along with its properties). You must
use the same method for restoring an object that you used to save it.
4.3 Data Directories
4.4 Saving Objects
|
If no file is specified in the call to Export(), the key's savefile is deleted. If a value other than a savefile is given, this is written into a temporary savefile and exported to the key. Similarly, an object may be passed to Import() and it will be automatically read from the file.
One reason to use client-side saving is if the player will be connecting to a
group of worlds which all share the same savefile format. That way, changes
made to the player's mob in one world would be automatically transferred to
any of the other worlds the player accesses.
The following example outlines how player information can be saved in the
key file.
Like the previous server-side example, the player is loaded in
Login(). However, it is not a good idea to save the player in
Logout() as we did before, because in this case the client is needed
in order to export the file. By the time Logout() is called, the
player may have already disconnected.
It is therefore necessary to save the player before logging out. The
SavePlayer() proc in this example was defined for that purpose, but
it needs to be called from somewhere else in the code. You could do that
every time a change is made or whenever the player requests it. Another
method is to require the player to exit properly from the world in order to
be saved rather than simply disconnecting without warning.
Client-side savefiles are one method for transferring player information
between worlds. This is best used when the player is free to randomly
connect to any one of the worlds. It is also suited for situations in which
the contents of the player savefile are not sensitive--that is, the player
can't cheat by modifying or backing up the file.
There are times when a client-side savefile is not appropriate. In that
case, savefiles can be transferred directly between worlds without using the
player's key as an intermediary. That gives you assured control over the
contents of the file.
The procedures for transferring files between worlds are similar to the ones
for manipulating the player's key file. The procedure
world.Export() can be used to send a file. This causes a message
to be sent to the remote world, which handles it in the
world.Topic() procedure. If the remote world expects and is
willing to receive a savefile, it calls world.Import() to download
it. These three procedures were already introduced in section
8.2.4.
There are a few differences between the world Import and
Export procs and the corresponding client procs. In the case of
exporting from one world to another, the remote world's network address and
import topic must be specified in addition to the file being sent.
On the remote end, world.Import() is used to receive the file, just
as with a key file. However, this results in the savefile being
downloaded into the world's resource cache. A reference to this cache file
is returned instead of automatically creating a savefile object as
client.Import() does. That allows for the transferral of other
types of files. The cache reference can be easily opened as a temporary
savefile by simply specifying it in place of the name of the file to open.
The following code demonstrates how player savefiles can be transferred
directly from one world to another. The destination world is imagined here
to be running at the address dm.edu on port 2001. This address
could be any other hard-coded value, or even a variable set at run-time.
The
The code for world.Topic() actually belongs in the code for the
remote world, but here we assume that both worlds have the same player
import facilities. The "player" topic causes a savefile to be imported and
read. The mob which is created as a result is ready and waiting when the
player connects to the new world. One could instead store the savefile and
load it when the player arrives.
You may not want just anyone sending savefiles to your worlds. There are a
couple strategies to limit access. One is to keep the import topic a
secret. Another would be to put a password in the savefile itself.
Access can also be limited to specific network addresses. The address of
the sending world is passed as an argument to world.Topic. This
could be compared to a list of permitted addresses and permission granted
accordingly.
To get such a system working, it might be useful to use something like the
following code to test the sending and receiving of messages. It simply
allows you to specify the address and topic you wish to send and displays
the result.
Care must be taken when transferring objects from one world to another that
the same type of object is defined in both places. If an object type that
was saved in a file does not exist when it is loaded, it cannot be created
and null will be returned instead.
The most fool-proof strategy is to compile every world with the same code
base in which all the transferable object types are defined. Techniques for
splitting the code in large projects into multiple files is a topic that
will be discussed in chapter 19 and is a useful method
of re-using code for this purpose.
Most of the time, the default reading and writing behavior works and you
don't have to think too hard about how it works. I am going to tell
you how it works anyway. If you don't want to know, please shut your eyes
while reading this section.
Those of you who still have your eyes open get to learn about a few powerful
elements of the language. You will probably never use them, but powerful
knowledge should rarely be used anyway. (Knowing how to build an
H-bomb is a good example.) It just helps put everything else in perspective.
I will start by showing you how you could soft-code the default save
routines. Then I will explain how it works. Then I will explain how it
really works. Then the rest of you can open your eyes again.
This same code would, of course, be defined for object types other than mob
as well. It depends only on the existence of a special variable named
vars, which happens to be defined for all object types. It is a
list of the variables belonging to an object.
You can access and loop through vars like any other list. The only
special behavior is that when you index it with the name of a variable, you
get, not the name, but the value of the variable. So whenever you see
The next thing you must be wondering about in the above code is
issaved(). That is where we check to see if the variable being
considered is temporary. If the variable is marked global, const, or tmp,
it is not saved to the file because the issaved instruction returns
0.
Finally, before saving the variable, we check if it has changed. We do that
by using the initial() instruction to get the original compile-time
value of the variable. Only if the variable has changed is it saved. That
saves space, but it also makes the savefile more adaptable. In the future,
you may wish to change the default value of a variable. When you recompile
your world, existing savefiles will automatically use the new value if the
variable still had its initial value when saved.
5.1 Client-Side Saving
mob
Login()
var/savefile/F = client.Import()
if(F) Read(F) //restore properties
..()
proc/SavePlayer()
var/savefile/F = new()
Write(F) //save properties
client.Export(F)
6. Transmission Between Worlds
6.1 Export, Import, and Topic
6.2 A Sample Player Transferal System
mob/proc/GotoMars()
var/savefile/F = new()
F << src
if(!world.Export("dm.edu:2001#player",F))
usr << "Mars is not correctly aligned at the moment."
return
usr << link("dm.edu:2001")
world/Topic(T)
if(T == "player")
//download and open savefile
var/savefile/F = new(Import())
//load mob
var/mob/M
F >> M
return 1
GotoMars()
proc sends a player to the remote world. It simply
writes the player to a file and then sends it. In this example we happen to
be using a full object save on the mob rather than a mere property save, but
it could be done another way. Note how the return value of Export()
is checked to ensure that the remote world is alive and well before sending
the player along.
6.3 Security
mob/verb/export(Addr as text)
usr << "Export([Addr]) == \..."
usr << world.Export(Addr)
world/Topic(T,Addr)
world.log << "Topic([T]) from address [Addr]."
return 1
6.4 Sharing Object Types
7. Advanced Savefile Mechanics
mob/Write(savefile/F)
var/V
for(V in vars)
if(issaved(vars[V]))
if(initial(vars[V]) == vars[V])
F.dir.Remove(V) //just in case
else F[V] << vars[V] //write variable
mob/Read(savefile/F)
var/V
for(V in vars)
if(issaved(vars[V]))
if(V in F.dir)
F[V] >> vars[V] //read variable
vars[V]
in the above code, it is accessing the value of the mob's
variable whose name is stored in V
. Note that this value can even
be modified. It is just as if you were accessing the variable directly.