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