本页尚未翻译,当前显示英文版本。
Profile
玩家资料是一个视图,其中几乎没有哪一部分归平台所有。 平台为玩家保存的只有 player_id 及其背后的系统资料——身份、会话、提供方关联,这些都在 Auth & Players 中。玩家拥有的一切都是你自己的实体,归该玩家所有。玩家资料就是项目声明的这组实体,针对一个所有者一次读出。
何时使用
- 某个界面需要用一次调用取得一名玩家的全部相关数据:声明的集合会展开到该玩家拥有的各个实体(entity),不必由客 户端拼接多个查询。
- 平台界面必须显示一个人,而不是一个标识符:排行榜、审核队列和客服工单只保存
player_id,直到项目指定用哪条记录来显示玩家。 - 需要为另一名玩家展示名片:对另一个所有者执行同样的读取,由 Access & Roles 中已声明的行谓词(row predicate)和字段掩码(field mask)收窄。
- HUD 必须实时跟踪玩家拥有的状态:这次读取是一个选择集(selection),而选择集可以订阅。
- 无需使用:如果数据不归某个玩家所有。共享行和全局行用普通的 Entity 选择集读取,没有可以展开的所有者。
分工
| 主体 | 在本页中的职责 |
|---|---|
schema-author | 把实体标记为玩家所有,并声明其中哪些构成玩家资料 |
player | 读取自己的玩家资料;写入直接作用于实体本身 |
room-visitor | 在对方的谓词和掩码允许的范围内,读取另一名玩家的资料 |
概览
是否属于玩家资料按实体声明,而不是按字段。实体声明自己属于玩家资料;其他玩家能看到其中哪些内容,由读取它的角色上的字段掩码决定(Access & Roles)。
loadout 和 progress 标记为玩家所有 ,并加入玩家资料集合[Entity("loadout"), OwnedBy(Owner.Player), InProfile]
public class Loadout { public string Primary = ""; }
[Entity("progress"), OwnedBy(Owner.Player), InProfile]
public class Progress
{
public int Level;
public string Title = "";
public int SecretMmr; // no reading role's mask names it: it stays server-side
}@Entity('loadout') @OwnedBy(Owner.player) @InProfile()
export class Loadout { primary = ''; }
@Entity('progress') @OwnedBy(Owner.player) @InProfile()
export class Progress {
level = 0;
title = '';
secretMmr = 0; // no reading role's mask names it: it stays server-side
}@entity("loadout")
@owned_by(Owner.PLAYER)
@in_profile
class Loadout:
primary: str = ""
@entity("progress")
@owned_by(Owner.PLAYER)
@in_profile
class Progress:
level: int = 0
title: str = ""
secret_mmr: int = 0 # no reading role's mask names it: it stays server-side即将推出 —— Go
Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
UCLASS(PSEntity = "loadout", PSOwnedBy = "Player", PSInProfile)
class ULoadout : public UObject
{
GENERATED_BODY()
UPROPERTY() FString Primary;
};
UCLASS(PSEntity = "progress", PSOwnedBy = "Player", PSInProfile)
class UProgress : public UObject
{
GENERATED_BODY()
UPROPERTY() int32 Level;
UPROPERTY() FString Title;
UPROPERTY() int32 SecretMmr; // no reading role's mask names it: it stays server-side
};
// declarations compile into the same pushed model — playserv push from the UE project or CI[Entity("loadout"), OwnedBy(Owner.Player), InProfile]
public class Loadout { public string Primary = ""; }
[Entity("progress"), OwnedBy(Owner.Player), InProfile]
public class Progress
{
public int Level;
public string Title = "";
public int SecretMmr; // no reading role's mask names it: it stays server-side
}这次读取是限定所有者的选择集:即 Entity 的查询接口,所有者固定,实体列表取自声明。Profile 是这次读取的名字,背后并没有一个模块:权限、谓词、过滤条件、订阅全都相同,因为它就是同一个操作。
一次读取我自己的数据行并保持实时;再在掩码允许的范围内读取对手的数据
var mine = playserv.Profile.Mine(); // a selection, not a record
var rows = await mine.Query(); // loadout + progress, one pass
mine.Subscribe(changed => Hud.Refresh(changed)); // the selection stays live
var rival = await playserv.Profile.Of(rivalId).Query(); // only what the mask leavesconst mine = playserv.profile.mine(); // a selection, not a record
const rows = await mine.query(); // loadout + progress, one pass
mine.subscribe((changed) => hud.refresh(changed)); // the selection stays live
const rival = await playserv.profile.of(rivalId).query(); // only what the mask leavesmine = playserv.profile.mine() # a selection, not a record
rows = await mine.query() # loadout + progress, one pass
mine.subscribe(lambda changed: hud.refresh(changed)) # the selection stays live
rival = await playserv.profile.of(rival_id).query() # only what the mask leaves即将推出 —— Go
Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
TPSSelection<UPSProfile> MyProfile = Client->Entities->Of<UPSProfile>()->Select().GetMine(); // a selection, not a record
MyProfile.Then(TPSOnResult<FPSProfileRows>::CreateWeakLambda(this, [this](const TPSResult<FPSProfileRows>& Result)
{
if (!Result.HasValue()) { return; }
Hud->ShowProfile(Result.Value()); // loadout + progress, one pass
}));
TPSSubscription ProfileWatch = MyProfile.Subscribe(
[this](const FPSProfileChange& Changed) { Hud->Refresh(Changed); });
Client->Entities->Of<UPSProfile>()->Get(RivalId,
TPSOnResult<FPSProfileRows>::CreateWeakLambda(this, [this](const TPSResult<FPSProfileRows>& Rival)
{
if (!Rival.HasValue()) { return; }
Hud->ShowRival(Rival.Value());
}));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.var mine = playserv.Profile.Mine(); // a selection, not a record
var rows = await mine.Query(); // loadout + progress, one pass
mine.Subscribe(changed => Hud.Refresh(changed)); // the selection stays live
var rival = await playserv.Profile.Of(rivalId).Query(); // only what the mask leaves模型
| 概 念 | 含义 |
|---|---|
player_id | 平台对玩家的全部认知,外加其背后的系统资料——见 Auth & Players |
| 玩家资料集合 | 项目声明为玩家资料(profile)的那些玩家所有实体;是否声明可选 |
| 所有者选择集 | 即这次读取:输入一个所有者,输出其在集合中的所有数据行——权限、谓词、过滤条件和订阅与任何 Entity 选择集相同 |
| 公开读取 | 对另一个所有者执行该选择集,由读取方角色的行谓词和字段掩码收窄(Access & Roles) |
每次读取玩家资料都成立的规则。
| 始终成立 | 含义 |
|---|---|
不存在玩家资料记录 | 它没有自己的标识符、修订版本、历史和生命周期,因为它是基于数据行的视图,而这四样东西数据行都有 |
写入落在数据所在的位置 | 修改 progress 行后,每个包含它的玩家资料读取都会在下一次读取时看到新值 |
不存在公开写入 | 视图没有可写的对象,共享可写的状态由服务端代码处理 |
所有权是一个谓词 | 而不是平台额外添加的一列(Access & Roles):owner == caller.player 是这一机制的一个实例,“参与者之一”是另一个 |
派生字段归 hook 管 | 由变更后的观察型 hook 在 Level 越过阈值时写入 Progress.Title。它跟在写入之后,无法拒绝写入 |
错误
- 没有声明玩家资料集合的项目不提供玩家资料读取,调用以 unavailable 拒绝——而不是返回空结果,那样等于说玩家有资料、只是恰好为空。
- 谓词隐藏的数据行返回
not found,不存在的数据行也一样:公开读取绝不会成为探知“存在但不可见”数据的途径。 - 读取方角色掩码之外的字段不会出现在结果中,而不是出现但为空。
- 代表玩家写入却未指明玩家,属于校验拒绝。
- 声明玩家资料集合是 schema 操作——只能在
fn或adm中执行。玩家密钥尝试这样做会以 forbidden 拒绝,整个推送都会驳回,而不是只声明其中一部分。
限制
具体数值会随平台限制一章给出;触及每个上限时的行为现在已经确定。
- 所有者选择集的分页 大小:截断到上限,同时“还有更多”标志保持为 true;少返回却不设该标志是禁止的。
- 每行包含集合的大小:按同样的规则截断,标志设在该包含项上。
- 单个实例的变更频率:以限流拒绝,并附带截止时间。玩家资料读取和其他选择集一样,沿用实体的上限,不单独声明。