Files
DorkNet/DorkNet.Server/Data/Entities/PlayerEntity.cs
T
Alexa 6ee1d6f9b3 fix: presence, profile edits, outfit menu and RecNetResult writes for the 2023 client
Every change below was checked against the 2023-03-21 client disassembly;
file:line evidence is in the code comments and in
docs/recroom-2023-client-api-complete.md.

Presence
- matchmake/none means "online, in no room" (its only caller is
  TryGoToOfflineRoom, which ignores the reply). The server used to keep the
  old room or invent a dorm, so the next heartbeat looked out of sync and
  the client cancelled its load to chase that room.
- The join watchdog no longer clears presence 30 s into a long room load;
  it is a 3 minute backstop that drops the room only.
- One fan-out (PresenceBroadcastService) for every transition: deferred
  joins, matchmake/none, logout, failed joins, visibility changes, SignalR
  connect/disconnect. isOnline is sent on its own; pushes never go to the
  subject, because a self push moves the player.
- Online-status visibility (Public / Friends / Favorites / Appear offline)
  is stored and honoured by GET /player and by the live pushes.

Profile
- PUT account/me/{emoji,personalpronouns,identityflags,bannerimage} were
  404; added with the client's real form keys and persisted (new Players
  columns plus migration). Every account read now returns them, and edits
  push AccountUpdate to everyone.

Outfit menu
- Saved outfits kept OutfitSelectionsV2 and CustomAvatarItems, so loading a
  slot restores what was saved instead of a stale outfit.
- isCreationAllowedForAccount answers the {Success, Error} envelope the
  client parses; minPriceForPublicItem is a bare int; gifts/generate accepts
  form bodies; lockeditems returns the full owned-item shape; colourways no
  longer share their base item's AvatarItemId.

RecNetResult writes
- About 40 writes look status-only but parse the body as {Success, Error}.
  An empty 200 showed "Failed to ..." after the server applied the change,
  and a body without Success threw "Error message cannot be null". Clubs,
  announcements, keepsakes, comments/read, subscriptions, purchase offers
  and privacy settings now answer RecNetResult.
- Login token and device-code grants are real; remoteauth approves the
  device code; createlogintoken returns a bare string.
- New: incentivizedreferrals/referrals, account/bulk/{email,phone},
  GET strings, account/me/confirmphone, PlayMenuCarouselOrdering_1.
  Route inventory and coverage test extended to match.

README now describes the 2023 branch.
2026-10-15 14:08:11 +03:00

147 lines
6.6 KiB
C#

