Эта страница ещё не переведена — показана английская версия.
Files & UGC
Файлы приходят частями и обрабатываются по мере поступления. Загрузки, ассеты и их производные варианты, а также контент игроков с модерацией.
Когда это нужно
- Игроки или сервисы загружают бинарные данные: частями, в сессиях, которые можно возобновить, и с квотами на игрока, которые можно прочитать.
- Обработка должна начинаться до конца загрузки: файл читается потоком, часть за частью.
- Контенту, который создают игроки, нужна модерация:
SubmitUgc, очередь, вердикт и хуки с обеих сторон. - Одно исходное изображение должно обслуживать много платформ: из него получают варианты (масштабирование, перекодирование), а каноническим остаётся оригинал.
- Не нужен для небольших структурированных данных. Их переносит поле записи Data & Subscriptions — без сессии загрузки.
Кто что делает
| Актор | Что делает на этой странице |
|---|---|
player | загружает части, читает файлы потоком, отправляет UGC |
moderator | разбирает очередь, одобряет или отклоняет присланное |
backend-service | создаёт варианты ассетов; вешает хуки на загрузку и модерацию; задаёт квоты |
Коротко
tank-07.png частями, прочитать его ещё во время загрузки и прикрепить как декаль// 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)Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.
// 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 — путь со стороны игрока:
SubmitUgc с клиента — один вызов в каждом биндингеvar 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") # clКак объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.
// 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"); // clПроверки вокруг него — это хуки с тем же контрактом, что и везде:
[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)Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.
Хук — это облачная функция: он выполняется на платформе, а не в движке. Пишите его на C#, TypeScript или Python, а Unreal подписывается на события, которые он порождает. Функции, написанные в движке, запланированы: графы Blueprint и C++ (его будут транслировать в скриптовый формат, который проверяет платформа). И те и другие анализируются и отправляются на платформу как любое объявление, а выполняются по-прежнему на платформе.
Хук — это облачная функция: он выполняется на платформе, а не в движке. Пишите его на C#, TypeScript или Python, а Unity подписывается на события, которые он порождает.
Оба хука оборачивают шаг операции, а не событие. Before(Files.Upload) решает, начнётся ли загрузка. After(Files.SubmitUgc) срабатывает, когда присланный контент уже существует, и ставит его в очередь модерации через собственный Moderation.Enqueue модуля. События — загрузка завершена, вариант готов, UGC прислан, вынесен вердикт модерации — получают подписчики, а подписчик ничего запретить не может.
Модель
Файл — это непрозрачные байты плюс объявленные метаданные: источник, тип содержимого, размер. Хранилищем состояния, по которому принимаются решения, он не служит. Связь с моделью игры идёт в обратную сторону: ссылку хранит поле вашего типа, а сам файл об игре ничего не знает.
Что объявляет вид файла.
| Объявление | Что это |
|---|---|
origin | авторский контент, созданный игрой или созданный пользователями; от этого зависят ограничения размера и политики |
допустимые типы содержимого | задаются объявленным списком и никогда не угадываются по байтам |
ограничения размера | прове ряются по объявленному размеру при открытии сессии, а не на последней части |
размер и порядок частей | загрузку выполняет сессия: объявленный размер части, порядок частей, точка возобновления |
derivatives | при желании — именованные варианты, которые создаёт обработчик. Готовность каждого объявленного варианта видна, поэтому клиенту не приходится гадать, есть ли уже миниатюра |
префикс хранилища | задаётся на поле схемы, которое хранит ссылку, и определяет только то, где лежат байты. Это не каталог: переименования, перемещения, разрешений на префикс и рекурсивных операций нет. Файл по-прежнему адресуется по id или key, и префикс в этом не участвует |
Что верно для любого файла.
| Всегда | Что это |
|---|---|
завершение | идемпотентно в пределах сессии: повторное завершение возвращает тот же файл, а не второй |
контрольная сумма | обязательна, а несовпадение — это отказ, а не молчаливое принятие испорченных байтов |
опубликованный файл | неизменяем: правка создаёт новую версию, а ссылка на версию продолжает указывать туда же, куда указывала |
авторский контент | адресуется по ключу и версии и находится в режиме managed: в админ-консоли его не редактируют, потому что им владеет код |
незавершённая сессия | завершается заметно: после истечения срока её прерывают с событием, а её части освобождаются |
владение | следует предикату владельца: у файлов, созданных пользователями и игрой, есть владелец, как у любой строки с владельцем. Файлы владельца подчиняются политике удаления игрока — каскадное удаление, отказ или анонимизация, и эта по литика объявляется, а не подразумевается |
доступ на чтение | может зависеть от права владения (entitlement): платный ассет закрыт правом владения из модуля Catalog & Commerce, а не второй системой разрешений |
Каждая точка расширения называет тип, который она передаёт хуку: до загрузки — намерение загрузки, после — присланный контент. Нетипизированный набор полей хук не получает никогда.
Ошибки
- Без права владения ответ —
not found, а неforbidden, чтобы по списку отказов нельзя было понять, какие дополнения существуют. Снятый с публикации файл отвечает так же. - Истёкшая сессия — конфликт: откройте новую.
- Часть вне объявленного порядка или размера, необъявленный тип содержимого и превышение допустимого размера — отказы валидации. Отказ по размеру приходит при открытии сессии, а не после того, как байты уже переданы.
- Несовпадение контрольной суммы — отказ валидации, который стоит повторить: отправьте часть заново.
- Квота исчерпана — конфликт; повторить можно, освободив место.
- Истёк срок выданных прав (grant) на чтение — ответ not authenticated: запросите права заново, а не разбирайтесь с этим как с проблемой разрешений.
- Отклонение после проверки — это вердикт, а не отказ: присланное рассмотрели, ответ отрицательный, и у него есть объявленная причина. Что делать дальше, зависит от причины.
- Превышена частота загрузок — ответ из категории ограничения частоты, с указанием срока.
Ограничения
Конкретные числа появятся вместе с главой об ограничениях платформы, а поведение на каждом пределе зафиксировано уже сейчас.
- Размер файла в зависимости от источника: загрузка отклоняется до того, как принята первая часть, а не на последней.
- Размер части: часть отклоняется как не прошедшая валидацию.
- Время жизни сессии:
expiredс событием, а части освобождаются. - Квота хранилища на проект и на игрока: новая сессия отклоня ется как конфликт, и уже опубликованное никогда не удаляется молча, чтобы освободить место.
- Число хранимых версий авторского контента: самая старая снимается с публикации, но версия, на которую ссылается действующее окружение (environment), — никогда.
- Частота загрузок на актора: ограничение частоты с указанием срока.
- Хранение контента, созданного игрой: после срока — снятие с публикации с событием.