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 items are intended to be controlled by the game (e.g. replays attached to leaderboard entries) and are never returned via listUGCItems. Fetch them directly by ugcId instead — for example via the ugcId on a 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(
        WavedashConstants.UGC_TYPE_COMMUNITY,
        "Custom arena",
        "Uploaded from editor",
        WavedashConstants.UGC_VISIBILITY_PUBLIC,
        abs_path
    )

func _on_ugc_created(response):
    if response.get("success", false):
        print("ugc id: ", response["data"])
var result = await Wavedash.SDK.CreateUGCItem(
    ugcType: WavedashConstants.UGCItemType.COMMUNITY,
    title: "Custom arena",
    description: "Uploaded from editor",
    visibility: WavedashConstants.UGCVisibility.PUBLIC,
    localPath: localPath
);
if (result != null)
    Debug.Log($"ugc id: {result["id"]}");
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
    )
    if response.success then
        print("ugc id:", response.data)
    end
end)
assert(coroutine.resume(co))
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 filePath / localPath argument 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": WavedashConstants.UGC_VISIBILITY_PRIVATE
})
await Wavedash.SDK.UpdateUGCItem(ugcId, title: "Renamed level", description: "Fixed spawn", visibility: WavedashConstants.UGCVisibility.PRIVATE);
local co = coroutine.create(function()
    wavedash.update_ugc_item_async(
        ugc_id,
        "Renamed level",
        "Fixed spawn",
        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["title"])
)

var response = await WavedashSDK.list_ugc_items({
    "createdBy": user_id,
    "ugcType": WavedashConstants.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 = userId,
        ugcType = wavedash.UGC_TYPE_COMMUNITY,
        titleSearch = "Arena"
        numItems = 50
    })
    if response.success and not response.isDone then
        local next = wavedash.list_ugc_items_async({
            continueCursor = response.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)
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 = sys.get_save_file("my_game", "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
);
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);
}