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ê
| Ator | O que faz nesta página |
|---|---|
player | envia blocos, lê arquivos como fluxo, envia UGC |
moderator | analisa a fila, aprova ou rejeita envios |
backend-service | gera variantes de assets; engata hooks no upload e na moderação; define cotas |
Resumo
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)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.// 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 bindingsvar 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") # clAinda 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.var submission = await playserv.Files.SubmitUgc(file, kind: "level"); // clOs controles em volta dele são hooks, com o mesmo contrato de sempre:
[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)Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.
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.
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.
| Declara | O que é |
|---|---|
origin | conteú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 admitidos | como uma lista declarada, nunca adivinhados a partir dos bytes |
limites de tamanho | verificados quando a sessão é aberta, pelo tamanho declarado, e não na última parte |
tamanho e ordem das partes | o upload é feito por uma sessão: um tamanho de parte declarado, a ordem das partes, um ponto de retomada |
derivados | opcionalmente, 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 armazenamento | no 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.
| Sempre | O 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ída | morre de forma observável: passado o prazo, é encerrada com um evento, e as partes são liberadas |
a posse | segue 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 leitura | pode 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ãoforbidden, 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:
expiredcom 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.