BYOND Basic Savefiles

Deadron

Revision History
Revision 216 January 2001rjh
Added coverage of auto-saving functionality.
Revision 110 August 2000rjh - ron@deadron.com
Created.

Abstract

Savefiles are a very convenient, compact, and fast way to store game data. Almost all games need to make regular use of savefiles for storing player info, character data, world information, etc.

It's a good idea to get very familiar with savefiles early in your BYOND career, and this tutorial aims to make the process as painless as possible by showing you how to perform a typical savefile task: saving information about your players and their characters.

The tutorial starts by giving you a grounding in reading and writing data to savefiles, then shows how BYOND's auto-saving functionality makes the process very simple. I strongly recommend that you get comfortable with the basic concepts before working through the auto-saving section. It will save you a lot of future headaches.

Finally, while this tutorial is a great starting point, if you find yourself using BYOND regularly you will really want to get the official word on programming for the system, The Dream Maker: Designer's Guide to World's BYOND, which is a most excellent book (not written by me, dammit), available at http://www.byond.com/products/dmguide/index.html.


Table of Contents

How to be saved
Figure out your directory structure
Put stuff in the file
Read stuff from the file
Delete stuff from the file
Auto-saving: How to be saved with a lot less typing
Saving an entire object
Write() and the importance of tmp
Customizing the auto-save behavior

How to be saved

Players create characters, and they would be most appreciative if you were to save the characters for them between game sessions. The approach used here will support a player having multiple characters.

Figure out your directory structure

A savefile is a single file, but it lets you create a file system inside that file. You can create directories and subdirectories as much as you'd like, and move between them easily. So the first question to ask yourself is how you want the directories laid out, preferably such that you can find things later.

For the task at hand we can figure this out by thinking through what will happen when players log in. When they first log in, we won't know which character they want to play yet. So we'll need to ask them to select a character.

But how will we know what characters they have? Well, the simplest approach would be to have a directory for each player, and then a subdirectory for each character the player has created:

      player1
        character1
        character2
      player2
        character1
        character2
	  

This is a good structure, but what should we actually use for directory names? We actually have to be careful about this, because some ASCII characters can't be used in a directory name. Player ckeys are always valid, fortunately. And if you ever want to use something for a directory name that might have illegal characters, you can call the ckey() function to strip out anything bad.

So the directory names will use ckeys for this task:

      player_ckey         (the ckey variable assigned to the player by BYOND)
        character1_ckey   (derived by calling ckey(character_name))
	  

My player ckey is deadron, and some of my standard characters are Dangeron, Nomicron, and Roch'nor/flam, so my listing will look like this in the savefile:

      deadron
        dangeron
        nomicron
        rocknorflam
	  

Now when I log in, the game can check my directory and see that I have these characters to choose from.

There's only one problem. The ckey() function strips out illegal characters, so while I want my character to show up as Roch'nor/flam, if the game uses the safe ckey() version of the name it found in the directory, I will be boring old rochnorflam. This won't do.

While directory names have special restrictions, we can store any old value inside a directory, funky ASCII characters and all. So we'll create a directory just to hold the full name:

      player_ckey
        character1_ckey
          full_name = ""
	  

Which means my entry now looks like this:

      deadron
        dangeron
          full_name = "Dangeron"
        nomicron
          full_name = "Nomicron"
        rocknorflam
          full_name = "Roch'nor/flam"
	  

Now we have our directory hierarchy defined for the savefile.

Put stuff in the file

Of course, before the game can get anything out of the file, the data has to be put in.

We'll create a CreateNewCharacter() to get the character name from the player, and a SaveCharacter() function to save character to the file.

Here is the code:

    mob/player/proc
      CreateNewCharacter()
        var/char_name = prompt("New character", null, "What is the character's name?") as text
        SaveCharacter(char_name)
        return char_name
		
      SaveCharacter(char_name)
        var/savefile/F = new("players.sav")
				
        // Get the ckey() version of the name to avoid illegal characters for a directory name.
        var/safe_name = ckey(char_name)
				
        // Move to the directory for this character, which is:
        // /player_ckey/character_ckey
        F.cd = "/[ckey]/[safe_name]"
				
        // Storing the actual name as a value (not a directory), so we don't have to worry about what characters it has.
        F["full_name"] << char_name
		

And here is a walk-through of the SaveCharacter() function. The savefile new() function returns a reference to the specified file. If the file doesn't already exist, it is created for you.

var/savefile/F = new("players.sav")

After getting the ckey() of the mob's name,

var/safe_name = ckey(char_name)

we're ready to move to the character's directory:

F.cd = "/[ckey]/[safe_name]"

