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()
-- Paths name entries in the SDK's local store, not Defold save files:
-- write with write_local_file_async, then upload the same path.
wavedash.write_local_file_async(file_name, json.encode(game_state))
wavedash.upload_remote_file_async(file_name)
wavedash.download_remote_file_async(file_name)
end)
assert(coroutine.resume(co))In Defold, file paths name entries in the SDK's own local store (the same store write_local_file_async writes to), not files saved with sys.save(). Write your data with write_local_file_async before uploading. read_local_file_async currently returns a table of byte values keyed "0".."n-1" rather than a string.
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))Rate limits
Uploads are rate limited per player, per game:
- 30 uploads per minute
- 300 uploads per hour
Over the limit, uploadRemoteFile fails and its error says how long to wait. Until then, the SDK fails further uploads right away without contacting the server, so retrying in a loop won't get a save through sooner.
Upload at the points in your game where a cloud save matters:
- A level, run, or match ends
- The player reaches a checkpoint or saves manually
- The player quits to the menu or closes the game
Write locally as often as you like; writeLocalFile and engine file writes aren't limited. Then upload once at the next key point, not on every autosave tick or state change. If your save is split across several files, consider combining them into one so each save is a single upload.
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.success and 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 = "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 = 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 = "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/");In JavaScript, listRemoteDirectory returns each key as an absolute path (/saves/slot1.json), and downloadRemoteDirectory stores files under that key. Read them back with readLocalFile(file.key) — readLocalFile("saves/slot1.json") without the leading slash won't see the downloaded copy.
File metadata
interface RemoteFileMetadata {
exists: boolean;
key: string;
name: string;
lastModified: number; // Unix seconds
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)using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Newtonsoft.Json;
using UnityEngine;
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));
try
{
await Wavedash.SDK.UploadRemoteFile(path);
return true;
}
catch (Exception ex) // failed calls throw in WebGL builds
{
Debug.LogException(ex);
return false;
}
}local SAVE_VERSION = 2
-- Call from inside a coroutine so the *_async calls return their results.
local function cloud_save(slot, game_state)
local payload = {
version = SAVE_VERSION,
timestamp = os.time(),
state = game_state,
}
local path = ("saves/slot%d.json"):format(slot)
local wrote = wavedash.write_local_file_async(path, json.encode(payload))
if not wrote then -- write_local_file_async returns a plain boolean
return false
end
local uploaded = wavedash.upload_remote_file_async(path).success
return uploaded
endasync 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;
}