DM Guide 12

Материал из Chaotic Onyx
Перейти к навигацииПерейти к поиску


A long time ago in a galaxy far, far away...


Данная статья помечена как устаревшая, её содержание может быть неверным или неактуальным.

Если она не будет актуализирована или не появится веского повода для снятия этой плашки, то вскоре она будет перемещена в Священный архив.

При желании вы можете помочь проекту Onyx и сообществу Animus-logo.png SS13 в целом — загляните на наш Bus Mainframes.gif Портал сообщества.
Morgue.png


Это 12 глава перевода оригинального руководства по Dream Maker от разработчиков.

Остальные главы руководства и статьи о программировании на коде BYOND от любителей на русском.

Оригинальная 12 глава руководства на английском языке

Chapter 12

Savefiles

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. --John Crowley, Little Big Сейв-файлы используется для записи информации на диск. Делается это, в основном, по двум причинам. Во-первых, это хранение информации о мире, которая сохраняется, а потом может быть загружена вновь. Примером может служить сохранение книг в библиотеке. Другая причина - хранение информации игроков и о игроках. Сейв-файлы используется для сохранения персонажей игроков, и даже передачи их с сервера на сервер.


1. The savefile Object

The savefile data object provides control over a savefile on the disk. It is another built-in object type in DM. All data that is read from or written to the file goes through the savefile object.

To create a savefile object, the new instruction is used.

new /savefile(Name) Name is the optional file name. 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.


2. Saving Players

The first case we shall consider is how to save a player when they log out and restore them when they log back in. By doing that, you are free to delete their mob when they aren't around; it is just taking up memory and probably getting into mischief--best to put the avatar into suspended animation until the player returns. Another practical reason is that you can freely reboot (or horror of horrors) crash the world without losing player information. (And if you make a backup of the savefile, you are "free" to crash your hard-drive too. What fun!)

The mob Write proc is used to store information about a player and the Read proc is used to restore it. These both take a savefile as an argument.

object.Write (F) object.Read (F) F is the savefile. 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.)


2.1 tmp Variables

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.

mob

  var/tmp
     mob/leader
     followers[]

2.2 Overriding Write and Read

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.

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))

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.


3. The Structure of a Save File

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.


3.1 cd variable

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 "/", which also serves as the delimiter between directory names like this: "/dir1/dir2".

By assigning cd, the current directory can be changed. An absolute path may be specified (beginning with the root "/") 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.

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".


3.2 dir variable

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.

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]!"
  ..()

Note that we are using ckey as the unique data directory name for each player.


4. Data Buffers

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.


4.1 File Input/Output

The << operator places a value in a buffer and the >> operator reads a value from a buffer. If no directory is specified, the buffer in the current directory is used.

savefile << value savefile << variable Or savefile [}"directory" << value savefile [}"directory" >> variable 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.


4.2 Stored Variables

The way variables are normally stored is to make a separate directory for each one with the same name as the variable. If you wanted complete control over the savefile format, you could do that yourself with something like the following code:

mob/Write(savefile/F)

  F["name"]   << name
  F["gender"] << gender
  F["icon"]   << icon

mob/Read(savefile/F)

  F["name"]   >> name
  F["gender"] >> gender
  F["icon"]   >> icon

The advantage of having each variable in its own directory is flexibility. In the future, you could add or remove variables and old savefiles would still work. That saves you the headache of trying to maintain backwards compatibility in a single sequential buffer as items come and go.

On the other hand, values which will always be grouped together can be more efficiently saved in a single buffer. That is how we handled the three map coordinates in the previous example. There is less overhead, the fewer times you have to change directories.

Figure 12.23: Sample Savefile Format

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'

4.3 Data Directories

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.


4.4 Saving Objects

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.4.1 Saving a Player Instance

Up until now, we assumed that the mob created for a player logging in was the same type as the mob which was last saved. If your world allows players to inhabit other types of mobs, however, this assumption could be wrong. In that case, you would want to create a new mob from the savefile rather than using the default world.mob. In other words, you would want to save the mob instance rather than just saving its properties.

client/New()

  var/savefile/F = new(ckey)
  F >> usr
  return ..()

client/Del()

  var/savefile/F = new(ckey)
  F << usr
  del(usr)

In this case, we handled saving in client.New rather than mob.Login. There are two reasons. One is that we want to create the mob from the savefile before the default client.New creates one for us. The second reason is that presumably this world allows the player to switch from one mob to another. If that is the case, we can't use Login as an indicator that the player is signing into the game; it might just be a movement from one mob to another.

In all the examples up to this point, we have been using server-side savefiles. That just means they are stored on the server's filesystem. It is also possible to store savefiles on the player's computer. These are called client-side savefiles.


5. The Key Save File

The player's key may have a savefile attached to it. This file can be obtained by calling client.Import(). It returns a savefile reference or null if there is none.

To copy a savefile back to the key, client.Export() is used. Note that if changes are made to an imported file, these are not saved to the key unless that file is exported. That is because behind the scenes, Import and Export upload and download the savefile from the client to the server. The imported file on the server side is merely a temporary copy of the key's savefile.

client.Import (Object) Object is the optional object to read. Returns copy of key savefile. client.Export (File) File is the savefile to export. 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.


5.1 Client-Side Saving

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.

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)

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.


6. Transmission Between Worlds

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.


6.1 Export, Import, and Topic

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.


6.2 A Sample Player Transferal System

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.

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

The 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.

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.


6.3 Security

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.

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

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.


7. Advanced Savefile Mechanics

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.

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

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 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.

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.


7.1 Duplicate References

If you think for a while (maybe a long while) about the above saving scheme, you will come to the conclusion that it is subtly flawed. Fortunately, the savefile is smarter than you think.

Each variable is written to its own directory using the << operator, be it a number, text string, object, or list of objects. That means, of course, that objects are written in full. If we are saving a player who has a bunch of items in the contents list, an instance of each of these is written to the file.

But what happens if two variables refer to the same object? If these were each written as a separate instance, the savefile would contain two independent instances of the same object. When loading this information, a new object would be created for each reference to the original one. That could lead to all sorts of subtle problems. Ick.

Fortunately, the savefile can detect this case and handle it correctly. During any single savefile operation, such as writing an object, a special de-duplication mode is entered. In this mode, the first occurrence of an object is written in full but subsequent occurrences are written as a reference to the first one. This will even handle recursive references that would otherwise cause an infinite loop.

De-duplication mode lasts for the duration of the first operation that initiated it. For example, if you write a mob to the savefile, de-duplication mode will extend through the writing of each mob variable, which might in turn include writing entire objects belonging to the mob. All duplicate references within the single top-most operation will be detected.

The only rule you must abide by is to make sure things are always read back in the same order they were written. That ensures that the first occurrence of an object (which is saved in full) will be restored before any references to it are encountered.