ID:2424071
 
Code:
mob/player/proc
ExportFile()
if(level < CHAR_EXPORT_LEVEL)
src << "<div style='font-size: 0.7; color: #AA0000'><b>You need to be at least level [CHAR_EXPORT_LEVEL] to be able to export.</b></div>"
return

if(alert("This will delete your current savefile so your char can be available on adventurer dating. \
Are you sure you want to do this?"
,,"No","Yes") == "No")
return


var/filelevel = round(level,5)
var/savefile/F = new("_EXPORT/[filelevel-2]-[filelevel+2]/[ckey]_[name]_[time2text(world.realtime,"DDMMYYYY")]_[time2text(world.timeofday,"hhmmss")].char")
var/clientholder = client
client = null
F.eof = -1
F["player"] << src
Write(F)

if(second_char)
filelevel = round(second_char.level,5)
F = new("_EXPORT/[filelevel-2]-[filelevel+2]/[ckey]_[second_char.name]_[time2text(world.realtime,"DDMMYY")]:[time2text(world.timeofday,"hhmmss")].char")
F.eof = -1
F["player"] << second_char
Write(F)

var/currentfile = "savefiles/[copytext(ckey,1,2)]/[ckey]/[usedslot]"
if(fexists(currentfile))
fdel(currentfile)
nosave = TRUE
del(clientholder)

mob/player/var/tmp/nosave = FALSE

mob/player/proc
SpeedDating()
var/filelevel = round(level,5)
var/filename = "_EXPORT/[filelevel-2]-[filelevel+2]/"
//somehow get a random file out of this and then use it to advertise a new player
var/list/saved_chars = flist(filename)
if(saved_chars.len > 0)
Advertise("[filename][pick(saved_chars)]")

Advertise(var/saved_char)
world << saved_char
var/mob/player/Suitor = new
Suitor.loc = src
Suitor.LoadChar()
world << Suitor.name
world << Suitor.strength

LoadChar(var/savefile)
var/savefile/sf = new(savefile)
Read(sf)
sf["player"] >> src
src = sf["player"]


Problem description:

I've been trying to code a feature that lets people recruit another person to a party. Not any NPC person, mind you, but another char, made from another player (and by luck, yours originally), to later on play as 2 chars. That second player mob will be shortly presented, with its highest stats, class and if it knows many skills, to obfuscate a bit more and mess with those trying to roll their old char.

While I've gotten to the file saving part (though messy), I have some problems with the file loading:

The problem with this now is that it creates a new mob and all, but somehow doesn't put the savefile's data into the new mob. I'm stuck with a player mob that has all the initial variables (name ???, level 0 etc.), instead of the mob it was derived from. The exported files make up to 8Ko, so there must be something in it. How do I prevent this from happening, and forcing the loaded savefile to only load the player datas into the newly created Suitor mob?
If I understand this right, you want to load a savefile created from a player mob into something like an NPC without strange side effects, such as a player switching to another mob. Is that right?

If so, this forceloadfile() proc I wrote should easily make that happen.

The raw pastebin link makes it easier to copy and paste, since it preserves the tabs.

For example if I was to use the proc in place of the normal loading under your LoadChar() proc, it should look like this:
forceloadfile(sf, "player", src, list("key", "ckey", "client"))

I didn't do much testing, so I'm being very paranoid with that exclusions list.
This proc is very powerful. It can turn a mob into a turf, or just about anything into anything. Of course, some of the data will go missing if the variables don't match up.

I hope this helps. Let me know if it works.
That's pretty much what I'm trying. On the old script, I managed to go around the switching out part, but it isn't sure if the mob loaded at all. I'll post an update once I've tried it out.
As Multiverse stated you'll definitely need to exclude things like key and the sort from loading (or saving at all if you want to be throwing data around like this, saving the key is meant to be an easy way to make sure that mob belongs to the same player, for edge cases like saving a bunch of mobs into one file).

Next you'll want to throw in some output along the way as your code works and see exactly why and where things are failing. You can also run the savefile through ExportText() to see exactly what's in it.
Thanks, I'll add the ExportText() just to be sure. I'll also take all the exception vars and possibly put turn them null at the file export proc, that way I get a lean, clean char file.

Also, I put the var output after the check to see if a var is not in the exception list, that way I can know what might be all involved in the mob-to-client attachment thing.

The current problem is that certain procs for huds are still initialized for the caller's client. Since I try to make it use nearly only keyboard stuff, I use tons of hud objects and recycle them heavily, so they are all only created on player login. I'll see what vars I can further filter out to uncouple this.

EDIT: It seems the exclusion list didn't help much, but turning stuff null before writing the file did. In other words: this suggests that dm always forces all the vars to be read from a savefile. Here's the custom export proc I made for this:

mob/player/proc/CreateCharFile(savefile/F, mob/M)
M.client = null
M.key = null
M.ckey = null
F.eof = -1
F["player"] << M
Write(F)


Also, I really liked that debug macro you did there, going to implement it in the future.
In response to Kayren
Kayren wrote:
It seems the exclusion list didn't help much, but turning stuff null before writing the file did. In other words: this suggests that dm always forces all the vars to be read from a savefile.

I hope you don't mean the exclusion list used with my proc, because forceloadfile() doesn't save anything to a file. It loads it. It approaches the problem by loading the variables one by one rather than the entire object at once, so there is no way the excluded variables could ever be modified by that proc. If that is happening, then you are also loading somewhere else in your code.

