Skip to main content
Services around the game

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

ActorOn this page
playeruploads chunks, reads streamed files, submits UGC
moderatorreviews the queue, approves or rejects submissions
backend-servicederives asset variants; hooks upload and moderation; sets quotas

At a glance

Upload 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)
Coming soon — Go

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.
Unity C# is the same C# API here — runs as-is in Unity against the generated types
// 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 binding
var submission = await playserv.Files.SubmitUgc(file, kind: "level");   // cl
const submission = await playserv.files.submitUgc(file, { kind: 'level' });   // cl
submission = await playserv.files.submit_ugc(file, kind="level")   # cl
Coming soon — Go

Attribute-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.
Unity C# is the same C# API here — runs as-is in Unity against the generated types
var submission = await playserv.Files.SubmitUgc(file, kind: "level");   // cl

The gates around it are hooks, same contract as everywhere:

Hooks gate the upload size and enqueue moderation on submission
[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)
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

Runs off the engine

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.

Runs off the engine

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.

DeclaresWhat it is
originauthored content, game-generated, or user-generated; the size limits and policies follow from it
admissible content typesas a declared list, never sniffed from the bytes
size limitschecked when the session is opened, by the declared size, rather than on the last part
part size and orderthe upload is performed by a session: a declared part size, the order of parts, a resumption point
derivativesoptionally, named variants produced by a handler. Every declared variant's readiness is observable, so a client never guesses whether the thumbnail exists yet
storage prefixon 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.

AlwaysWhat it is
completionis idempotent by session: a repeat completion returns the same file rather than a second one
a checksumis obligatory, and a mismatch is a refusal, never a silent acceptance of corrupted bytes
a published fileis immutable: an edit is a new version, and a reference to a version keeps pointing at what it pointed at
authored contentis addressed by key plus version, and it is managed: not edited in the admin console, because the code owns it
an unfinished sessiondies observably: past its deadline it is terminated with an event and its parts are freed
ownershipfollows 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 accessmay 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, not forbidden, 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: expired with 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.