Saltar al contenido principal
Servicios alrededor del juego

Esta página aún no está traducida: se muestra la versión en inglés.

Files & UGC

Los archivos llegan en partes y se procesan a medida que llegan. Subidas, assets y sus variantes derivadas, y contenido creado por los jugadores con un flujo de moderación.

Cuándo usarlo

  • Los jugadores o los servicios suben blobs: sesiones por partes, reanudables, con cuotas por jugador que se pueden consultar.
  • El procesamiento debe empezar antes de que termine la subida: lee el archivo como un flujo (stream), parte por parte.
  • El contenido que crean los jugadores necesita un flujo de moderación: SubmitUgc, una cola, un veredicto y hooks en ambos extremos.
  • Una sola imagen maestra debe servir para muchas plataformas: genera variantes (cambio de tamaño, transcodificación) y conserva el original como versión canónica.
  • No lo necesitas para payloads pequeños y estructurados. Un campo de un registro de Data & Subscriptions los lleva sin necesidad de una sesión de subida.

Quién hace qué

ActorQué hace en esta página
playersube partes, lee archivos como flujo y envía UGC
moderatorrevisa la cola y aprueba o rechaza los envíos
backend-servicegenera variantes de los assets; intercepta con hooks la subida y la moderación; define las cuotas

En resumen

Sube tank-07.png por partes, léelo mientras se sigue subiendo y úsalo como calcomanía
// 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)
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

// 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 — funciona tal cual en Unity con los tipos generados
// 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, el camino del jugador:

SubmitUgc desde el cliente: una sola llamada, en todos los bindings
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
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

// 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 — funciona tal cual en Unity con los tipos generados
var submission = await playserv.Files.SubmitUgc(file, kind: "level");   // cl

Los controles a su alrededor son hooks, con el mismo contrato que en todas partes:

Los hooks controlan el tamaño de la subida y, al enviar, ponen el contenido en la cola de moderación
[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)
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

Se ejecuta fuera del motor

Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; Unreal se suscribe a los eventos resultantes. Están planeadas funciones escritas desde el motor: grafos de Blueprint y C++ (traducido a un formato de script que valida la plataforma). Ambas se analizan y se suben como cualquier declaración, y la ejecución sigue ocurriendo en la plataforma.

Se ejecuta fuera del motor

Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; Unity se suscribe a los eventos resultantes.

Los dos hooks envuelven un paso de una operación, nunca un evento: Before(Files.Upload) decide si la subida empieza, y After(Files.SubmitUgc) se ejecuta cuando el envío ya existe y lo pone en la cola de moderación con el propio Moderation.Enqueue del módulo. Los eventos —subida completada, variante lista, UGC enviado, veredicto de moderación— van a los suscriptores, y un suscriptor no veta nada.

El modelo

Un archivo son bytes opacos más metadatos declarados —origen, tipo de contenido, tamaño—, y no es un almacén de estado a partir del cual se toman decisiones. Su vínculo con el modelo del juego va en sentido contrario: un campo de tu tipo guarda la referencia, y el archivo no sabe nada del juego.

Qué declara un tipo de archivo.

DeclaraQué es
origincontenido creado por el equipo, generado por el juego o generado por los usuarios; de eso dependen los límites de tamaño y las políticas
tipos de contenido admitidoscomo una lista declarada, nunca deducidos a partir de los bytes
límites de tamañose comprueban al abrir la sesión, con el tamaño declarado, y no en la última parte
tamaño y orden de las partesla subida la hace una sesión: un tamaño de parte declarado, el orden de las partes y un punto de reanudación
derivativesopcionalmente, variantes con nombre que genera un handler. Se puede observar si cada variante declarada está lista, así que un cliente nunca tiene que adivinar si la miniatura ya existe
prefijo de almacenamientova en el campo del esquema que lleva la referencia: indica dónde viven los bytes y nada más. No es un directorio: un prefijo no se renombra ni se mueve, no tiene permisos y no admite operaciones recursivas. El archivo se sigue identificando por su id o su key, y el prefijo no participa en eso

Qué se cumple para todo archivo.

SiempreQué es
la finalizaciónes idempotente por sesión: finalizar otra vez devuelve el mismo archivo, no uno nuevo
una suma de verificaciónes obligatoria, y si no coincide, hay un rechazo, nunca una aceptación silenciosa de bytes corruptos
un archivo publicadoes inmutable: una edición es una versión nueva, y una referencia a una versión sigue apuntando a lo mismo
el contenido creado por el equipose identifica por clave y versión, y es managed: no se edita en la consola de administración, porque su dueño es el código
una sesión sin terminarmuere de forma observable: pasado su plazo, se cierra con un evento y sus partes se liberan
la propiedadsigue el predicado de dueño: los archivos generados por usuarios y por el juego tienen dueño como cualquier fila con dueño, y los archivos de un dueño siguen la política de eliminación de jugadores —en cascada, rechazo o anonimización—, que se declara y no se da por supuesta
el acceso de lecturapuede depender de un derecho de uso (entitlement): un asset pago queda protegido por el derecho de uso de Catalog & Commerce, no por un segundo sistema de permisos

Cada punto de extensión indica el tipo que le entrega al hook —antes de la subida, la intención de subida; después, el envío—, así que un hook nunca recibe un conjunto de datos sin tipo.

Errores

  • Sin derecho de uso, la respuesta es not found, no forbidden, para que la lista de rechazos no revele qué complementos existen. Un archivo retirado responde igual.
  • Una sesión expirada es un conflicto: abre una nueva.
  • Una parte fuera del orden o del tamaño declarados, un tipo de contenido no declarado y un tamaño por encima del límite son rechazos de validación, y el del tamaño llega al abrir la sesión, no después de que los bytes ya viajaron.
  • Una suma de verificación que no coincide es un rechazo de validación que vale la pena repetir: vuelve a enviar la parte.
  • La cuota agotada es un conflicto; puedes reintentar después de liberar espacio.
  • Un permiso de lectura otorgado que ya expiró responde not authenticated: pide un permiso nuevo en lugar de tratarlo como un problema de permisos.
  • Que la revisión rechace un envío es un veredicto, no un rechazo: el envío se evaluó y la respuesta es negativa, con un motivo declarado, así que el siguiente paso depende del motivo.
  • Superar la frecuencia de subida responde en la categoría de límite de frecuencia, con un plazo.

Límites

Las cifras llegarán con el capítulo sobre los límites de la plataforma; lo que pasa al alcanzar cada tope ya está definido.

  • Tamaño de un archivo según su origen: la subida se rechaza antes de aceptar cualquier parte, no en la última.
  • Tamaño de parte: la parte se rechaza como falla de validación.
  • Vida útil de la sesión: expired con un evento, y las partes se liberan.
  • Cuota de almacenamiento por proyecto y por jugador: una sesión nueva se rechaza como conflicto, y lo ya publicado nunca se borra en silencio para hacer lugar.
  • Versiones guardadas del contenido creado por el equipo: se retira la más antigua, pero una versión a la que hace referencia un entorno vigente nunca se retira.
  • Frecuencia de subida por actor: un límite de frecuencia con un plazo.
  • Retención del contenido generado por el juego: pasado el período, se retira con un evento.