1. Are you planning to upload your awesome spell or system to Hive? Please review the rules here.
    Dismiss Notice
  2. Updated Resource Submission Rules: All model & skin resource submissions must now include an in-game screenshot. This is to help speed up the moderation process and to show how the model and/or texture looks like from the in-game camera.
    Dismiss Notice
  3. DID YOU KNOW - That you can unlock new rank icons by posting on the forums or winning contests? Click here to customize your rank or read our User Rank Policy to see a list of ranks that you can unlock. Have you won a contest and still havn't received your rank award? Then please contact the administration.
    Dismiss Notice
  4. The Lich King demands your service! We've reached the 19th edition of the Icon Contest. Come along and make some chilling servants for the one true king.
    Dismiss Notice
  5. The 4th SFX Contest has started. Be sure to participate and have a fun factor in it.
    Dismiss Notice
  6. The poll for the 21st Terraining Contest is LIVE. Be sure to check out the entries and vote for one.
    Dismiss Notice
  7. The results are out! Check them out.
    Dismiss Notice
  8. Don’t forget to sign up for the Hive Cup. There’s a 555 EUR prize pool. Sign up now!
    Dismiss Notice
  9. The Hive Workshop Cup contest results have been announced! See the maps that'll be featured in the Hive Workshop Cup tournament!
    Dismiss Notice
  10. Check out the Staff job openings thread.
    Dismiss Notice
Dismiss Notice
60,000 passwords have been reset on July 8, 2019. If you cannot login, read this.

Codeless Save / Load in Wurst

Submitted by mori
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. Pyrogasm

    Pyrogasm

    Joined:
    Feb 27, 2007
    Messages:
    3,016
    Resources:
    1
    Spells:
    1
    Resources:
    1
    There’s another codeless save/load system here on THW you should probably use instead if you don’t understand what Wurst is for. Wurst builds and compiles maps like a software project and uses its own syntax language that’s different from JASS.
     
  2. Uncle

    Uncle

    Joined:
    Aug 10, 2018
    Messages:
    484
    Resources:
    0
    Resources:
    0
    I tried TriggerHappy's Codeless save & load system and I immediately reached the limit of how much I could save. I figure they all have this issue if you plan on saving a lot of variables. But thanks for the response, I guess i'll just wait for Reforged or some future patches to make this possible with GUI/JASS.
     
  3. Frotty

    Frotty

    Wurst Reviewer

    Joined:
    Jan 1, 2009
    Messages:
    1,437
    Resources:
    11
    Models:
    3
    Tools:
    1
    Maps:
    5
    Tutorials:
    1
    Wurst:
    1
    Resources:
    11
    Yes, you're missing that wurst (short for wurstscript) is the tool required to build and use this system. See WurstScript • Home

    Then you're doing it wrong.
     
  4. SpectreCular

    SpectreCular

    Joined:
    Aug 10, 2019
    Messages:
    8
    Resources:
    0
    Resources:
    0
    Seeing as there have been some slight changes since this was originally submitted, I've updated the example code to be more friendly for 1.30+ (currently for wurst build 1199):

    Code (vJASS):

    package Test

    import RegisterEvents
    import Persistable
    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.getFacingAngle().degrees())
                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(LoadStatus _status)
            owner.setName(myName)
            footieData.initializeLoaded()

    PlayerData array playerData

    function loadData(player runner, boolean createNew)
    let network = new Network(runner)
    network.start((NetworkResult status, HashBuffer buffer) -> begin
    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((LoadStatus status) -> begin
    if status == LoadStatus.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()
        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)
        network.start((NetworkResult status, HashBuffer buffer) -> begin
    playerData[runner.getId()].save(() -> begin
    Log.info("Saved data for " + runner.getNameColored())
    end)
        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)
     

    It'd be nice if the submission could include both legacy code and post-1.30 code, just to make it a more "out-of-the-box" example.
     
    Last edited: Aug 29, 2019