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

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);
}