Цю сторінку ще не перекладено — показано англійську версію.
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 | за бажанням — іменовані варіанти, які створює обробник. Готовність кожного оголошеного варіанта видно, тож клієнтові не доводиться вгадувати, чи мініатюра вже є |
префікс сховища | задають на полі схеми (schema), яке зберігає посилання, і він визначає лише те, де лежать байти. Це не каталог: немає ні перейменування, ні переміщення, ні дозволів на префікс, ні рекурсивних операцій. До файлу, як і раніше, звертаються за id або key, і префікс у цьому участі не бере |
Що справджується для кожного файлу.
| Завжди | Що це |
|---|---|
завершення | ідемпотентне в межах сесії: повторне завершення повертає той самий файл, а не другий |
контрольна сума | обов’язкова, а розбіжність — це відмова, а не мовчазне прийняття пошкоджених байтів |
опублікований файл | незмінний: правка створює нову версію, а посилання на версію й далі вказує туди, куди вказувало |
авторський контент | адресують за ключем і версією, і він перебуває в режимі managed: в адмін-консолі його не редагують, бо ним володіє код |
незавершена сесія | завершується помітно: після спливу строку її переривають з подією, а її частини звільняють |
володіння | підпорядковане предикату власника: у файлів, створених користувачами й грою, є власник, як у будь-якого рядка з власником. Файли власника підлягають політиці видалення гравця — каскадне видалення, відмова або анонімізація, — і цю політику оголошують, а не мають на ув азі за замовчуванням |
доступ на читання | може залежати від права володіння (entitlement): платний ассет закриває право володіння з модуля Catalog & Commerce, а не друга система дозволів |
Кожна точка розширення називає тип, який вона передає хуку: перед завантаженням — намір завантаження, після — надісланий контент. Тож нетипізованого набору даних хук не отримує ніколи.
Помилки
- Без права володіння відповідь —
not found, а неforbidden, щоб за списком відмов не можна було зрозуміти, які доповнення існують. Знятий із публікації файл відповідає так само. - Сесія, строк якої сплив, — це конфлікт: відкрийте нову.
- Частина поза оголошеним порядком чи розміром, неоголошений тип вмісту і розмір понад ліміт — це відмови валідації. Відмова через розмір надходить уже під час відкриття сесії, а не тоді, коли байти вже передано.
- Розбіжність контрольної суми — відмова валідації, після якої варто повторити: надішліть частину ще раз.
- Квоту вичерпано — конфлікт; повторіть, коли звільните місце.
- Минув строк виданих прав (grant) на читання — відповідь not authenticated: запросіть права заново, а не розбирайтеся з цим як із проблемою дозволів.
- Відхилення під час модерації — це вердикт, а не відмова: надіслане розглянули, і відповідь негативна з оголошеною причиною, тож що робити далі, залежить від причини.
- Перевищено частоту завантажень — відповідь категорії обмеження частоти з указаним терміном.
Обмеження
Конкретні числа з’являться в розділі про ліміти платформи, а що відбувається на кожній межі, визначено вже зараз.
- Розмір файлу залежно від джерела: завантаження відхиляють ще до прийняття будь-якої частини, а не на останній.
- Розмір частини: частину відхиляють як помилку валідації.
- Час життя сесії:
expiredз подією, а частини звільняють. - Квота сховища на проєкт і на гравця: нову сесію відхиляють як конфлікт, і вже опубліковане ніколи не видаляють мовчки, щоб звільнити місце.
- Кількість збережених версій авторського контенту: найстарішу знімають із публікації, але версію, на яку посилається чинне середовище, — ніколи.
- Частота завантажень на актора: обмеження частоти з указаним терміном.
- Зберігання контенту, створеного грою: після спливу строку — зняття з публікації з подією.