本页尚未翻译,当前显示英文版本。
Core
**Core 是你唯一需要创建的对象,其余一切都挂在它上面。**传入一个密钥,你就拿到了上下文、身份、类型化的错误、追踪和批量操作。每个模块调用都经过 Core,没有哪个模块自带一份副本。
何时使用
- 你需要知道自己是谁、身在何处:身份(identity)、角色(role)、已解锁的模块、项目、环境和区域。
- 云函数(cloud function)需要以玩家身份写入。这次写入记在该玩家名下,记录里同时写明函数和玩家。
- 重试不能生效两次:批量(batch)操作带有幂等键(idempotency key)。
- 失败必须能用来分支、能被检索:每次抛出的都是带稳定错误 码的类型化
Problem。 - 无需使用:如果你要处理消息、调用和状态。这些属于原语(primitive):Events、RPC 和 Data & Subscriptions,通过 Entity 寻址,受众由 Groups 定义。上传和下载见 Files & UGC,挂载与命名空间见深入底层。
分工
| 主体 | 在本页中的职责 |
|---|---|
任意主体 | 通过 Whoami 读取身份、上下文和角色 |
backend-service | 以玩家身份操作;把幂等操作合并成批量 |
operator | 查看失败或重试调用的追踪记录 |
概览
Whoami、隐式上下文,以及可以安全重试的批量操作var me = PlayServ.Whoami(); // identity, roles, unlocked modules
var env = PlayServ.Context; // project · env · region
// retries never double-apply: the batch carries an idempotency key
await PlayServ.Batch(key: orderId, b =>
{
b.Inventory.Grant(playerId, "starter.pack");
b.Inventory.Grant(playerId, "starter.emote");
});const me = playserv.whoami(); // identity, roles, unlocked modules
const env = playserv.context; // project · env · region
// retries never double-apply: the batch carries an idempotency key
await playserv.batch(orderId, (b) => {
b.inventory.grant(playerId, 'starter.pack');
b.inventory.grant(playerId, 'starter.emote');
});me = playserv.whoami() # identity, roles, unlocked modules
env = playserv.context # project · env · region
# retries never double-apply: the batch carries an idempotency key
async with playserv.batch(key=order_id) as b:
b.inventory.grant(player_id, "starter.pack")
b.inventory.grant(player_id, "starter.emote")即将推出 —— Go
Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
const FPSActor Me = Client->Whoami(); // identity, roles, unlocked modules
const FPSPlatformContext Env = Client->Context(); // project · env · region
// retries never double-apply: each keyed operation is safe to repeat
Client->Commerce->Entitlements->Grant(FPSIdempotencyKey(PackGrantId), PlayerId, PSKeys::Item::StarterPack);
Client->Commerce->Entitlements->Grant(FPSIdempotencyKey(EmoteGrantId), PlayerId, PSKeys::Item::StarterEmote);
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.var me = PlayServ.Whoami(); // identity, roles, unlocked modules
var env = PlayServ.Context; // project · env · region
// retries never double-apply: the batch carries an idempotency key
await PlayServ.Batch(key: orderId, b =>
{
b.Inventory.Grant(playerId, "starter.pack");
b.Inventory.Grant(playerId, "starter.emote");
});身份保存在 Core 中,所以任何调用都不需要 session 或 ctx 参数。
模型
每个错误都包含什么。
| 字段 | 含义 |
|---|---|
code | 拒绝的机器可读名称。稳定:沿用平台现有的错误码,不另造新码 |
category | 拒绝的类别,据此判断重试是否有意义 |
追踪标识 | 本次发生的 id。始终存在,本地错误也不例外,因此技术支持无需复现问 题 |
explanation | 给人看的文字。不稳定:随时可能修改或本地化 |
逐字段错误 | 用于校验拒绝:每个被拒字段的字段名、错误码和消息 |
按错误码分支,而不是按文字
逻辑里用 code 和 category。explanation 是给人看的,随时会变,不另行通知。只暴露消息文本的绑定就是有 bug。
三种来源。
| 来源 | 发生了什么 |
|---|---|
platform | 平台拒绝,错误码来自平台的目录 |
local | SDK 在发送前就拒绝了,错误码来自 SDK 自己公布的列表 |
unknown | 调用已经发出,但没有收到回复,所以结果未知 |
所有拒绝都满足的几点。
- 被拒绝的操作什么都没有生效,所以错误分支里不需要补偿性读取。例外有两个,出现的地方都会明确说明:结果未知的超时,以及按元素处理的批量操作。
- 无论错误以什么方式到达,它都是同一个:绑定抛出异常、返回结果,还是在订阅(subscription)上回调,错误码、类别和来源都相同。
- 超时属于
unknown来源,不 是一个回复。遇到超时怎么办,由每个操作分别声明。
错误
同一个调用被拒绝。
rate_limited 还会告诉你何时可以重试try { await PlayServ.Inventory.Grant(playerId, "starter.pack"); }
catch (Problem p) when (p.Code == "rate_limited")
{
Hud.RetryAt(p.RetryAfter);
}try { await playserv.inventory.grant(playerId, 'starter.pack'); }
catch (p) {
if (Problem.code(p) === 'rate_limited') hud.retryAt(p.retryAfter);
else throw p;
}try:
await playserv.inventory.grant(player_id, "starter.pack")
except Problem as p:
if p.code == "rate_limited":
hud.retry_at(p.retry_after)
else:
raise即将推出 —— Go
Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
// UE builds run without exceptions — the completion carries the result, read explicitly
Client->Commerce->Entitlements->Grant(FPSIdempotencyKey(GrantId), PlayerId, PSKeys::Item::StarterPack,
TPSOnResult<void>::CreateWeakLambda(this, [this](const TPSResult<void>& Result)
{
if (Result.IsRefused() && Result.Refusal().Code == FPSFailureCode::RateLimited)
{
Hud->RetryAt(Result.Refusal().RetryNotBefore); // TOptional<FDateTime> — an instant, not a delay
}
}));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.try { await PlayServ.Inventory.Grant(playerId, "starter.pack"); }
catch (Problem p) when (p.Code == "rate_limited")
{
Hud.RetryAt(p.RetryAfter);
}**缺少角色的调用方会被直接拒绝,而不是拿到打了折扣的结果。**对于玩家或客户端会话,fn 和 adm 两行都会失败:调用 以玩家身份操作、发出追踪或读取追踪,都会返回错误码为 forbidden 的 Problem。凭据本身有效,只是它的角色不包含这项权限。用同一个幂等键重试也不会有任何变化。
限制
不要重试已被接受的调用
限制在准入时检查。已被接受的调用已经通过了限制,无论等多久都会执行。之后某个调用被拒绝不会影响它,所以重试已被接受的调用会把工作做两遍。
**只有被拒绝时,你才会知道限制的存在。**SDK 不公开当前生效的数值和剩余余量,也不会在接近上限时发出警告,玩家更看不到任何限制。你需要的信息都在拒绝里:
- 类别,说明重试是否有意义
- 这 是谁的限制
- 何时可以重试,以及限制按什么窗口计算