ID:1187245
 
I sat down to help someone with their saving system earlier today, and found out that people still don't fully understand how the saving system of BYOND really works, and that they could be dropping the size of their savefiles significantly by making an effort to actually understand how the savefiles work.

Looking around at the other saving tutorials on the site, I can understand how we got to where we are today. There's actually a lot of really bad information going around (and has been for years) that just absolutely needs to be banished, and I'm here to show you the way.


Getting started:

Creating a new savefile is easy. You can create a new file, or open an existing file using the same command:

var/savefile/F = new("path/file.ext")


If the file exists, it will open it. If it doesn't, it will create an entirely new one for you at the specified path, with the specified filename and extension.

If you want to check to see if the file exists, you can use fexists before opening the file, that way you can distinguish between a new and a returning player:

if(fexists("path/file.ext"))
client << "welcome back!"
else
client << "welcome!"


Let's say you want to delete a savefile?

fdel("path/file.ext")


Make a copy?

fcopy("path/file.ext","newpath/newfile.ext")


Move a file?

if(fcopy("path/file.ext","newpath/newfile.ext"))
fdel("path/file")


That's all there is to it. Working with files in BYOND couldn't be easier.


Under the hood

Okay, so savefiles are easy, right? You just put a variable in each directory like this:

F["path"] << variable


Well, if you are doing it this way, you aren't using the tools Dan, Tom, and Lummox gave you to their most.

Let's start talking about buffers and how they work.


Buffers and you:

What is a buffer exactly? Well, a buffer is a receptacle for storing data until it can be processed.

Every directory in a savefile is actually a buffer.

BYOND by default stores quite a lot of information about a piece of data that you are putting into each buffer, and you can save significant space by using them for more than one tiny bit of a data at a time.

Let's take a look at how we would use a buffer:

var/savefile/F = new("test.sav")
F << variable1
F << variable2
F << variable3


This writes the values of three variables into a savefile, in the default buffer.

Now, let's look at how we'd read them back out of the buffer:

var/savefile/F = new("test.sav")
F >> variable1
F >> variable2
F >> variable3


And there you have it, all three files popped back out of the savefile, easy as cake.

Now, let's move on. We're going to be talking about paths.

Paths: When to use them.

Paths are useful for storing objects saved within objects, or when you really need to segregate your data within a separate buffer, for instance, in the case of a list.

Really, though, there isn't a lot of reason to explicitly use them except in these two cases.

If you do, they tend to bloat your savefile, and make it more of a nightmare to deal with.

So let's look at how we navigate paths.

We can write to a path by typing the name between square brackets:

F << v //write to default buffer
F["path"] << v //write to "path" buffer
F["path/to"] << v //write to "path/to" buffer
F["path/to/buffer"] << v //write to "path/to/buffer" buffer


We can also navigate to the path using a savefile variable valled cd.

F << v //writing to default buffer
F.cd = "path"
F << v //writing to "path" buffer
F.cd = "to"
F << v //writing to "path/to" buffer
F.cd = "buffer"
F << v //writing to "path/to/buffer" buffer


And, we can navigate back up a level to the last path we were in before we set the cd last.

F.cd = "path" //path is now "path"
F.cd = "to" //path is now "path/to"
F.cd = "buffer" //path is now "path/to/buffer"
F.cd = ".." //path is now "path/to"
F.cd = ".." //path is now "path"
F.cd = ".." //path is now default


The Neglected Temporary Variable:

Let's face it. You guys don't bother marking your variables as temporary, global, or const. I know you do. I've seen your code. This is one of the most neglected portions of code I see posted on the hub. Even a few of the "gurus" do it... I'm not naming any names... But seriously guys, you need to understand what these modifiers do, and when to use them.

Three types of variables are not saved. Ever. They are tmp, global, and const.

Tmp variables stand for temporary. The only way they are different, is in the fact that the default system is flagged to not save them during Write(). If you have a variable that doesn't stay constant between saves/loads, such as variables for targeting people, the party the player is in, the last mob that punched them, etc. you really ought to set it to temp.

