ID:1187381
 
Previous: Part 1: Getting to know savefiles

Okay, I know I ranted and raved about how people save their files wrong constantly, then just gave you an intro to how savefiles work. "But where's the code, Ter?". It's in this tutorial. Just stick around.


Making it easy.

Let's talk about ease of use. A lot of people don't bother to write in saving early in their project because they don't want to constantly keep editing their save function every time they add or remove a variable. Yeah, I get you.

Some people also resort to doing this:

F << src


And that's their entire saving system.

Well, I've got some news for you. Doing it right the first time is easier than you would think.

Let's start talking about what it needs to do.

By default BYOND saves EVERYTHING that isn't the compile-time value, or a tmp/const/global/static variable.

Since in part 1, I spent most of my time telling you when to NOT save things. Well, I've just made it a bit easier on you. There are some built-in variables that save whenever they are changed, and aren't temporary. A lot of them are the variables that cause serious problems when they are being saved, so let's just rig up a system that will allow us to specify variables that aren't temporary that should never be saved.

//modify standard saving/loading behavior a bit for all objects
datum
//set up the new save behavior.
Write(var/savefile/F,var/list/neversave=null)
//make sure neversave has been initialized as a list with contents
if(neversave!=null&&istype(neversave,/list)&&neversave.len>=1)
//define i for storing initial variables
var/i
//run through neversave one time
for(var/v in neversave)
//if the variable is not savable (tmp/const/global/static) just remove it,
//as it will never save anyway.
if(!issaved(src.vars[v]))
neversave.Remove(v)
else
//get the initial value of the variable.
i = initial(src.vars[v])
//now, check if the variable would normally save.
if(i!=src.vars[v])
//if it has been changed since runtime, store the current value in neversave.
neversave[v] = src.vars[v]
//and set the variable to default so it won't save.
src.vars[v] = i
else
//remove the variable as it won't save because it's the default value.
neversave.Remove(v)
//call the default behavior.
. = ..(F)
//go back through everything and set it to whatever it should be.
for(var/v in neversave)
src.vars[v] = neversave[v]
//return whatever default would have returned.
return.
else
//fall back to normal behavior if neversave is not set up.
return ..(F)

//set up the new load behavior.
Read(var/savefile/F,var/list/neversave=null)
//check if neversave has been initialized as a list with contents
if(neversave!=null&&istype(neversave,/list)&&neversave.len>=1)
//run through neversave once
for(var/v in neversave)
//if the variable shouldn't be saved (tmp/const/global/static)
if(!issaved(src.vars[v]))
//remove the variable
neversave.Remove(v)
else
//store the current value.
neversave[v] = src.vars[v]
//call the default behavior.
. = ..(F)
//run back through the list
for(var/v in neversave)
//reset the values that may have been overwritten
src.vars[v] = neversave[v]
//return the default return.
return .
else
//fall back to normal behavior if neversave is not set up.
return ..(F)


It's not really that important that you deeply understand that bit of code. It's pretty simple actually.

Next, let's figure out how to use it.

In order to use the saving system, we need to start telling the game what to save, and what not to save.

We can actually do this by overriding the default variables for datum (already done), atom, turf atom/movable, mob, and obj.

Let's get cracking:

Looking through the reference, I don't ever want to save icon variables, icon_state, overlays, or underlays. Most of that is introduced by atom. For movable, I don't want to save screen_loc, and for mob, I never want to save the key.

We're going to introduce a new function to the mix, so that we can better control what we don't save later on down the road.

The function itself is designed to build a list of variables that won't be saved every time it's run. The reason I'm adding it as a separate function, is so that I can later specify that there are neversave elements that I want to remove from the list, thus allowing "neversave" variables to be saved by subclasses of an object that won't save them.


datum
proc
NeverSave(var/list/L)
return L
atom
NeverSave(var/list/L)
//add what we don't want to save
L.Add("icon","icon_state","overlays","underlays")
return ..(L) //return whatever the parent type does.
Write(var/savefile/F,var/list/neversave=null)
//if we don't have any defined nonsavables yet.
if(neversave==null)
neversave = src.NeverSave(list())

