# User-generated content

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

Source: https://docs.wavedash.com/sdk/ugc

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 |

<Note>
`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.
</Note>

## Visibility

| Value | Constant | Who sees it |
|-------|----------|-------------|
| `0` | `PUBLIC` | Everyone |
| `2` | `PRIVATE` | The creator only |

## Create and upload

<PlaytestLink href="https://wavedash.com/playtest/three-js-example/db0f308d-d967-4098-bb40-999172e9bb0c" />
<GithubLink href="https://github.com/wvdsh/examples/tree/main/example-threejs-cloud" />

<CodeGroup>
```gdscript Godot
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"])
```

```csharp Unity
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"]}");
```

```lua Defold
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))
```

```javascript JavaScript
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);
}
```
</CodeGroup>

<Note>
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.
</Note>

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.

```javascript
// 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.

<CodeGroup>
```gdscript Godot
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
})
```

```csharp Unity
await Wavedash.SDK.UpdateUGCItem(ugcId, title: "Renamed level", description: "Fixed spawn", visibility: WavedashConstants.UGCVisibility.PRIVATE);
```

```lua Defold
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))
```

```javascript JavaScript
await Wavedash.updateUGCItem(ugcId, {
  title: "Renamed level",
  description: "Fixed spawn",
  visibility: Wavedash.UGCVisibility.PRIVATE,
});
```
</CodeGroup>

## List items

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

<CodeGroup>
```gdscript Godot
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"] })
```

```csharp Unity
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"]
    );
}
```

```lua Defold
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))
```

```javascript JavaScript
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 });
  }
}
```
</CodeGroup>

## Delete an item

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

<CodeGroup>
```gdscript Godot
# 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)
```

```csharp Unity
await Wavedash.SDK.DeleteUGCItem(ugcId);
```

```lua Defold
local co = coroutine.create(function()
    wavedash.delete_ugc_item_async(ugc_id)
end)
assert(coroutine.resume(co))
```

```javascript JavaScript
await Wavedash.deleteUGCItem(ugcId);
```
</CodeGroup>

## Download content

<CodeGroup>
```gdscript Godot
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)
```

```csharp Unity
var dir = Path.Combine(Application.persistentDataPath, "downloads");
Directory.CreateDirectory(dir);
await Wavedash.SDK.DownloadUGCItem(ugcId, Path.Combine(dir, fileName));
```

```lua Defold
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))
```

```javascript JavaScript
const response = await Wavedash.downloadUGCItem(ugcId, "downloads/pack.bin");
if (response.success) {
  const bytes = await Wavedash.readLocalFile("downloads/pack.bin");
}
```
</CodeGroup>

## Attach UGC to a leaderboard entry

Pass the `ugcId` into the score upload call:

```lua
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))
```

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