Pular para o conteúdo principal
Serviços em torno do jogo

Esta página ainda não foi traduzida: mostrando a versão em inglês.

Files & UGC

Os arquivos chegam em blocos e são processados à medida que chegam. Uploads, assets e suas variantes derivadas, e conteúdo criado por jogadores com um fluxo de moderação.

Quando usar

  • Jogadores ou serviços enviam blobs: sessões de upload em blocos (chunks), que podem ser retomadas, com cotas por jogador que dá para consultar.
  • O processamento precisa começar antes de o upload terminar: leia o arquivo como um fluxo (stream), bloco a bloco.
  • Conteúdo feito por jogadores precisa de um fluxo de moderação: SubmitUgc, uma fila, um veredicto e hooks nas duas pontas.
  • Uma imagem mestre precisa atender muitas plataformas: gere variantes (redimensionamento, transcodificação) e mantenha o original como a versão de referência.
  • Dispensável para payloads pequenos e estruturados. Um campo de registro do Data & Subscriptions os carrega sem sessão de upload.

Quem faz o quê

AtorO que faz nesta página
playerenvia blocos, lê arquivos como fluxo, envia UGC
moderatoranalisa a fila, aprova ou rejeita envios
backend-servicegera variantes de assets; engata hooks no upload e na moderação; define cotas

Resumo

Enviar tank-07.png em blocos, ler de volta no meio do upload e anexar como 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)
Em breve — Go

Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.

// 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 — roda sem mudanças no Unity com os tipos gerados
// 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, o caminho do jogador:

SubmitUgc no cliente: uma chamada, em todos os 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
Em breve — Go

Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.

// 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 — roda sem mudanças no Unity com os tipos gerados
var submission = await playserv.Files.SubmitUgc(file, kind: "level");   // cl

Os controles em volta dele são hooks, com o mesmo contrato de sempre:

Hooks limitam o tamanho do upload e colocam o envio na fila de moderação
[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)
Em breve — Go

Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.

Executa fora da engine

Um hook é uma cloud function: ele roda na plataforma, não na engine. Escreva em C#, TypeScript ou Python; o Unreal assina os eventos resultantes. Estão planejadas funções escritas na engine: grafos de Blueprint e C++ (traduzido para um formato de script validado pela plataforma). As duas são analisadas e publicadas como qualquer declaração, e a execução continua na plataforma.

Executa fora da engine

Um hook é uma cloud function: ele roda na plataforma, não na engine. Escreva em C#, TypeScript ou Python; o Unity assina os eventos resultantes.

Os dois hooks envolvem uma etapa de operação, nunca um evento: Before(Files.Upload) decide se o upload começa, e After(Files.SubmitUgc) roda assim que o envio existe e o coloca na fila de moderação pelo Moderation.Enqueue do próprio módulo. Os eventos (upload concluído, variante pronta, UGC enviado, veredicto da moderação) vão para os assinantes, e um assinante não veta nada.

O modelo

Um arquivo é um conjunto de bytes opacos mais metadados declarados (origem, tipo de conteúdo, tamanho) e não é um repositório de estado com base no qual se tomam decisões. O vínculo com o modelo do jogo vai no sentido contrário: um campo do seu tipo guarda a referência; o arquivo não sabe nada do jogo.

O que um tipo de arquivo declara.

DeclaraO que é
originconteúdo criado pela equipe, gerado pelo jogo ou gerado pelo usuário; os limites de tamanho e as políticas decorrem disso
tipos de conteúdo admitidoscomo uma lista declarada, nunca adivinhados a partir dos bytes
limites de tamanhoverificados quando a sessão é aberta, pelo tamanho declarado, e não na última parte
tamanho e ordem das parteso upload é feito por uma sessão: um tamanho de parte declarado, a ordem das partes, um ponto de retomada
derivadosopcionalmente, variantes nomeadas produzidas por um handler. A disponibilidade de cada variante declarada pode ser observada, então um cliente nunca precisa adivinhar se a miniatura já existe
prefixo de armazenamentono campo do esquema que carrega a referência: onde os bytes ficam, e nada mais. Não é um diretório: sem renomear, sem mover, sem permissões num prefixo, sem operação recursiva. O arquivo continua sendo endereçado pelo id ou pela key, e o prefixo não participa disso

O que vale para todo arquivo.

SempreO que é
a conclusãoé idempotente por sessão: concluir de novo devolve o mesmo arquivo, e não um segundo
um checksumé obrigatório, e uma divergência é uma recusa, nunca a aceitação silenciosa de bytes corrompidos
um arquivo publicadoé imutável: uma edição é uma nova versão, e uma referência a uma versão continua apontando para o que apontava
o conteúdo criado pela equipeé endereçado por chave mais versão, e é managed: não é editado no console de administração, porque pertence ao código
uma sessão não concluídamorre de forma observável: passado o prazo, é encerrada com um evento, e as partes são liberadas
a possesegue o predicado de dono: arquivos gerados pelo usuário ou pelo jogo têm dono como qualquer linha com dono, e os arquivos de um dono obedecem à política de exclusão de jogador (cascata, recusa ou anonimização), declarada, e não presumida
o acesso de leiturapode depender de um direito de uso (entitlement): um asset pago é liberado pelo direito de uso do comércio, e não por um segundo sistema de permissões

Cada ponto de extensão diz o tipo que entrega ao hook (a intenção de upload antes do upload, o envio depois), então um hook nunca recebe um saco de dados sem tipo.

Erros

  • Sem direito de uso, a resposta é not found, não forbidden, para que a lista de recusas não revele quais add-ons existem. Um arquivo removido responde do mesmo jeito.
  • Uma sessão expirada é um conflito: abra uma nova.
  • Uma parte fora da ordem ou do tamanho declarados, um tipo de conteúdo não declarado e um tamanho acima do limite são recusas de validação, e a de tamanho chega quando a sessão abre, não depois que os bytes já viajaram.
  • Um checksum divergente é uma recusa de validação que vale repetir: reenvie a parte.
  • A cota esgotada é um conflito, que pode ser repetido depois de liberar espaço.
  • Uma autorização de leitura expirada responde not authenticated: peça uma nova autorização, em vez de tratar isso como problema de permissão.
  • A rejeição na revisão é um veredicto, não uma recusa: o envio foi avaliado e a resposta é negativa, com um motivo declarado, então o próximo passo depende do motivo.
  • A frequência de upload excedida responde na categoria de limite de frequência, com prazo.

Limites

Os números chegam com o capítulo de limites da plataforma; o que acontece em cada teto já está definido.

  • Tamanho de um arquivo por origem: o upload é recusado antes que qualquer parte seja aceita, e não na última.
  • Tamanho da parte: a parte é recusada como falha de validação.
  • Tempo de vida da sessão: expired com um evento, e as partes são liberadas.
  • Cota de armazenamento por projeto e por jogador: uma nova sessão é recusada como conflito, e o que já foi publicado nunca é apagado em silêncio para abrir espaço.
  • Versões guardadas de conteúdo criado pela equipe: a mais antiga é removida, e uma versão referenciada por um ambiente em vigor nunca é.
  • Frequência de upload por ator: limite de frequência com prazo.
  • Retenção de conteúdo gerado pelo jogo: passado o período, remoção com um evento.