The UGC API covers replays, screenshots, custom levels, mods, and other player-owned files.
UGC types
| Type | Value | Use for |
|---|---|---|
SCREENSHOT | 0 | Still captures |
VIDEO | 1 | Clips or recordings |
COMMUNITY | 2 | Levels, mods, maps |
GAME_MANAGED | 3 | Replays, saves |
OTHER | 4 | Anything 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
| Value | Constant | Who sees it |
|---|---|---|
0 | PUBLIC | Everyone |
2 | PRIVATE | The creator only |
Create and upload
Playtest the example project View example project on GitHubfunc _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);
}