1. Are you planning to upload your awesome spell or system to Hive? Please review the rules here.
    Dismiss Notice
  2. Evolution complete! Make Darwin proud and go vote in the Techtree Contest #12 - Poll.
    Dismiss Notice
  3. Icon Contest #17 - Results are out! Step by to congratulate our winners!
    Dismiss Notice
  4. Succumb to the whispers and join our Texturing Contest #29 - Old Gods!
    Dismiss Notice
  5. The results for Texturing Contest #28 are out! Step by to congratulate our winners!
    Dismiss Notice
  6. We've created the Staff Job Openings thread. We're currently in need of icon, video production, and social/multimedia positions to be filled. Thank you!
    Dismiss Notice
  7. On May 20th a new law about privacy and data processing comes into work in the EU. I am no lawyer and I need help figuring out if we comply and if not, what we must do about it. Please message me if you can provide any assistance. Read more. Ralle
    Dismiss Notice

Codeless Save / Load in Wurst

Submitted by Sir Moriarty
This bundle is marked as approved. It works and satisfies the submission rules.
This is an example of a codeless Save/Load example done in Wurst, using Wurst's most recent additions for FileIO operations, created by yours truly.

It relies on the newly added Persistable API which allows easy and seamless saving/loading of arbitrary data. That means you can literally save anything, as much as you want. This is backed up by a rather robust MultifileIO system that splits up data between multiple files if there's too much of it.

Until Blizzard enables LocalFiles for all players, however, you will obviously need to enable LocalFiles in the registry. If you haven't heard of this LocalFiles trickery, it is basically a WC3 setting that allows you to read from files written from inside the game. Here's a FAQ entry from the fortress survival forums: How to Allow Local Files and what is it? - FAQ - Frequently Asked Questions

If you are curious, Wurst now also provides the libraries to:
* Synchronize async events between players (port of SyncInteger by TriggerHappy)
* Send arbitrary amounts of async data between players (port of Sync by TriggerHappy)
* Write/Read arbitrary amounts of data to/from disk (inspired by FileIO by Nestharus)
* Facilities to make Save/Load a breeze

However, contrary to their vJass counterparts, these libraries were made with ease-of-use in mind, together with extensive documentation regarding almost every little bit of the inner workings.
In addition, they also extensively leverage Wurst's superior language facilities in order to provide a sane, easy-to-use callback-based API, instead of the event-based APIs of their predecessors.

The code is only about 200 lines long, and most of that is just boilerplate to plug into the provided API. All the heavy lifting is done by libraries in the background, with the exception of LocalFiles checking, which for now, you have to do yourself - but it might be integrated into the libraries in the future.

Note: You may receive an error message upon trying to load a non-existent save file, however, this is simply a side effect of the library trying to load data and not finding any. The error is caught correctly by the framework, and won't affect your code at all.
This will probably be addressed in later versions.

You can download the map and play around in it by yourself. To create a new save file, type -new, to load an existing one, type -load, to save, type -save. You can also change the player's name with -name, which will also get saved.

The save consists of data about a single footman, it's position, angle, and hit points, as well as the player's name.

Here's the code:
Code (vJASS):

package Test

import RegisterEvents
import Persistable
import SimpleIO
import Network

/**
    This is the class that contains data for our unit.
    Note that it implements BufferSerializable, meaning Buffer.write / Buffer.read can be called on it
**/

class UnitData implements BufferSerializable
    player owner
    unit what

    vec2 pos
    angle facing
    real hp
    boolean alive

    construct(player owner)
        this.owner = owner

    ondestroy
        if what != null
            what.remove()
 
    /**
        This function initialiazes an instance of this class with a new unit.
    **/

    function initializeBlank()
        what = createUnit(owner, 'hfoo', vec2(0, 0), 0 .asAngleDegrees())

    /**
        This function uses the loaded values (see deserialize) to recreate a saved unit.
    **/

    function initializeLoaded()
        if alive
            what = createUnit(owner, 'hfoo', pos, facing)
            ..setHP(hp)

    /**
        This function puts all the data about a unit into a Buffer, which is then
        written to a file, and can be loaded later on.
    **/

    override function serialize(Buffer buffer)
        // the first boolean in the file tells us whether the unit is alive or not
        if what == null or not what.isAliveTrick()
            buffer.writeBoolean(false)
        else
            buffer.writeBoolean(true)
            // next we save the unit's X/Y, angle and HP
            // you can obviously save much more data about a unit, such as
            // it's id, level, items, and so on
            buffer.writeReal(what.getX())
            buffer.writeReal(what.getY())
            buffer.writeReal(what.getFacing())
            buffer.writeReal(what.getHP())

    /**
        This function reads and remembers all the data about a unit from a Buffer,
        which is loaded from a file.
    **/

    override function deserialize(Buffer buffer)
        if buffer.readBoolean()
            pos.x = buffer.readReal()
            pos.y = buffer.readReal()
            facing = buffer.readReal().asAngleDegrees()
            hp = buffer.readReal()
            alive = true
        else
            alive = false