Here's the custom export proc I made for this:

> mob/player/proc/CreateCharFile(savefile/F, mob/M)
> M.client = null
> M.key = null
> M.ckey = null
> F.eof = -1
> F["player"] << M
> Write(F)
>


If you nullify those variables while that player is online, they will get logged out of your world, or maybe put into a state where they can't do anything.

That's a rather crude way of excluding data from being saved. If you wanted to do the exclusion during the saving process, it would be better to create a keyless copy of a normal savefile. I considered the idea, but decided against saving extra data unnecessarily, so I designed the exclusion around a custom loading system.

Also, I really liked that debug macro you did there, going to implement it in the future.

Thanks. Part of designing a polished game is making sure it doesn't spam error messages at the players.
When using << and >> to save and load entire objects it will save and load everything by default.

If you want to change the behavior you'd want to override the Read() and Write() procs for that object, using ..() to trigger the default action of saving/loading everything.

For this case, you'd only need to handle it on the loading side, and only for the mob type that handles your special case.

mob/kind_of_a_player
Read(savefile/F)
..()
key = null
client = null
// etc...


However, that tick between setting the key and whatnot to null and calling ..() you might end up disconnecting a player with that same key, so what you'd want to do is add some kind of check to see if said player exists in the world and make a reference to their current mob and shift them back to it after calling ..(), you can check if their current mob and old mob match first so you don't end up calling Login() and the sort if they do happen to jump back and forth, which you should definitely handle inside of /mob/player/Login().

As for your CreateCharFile() proc, you rarely ever want to manually be calling Read() and Write(), they're not intended to be used that way, just overridden like I did above. It's the same as using << on an object but you end up causing a few things internally to happen twice because of the argument use and things of that nature and you lose some performance. It's also not as "with the flow" with how you save other data to a savefile using << and tends to look awkward at the end of a line of manually saving stuff using <<
In response to Nadrew
You are just fixing the symptom, rather than fixing the cause of the problem. I don't think disconnecting players and shifting them around is an acceptable solution to this problem, or almost any problem.

If the real culprit lies in the savefile, why not just fix it there instead? I wrote a new loadobjectas() proc which can do this. (Update: This one is faster.) Unlike the proc I posted earlier, this one lets BYOND handle the loading as expected, so it creates the object for you.

Here is how it would look with Kayren's Advertise() proc:
mob/player/proc
Advertise(var/saved_char)
world << saved_char
var/mob/player/Suitor = loadobjectas(new /savefile(saved_char), "player", /mob/player, list("key", "ckey", "client"))
Suitor.loc = src
//Suitor.LoadChar() This is no longer needed.
world << Suitor.name
world << Suitor.strength

With this, you shouldn't need the LoadChar() proc at all.
That proc is quite uh, not good for this purpose. You don't need to split ExportText() at all for what you're doing, you simply need to loop over savefile.dir and pull out the data manually and ignore excluded data. No need to rebuild a brand new savefile on the fly for it.

The reason you don't do it that way is because on a larger savefile it's going to be dramatically slower than using the built in methods of loading the savefile, especially since text parsing isn't as performant than most people assume.

This would bog your game down quite a bit if used more than sparingly, and get worse as the savefiles became more complicated.

It's fine for more basic use, but it would probably choke pretty bad on something like a 2MB savefile (even ExportText() alone gets a bit touchy when you get passed a certain point).

Proper handling of it would mean the player wouldn't even notice a "disconnect", it's actually quite normal to shift mobs around on BYOND.
In response to Nadrew
Nadrew wrote:
That proc is quite uh, not good for this purpose. You don't need to split ExportText() at all for what you're doing, you simply need to loop over savefile.dir and pull out the data manually and ignore excluded data. No need to rebuild a brand new savefile on the fly for it.

I agree. That one does look quite slow. It's almost like I forgot what I learned when I wrote the first one. I was rebuilding the savefile to load the whole object at once, that way it gets filtered through the built-in Read() proc by default. That doesn't happen if you are manually loading in the variables one at a time.

The reason you don't do it that way is because on a larger savefile it's going to be dramatically slower than using the built in methods of loading the savefile, especially since text parsing isn't as performant than most people assume.

This would bog your game down quite a bit if used more than sparingly, and get worse as the savefiles became more complicated.

It's fine for more basic use, but it would probably choke pretty bad on something like a 2MB savefile (even ExportText() alone gets a bit touchy when you get passed a certain point).

I wrote a functionally identical version that makes use of the built-in methods instead of that mess of text parsing I had going on. Due to a bug I discovered, I had to fall back on using ExportText() and ImportText() only for the special case of temporary savefiles. What do you think of this new one? Surely it's several orders of magnitude more performant than the last one.

Proper handling of it would mean the player wouldn't even notice a "disconnect", it's actually quite normal to shift mobs around on BYOND.

Sure, there are good reasons to Login() to a different mob, but I don't think this is one of them. It really depends on how the game is designed. If many things are loaded and unloaded as part of a Login(), players are definitely going to notice. I know that would mostly be from bad design, but two wrongs don't make a right. There are times when players will expect a relog to occur, and that's okay, but if they don't expect it, then it will just feel wrong. I hope this makes sense.

@OP
You should take a look at Ter13's savefile tutorial. He included a custom saving system that looks promising, although I haven't tested it.