//we want to get a local copy of the overlays and underlays
//because you can't just assign a list to overlays or underlays
//at runtime. Things get messy.
var/list/ol
var/list/ul

if(src.overlays!=initial(src.overlays)&&neversave.Find("overlays"))
ol = src.overlays.Copy(1,0)
src.overlays = initial(src.overlays)
neversave.Remove("overlays")
if(src.underlays!=initial(src.underlays)&&neversave.Find("underlays"))
ul = src.underlays.Copy(1,0)
src.underlays = initial(src.underlays)
neversave.Remove("underlays")

. = ..(F,neversave)

if(ol!=null&&ol.len)
src.overlays.Add(ol)
if(ul!=null&&ul.len)
src.underlays.Add(ul)
Read(var/savefile/F,var/list/neversave=null)
if(neversave==null)
neversave = src.NeverSave(list())
return ..(F,neversave)
movable
NeverSave(var/list/L)
L.Add("screen_loc")
return ..(L)
mob
Write(var/savefile/F,var/list/neversave=null)
. = ..(F,neversave)
F.dir.Remove("key")
return .


Note here that I have to remove key from the savefile manually. This is because keys cannot be unset. If you change the key, the client disconnects from the mob. So for this one and only variable, we let it save, then delete it.

Now, let's say I want to later on down the road, allow a player to save their icon?

Well, because we implemented NeverSave as a separate callback, we can remove "icon" from the list like so:

mob
player
NeverSave(var/list/L)
L = ..(L)
L.Remove("icon")
return L


And now you will no longer find "icon" in your neversave list for player or any subclass of it unless you readd it in a child.

So you want to add a variable to the nosave list later?

This is how easy it is:

mob
player
var
list/equipment = list()
NeverSave(var/list/L)
L.Add("equipment")
. = ..(L)
return .


Or, you can do it the way you are supposed to:

mob
var
tmp
equipment = list()


As long as you use temp variables the way you are supposed to, you should never have to change a line of this code.

How do I actually save?

I know, we're this far into a tutorial and we haven't saved anything at all yet. You're probably getting rather upset trying to read through all of this and understand it, when we haven't even shown you how to save yet.

Well, lucky for you, this is part 2/3. You finally get to learn how to save.

Well, let's set up a simple environment:

world
mob = /mob/logging
Del()
for(var/mob/player/p in world)
del p

world
mob = /mob/logging
Del()
for(var/mob/player/p in world)
del p

mob
logging
Login()
spawn()
if(fexists("saves/[copytext(src.ckey,1,2)]/[src.ckey].sav"))
var/savefile/F = new/savefile("saves/[copytext(src.ckey,1,2)]/[src.ckey].sav")
var/mob/player/p = null
F >> p
p.key = client.key
del src
else
client.mob = new/mob/player()
return 1
player
Login()
spawn(10)
src.save()
//set up your icons, icon_state, screen objects, screen locs, underlays, and overlays here.
return ..()
Del()
src.save()
return ..()
proc
save()
var/savefile/F = new/savefile("saves/[copytext(src.ckey,1,2)]/[src.ckey].sav")
F << src


Yeah, I know I told you guys that it was going to be harder than "F << src", but with the setup work we did, it eliminates the need for the player to specify much of anything. There is one thing you really should do, though, which is save the player's location on the map. I'll show you here how to save additional data, and not just less:

mob
player
Write(var/savefile/F,var/list/neversave=null)
. = ..(F,neversave)
var/ocd = F.cd
F.cd = "location"
F << src.x
F << src.y
F << src.z
F.cd = ocd
return .
Read(var/savefile/F,var/list/neversave=null)
. = ..(F,neversave)
var/ocd = F.cd
F.cd = "location"
F >> src.x
F >> src.y
F >> src.z
F.cd = ocd
return .


Note the change of directory to "location". When you save a player, it puts all your data in object zero (".0"). The period means that the directory is hidden. The first object it finds in your variables is object .1, and so forth. We're using that location just for consistency with the default system.


And that concludes part 2.

Next time, we're going to cover some more advanced topics, like handling equipment, overlays, underlays, and the like.

See you then!</<>
You don't have to "return ." because by definition, it is returned at the end of all functions.