/**
    This is the class that stores our data about a player.
    Note that it extends Persistable, which allows us to call
    .load() and .save() on it.

    To find out what each method does, you can read the source file for Persistable.wurst
**/

class PlayerData extends Persistable
    private UnitData footieData
    private string myName

    construct(player owner)
        super(owner)
        footieData = new UnitData(owner)

    ondestroy
        destroy footieData

    override function serialize(Buffer buffer)
        buffer.write(footieData)
        buffer.writeString(myName)

    override function deserialize(Buffer buffer)
        buffer.read(footieData)
        myName = buffer.readString()

    override function getClassId() returns string
        return "exampleplayerdata"

    override function getId() returns string
        return "config"

    override function supplyDefault()
        footieData.initializeBlank()
        myName = owner.getName()
 
    override function onLoaded(boolean success)
        owner.setName(myName)
        footieData.initializeLoaded()

PlayerData array playerData
function loadData(player runner, boolean createNew)
    // we need to check for local files first before we try do any IO operations,
    // because players who don't have local files enabled won't be able to load
    // we also need to send the result of the check to other players, because
    // a localfile check is inherently async, so we use network to synchronize the value
    let network = new Network(runner)

    if localPlayer == runner
        network.getData().writeBoolean(LocalFiles.isEnabled())

    network.start((Buffer buffer) -> begin
        let localFileEnabled = buffer.readBoolean()

        if localFileEnabled
            let id = runner.getId()
            // if this is null, it means we haven't loaded yet
            if playerData[id] == null and not createNew
                playerData[id] = new PlayerData(runner)
                if not createNew
                    playerData[id].load((boolean success) -> begin
                        if success
                            Log.info("Loaded data for " + runner.getNameColored())
                        else
                            Log.warn("Failed to load data for " + runner.getNameColored())
                    end)
            else if createNew
                // normally .supplyDefault() is called when a save file is missing
                // to supply reasonable defaults for the class, but
                // we can also call it directly to create a new save file

                // so we destroy the old one first...
                if playerData[id] != null
                    destroy playerData[id]
                // create a new one
                playerData[id] = new PlayerData(runner)
                // and supply defaults
                playerData[id].supplyDefault()

        else
            Log.warn("Failed to load data for " + runner.getNameColored() + " because they don't have LocalFiles enabled.")
    end)

function saveData(player runner)
    if playerData[runner.getId()] == null
        if localPlayer == runner
            Log.warn("You should -load or -new before saving.")
        return

    let network = new Network(runner)

    if localPlayer == runner
        network.getData().writeBoolean(LocalFiles.isEnabled())

    network.start((Buffer buffer) -> begin
        let localFileEnabled = buffer.readBoolean()

        if localFileEnabled
            playerData[runner.getId()].save(() -> begin
                Log.info("Saved data for " + runner.getNameColored())
            end)
        else
            Log.warn("Failed to save data for " + runner.getNameColored() + " because they don't have LocalFiles enabled.")
    end)

init
    for i = 0 to bj_MAX_PLAYER_SLOTS
        for j = 0 to bj_MAX_PLAYER_SLOTS
            SetPlayerAllianceStateBJ(players[i], players[j], bj_ALLIANCE_ALLIED_VISION)

    let saveTrigger = CreateTrigger()
    for i = 0 to 11
        saveTrigger.registerPlayerChatEvent(players[i], "-save", true)
    saveTrigger.addAction(() -> begin
        saveData(GetTriggerPlayer())
    end)

    let newTrigger = CreateTrigger()
    for i = 0 to 11
        newTrigger.registerPlayerChatEvent(players[i], "-new", true)
    newTrigger.addAction(() -> begin
        loadData(GetTriggerPlayer(), true)
    end)

    let loadTrigger = CreateTrigger()
    for i = 0 to 11
        loadTrigger.registerPlayerChatEvent(players[i], "-load", true)
    loadTrigger.addAction(() -> begin
        loadData(GetTriggerPlayer(), false)
    end)

    let nameTrigger = CreateTrigger()
    for i = 0 to 11
        nameTrigger.registerPlayerChatEvent(players[i], "-name", false)
    nameTrigger.addAction(() -> begin
        GetTriggerPlayer().setName(GetEventPlayerChatString().substring(6))
    end)

    registerPlayerEvent(EVENT_PLAYER_LEAVE, () -> begin
        Log.warn(GetTriggerPlayer().getNameColored() + " has left the game!")
    end)
 
