1. Are you planning to upload your awesome spell or system to Hive? Please review the rules here.
    Dismiss Notice
  2. The Melee Mapping Contest #4: 2v2 - Results are out! Step by to congratulate the winners!
    Dismiss Notice
  3. We're hosting the 15th Mini-Mapping Contest with YouTuber Abelhawk! The contestants are to create a custom map that uses the hidden content within Warcraft 3 or is inspired by any of the many secrets within the game.
    Dismiss Notice
  4. The 20th iteration of the Terraining Contest is upon us! Join and create exquisite Water Structures for it.
    Dismiss Notice
  5. Check out the Staff job openings thread.
    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. Pyrogasm

    Pyrogasm

    Joined:
    Feb 27, 2007
    Messages:
    2,390
    Resources:
    0
    Resources:
    0
    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:
    153
    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,391
    Resources:
    10
    Models:
    3
    Tools:
    1
    Maps:
    4
    Tutorials:
    1
    Wurst:
    1
    Resources:
    10
    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.