Also, it's very redundant to do ". = ..()" and then "return ."
Just do "return ..()"

I'm being picky, I guess.
Just out of curiosity why would you want to make Equipment a temp variable?

You may of just used it as an example to show how to add/remove a specific thing but it striked me as odd if you actually dont save that list seeing as my equipment list is something like (inside the savefile) then contents has a ref link which i personally find fine i guess.

    equipped = list("weapon" = object(".0"),"back" = object(".1"))
.0
type = /item/equipment/Testsword
.1
type = /item/equipment/Testcoat
I made it a temp variable with an equipment system in mind.

For example:

obj
item
equipment
var
equipped = FALSE
proc
equip()
if(ismob(src.loc))
var/mob/m = src.loc
if(!m.equipment.Find(src))
. = m.equip(src)
if(!.)
src.equipped = FALSE
else
src.equipped(m)
else
src.equipped = FALSE
equipped(var/mob/m)
m.equipped(src)
Read(var/savefile/F)
. = ..(F)
if(src.equipped==TRUE)
src.equip()


...And setting up our mob...

mob
var
tmp/list/equipment = list()
proc
equip(var/obj/item/equipment/e)
return 1
equipped(var/obj/item/equipment/e)
src.equipment.Add(e)


Now, you might ask why I'm setting it up like this?

Advantages:

1) You can forcibly equip objects on NPCs just like you would players.

2) You don't have to save a list of items. Just leave it in the contents of the player.

3) If you want to hide the object from inventory builders, it's as simple as checking a single boolean.

4) If you want to show only equipment from a list, it's as simple as just running through an equipment list.

5) Simplistic handling of equippables that don't take up item slots, as well as the ability to specify item slots for equipment, as well as the ability to specify a number of items that can be equipped in a number of slots:

mob
var
tmp/list/equipment = list()
list/equipment_slots = list("head","chest","left foot","right foot","finger1","finger2")
proc
equip(var/obj/item/equipment/e)
return 1
equipped(var/obj/item/equipment/e,var/slot=null)
if(slot!=null)
src.equipment[slot] = e
else
src.equipment.Add(e)

obj
item
equipment
slotted
var
slot
equipped(var/mob/m)
if(src.slot!=null)
if(copytext(src.slot,-1)=="#")
var/testpos = 1
slot = copytext(slot,1,length(slot)-1)
var/testslot = "[slot][testpos]"
while(src.equipment[testslot]!=null)
testslot = "[slot][++testpos]"
if(m.slots.Find(testslot))
src.slot = testslot
m.equipped(src,src.slot)
return 1
else
if(m.equipment[slot]==null)
m.equipped(src,src.slot)
return 1
return 0


Now, setting up multiple equippable rings couldn't be easier:

obj/item/equipment/slotted/ring
var
slot = "finger#"
obj/item/equipment/slotted/buckler_shield
var
slot = "left arm"
obj/item/equipment/slotted/helmet
var
slot = "head"


You'll notice that the number of rings that can be equipped is equivalent to the number of "ring1".."ring2".."ring3" slots in the mob's slot list.


Part of the reason that I focus on a system like this, is flexibility and the size of the savefiles.

Sure, saving the equipment list is easy, but if you have to run through your equipment list on loading the savefile anyway to rebuild your overlays, you might as well get smaller file sizes out of it.

I just don't really see the need for setting up a separate saved list, when you have it in the contents list anyway. But I guess you can leave that kind of thing out if you'd like.
hmm i see where you are coming from on that one i just never looked at it that way.

mainly cause i figured you were basically using the same space in save files either way but that definitely adds some more flexibility to it that i never really considered.

Cause the way im now seeing it is if i had the equipped list im technically using 5 lines of text for 2 items (when viewing in .txt or foomer editor) as opposed to just 1 additional line on the already existing object. so 2 lines vs 5. i can see how that could reduce save sizes in the long haul even if only be a little..

At the end of the day the smaller my saves the happier i will be cause i dont want my save directory being HUGE when i for example change servers and need to export the saves over.
In response to Midgetbuster
Midgetbuster wrote:
At the end of the day the smaller my saves the happier i will be cause i dont want my save directory being HUGE when i for example change servers and need to export the saves over.