Previews
Contents

Frentity Test (Map)

Reviews
Frotty
I was thinking more about something like: Codeless Save and Load (Multiplayer) - v1.3.9 This is indeed a bit barren, and the description, code and preview image could use some improvements :P Other than that, the library code has been reviewed and...
  1. Chaosy

    Chaosy

    Joined:
    Jun 9, 2011
    Messages:
    10,109
    Resources:
    17
    Maps:
    1
    Spells:
    10
    Tutorials:
    6
    Resources:
    17
    DRY
    Dont Repeat Yourself

    In your "init" block there is a lot of repeating code.

    1. You have one unique trigger name for each trigger, this is not needed you can re-use a single trigger variable.
    2. Some of the loops can be merged

    Alternatively, make a function that does that trigger creation + loop, then just use a callback function for the rest.
     
  2. Sir Moriarty

    Sir Moriarty

    Joined:
    Jun 13, 2016
    Messages:
    124
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    This was mostly a quickly thrown together map to showcase the underlying API in Wurst itself, and how to use it. Frotty wanted me to post an example of how to use my libs for like a month now, so I just quickly edited my test case and posted it on here.

    I'll probably rewrite this example some time later on, but it's not meant to be an example of a good map in Wurst, but rather, what you can now do with the new Wurst APIs.
     
  3. KILLCIDE

    KILLCIDE

    Administrator

    Joined:
    Jul 22, 2015
    Messages:
    3,394
    Resources:
    20
    Models:
    2
    Icons:
    10
    Spells:
    7
    Tutorials:
    1
    Resources:
    20
    Please don't just quickly make something and upload it. I'd rather not waste the time of our reviewers / moderators that look at this give you suggestions you would have obviously implemented yourself.

    As far as I know, our official reviewers for this section, including myself, don't have the required expertise to properly review this. Hopefully @Frotty or @Cokemonkey11 can take this one.
     
  4. HappyTauren

    HappyTauren

    Joined:
    Nov 3, 2006
    Messages:
    8,427
    Resources:
    87
    Models:
    61
    Icons:
    23
    Packs:
    1
    Tutorials:
    2
    Resources:
    87
    I don't think you quite realize what this is. This is merely a demo on how to use Wurst's functionalities. This code cannot be used directly by mapmakers, it's a showcase of how to create your own system and how to implement serialization of your own classes.

    Wurst is very different from vjass in that it actually has a standard library. And this code uses it.

    The actual "resource" that's used here is already in wurst stdlib, and was tested and reviewed for a very long time.
     
  5. Chaosy

    Chaosy

    Joined:
    Jun 9, 2011
    Messages:
    10,109
    Resources:
    17
    Maps:
    1
    Spells:
    10
    Tutorials:
    6
    Resources:
    17
    Does not really matter if it uses the standard library does it?
    There is no difference between a vjass dependency and a wurst dependency.

    It should still be structured like a system with configurables and whatnot.
     
  6. HappyTauren

    HappyTauren

    Joined:
    Nov 3, 2006
    Messages:
    8,427
    Resources:
    87
    Models:
    61
    Icons:
    23
    Packs:
    1
    Tutorials:
    2
    Resources:
    87
    Creating a general-purpose system for this is actually stupid because every user has specific needs, and every class needs to implement fitting serialization.

    So instead of that, this is essentially a showcase of how to easily make a system that does everything you need, yourself, using functionalities of the wurst's stdlib.

    And it is very different from dependencies of vjass because there's only one dependency here, that's actually a language standard - not 17 user-specified dependencies.
     
  7. Chaosy

    Chaosy

    Joined:
    Jun 9, 2011
    Messages:
    10,109
    Resources:
    17
    Maps:
    1
    Spells:
    10
    Tutorials:
    6
    Resources:
    17
    I know that the dependencies are different behind the scenes, but for the coder there is no difference.

    Technically codeless save/load is a very general title, but in practice most maps would just want to load a unit.
    But if that is not the desired outcome, then why not make the system in that way?

    something like
    new OnSave()
    ..addReal(callbackFunction)
    ..addBoolean(true)

    I have not tried it, but I think it should be possible.
     
  8. HappyTauren

    HappyTauren

    Joined:
    Nov 3, 2006
    Messages:
    8,427
    Resources:
    87
    Models:
    61
    Icons:
    23
    Packs:
    1
    Tutorials:
    2
    Resources:
    87
    There's quite a bit of a difference. Since this one ships with wurst, the prerequisites for using/creating this kind of a system is having wurst set up to run. If something in those doesn't work correctly, it's a wurst stdlib issue - not ThirdPartyLinkedListImplementation#23423 issue. The responsibility of functionalities is on behalf of wurst's stdlib itself. You don''t go around trying to import a ton of systems into your map, you don't create a ton of 'triggers' to add the systems (if this is a script resource), etc.

    Therefore, there's a lot of difference for the coder, but no difference for the PLAYER.

    A unit with specific items... with specific abilities... with levels... etc. How this is implemented depends on the map. And therefore, how you (de)serialize it needs to be specifically implemented to your own needs.

    I don't think you understand the core concept behind this.

    Yes, technically, it's possible to do what you describe, if you read wurst's documentation for FileIO, but the point of this save/load system is to provide that through (de)serialization, in a way that real programming languages do it.
     
    Last edited: Jan 30, 2018
  9. KILLCIDE

    KILLCIDE

    Administrator

    Joined:
    Jul 22, 2015
    Messages:
    3,394
    Resources:
    20
    Models:
    2
    Icons:
    10
    Spells:
    7
    Tutorials:
    1
    Resources:
    20
    The closest this can be then is a template, and judging from how you describe it, it's not. With that in mind, I think this would be better off in the tutorial section.
     
  10. Chaosy

    Chaosy

    Joined:
    Jun 9, 2011
    Messages:
    10,109
    Resources:
    17
    Maps:
    1
    Spells:
    10
    Tutorials:
    6
    Resources:
    17
    Everything you bring up that you claim to make a difference, does not actually make a difference in this case.
    Yes, it is encouraged to use standard libraries since they are well tested.
    But that does not change anything regarding how this submission should be treated.
    No matter if it is a demo or a system, or whatever really, if it is standard dependency or a random one does not matter.

    I know there is a difference, I just don't see how it is supposed to be relevant to the submission.

    If it is just a parser, why not call it that? I do not get it. (with unit demo attached)

    edit: apparently the parser is a part of one of the requirements, which means I am back to square one, wondering why it is not a proper system.
     
  11. HappyTauren

    HappyTauren

    Joined:
    Nov 3, 2006
    Messages:
    8,427
    Resources:
    87
    Models:
    61
    Icons:
    23
    Packs:
    1
    Tutorials:
    2
    Resources:
    87
    That's probably true, though. This belongs to a tutorial or snippet section, since technically it's not exactly a template.

    In terms of the process, again, as I said, it makes a world of difference. It's a completely different workflow.

    I don't even know why @Sir Moriarty submitted this as a system since it's basically an example of how to use wurst, and as I agreed with @KILLCIDE, it is probably more fitting to have it in tutorial/snippet section.

    Not quite. If it is a demo of third party stuff, then it should also ship with third party stuff. This is more equivalent to a demo of jass' functionalities, as stdlib is presumed to be working correctly as much as jass is (though obviously there are going to be bugs which I hope get reported).

    What is just a parser?

    Because the real SYSTEM for this is actually the API provided in wurst's stdlib. That's as "general" as it goes.
    Code (WurstScript):
    import Persistable
    import SimpleIO
    import Network
    There's no reason to make this more specific by making it a system - since you need to decide what you want to save, why, and how, yourself, and especially, how you load it after saving it.

    This framework gives you the functionalities to do all of this in as specific manner as you need, in as good framework as you can create for persisting data yourself.
     
    Last edited: Jan 30, 2018
  12. Sir Moriarty

    Sir Moriarty

    Joined:
    Jun 13, 2016
    Messages:
    124
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    Woah, I didn't expect this to create this level of controversy.

    I'd like to make it clear that I submitted this example on the request of @Frotty , who wanted it to go into the spells submission under the Wurst tag to be easily visible.
    I do understand the rationale of making it a tutorial or a snippet rather than a spell submission, and if you guys believe this to be the better way - then I can work on that as well.

    I originally intended for my save/load stuff (that is, Sync, Network, MultifileIO, Persistable) to be an external system to Wurst's standard lib, but over time and some discussion, it was chosen to be integrated into the standard lib following a fairly rigorous review process and testing.

    If it wasn't integrated into the stdlib, then this example map would come with all the relevant code packaged in it, and I'd argue that it *would* make it a system. However, simply due to the fact that the actual 'system' code was externalized in the stdlib for ease of use, testing and easier updates, this doesn't leave much for the example map itself, other than to showcase the existing API.

    Every map has their own needs regarding what needs to be saved/loaded. Some would like to load specific heroes, some want to load units, some want to load a leaderboard and other player settings. How you structure your code will vary wildly depending on that, and it is simply impossible to capture it in a specific system, since all the other code that actually does the saving/loaded is already in the stdlib. I don't want to limit anyone's imagination by providing them yet another level of abstraction that isn't really needed (Persistable is the highest level of abstraction in the Stdlib, and it handles saving/loading of arbitrary data in a reliable and predictable fashion).

    If I gave the users some kind of system in this example, then most of them would probably have to rewrite or significantly modify it to suit their needs in the end.

    But... The system is already there, it's just that instead of being in the map itself, it's in the stdlib.
    At the end of the day, every example map creating some kind of system will usually ship with some simple example application of said system that you are meant to tweak and edit based on your own needs, without touching the system's code itself. Since the system here is externalized, all you have to do is to change the example to suit your needs.
     
  13. KILLCIDE

    KILLCIDE

    Administrator

    Joined:
    Jul 22, 2015
    Messages:
    3,394
    Resources:
    20
    Models:
    2
    Icons:
    10
    Spells:
    7
    Tutorials:
    1
    Resources:
    20
    As I mentioned, I lack the expertise to make a comment on this system. If this is a functioning system, and it does what its name suggest, then just ensure you are following the Spell Submission Rules. More specifically this one:
     
  14. Sir Moriarty

    Sir Moriarty

    Joined:
    Jun 13, 2016
    Messages:
    124
    Resources:
    2
    Spells:
    1
    Tutorials:
    1
    Resources:
    2
    Alright, thanks, I'll try to address that when I can.
     
  15. Frotty

    Frotty

    Wurst Reviewer

    Joined:
    Jan 1, 2009
    Messages:
    1,201
    Resources:
    9
    Models:
    3
    Tools:
    1
    Maps:
    3
    Tutorials:
    1
    Wurst:
    1
    Resources:
    9
    I was thinking more about something like: Codeless Save and Load (Multiplayer) - v1.3.9
    This is indeed a bit barren, and the description, code and preview image could use some improvements :p
    Other than that, the library code has been reviewed and merged into the stdlib and I'm fine with having a usecase/demo only spell map.
     
  16. KILLCIDE

    KILLCIDE

    Administrator

    Joined:
    Jul 22, 2015
    Messages:
    3,394
    Resources:
    20
    Models:
    2
    Icons:
    10
    Spells:
    7
    Tutorials:
    1
    Resources:
    20
    Sorry for not responding in this thread.

    Maybe this will be more approriate for the "JASS" section then. I don't really see a purpose for systems and such to be in the Spell section given youre going to add them into the stdlib?
     
  17. Frotty

    Frotty

    Wurst Reviewer

    Joined:
    Jan 1, 2009
    Messages:
    1,201
    Resources:
    9
    Models:
    3
    Tools:
    1
    Maps:
    3
    Tutorials:
    1
    Wurst:
    1
    Resources:
    9
    No?
    This is an application of a system, just like TriggerHappys one I linked above.
    The application of it, i.e. this example code (which should be a bit extended/beatified maybe) is obv not inside the stdlib.
     
  18. KILLCIDE

    KILLCIDE

    Administrator

    Joined:
    Jul 22, 2015
    Messages:
    3,394
    Resources:
    20
    Models:
    2
    Icons:
    10
    Spells:
    7
    Tutorials:
    1
    Resources:
    20
    Gotcha. Marking your post as a review! Approved.

    Please consider his suggestions about improving the presentation ^^