Setting the savefile's current directory variable, cd, causes it to move to that directory. You never actually need to create a directory; anytime you refer to a directory, it is automatically created for you if it doesn't already exist. However, it won't actually be saved to the file unless you store a value in the directory or in one of its subdirectories, which is what we do next.

Once the savefile is set to the player's directory, such as /deadron/dangeron, we can create a full_name subdirectory and store the char_name value inside it:

F["full_name"] << char_name

This statement contains a couple of special notations: The F["full_name"] portion is just a convenient way to refer to a directory without having to cd to it. It says "I'm using the full_name subdirectory for this command, but I want to stay where I am and not move to it". In fact, we could have taken a shortcut and not used cd at all, with the command:

F["/[ckey]/[safe_name]/full_name"] << char_name

This would have stored the char_name value in, for example, /deadron/dangeron/full_name, without changing our current location in the savefile.

The other part of this command is << char_name. The << operator means "write this value to the file".

So any time we call SaveCharacter(), the player's character name will be stored in the player's directory. You don't need to worry about closing the file or telling it to save itself to disk; that is handled automatically.

Read stuff from the file

Now whenever a player creates a character, the character's name is stored to disk in the player's directory. The next time the player logs in, we need to look in their directory to see what characters are available for them to play.

If they are a new player with no characters created yet, we will let them create a character.

    mob/player/Login()
      var/savefile/F = new("players.sav")
		
      // What characters does this player have?
      F.cd = "/[ckey]"
      var/list/characters = F.dir
		
      // Put together the menu options.
      var/newCharacterChoice = "<Create new character>"
      var/list/menu = new()
      menu += characters
      menu += newCharacterChoice
		
      // Find out which character they want to play.
      var/result = prompt("Who do you want to be today?", null, "Choose a character or create a new one") in menu
		
      if (result == newCharacterChoice)
        name = CreateNewCharacter()
      else
        // We need to get the full name for this character, 
        // which is stored in the safefile at '/player_ckey/character_ckey/full_name'.
        F.cd = "/[ckey]/[result]"
        F["full_name"] >> name
      return ..()
		

After opening the file, this uses cd to move to the directory named after the player's ckey:

F.cd = "/[ckey]"

In my case, this would be /deadron.

Then a new variable is used -- F.dir -- which returns a list of subdirectories in this directory:

var/list/characters = F.dir

In our case, the subdirectories are named after the player's characters, so this is a list of character ckeys. For me, the list would contain dangeron, nomicron, rochnorflam.

We then create a menu for the player which contains all of their characters and the option to create a new character. If they don't have any characters yet, they'll just get the option to create a new character.

Now it's time to set the mob name based on the player's character full name. If the player chooses to create a new character, then we get the name from the CreateNewCharacter() function. Otherwise we get the character's full name from the savefile, with these two lines:

        F.cd = "/[ckey]/[result]"
        F["full_name"] >> name
		

These lines move to the /player_ckey/character_ckey directory, then set the player mob's name to the value stored in full_name. We have loaded this character from the file and are ready to play.

As before, we could avoid using cd if we wanted by specifying the complete directory path when we get full_name:

F["/[ckey]/[result]/full_name"] >> name

Delete stuff from the file

Once you've put a value in a savefile, it will stay there until you explicitly replace it with another value or remove it or a parent directory. To remove a value, get the directory list and use the list.Remove() command. This code lets the player delete one of their characters from the savefile created above:

    mob/player/DeleteCharacter()
      // You might want to add a cancel option in here somewhere, and a deletion confirmation...
      var/savefile/F = new("players.sav")
		
      // What characters does this player have?
      F.cd = "/[ckey]"
      var/list/characters = F.dir
		
      // Put together the menu options.
      var/list/menu = new()
      menu += characters
		
      // Find out which character they want to delete.
      var/result = prompt("DELETING a character", null, "Which character do you want to delete?") in menu
		
			if (result)
        F.cd = "/[ckey]"
        F.dir.Remove(result)
		

Auto-saving: How to be saved with a lot less typing

From the discussion above you can probably see how easy it would be to store other character attributes in the file for each character, such as strength, hit points, and the like. However, it would involve a lot of typing to create the code to read and write every character attribute, so BYOND provides some functionality to do a lot of the work for you.

Saving an entire object

In the same way you can use the << operator to store a single value in a savefile, you can use it to store an object or a hierarchy of objects, including something as complex as a player's mob. This code tells the client to save its mob to the savefile:

    client/proc/SaveMob()
      var/savefile/F = new("players.sav")
      var/char_ckey = ckey(mob.name)
      F["[ckey]/[char_ckey]"] << mob
    

