ID:80125
 
As any person who has done much more than a board game probably knows is that BYOND offers a very friendly and powerful data management system known as savefiles.

Most people just make use of the Read() and Write() atom procs to handle saving player data. This method allows direct saving of an entire atom. While this system is usually robust enough to handle most of your saving needs it can sometimes be too robust.

A rather common problem that sprouts up on BYOND is what we've grown to call the 'rollback bug' -- this bug causes one player to be loaded back to a previous instance of themself when another player loads their own file. The cause of this is what's known as reference saving, when a player has a non-tmp variable that can contain a reference to another atom you end up saving an instance of that atom when you save that variable.

This of course causes that atom be loaded back to the point where the variable was saved to the file. The best way to solve the problem is simply not letting any references get saved if they shouldn't. It's also a good idea to override the Write() function of specific atoms to prevent them from being saved at all. This is especially useful when saving areas if you want to move things off of the area before the contents variable is saved.

mob
nosave
Write(savefile/F) return


Simple as that, no matter what variable is a reference to /mob/nosave it won't be saved when the parent Write() is called.

That handles the automated saving system in place, now it's time to get into the stuff you'll want to know if you wish to save data manually.

The savefile datum works like a file tree, it has directories and paths. There are a few variables that allow making use of these paths.

Say you want to save the 'test' variable in the savefile under the virtual directory 'mydir', first you have to create a directory if it doesn't exist already, then you have to navigate to that directory before saving, unless you want to manually enter path information.

var/savefile/F = new("mysave.sav")
if(!F.dir.Find("mydir")) F.dir += "mydir"
F.cd = "mydir"
F["test"] << "test!"


This will access the savefile located in 'mysave.sav', or it will create a new file it one doesn't already exist. Next we check the savefile's 'dir' list which contains all of the directories of the current savefile path, in the absence of a proper value we simply add a new one.

Now we use the 'cd' variable to tell the system that we want to add data to a specific directory. Then we simply output a variable to the savefile using the << operator and tada we have our own structured savefile. It will look something like:

mydir
test = "test!"


Now after this you'll usually want to return to the top-most directory of the savefile; simply set the 'cd' variable to "/" and you're good to continue with the structuring.

You can also used nested directories and variables.

var/savefile/F = new("mysave.sav")
F.cd = "mydir"
if(!F.dir.Find("subdir")) F.dir += "subdir"
F.cd = "subdir"
F["another_test"] << "Another test!"
F.cd = "/"


Pretty self-explanatory after the previous example, this simply just creates a new directory after navigating to 'mydir' so you'll end up with something like:
mydir
subdir
another_test = "Another test!"


Easy as pie. (The kind you buy, not the kind you make, that's not always easy at all!)

The next handy savefile trick allows you to save data to a temporary or random file, to do this you simply don't provide a file name to the new() call when making the savefile datum. This is where the 'name' variable comes in handy. It'll let you view what the file's name is, even if it has been randomly generated.
var/savefile/F = new()
if(F) // Just in case
src << "File is named: [F.name]"


Most useful for making your own dynamic data storage system or savefiles that aren't based on a key or ckey.

Sometimes you'll need to prevent other worlds or programs from accessing a savefile at the same time as you are, luckily the savefile datum provides procs allowing to lock and unlock the file. When locked the savefile can't be accessed by any other programs.
var/savefile/F = new("mysave.sav")
F.Lock(-1)
F["test"] << "test"
F.Unlock()


This example will lock the file while it saves the data to it, the argument for Lock() is a timeout, it is measured in seconds; -1 means no timeout. This lets you lock the file without having to worry about unlocking it, simply set a timeout and it'll automatically unlock when the timer expires.

Now you're probably wondering 'how do I edit these files manually?' -- the answer is simple and of course provided in the form of a couple of savefile procs. The ImportText() and ExportText() procs allow you to dump the savefile into readable text and then to dump it back into savefile format.
var/savefile/F = new("mysave.sav")
var/output_file = file("mysave.txt")
fdel(output_file) // So you don't get overlaping values.
F.ExportText("/",output_file)


This example will dump the contents of mysave.sav into mysave.txt. Now once you're done editing the file you'll want to convert it back, of course.

var/savefile/F = new("mysave.sav")
var/input_file = file("mysave.txt")
F.ImportText("/",input_file)


Simple as that. You'll notice that the first argument of both procs was set to "/", this is just telling the system to export/import everything under the / directory. You can set this value to any directory in the file to make only that data get dumped.

Now you're probably thinking 'Wait! That means anyone with access to the files can edit them!', sure does. The most common and easiest method to stop people from editing their files is a simple hash check. You store a hash in the savefile that equals some value that can only be reached using valid data from that save, then you check that hash against the same process to make sure nothing has been altered.
var/savefile/F = new("mysave.sav")
F["test"] << test
F["other_test"] << other_test
F["hash"] << md5("myhash_[test]_blah_[other_test]")


This will create a hash that makes use of some plain-text and the values of the two variables being stored. Now we check to make sure the data is valid when the file is loaded.

var/savefile/F = new("mysave.sav")
var
test_loaded = F["test"]
other_loaded = F["other_test"]
hash_loaded = F["hash"]
if(md5("myhash_[test_loaded]_blah_[other_loaded]") != hash_loaded)
src << "Savefile tampering detected!"
del(src)


From reading you probably realized exactly what this is doing, it's just checking the loaded values against the same combination that was used to create the hash. This should produce an identical hash to the one being stored if no data has been altered.

Some people have even taken to encrypting the contents of the savefile completely. Unfortunately this method has had little good success, the most well-known instance of this method going wrong was from Leftley's Lode Wars game, where after so much saving and loading was done the data would corrupt itself and become impossible to use.

So remember, savefiles aren't just for saving players, they can be used as entirely self-contained data structures.

Until next time!
[0:18:38 pm] Andre-g1: Uhh, how can a savefile after being encrypted and "decrypted" so many times become unusable after sometime ?

[0:18:49 pm] Andre-g1: If the encryption and decryption are done well, there should be no problem, right ?
The fact remains that if even a single character is encrypted or decrypted just SLIGHTLY askew it'll ruin the entire save.
Your /mob/nosave example doesn't really work. Write() is called after the type is already written into the savefile, so if there's a reference to a /mob/nosave you'll just end up with a default one being created when the file is loaded. None of its attributes are saved, resolving any circular references, but it's still creating a mob you don't want created.