Not to mention should you ever decide to start housing the savefiles on a central server so that they can be downloaded and uploaded by different hosted versions of your game.
In response to Ter13
Indeed that is true. I guess i got some fiddling to do with my saving then :P
Update: Fixed a bug in how overlays and underlays are managed by the system. Overlays/Underlays should now be restored without issue when saving.
Hi again, Haven't fully dived into your take of equipping yet but i did give it a shot by making equipment a /tmp and using a isequipped var.

Not sure if i have done something wrong or not (as i havnt changed my equip proc or anything yet) but noticed a couple things pop up that pique my interest.

1. When doing the read proc for equipment it returns a null.equip proc due to no mob for src.loc, seems to work fine if i add a spawn() though.

2. Prolly because im missing a vital part. but i can equip, logout (which saves) then relaunch and it will load up fine. BUT then will remove the isequipped part which seems odd.
//item bla bla bla
//equipment indent bla bla bla.
Read(var/savefile/F)
. = ..(F)
if(src.isequipped==TRUE)
src.equip()


proc
equip()
spawn()
if(ismob(src.loc))
var/mob/human/m = src.loc
if(!m.equipped.Find(src))
. = m.equip(src)
if(!.)
src.isequipped = FALSE //This part here.
else
src.equipped(m)
else
src.isequipped = FALSE

equipped(var/mob/human/m)
m.equip(src)


isequipped is set by default to FALSE naturally even though in your dem had it as true which i assume is a typo?
From the DM Reference:

Spawn(Delay=0) { statement }

Run Statement after a delay. Statement may be a single statement or a code block enclosed in (optional) braces and indented. If delay is negative, the spawned code is executed before continuing in the main code. If it is zero, the spawned code is scheduled to happen right after other existing events that are immediately pending.

<< operator (Savefile):

In the case of turfs, the location of the turf is also recorded so that it can be recreated at the same position. All other objects must be repositioned after the object is recreated (like in the object's Read() proc).

Basically, what's going on, is the Read() proc internally relocates objects created from within an object's identity block within the savefile.

Meaning, an object in the contents list of another object within a savefile is relocated after Read() executes within that object. The loc of objects are set within the saved parent's Read() proc internally, so the location will be null until after the Read() proc is finished.

By using spawn(0), you are effectively pushing the processing of the code block within spawn until the end of the current tick. Everything that's already scheduled will execute, and then the code that's been injected into the internal pending proc queue by spawn(0) is executed in the order that it was called on the same tick.

So yes, obj.loc will be null until immediately after obj.Read() finishes --except in the case of atoms/turfs. All movable objects will have a null loc until after the calling Read() assigns it internally.
Question.

Wouldn't you want to save overlays?
It takes overhead to save additional graphics. If they're in the rsc, then it is a redundancy to save them in the save files too.
What about player overlays? Wouldn't you want the equipment they had to be saved?
Reconstruct the overlays when they log back in.
Pirion is on point here with the how, but hasn't mentioned the why.

When you save overlays, they have a tendency to become immutable, meaning you can no longer remove them through normal means.

This leads to a huge number of graphical bugs, and for an example of how people wrongly deal with this situation, you can take a gander at just about any anime game.

If you look up above at the information I gave to midgetbuster regarding my proposed equipment system, you'll see there's a logical place to re-apply equipment overlays already included.

In addition, I suggest keeping track of buffs and debuffs on the player in a self-contained datum, and performing login upon re-initialization and population.
is there a part 3?
There was going to be, but I decided it was out of the comfort zone of this community.
And how is not posting it beneficial for the community?
In response to Fushimi
Fushimi wrote:
And how is not posting it beneficial for the community?

Some of the things I've said on the boards, and some of the advice I've offered has been good. However, misunderstandings have led to problems and misinterpretations.

Some concepts are just too difficult for this community to master right now, and are just going to result in confusion at best.
In response to Ter13
Ter13 wrote:
Some concepts are just too difficult for this community to master right now and are just going to result in confusion at best.

That doesn't mean it isn't worth the shot of enlightening the few that will learn from it :c
Page: 1 2