This next bit is important: You want to know a fast way to really mess some things up? Saving variables that reference objects that the player doesn't actually "own". Watch what happens when you save a player with a reference to another player in their variables. Not only do you wind up saving the player you meant to, but the player you didn't mean to. If you reference objects that shouldn't log off with the player, set them to temporary. Always.

Const variables are set at compile time, and after being set, will never change during runtime. This is great for monster data, such as max_health, or move_speed, so long as you don't plan on changing it during the game. As soon as you need to, make sure you start saving it. This modifier isn't actually as important as tmp, because BYOND doesn't save any variables that are equal to their initial compile-time value. So you can get lazy here. I'll forgive you.

Global variables are variables within the global scope. Meaning every mob that has a global variable defined will share that variable with every other instance of that type and its subtypes. Again, this one isn't too important to think about. You probably won't have many cases where you should have to ask yourself if you need to use this modifier. You'll know if you need to, for the most part.

It should also be noted that static vars are the same thing in BYOND. The static keyword is just an undocumented keyword that is typically ignored because BYOND refers to global instead.

The most important thing to note, is that you need to think very clearly what needs to be saved, and what doesn't before you just plop your players out into a savefile, or you are going to to be wasting time, energy, and disk space. Not to mention headaches later with wiping your massive cache of savefiles because you were lazy to add a tmp in front of a new variable that referenced a list of all the players in the world.


The End of the Buffer

We don't always know exactly how many things we put into a savefile. For instance, we don't know how many items are going to be in every player's inventory. We could save the contents of a player like this:

F.cd = "list"
F << L.len
for(var/v in L)
F << v
F.cd = ".."


But the above way is wasteful. Especially if we want to save only certain types of things in L, and we don't know ahead of time how many that is. We would have to spend all of our energy (and processing time) counting up how many we are going to save, then go through them all over again, and save them.

Well, fear not, you can actually check if you have reached the last item in a buffer pretty easily.

Let's show you how to write the buffer properly:

F.cd = "list"
for(var/v in L)
F << v
F.cd = ".."


That's it! Now, let's load that list!

var/list/L = list()
var/v = null
F.cd = "list"
while(!F.eof)
F >> v
L += v
F.cd = ".."


Of course, you can always just pack the list using BYOND's default setup, but you will note that BYOND's default saving system is a little bit less space-efficient than how I tend to pack files. (Even though the default is a lot faster because it's written in native C, and not DM.)

Write:
F << L


Read:
var/list/L
F >> L



References and saves, oh my!

So you have multiple references to objects, and you are worried about duplicating them? Don't. BYOND by default keeps track of what objects it has saved, and what it hasn't. If it catches the same object in two different variables, it saves it the first time, then saves a reference to the object the second time, making it so that the object can't be duplicated by saving.

That's all for tutorial #1. I'll catch you guys in #2, where we are going to cover writing a real-deal saving and loading system.


Next: Part 2: Are We There yet?
I'm confused here when you say

Ter13 wrote:
Of course, you can always just pack the list using BYOND's default setup, but you will note that BYOND's default saving system is a little bit less space-efficient than how I tend to pack files. (Even though the default is a lot faster because it's written in native C, and not DM.)

Do you mean while(!f.eof) f >> thing is faster or slower than var/list/l;f>>l

Also you have a typo...

Read:
var/list/L
F >> L
My concerns about BYOND's default method of saving lists actually isn't about read/write speed. The savefile system is pretty fast regardless.

It's actually in the way it creates references to objects. Since I wrote this, I found, actually, that the savings were more negligible than I thought, so I really should adjust that line.

Most of the benefits that I thought were due to my list packing, was actually the effect of some anomalies in the way I thought it worked vs how it actually worked.

Moreover, the problem was in fact, the way the engine saves appearances, when in about 90% of cases, it should never bother saving them.
Great article by the way. Thanks.