Not translated yet — showing English.
Profile
A profile is a view, and the platform owns almost none of it. What the platform keeps about a player is the player_id and the system profile behind it — identities, sessions, provider links, all of that in Auth. Everything a player has is your own entity, owned by that player. A profile is the set of those entities your project declares, read for one owner in one pass.
When to use it
- A screen needs one player's slice in one call: the declared set fans out across their owned entities instead of the client stitching several queries together.
- Platform surfaces must show a person, not an identifier: a leaderboard, a moderation queue and a support ticket hold a
player_idand nothing else until the project names the record that displays a player. - Another player needs a card: the same read against another owner, narrowed by the row predicate and column mask already declared in Access.
- A HUD must track owned state live: the read is a selection, and a selection subscribes.
- Skip it when the data is not owned by a player. Shared and global rows are an ordinary Entity selection, with no owner to fan out from.
Who does what
| Actor | On this page |
|---|---|
schema-author | marks entities as player-owned and declares which of them form the profile |
player | reads their own profile; writes go to the entities themselves |
room-visitor | reads another player's profile, as far as that player's predicate and mask allow |
At a glance
Membership is declared per entity, not per field. The entity says it belongs to the profile; what another player may see of it is the column mask on the role that reads it (Access).
loadout and progress marked player-owned and put in the profile set[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-sideAttribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.
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
}The read is an owner-scoped selection: the Entity query surface with the owner fixed and the entity list taken from the declaration. Profile is the name of that read, not a module standing behind it: same rights, same predicates, same filters, same subscription, because it is the same operation.
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 leavesAttribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.
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 leavesThe model
| Concept | What it is |
|---|---|
player_id | the platform's whole idea of a player, plus the system profile behind it — Auth |
| profile set | the player-owned entities the project declares as its profile; declaring it is optional |
| owner selection | the read: one owner in, their rows across the set out — the same rights, predicates, filters and subscription as any Entity selection |
| public read | that selection against another owner, narrowed by the reading role's row predicate and column mask (Access) |
What is true of every profile read.
| Always | What it is |
|---|---|
there is no profile record | it has no identifier of its own, no revision, no history and no lifecycle, because it is a view over rows that have all four |
writes go where the data lives | patch the progress row, and every profile read that includes it sees the new value on its next pass |
there is no public write | a view has nothing to write to, and shared-writable state goes through server code |
ownership is a predicate | not a column the platform adds (Access): owner == caller.player is one instance of the mechanism, "one of the participants" is another |
a derived field belongs to a hook | a post-change observer is what stamps Progress.Title when Level crosses a threshold. It follows the write; it cannot refuse it |
Errors
- A project with no declared profile set has no profile read, and the call is refused as unavailable — not answered with an empty result, which would say the player has a profile and it happens to be blank.
- A row a predicate hides answers
not found, and so does one that does not exist: a public read never becomes a way to learn what exists but is not visible. - A field outside the reading role's mask is absent from the answer, not present and empty.
- A write on a player's behalf that does not name the player is a validation refusal.
- Declaring the profile set is a schema act —
fnoradm. A player key attempting one is refused forbidden, and the push is rejected whole rather than declared in part.
Limits
The numbers arrive with the platform limits chapter; what happens at each ceiling is fixed now.
- The page size of the owner selection: trimmed to the cap with the "there is more" flag still true; returning fewer without the flag is forbidden.
- The size of an included set per row: trimmed by the same rule, with the flag on the inclusion.
- The rate of changes to one instance: a rate-limit refusal with a deadline. The profile read is a selection like any other and inherits entity's ceilings rather than declaring its own.