using System.ComponentModel.DataAnnotations;
namespace DorkNet.Server.Data.Entities;
public class PlayerEntity
{
public long Id { get; set; }
// Stable per-installation identifier the client sends as `deviceId` in
// platformlogin / account-create form bodies. We deliberately use this
// (not `platformId`) so accounts are NOT keyed to Steam IDs — anyone
// running the client gets a unique persistent account based on the
// hardware-derived device hash, regardless of which Steam emulator
// (Goldberg, none, real Steam) is in front. Indexed unique below.
[MaxLength(128)]
public string DeviceId { get; set; } = string.Empty;
// Last-seen platform info — informational, NOT part of account identity.
public int LastPlatform { get; set; } = 0;
[MaxLength(128)]
public string LastPlatformId { get; set; } = string.Empty;
[MaxLength(64)]
public string Username { get; set; } = string.Empty;
[MaxLength(64)]
public string DisplayName { get; set; } = string.Empty;
[MaxLength(512)]
public string Bio { get; set; } = string.Empty;
public int Level { get; set; } = 1;
public int XP { get; set; } = 0;
public int Reputation { get; set; } = 0;
public bool IsVerified { get; set; } = false;
// New accounts default to non-developer. Admins grant the badge
// privilege via the SPA's Players → Profile flags tab on a
// per-player basis (used to default to true while we were
// bootstrapping the emulator; flipped off now that the build is
// stable and we don't want every walk-in to spawn with the dev
// console unlocked).
public bool IsDeveloper { get; set; } = false;
// Mirrors IsDeveloper for the overhead badge slider — the 2020 watch
// gates the in-settings "Developer Display Mode" slider on a single
// role check (`role/developer/{id}`), and the slider's positions
// render as "Community Team" / "Developer" above the player's head
// (verified in Cpp2IL_ISIL/.../PlayerUI.txt:9085-9099 — slider value
// 1 → "Community Team", 2 → "Developer"). Keep this field separate
// from IsDeveloper so admins can mark someone as community-team
// without granting full dev privileges; the role endpoint ORs both.
public bool IsCommunityTeam { get; set; } = false;
public bool CanReceiveInvites { get; set; } = true;
public bool IsJunior { get; set; } = false;
[MaxLength(256)]
public string? ProfileImageName { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime LastSeenAt { get; set; } = DateTime.UtcNow;
// ── Phase 1 account hardening ─────────────────────────────────────────
// Optional contact email. Nullable because the 2020 client never asks
// for one; reserved for future password-reset / admin-recovery flows.
[MaxLength(256)]
public string? Email { get; set; }
// When non-null AND `> DateTime.UtcNow`, BanCheckMiddleware short-circuits
// every authenticated request from this account with 401. Setting to a
// past timestamp acts as a "previously banned" audit marker without
// blocking access.
public DateTime? BannedUntil { get; set; }
// Privilege flag for `[AdminOnly]` endpoints (Phase 5). Defaults false;
// PlayerService bumps this to true when the very first account on a
// fresh DB is created so there's always at least one root admin.
public bool IsAdmin { get; set; } = false;
// Last IP that authenticated against this account. Updated by the
// request-tracing pipeline so we don't write IPs from anonymous paths.
[MaxLength(64)]
public string? LastIp { get; set; }
// The deviceId that originally created this account. Immutable after
// creation — exists for the audit trail in case the live DeviceId
// rotates (e.g., user re-installs and the hardware hash shifts; the
// account stays bound to whatever they typed in /account/create the
// first time).
[MaxLength(128)]
public string? CreatedFromDeviceId { get; set; }
// BCP-47 locale tag (e.g. "en-US"). Optional; informs future
// localisation work and is currently set from the client's accept-
// language header at login.
[MaxLength(16)]
public string? Locale { get; set; }
/// <summary>BCrypt hash of the account password. Null when the
/// account has never set one — the in-game settings screen calls
/// /account/me/haspassword and prompts the user to create one
/// before sensitive operations. Format: BCrypt's standard
/// `$2a$&lt;cost&gt;$&lt;salt&gt;&lt;hash&gt;` so the verifier
/// reads cost + salt directly, no separate column needed.</summary>
[MaxLength(128)]
public string? PasswordHash { get; set; }
/// <summary>Player's birthday (date only; no timezone). Set during
/// signup and used to derive <see cref="IsJunior"/> on Junior cutoff
/// boundary crossing. Nullable for legacy accounts created before
/// the field existed.</summary>
public DateTime? Birthday { get; set; }
/// <summary>Optional phone number — used by the watch's
/// account-recovery flow. Stored verbatim, never validated.</summary>
[MaxLength(32)]
public string? Phone { get; set; }
// ── 2023 profile fields ──────────────────────────────────────────────
// Written by PUT account/me/{emoji,bannerimage,personalpronouns,
// identityflags} and read back on every account DTO (account/me,
// account/{id}, account/bulk, search, AccountUpdate push) under the
// keys displayEmoji / bannerImage / personalPronouns / identityFlags.
/// <summary>Emoji shown beside the display name ("" = none).</summary>
[MaxLength(32)]
public string DisplayEmoji { get; set; } = string.Empty;
/// <summary>Profile banner image name (a storage image the player
/// already uploaded, e.g. one of their photos). Null = no banner.</summary>
[MaxLength(256)]
public string? BannerImageName { get; set; }
/// <summary>Pronoun bitmask (client flags enum: SheHer=1, HeHim=2,
/// TheyThem=4, ZeHir=8, ZeZir=16, XeXem=32).</summary>
public int PersonalPronouns { get; set; } = 0;
/// <summary>Identity-flag bitmask (client flags enum: LGBTQIA=1,
/// Transgender=2, Bisexual=4, …).</summary>
public int IdentityFlags { get; set; } = 0;
// Navigation
public AvatarEntity? Avatar { get; set; }
public List<PlayerSettingEntity> Settings { get; set; } = [];
public List<RelationshipEntity> Relationships { get; set; } = [];
}