Search documentation

Find pages, sections, and content across all docs.

WavedashDocs

Cloud saves

Save and sync player data across sessions and devices

Cloud saves use the Remote Storage API: per-player files in the cloud, synced across devices. The SDK methods are named uploadRemoteFile, downloadRemoteFile, and similar.

Upload and download files

func save_to_cloud(file_name: String):
    var path = OS.get_user_data_dir() + "/" + file_name
    WavedashSDK.upload_remote_file(path)

func load_from_cloud(file_name: String):
    var path = OS.get_user_data_dir() + "/" + file_name
    WavedashSDK.download_remote_file(path)
public async void SaveToCloud(string localPath)
{
    await Wavedash.SDK.UploadRemoteFile(localPath);
}

public async void LoadFromCloud(string localPath)
{
    await Wavedash.SDK.DownloadRemoteFile(localPath);
}
const data = new TextEncoder().encode(JSON.stringify(gameState));
await Wavedash.writeLocalFile("saves/slot1.json", data);
await Wavedash.uploadRemoteFile("saves/slot1.json");

const dl = await Wavedash.downloadRemoteFile("saves/slot1.json");
if (dl.success) {
  const bytes = await Wavedash.readLocalFile("saves/slot1.json");
}
local co = coroutine.create(function()
    local save_path = sys.get_save_file("my_game", file_name)
    wavedash.upload_remote_file_async(save_path)
    wavedash.download_remote_file_async(save_path)
end)
assert(coroutine.resume(co))

Paths may be either user://... (auto-normalized) or absolute paths under OS.get_user_data_dir(). Paths outside the user data directory are rejected.

Signals. Each call emits a response signal when it resolves — connect to them once in _ready() if you prefer signal-based flow over await:

func _ready():
    WavedashSDK.remote_file_uploaded.connect(func(r): print("Uploaded: ", r.data))
    WavedashSDK.remote_file_downloaded.connect(func(r): print("Downloaded: ", r.data))
    WavedashSDK.remote_directory_downloaded.connect(func(r): print("Dir: ", r.data))
    WavedashSDK.got_remote_directory_listing.connect(func(r): print(r.data))

Checking existence

remoteFileExists issues a lightweight HEAD request so you can branch on whether a save is present without paying the full download cost.

# Await the response directly
func check_save(file_name: String):
    var path = OS.get_user_data_dir() + "/" + file_name
    var r = await WavedashSDK.remote_file_exists(path)
    print("Exists: ", r.success and r.data)

# Or listen via signal
func _ready():
    WavedashSDK.got_remote_file_exists.connect(func(r): print("Exists: ", r.success and r.data))
    WavedashSDK.remote_file_exists(OS.get_user_data_dir() + "/saves/slot1.json")
public async void CheckSave(string localPath)
{
    bool exists = await Wavedash.SDK.RemoteFileExists(localPath);
    Debug.Log($"Exists: {exists}");
}
const result = await Wavedash.remoteFileExists("saves/slot1.json");
if (result.success && result.data) {
  await Wavedash.downloadRemoteFile("saves/slot1.json");
}
-- `remote_file_exists` is not exposed in the current Defold binding.
-- Use `list_remote_directory_async()` or attempt a download instead.

To check whether a directory has any contents, use listRemoteDirectory — it returns an empty list if the directory is either empty or not found.

func _ready():
    WavedashSDK.got_remote_directory_listing.connect(func(r):
        print("Has saves: ", r.data.size() > 0))

func check_saves_folder():
    var path = OS.get_user_data_dir() + "/saves/"
    WavedashSDK.list_remote_directory(path)
