Перейти до основного вмісту
Сервіси навколо гри

Цю сторінку ще не перекладено — показано англійську версію.

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

Як оголошувати через атрибути в 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.
Unity C# is the same C# API here — в Unity працює без змін зі згенерованими типами
// 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");   // cl
const submission = await playserv.files.submitUgc(file, { kind: 'level' });   // cl
submission = await playserv.files.submit_ugc(file, kind="level")   # cl
Незабаром — Go

Як оголошувати через атрибути в 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.
Unity C# is the same C# API here — в Unity працює без змін зі згенерованими типами
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

Як оголошувати через атрибути в Go, поки не вирішено (O-1). Приклади з’являться, коли оберуть спосіб запису.

Виконується поза рушієм

Хук — це хмарна функція: він виконується на платформі, а не в рушії. Пишіть його на C#, TypeScript або Python, а Unreal підписується на події, які з цього виникають. Згодом можна буде писати функції і в рушії: графами Blueprint або на C++ (такий код перекладатимуть у скриптовий формат, який перевіряє платформа). Обидва варіанти проходять аналіз і відправку на платформу, як будь-яке оголошення, а виконуються все одно на платформі.

Виконується поза рушієм

Хук — це хмарна функція: він виконується на платформі, а не в рушії. Пишіть його на C#, TypeScript або Python, а Unity підписується на події, які з цього виникають.

Обидва хуки обгортають крок операції, а не подію. Before(Files.Upload) вирішує, чи почнеться завантаження. After(Files.SubmitUgc) спрацьовує, коли надісланий контент уже існує, і ставить його в чергу модерації через власний Moderation.Enqueue модуля. Події — завантаження завершено, варіант готовий, UGC надіслано, модерація винесла вердикт — отримують підписники, а підписник нічого заборонити не може.

Модель

Файл — це непрозорі байти плюс оголошені метадані: джерело, тип вмісту, розмір. Сховищем стану, на основі якого ухвалюють рішення, він не є. Зв’язок із моделлю гри йде у зворотний бік: посилання зберігає поле вашого типу, а сам файл про гру нічого не знає.

Що оголошує вид файлу.

ОголошенняЩо це
originавторський контент, контент, створений грою, або контент від користувачів; від цього залежать ліміти розміру й політики
допустимі типи вмістузадають оголошеним списком і ніколи не вгадують за байтами
ліміти розміруперевіряють за оголошеним розміром під час відкриття сесії, а не на останній частині
розмір і порядок частинзавантаження виконує сесія: оголошений розмір частини, порядок частин, точка відновлення
derivativesза бажанням — іменовані варіанти, які створює обробник. Готовність кожного оголошеного варіанта видно, тож клієнтові не доводиться вгадувати, чи мініатюра вже є
префікс сховищазадають на полі схеми (schema), яке зберігає посилання, і він визначає лише те, де лежать байти. Це не каталог: немає ні перейменування, ні переміщення, ні дозволів на префікс, ні рекурсивних операцій. До файлу, як і раніше, звертаються за id або key, і префікс у цьому участі не бере

Що справджується для кожного файлу.

ЗавждиЩо це
завершенняідемпотентне в межах сесії: повторне завершення повертає той самий файл, а не другий
контрольна сумаобов’язкова, а розбіжність — це відмова, а не мовчазне прийняття пошкоджених байтів
опублікований файлнезмінний: правка створює нову версію, а посилання на версію й далі вказує туди, куди вказувало
авторський контентадресують за ключем і версією, і він перебуває в режимі managed: в адмін-консолі його не редагують, бо ним володіє код
незавершена сесіязавершується помітно: після спливу строку її переривають з подією, а її частини звільняють
володінняпідпорядковане предикату власника: у файлів, створених користувачами й грою, є власник, як у будь-якого рядка з власником. Файли власника підлягають політиці видалення гравця — каскадне видалення, відмова або анонімізація, — і цю політику оголошують, а не мають на увазі за замовчуванням
доступ на читанняможе залежати від права володіння (entitlement): платний ассет закриває право володіння з модуля Catalog & Commerce, а не друга система дозволів

Кожна точка розширення називає тип, який вона передає хуку: перед завантаженням — намір завантаження, після — надісланий контент. Тож нетипізованого набору даних хук не отримує ніколи.

Помилки

  • Без права володіння відповідь — not found, а не forbidden, щоб за списком відмов не можна було зрозуміти, які доповнення існують. Знятий із публікації файл відповідає так само.
  • Сесія, строк якої сплив, — це конфлікт: відкрийте нову.
  • Частина поза оголошеним порядком чи розміром, неоголошений тип вмісту і розмір понад ліміт — це відмови валідації. Відмова через розмір надходить уже під час відкриття сесії, а не тоді, коли байти вже передано.
  • Розбіжність контрольної суми — відмова валідації, після якої варто повторити: надішліть частину ще раз.
  • Квоту вичерпано — конфлікт; повторіть, коли звільните місце.
  • Минув строк виданих прав (grant) на читання — відповідь not authenticated: запросіть права заново, а не розбирайтеся з цим як із проблемою дозволів.
  • Відхилення під час модерації — це вердикт, а не відмова: надіслане розглянули, і відповідь негативна з оголошеною причиною, тож що робити далі, залежить від причини.
  • Перевищено частоту завантажень — відповідь категорії обмеження частоти з указаним терміном.

Обмеження

Конкретні числа з’являться в розділі про ліміти платформи, а що відбувається на кожній межі, визначено вже зараз.

  • Розмір файлу залежно від джерела: завантаження відхиляють ще до прийняття будь-якої частини, а не на останній.
  • Розмір частини: частину відхиляють як помилку валідації.
  • Час життя сесії: expired з подією, а частини звільняють.
  • Квота сховища на проєкт і на гравця: нову сесію відхиляють як конфлікт, і вже опубліковане ніколи не видаляють мовчки, щоб звільнити місце.
  • Кількість збережених версій авторського контенту: найстарішу знімають із публікації, але версію, на яку посилається чинне середовище, — ніколи.
  • Частота завантажень на актора: обмеження частоти з указаним терміном.
  • Зберігання контенту, створеного грою: після спливу строку — зняття з публікації з подією.