The mob can then be recreated from the savefile like so (this assumes that the player has already specified the name of the mob to load, using the approach shown in earlier sections):

    client/proc/LoadMob(char_ckey)
      var/savefile/F = new("players.sav")
      F["[ckey]/[char_ckey]"] >> mob
    

This makes saving and loading objects extremely simple. But it's very important to understand exactly what is happening to avoid major unintended consequences, such as accidentally saving every object in your game along with the single mob or object you intend to save.

Write() and the importance of tmp

When the << operator is used above, the system saves the mob by calling mob.Write(). Almost every kind of object has a Write() function, whose default behavior is to save the object using these steps:

  1. Create a special subdirectory in the current savefile directory where the object will be saved.

  2. For each of the object's variables, call the issaved() function to see if it should be automatically saved. Variables specified as global, const, or tmp are not saved. This includes pre-defined variables that can't really be saved, like the object's loc.

  3. For variables that are supposed to be saved, call the initial() function to see if the variable is still set to its default value. If it has not been changed from the default compile-time value, then it is not saved. This is a nice little feature of auto-saving, because it allows you to change the default value of a variable in the future, and objects will pick up the new value if appropriate.

  4. The remaining variables are those that need to be saved. For each of these variables, create a subdirectory in memory named after the variable, and assign the variable's value to it.

  5. When all the variables have been handled, check to see if any of them are pointing to the same object. It could be possible to get into an infinite loop if objects pointed to each other, and it could be bad to save the same object in two places. So only save an object once, and in the other places just save a reference to where the object was saved the first time.

  6. Write the directory structure to the savefile.

This might seem a bit complicated, but the end result is very simple, as shown in the previous section: In almost all cases, any object or mob can be saved with a single line of code and read back in with a single line of code. Very elegant.

The only drawback is that it is easy to accidentally save much more than you intended. Suppose you have a game object that controls your entire game. It keeps a list of all the players and NPCs in the game, as well as lists of various other things. Player mobs might have a game variable which is assigned to the game object upon login. When the player logs out, you save their mob to a savefile.

This all seems fine, until you notice that your savefile is growing by a megabyte everytime a player is saved. How could this be? Well, remember that auto-saving saves any variable that has not been identified as untouchable. In this case, each player mob has a pointer to the game object. When the auto-saving code sees the game object, it saves it. And the game object has pointers to every other player and NPC mob in the game...so each of those is saved too. You only wanted to save one player's mob, and you ended up saving half the universe.

Fortunately, there is an easy solution to this problem: UNLESS YOU ARE SURE A VARIABLE SHOULD BE SAVED IN A SAVEFILE, MAKE IT A TMP VARIABLE. Always do this. Or you will be bitten. You specify a variable as tmp when you declare it, like so:

    mob
      var/tmp/game
		

Customizing the auto-save behavior

Almost all of your savefile needs should be handled by the auto-saving functionality. Most games can get by without doing anything more than using << and >> to save objects and mobs. But every once in a while you need to add some saving functionality that auto-save doesn't cover for you. Here are a few reasons for needing to customize the savefile behavior:

  • When you want access to some of the object's data without needing to read in the entire object. In particular, if you want access to some player data, such as their character's name, without having to read in the entire player mob.

  • When a variable should only be saved/read under circumstances that auto-save isn't aware of.

  • When a variable points to an object or data that you are storing elsewhere. For example, perhaps the player has a guild variable pointing to a guild object used by dozens of players, and guild objects are stored in their own place.

  • When you need to save information that isn't conveniently stored in a single variable or object, or needs to be derived at the time of saving/loading.

  • When you want to bypass the auto-saving functionality altogether.

The auto-saving functionality is very flexible, so you can customize it as much or as little as you need. If you wish to replace it entirely, then just provide your own Write() and Read() functions without calling the superclass (..()).

In the more common cases, you are likely to use the auto-saving for 90+% of your saving, but you have one or two special behaviors that you need to handle yourself. In this case, what you want to do is declare the variables in question as tmp so that the auto-saving skips them, then handle them yourself. In my case, I frequently create a "player" object that contains meta-information related to the player that doesn't apply to any single mob. For example, the player object might hold their real name and email address for account purposes. Since I want to be able to access this information without having to load a player mob, I put these objects in their own directory. Here is some typical code:

    mob
      var/tmp/player/MyPlayer		// This is not auto-saved, because it is declared as tmp.
      
      Write(savefile/F)
        // Let auto-save do its thing first.
        ..()
        
        // Now save the player object where it belongs.
        F["/players/[ckey]"] << MyPlayer
        return
        
      Read(savefile/F)
      	// Let auto-save stuff read in the rest of the mob first.
      	..()
      	
        // Now get the player object.
        F["/players/[ckey]"] >> MyPlayer
        return