var path = $"{Application.persistentDataPath}/saves/";
var files = await Wavedash.SDK.ListRemoteDirectory(path);
Debug.Log($"Has saves: {files.Count > 0}");
local co = coroutine.create(function()
    local path = sys.get_save_file("my_game", "saves")
    local files = wavedash.list_remote_directory_async(path)
    print("Has saves:", files.success and #files.data > 0)
end)
assert(coroutine.resume(co))
const list = await Wavedash.listRemoteDirectory("saves/");
const hasSaves = list.success && list.data.length > 0;

Deleting files

func delete_save(file_name: String):
    var path = OS.get_user_data_dir() + "/" + file_name
    WavedashSDK.delete_remote_file(path)

func _ready():
    WavedashSDK.remote_file_deleted.connect(func(r): print("Deleted: ", r.data))
public async void DeleteSave(string localPath)
{
    await Wavedash.SDK.DeleteRemoteFile(localPath);
}
local co = coroutine.create(function()
    local save_path = sys.get_save_file("my_game", file_name)
    local result = wavedash.delete_remote_file_async(save_path)
    if result.success then
        print("Deleted:", result.data)
    end
end)
assert(coroutine.resume(co))
const result = await Wavedash.deleteRemoteFile("saves/slot1.json");
if (result.success) {
  console.log("Deleted:", result.data);
}

Directories

func download_save_folder():
    var path = OS.get_user_data_dir() + "/saves/"
    WavedashSDK.download_remote_directory(path)

func list_remote_saves():
    var path = OS.get_user_data_dir() + "/saves/"
    WavedashSDK.list_remote_directory(path)
var path = $"{Application.persistentDataPath}/saves/";
await Wavedash.SDK.DownloadRemoteDirectory(path);

var files = await Wavedash.SDK.ListRemoteDirectory(path);
local co = coroutine.create(function()
    local path = sys.get_save_file("my_game", "saves")
    wavedash.download_remote_directory_async(path)
    local files = wavedash.list_remote_directory_async(path)
end)
assert(coroutine.resume(co))
await Wavedash.downloadRemoteDirectory("saves/");

const list = await Wavedash.listRemoteDirectory("saves/");

File metadata

interface RemoteFileMetadata {
  exists: boolean;
  key: string;
  name: string;
  lastModified: number;
  size: number;
  etag: string;
}

Path conventions

Use forward slashes in remote keys, even on Windows builds.

saves/slot1.json
settings.json
replays/run-001.dat

Remote keys are relative to the player root. Keep names short and stable so you can migrate formats later.

Full save system example

const SAVE_VERSION = 2
var save_root: String

func _ready():
    save_root = OS.get_user_data_dir() + "/saves/"

func cloud_save(slot: int, game_state: Dictionary) -> void:
    var payload = {"version": SAVE_VERSION, "timestamp": Time.get_unix_time_from_system(), "state": game_state}
    var path = save_root + "slot" + str(slot) + ".json"
    DirAccess.make_dir_recursive_absolute(save_root)
    var file = FileAccess.open(path, FileAccess.WRITE)
    file.store_string(JSON.stringify(payload))
    file.close()
    WavedashSDK.upload_remote_file(path)

func cloud_load(slot: int) -> void:
    var path = save_root + "slot" + str(slot) + ".json"
    WavedashSDK.download_remote_file(path)
public async Task<bool> Save(int slot, Dictionary<string, object> gameState)
{
    var payload = new Dictionary<string, object>
    {
        { "version", 2 },
        { "timestamp", DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
        { "state", gameState }
    };
    var path = $"{Application.persistentDataPath}/saves/slot{slot}.json";
    Directory.CreateDirectory(Path.GetDirectoryName(path));
    File.WriteAllText(path, JsonConvert.SerializeObject(payload));
    var result = await Wavedash.SDK.UploadRemoteFile(path);
    return !string.IsNullOrEmpty(result);
}
local SAVE_VERSION = 2

local function cloud_save(slot, game_state)
    local payload = {
        version = SAVE_VERSION,
        timestamp = os.time(),
        state = game_state,
    }
    local path = sys.get_save_file("my_game", ("saves/slot%d.json"):format(slot))

    local wrote = wavedash.write_local_file_async(path, json.encode(payload))
    if not wrote.success then
        return false
    end

    local uploaded = wavedash.upload_remote_file_async(path).success
    return uploaded
end
async function cloudSave(slot, gameState) {
  const payload = { version: 2, timestamp: Date.now(), state: gameState };
  const path = `saves/slot${slot}.json`;
  await Wavedash.writeLocalFile(path, new TextEncoder().encode(JSON.stringify(payload)));
  return (await Wavedash.uploadRemoteFile(path)).success;
}