Skip to main content
Services around the game

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_id and 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

ActorOn this page
schema-authormarks entities as player-owned and declares which of them form the profile
playerreads their own profile; writes go to the entities themselves
room-visitorreads 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-side
Coming soon — Go

Attribute-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
Unity C# is the same C# API here — the same attributes compile in Unity, on the 2021.3 baseline — no C# 12 syntax here
[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.

One pass over my own rows, live; then a rival's, as far as the mask allows
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
const 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 leaves
mine = 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
Coming soon — Go

Attribute-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.
Unity C# is the same C# API here — runs as-is in Unity against the generated types
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

The model

ConceptWhat it is
player_idthe platform's whole idea of a player, plus the system profile behind it — Auth
profile setthe player-owned entities the project declares as its profile; declaring it is optional
owner selectionthe read: one owner in, their rows across the set out — the same rights, predicates, filters and subscription as any Entity selection
public readthat selection against another owner, narrowed by the reading role's row predicate and column mask (Access)

What is true of every profile read.

AlwaysWhat it is
there is no profile recordit 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 livespatch the progress row, and every profile read that includes it sees the new value on its next pass
there is no public writea view has nothing to write to, and shared-writable state goes through server code
ownership is a predicatenot 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 hooka 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 — fn or adm. 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.