Search documentation

Find pages, sections, and content across all docs.

WavedashDocs

User-generated content

Let players upload, share, and download community-created content

The UGC API covers replays, screenshots, custom levels, mods, and other player-owned files.

UGC types

TypeValueUse for
SCREENSHOT0Still captures
VIDEO1Clips or recordings
COMMUNITY2Levels, mods, maps
GAME_MANAGED3Replays, saves
OTHER4Anything else

GAME_MANAGED is intended for items the game controls (e.g. replays attached to leaderboard entries). They aren't hidden from listUGCItems — filter by ugcType if you don't want them in player-facing lists, and fetch replays directly by the ugcId on the leaderboard entry.

Visibility

ValueConstantWho sees it
0PUBLICEveryone
2PRIVATEThe creator only

Create and upload

Playtest the example project View example project on GitHub
func _ready():
    WavedashSDK.ugc_item_created.connect(_on_ugc_created)

func create_level_share(abs_path: String) -> void:
    WavedashSDK.create_ugc_item(
        WavedashSDK.Constants.UGC_TYPE_COMMUNITY,
        "Custom arena",
        "Uploaded from editor",
        WavedashSDK.Constants.UGC_VISIBILITY_PUBLIC,
        abs_path
    )

func _on_ugc_created(response):
    if response.get("success", false):
        print("ugc id: ", response["data"])
string ugcId = await Wavedash.SDK.CreateUGCItem(
    ugcType: WavedashConstants.UGCItemType.COMMUNITY,
    title: "Custom arena",
    description: "Uploaded from editor",
    visibility: WavedashConstants.UGCVisibility.PUBLIC,
    filePath: localPath
);
Debug.Log($"ugc id: {ugcId}");
local co = coroutine.create(function()
    local response = wavedash.create_ugc_item_async(
        wavedash.UGC_TYPE_COMMUNITY,
        "Custom arena",
        "Uploaded from editor",
        wavedash.UGC_VISIBILITY_PUBLIC,
        local_path -- a path written with wavedash.write_local_file_async
    )
    if response.success then
        print("ugc id:", response.data)
    end
end)
assert(coroutine.resume(co))
// The path is a key in the SDK's local store — write the bytes there first.
await Wavedash.writeLocalFile("ugc/levels/arena.wdc", levelBytes);
const response = await Wavedash.createUGCItem(
  Wavedash.UGCType.COMMUNITY,
  "Custom arena",
  "Uploaded from editor",
  Wavedash.UGCVisibility.PUBLIC,
  "ugc/levels/arena.wdc"
);
if (response.success) {
  console.log("ugc id:", response.data);
}

In Godot, the file path may be either user://... (auto-normalized) or an absolute path under OS.get_user_data_dir(). Paths outside the user data directory are rejected.

The file path argument (filePath in JavaScript and Unity, local_file_path in Godot, file_path in Defold) is optional — you can create a UGC item with metadata only and attach a file later with updateUGCItem. This is useful when the file is still being rendered or generated.

// Create the item first
const response = await Wavedash.createUGCItem(Wavedash.UGCType.COMMUNITY, "Custom arena", "WIP");

// ...game renders the level file...

// Attach the file later
if (response.success) {
  const ugcId = response.data;
  await Wavedash.updateUGCItem(ugcId, { filePath: "ugc/levels/arena.wdc" });
}

Update an item

Pass only the fields you want to change. Any field omitted from the updates object/dict is left unchanged on the server.

WavedashSDK.ugc_item_updated.connect(func(response): print("Updated: ", response.success))
WavedashSDK.update_ugc_item(ugc_id, {
    "title": "Renamed level",
    "description": "Fixed spawn",
    "visibility": WavedashSDK.Constants.UGC_VISIBILITY_PRIVATE
})
await Wavedash.SDK.UpdateUGCItem(ugcId, title: "Renamed level", description: "Fixed spawn", visibility: WavedashConstants.UGCVisibility.PRIVATE);
local co = coroutine.create(function()
    -- The Defold binding forwards its second argument as the JS `updates`
    -- object, so pass the fields as a JSON string.
    wavedash.update_ugc_item_async(ugc_id, json.encode({
        title = "Renamed level",
        description = "Fixed spawn",
        visibility = wavedash.UGC_VISIBILITY_PRIVATE
    }))
end)
assert(coroutine.resume(co))
await Wavedash.updateUGCItem(ugcId, {
  title: "Renamed level",
  description: "Fixed spawn",
  visibility: Wavedash.UGCVisibility.PRIVATE,
});

List items

Pass filters on the first page; pass continueCursor on subsequent pages.

WavedashSDK.got_ugc_items.connect(func(response):
    if response.get("success", false):
        for item in response["data"]["page"]:
            print(item.get("title", ""))
)

var response = await WavedashSDK.list_ugc_items({
    "createdBy": user_id,
    "ugcType": WavedashSDK.Constants.UGC_TYPE_COMMUNITY,
    "titleSearch": "Arena",
    "numItems": 50,
})

