Not translated yet — showing English.
Files & UGC
Files arrive as chunks and are processed as they arrive. Uploads, assets and their derived variants, and player-generated content with a moderation path.
When to use it
- Players or services upload blobs: chunked, resumable sessions with readable per-player quotas.
- Processing must start before an upload finishes: read the file as a stream, chunk by chunk.
- Player-made content needs a moderation path:
SubmitUgc, a queue, a verdict, hooks on both ends. - One master image must serve many platforms: derive variants (resize, transcode) and keep the original canonical.
- Skip it for small structured payloads. A Data record field carries them without an upload session.
Who does what
| Actor | On this page |
|---|---|
player | uploads chunks, reads streamed files, submits UGC |
moderator | reviews the queue, approves or rejects submissions |
backend-service | derives asset variants; hooks upload and moderation; sets quotas |
At a glance
tank-07.png in chunks, read it back mid-upload, attach it as a decal// upload, chunked, resumable
var session = await PlayServ.Files.OpenUpload("skins/tank-07.png", contentType: "image/png");
await session.Write(chunk);
var file = await session.Complete();
// consume a file as a stream — start processing before the upload finishes
await using var read = PlayServ.Files.OpenRead(file);
await foreach (var chunk in read) Ingest(chunk);
// attach to an entity
await tank.Attach("decal", file);// upload, chunked, resumable
const session = await playserv.files.openUpload('skins/tank-07.png', { contentType: 'image/png' });
await session.write(chunk);
const file = await session.complete();
// consume a file as a stream — start processing before the upload finishes
const read = playserv.files.openRead(file);
for await (const chunk of read) ingest(chunk);
// attach to an entity
await tank.attach('decal', file);# upload, chunked, resumable
session = await playserv.files.open_upload("skins/tank-07.png", content_type="image/png")
await session.write(chunk)
file = await session.complete()
# consume a file as a stream — start processing before the upload finishes
async with playserv.files.open_read(file) as read:
async for chunk in read:
ingest(chunk)
# attach to an entity
await tank.attach("decal", file)Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.
// upload, chunked, resumable
Client->Files->Of<FSkin>()->Uploads->Create(FPSIdempotencyKey(UploadId),
FPSUploadSpec{ .Path = TEXT("skins/tank-07.png"), .ContentType = TEXT("image/png") },
TPSOnResult<FPSUpload*>::CreateWeakLambda(this, [this](const TPSResult<FPSUpload*>& Result)
{
if (!Result.HasValue()) { return; }
FPSUpload* Upload = Result.Value();
Upload->Parts->Create(PartNumber, Chunk);
Upload->Complete(TPSOnResult<FPSFileHandle*>::CreateWeakLambda(this, [this](const TPSResult<FPSFileHandle*>& Completed)
{
if (!Completed.HasValue()) { return; }
OnSkinUploaded(Completed.Value());
}));
}));
// consume a file as a stream — start processing before the upload finishes
TPSSubscription SkinBytes = Client->Files->Of<FSkin>()->Contents->Subscribe(File,
[this](const TArray<uint8>& Chunk) { Ingest(Chunk); });
// attach to an entity
Tank->Files->Attach(TEXT("decal"), File);
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.// upload, chunked, resumable
var session = await PlayServ.Files.OpenUpload("skins/tank-07.png", contentType: "image/png");
await session.Write(chunk);
var file = await session.Complete();
// consume a file as a stream — start processing before the upload finishes
await using var read = PlayServ.Files.OpenRead(file);
await foreach (var chunk in read) Ingest(chunk);
// attach to an entity
await tank.Attach("decal", file);UGC, the player path:
SubmitUgc from the client — one call, every bindingvar submission = await playserv.Files.SubmitUgc(file, kind: "level"); // clconst submission = await playserv.files.submitUgc(file, { kind: 'level' }); // clsubmission = await playserv.files.submit_ugc(file, kind="level") # clAttribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.
// client — a submission is keyed; a retried submit returns the same submission
Client->Files->Ugc->Create(FPSIdempotencyKey(SubmitId), File,
TPSOnResult<FPSSubmission*>::CreateWeakLambda(this, [this](const TPSResult<FPSSubmission*>& Result)
{
if (!Result.HasValue()) { return; }
Hud->ShowPending(Result.Value());
}));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.var submission = await playserv.Files.SubmitUgc(file, kind: "level"); // clThe gates around it are hooks, same contract as everywhere:
[Before(Files.Upload)]
public static Verdict CheckUpload(UploadIntent u) =>
u.Size > 20.Mb() ? Hook.Reject("too large") : Hook.Continue(u);
[After(Files.SubmitUgc)]
public static Task Screen(UgcSubmission s) => PlayServ.Files.Moderation.Enqueue(s);export const checkUpload = before(Files.upload, (u: UploadIntent) =>
u.size > mb(20) ? Hook.reject('too large') : Hook.continue(u));
export const screen = after(Files.submitUgc,
(s: UgcSubmission) => playserv.files.moderation.enqueue(s));@before(files.upload)
def check_upload(u: UploadIntent) -> Verdict:
return hook.reject("too large") if u.size > mb(20) else hook.continue_(u)
@after(files.submit_ugc)
async def screen(s: UgcSubmission):
await playserv.files.moderation.enqueue(s)Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.
A hook is a cloud function: it executes on the platform, not in the engine. Write it in C#, TypeScript or Python — Unreal subscribes to the resulting events. Engine-authored functions are planned: Blueprint graphs, and C++ (translated to a platform-validated script target). Both are analyzed and pushed like any declaration; execution stays on the platform.
A hook is a cloud function: it executes on the platform, not in the engine. Write it in C#, TypeScript or Python — Unity subscribes to the resulting events.
Both hooks wrap an operation step, never an event: Before(Files.Upload) decides whether the upload starts, After(Files.SubmitUgc) runs once the submission exists and puts it in the moderation queue through the module's own Moderation.Enqueue. Events — upload completed, variant ready, UGC submitted, moderation verdict — go to subscribers, and a subscriber vetoes nothing.
The model
A file is opaque bytes plus declared metadata — origin, content type, size — and it is not a store of state on which decisions are taken. Its link to the game model runs the other way: a field on your type holds the reference; the file does not know about the game.
What a file kind declares.
| Declares | What it is |
|---|---|
origin | authored content, game-generated, or user-generated; the size limits and policies follow from it |
admissible content types | as a declared list, never sniffed from the bytes |
size limits | checked when the session is opened, by the declared size, rather than on the last part |
part size and order | the upload is performed by a session: a declared part size, the order of parts, a resumption point |
derivatives | optionally, named variants produced by a handler. Every declared variant's readiness is observable, so a client never guesses whether the thumbnail exists yet |
storage prefix | on the schema field carrying the reference: where the bytes live, and nothing more. Not a directory: no renaming, no moving, no permissions on a prefix, no recursive operation. The file is still addressed by its id or key, and the prefix takes no part in that |
What is true of every file.
| Always | What it is |
|---|---|
completion | is idempotent by session: a repeat completion returns the same file rather than a second one |
a checksum | is obligatory, and a mismatch is a refusal, never a silent acceptance of corrupted bytes |
a published file | is immutable: an edit is a new version, and a reference to a version keeps pointing at what it pointed at |
authored content | is addressed by key plus version, and it is managed: not edited in the admin console, because the code owns it |
an unfinished session | dies observably: past its deadline it is terminated with an event and its parts are freed |
ownership | follows the owner predicate: user- and game-generated files have an owner like any owned row, and an owner's files obey the player-deletion policy — cascade, refusal or anonymisation, declared rather than assumed |
read access | may depend on an entitlement: a paid asset is gated by commerce's entitlement rather than by a second permission system |
Each extension point names the type it hands the hook — the upload intent before the upload, the submission after — so a hook never receives an untyped bag.
Errors
- No entitlement answers
not found, notforbidden, so the list of refusals doesn't reveal which add-ons exist. A taken-down file answers the same way. - An expired session is a conflict: open a new one.
- A part outside the declared order or size, an undeclared content type, and a size beyond the limit are validation refusals, and the size one lands when the session opens, not after the bytes have travelled.
- A checksum mismatch is a validation refusal worth repeating: resend the part.
- The quota exhausted is a conflict, retryable after freeing space.
- An expired read grant answers not authenticated: request a new grant rather than treating it as a permission problem.
- Rejection by review is a verdict, not a refusal: the submission was considered and the answer is negative with a declared reason, so what to do next depends on the reason.
- The upload rate exceeded answers in the rate-limit category with a deadline.
Limits
The numbers arrive with the platform limits chapter; what happens at each ceiling is fixed now.
- A file's size by origin: the upload is refused before any part is accepted, not on the last one.
- The part size: the part is refused as a validation failure.
- The session's lifetime:
expiredwith an event, and the parts are freed. - Storage quota per project and per player: a new session is refused as a conflict, and what is already published is never deleted silently to make room.
- Authored-content versions kept: the oldest is taken down, and a version referenced by an environment in force never is.
- The upload rate per actor: a rate limit with a deadline.
- Retention of game-generated content: past the period, a takedown with an event.