if response.get("success", false) and not response["data"]["isDone"]:
    WavedashSDK.list_ugc_items({ "continueCursor": response["data"]["continueCursor"] })
var response = await Wavedash.SDK.ListUGCItems(
    createdBy: userId,
    ugcType: WavedashConstants.UGCItemType.COMMUNITY,
    titleSearch: "Arena",
    numItems: 50
);

if (response != null && response.TryGetValue("isDone", out var isDone) && !(bool)isDone)
{
    var next = await Wavedash.SDK.ListUGCItems(
        continueCursor: (string)response["continueCursor"]
    );
}
local co = coroutine.create(function()
    local response = wavedash.list_ugc_items_async({
        createdBy = user_id,
        ugcType = wavedash.UGC_TYPE_COMMUNITY,
        titleSearch = "Arena",
        numItems = 50
    })
    if response.success and not response.data.isDone then
        local next_page = wavedash.list_ugc_items_async({
            continueCursor = response.data.continueCursor
        })
    end
end)
assert(coroutine.resume(co))
const response = await Wavedash.listUGCItems({
  createdBy: userId,
  ugcType: Wavedash.UGCType.COMMUNITY,
  titleSearch: "Arena",
  numItems: 50,
});
if (response.success) {
  for (const item of response.data.page) console.log(item.title);
  if (!response.data.isDone) {
    await Wavedash.listUGCItems({ continueCursor: response.data.continueCursor });
  }
}

Delete an item

Deletes a UGC item and frees the user's storage quota by the size of the deleted upload.

# Await the response directly
var response = await WavedashSDK.delete_ugc_item(ugc_id)
print("Deleted: ", response.success)

# Or listen via signal
WavedashSDK.ugc_item_deleted.connect(func(r): print("Deleted: ", r.success))
WavedashSDK.delete_ugc_item(ugc_id)
await Wavedash.SDK.DeleteUGCItem(ugcId);
local co = coroutine.create(function()
    wavedash.delete_ugc_item_async(ugc_id)
end)
assert(coroutine.resume(co))
await Wavedash.deleteUGCItem(ugcId);

Download content

func _ready():
    WavedashSDK.ugc_item_downloaded.connect(func(response): print("Downloaded: ", response.data))

func download_into_user_data(ugc_id: String, file_name: String) -> void:
    var dir = OS.get_user_data_dir() + "/downloads"
    DirAccess.make_dir_recursive_absolute(dir)
    WavedashSDK.download_ugc_item(ugc_id, dir + "/" + file_name)
using System.IO; // Path, Directory

var dir = Path.Combine(Application.persistentDataPath, "downloads");
Directory.CreateDirectory(dir);
await Wavedash.SDK.DownloadUGCItem(ugcId, Path.Combine(dir, fileName));
local co = coroutine.create(function()
    local path = "downloads/" .. file_name
    local response = wavedash.download_ugc_item_async(ugc_id, path)
    if response.success then
        print("Downloaded:", response.data)
    end
end)
assert(coroutine.resume(co))
const response = await Wavedash.downloadUGCItem(ugcId, "downloads/pack.bin");
if (response.success) {
  const bytes = await Wavedash.readLocalFile("downloads/pack.bin");
}

Attach UGC to a leaderboard entry

UGC is for payloads — replays, screenshots, ghost data. For a few small values like the character used or the number of deaths, leaderboard entry metadata stores them on the entry directly, with no file to upload or download. The same score upload takes both.

Pass the ugcId into the score upload call:

local co = coroutine.create(function()
    local lb = wavedash.get_or_create_leaderboard_async(
        "speedrun-times",
        wavedash.LEADERBOARD_SORT_ORDER_ASC,
        wavedash.LEADERBOARD_DISPLAY_TYPETIME_MILLISECONDS
    )
    local ugc = wavedash.create_ugc_item_async(
        wavedash.UGC_TYPE_GAME_MANAGED,
        "Replay",
        "replay of level 1",
        wavedash.UGC_VISIBILITY_PUBLIC,
        "replays/run.dat"
    )
    if lb.success and ugc.success then
        wavedash.upload_leaderboard_score_async(lb.data.id, 12345, true, ugc.data)
    end
end)
assert(coroutine.resume(co))
const lb = await Wavedash.getOrCreateLeaderboard(
  "speedrun-times",
  Wavedash.LeaderboardSortOrder.ASC,
  Wavedash.LeaderboardDisplayType.TIME_MILLISECONDS
);
await Wavedash.writeLocalFile("replays/run.dat", replayBytes);
const ugc = await Wavedash.createUGCItem(
  Wavedash.UGCType.GAME_MANAGED,
  "Replay",
  "replay of level 1",
  Wavedash.UGCVisibility.PUBLIC,
  "replays/run.dat"
);
if (lb.success && ugc.success) {
  const ugcId = ugc.data;
  await Wavedash.uploadLeaderboardScore(lb.data.id, 12345, true, ugcId);
}