Conversation
- keep packet handlers orchestration-only with grain factory extensions\n- keep PlayerDirectoryGrain focused on username/id lookup + case-insensitive reverse cache\n- map snapshot guilds into outgoing ExtendedProfile composer payload\n- align incoming messages with required properties and tidy interface formatting\n\nValidated with successful builds of Turbo.Primitives, Turbo.Players, and Turbo.PacketHandlers.
There was a problem hiding this comment.
Pull request overview
Adds end-to-end support for requesting and composing an “extended profile” response, including new Orleans snapshot types, player/directory grain APIs, and packet handlers for lookup by id or username.
Changes:
- Introduces
PlayerExtendedProfileSnapshotand exposes it viaIPlayerGrain.GetExtendedProfileSnapshotAsync. - Extends
IPlayerDirectoryGrain/PlayerDirectoryGrainwithGetPlayerIdAsync(userName)and a reverse name→id cache. - Implements extended profile request handlers (by id and by name) that fetch snapshots and send
ExtendedProfileMessageComposer.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| Turbo.Primitives/Players/IPlayerGrain.cs | Adds extended profile snapshot API to player grain contract. |
| Turbo.Primitives/Players/Grains/IPlayerDirectoryGrain.cs | Adds username→playerId lookup API to directory grain contract. |
| Turbo.Primitives/Orleans/Snapshots/Players/PlayerExtendedProfileSnapshot.cs | New Orleans-serializable snapshot type for extended profile data. |
| Turbo.Primitives/Messages/Outgoing/Users/ExtendedProfileMessageComposer.cs | Defines the outgoing extended profile composer payload (incl. guild list). |
| Turbo.Primitives/Messages/Outgoing/Users/ExtendedProfileChangedMessageComposer.cs | Adds UserId field to the “profile changed” composer. |
| Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileMessage.cs | Adds required UserId field for extended profile request-by-id. |
| Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileByNameMessage.cs | Adds required UserName field for extended profile request-by-name. |
| Turbo.Players/Grains/PlayerGrain.cs | Implements GetExtendedProfileSnapshotAsync using player state. |
| Turbo.Players/Grains/PlayerDirectoryGrain.cs | Adds reverse cache + DB query to resolve PlayerId from username. |
| Turbo.PacketHandlers/Users/GetExtendedProfileMessageHandler.cs | Fetches snapshot by user id and sends ExtendedProfileMessageComposer. |
| Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs | Resolves user id by name, fetches snapshot, and sends composer. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var loweredUserName = normalizedUserName.ToLowerInvariant(); | ||
| var player = await dbCtx | ||
| .Players.AsNoTracking() | ||
| .Where(x => x.Name != null && x.Name.ToLower() == loweredUserName) |
There was a problem hiding this comment.
The DB lookup uses x.Name.ToLower() == loweredUserName, which forces a LOWER(name) expression and can prevent the players.name index from being used (and also mixes MySQL lowercasing rules with .ToLowerInvariant() on the input). Prefer relying on the column collation for case-insensitive equality, or use an indexed normalized column / explicit collation via EF so the query stays sargable.
| var loweredUserName = normalizedUserName.ToLowerInvariant(); | |
| var player = await dbCtx | |
| .Players.AsNoTracking() | |
| .Where(x => x.Name != null && x.Name.ToLower() == loweredUserName) | |
| var player = await dbCtx | |
| .Players.AsNoTracking() | |
| .Where(x => x.Name != null && x.Name == normalizedUserName) |
| var response = new ExtendedProfileMessageComposer | ||
| { | ||
| UserId = (int)snapshot.UserId, | ||
| UserName = snapshot.UserName, | ||
| Figure = snapshot.Figure, | ||
| Motto = snapshot.Motto, | ||
| CreationDate = snapshot.CreationDate, | ||
| AchievementScore = snapshot.AchievementScore, | ||
| FriendCount = snapshot.FriendCount, | ||
| IsFriend = snapshot.IsFriend, | ||
| IsFriendRequestSent = snapshot.IsFriendRequestSent, | ||
| IsOnline = snapshot.IsOnline, | ||
| Guilds = snapshot | ||
| .Guilds.Select(x => new GuildInfo | ||
| { | ||
| GroupId = x.GroupId, | ||
| GroupName = x.GroupName, | ||
| BadgeCode = x.BadgeCode, | ||
| PrimaryColor = x.PrimaryColor, | ||
| SecondaryColor = x.SecondaryColor, | ||
| Favourite = x.Favourite, | ||
| OwnerId = x.OwnerId, | ||
| HasForum = x.HasForum | ||
| }) | ||
| .ToList(), | ||
| LastAccessSinceInSeconds = snapshot.LastAccessSinceInSeconds, | ||
| OpenProfileWindow = snapshot.OpenProfileWindow, | ||
| IsHidden = snapshot.IsHidden, | ||
| AccountLevel = snapshot.AccountLevel, | ||
| IntegerField24 = snapshot.IntegerField24, | ||
| StarGemCount = snapshot.StarGemCount, | ||
| BooleanField26 = snapshot.BooleanField26, | ||
| BooleanField27 = snapshot.BooleanField27 | ||
| }; |
There was a problem hiding this comment.
This handler (and GetExtendedProfileByNameMessageHandler) duplicate the same snapshot->composer mapping logic. Consider extracting a shared mapper/helper (e.g., a private static method or extension) to avoid divergence when fields change and to keep the handlers focused on orchestration.
| var response = new ExtendedProfileMessageComposer | ||
| { | ||
| UserId = (int)snapshot.UserId, | ||
| UserName = snapshot.UserName, | ||
| Figure = snapshot.Figure, | ||
| Motto = snapshot.Motto, | ||
| CreationDate = snapshot.CreationDate, | ||
| AchievementScore = snapshot.AchievementScore, | ||
| FriendCount = snapshot.FriendCount, | ||
| IsFriend = snapshot.IsFriend, | ||
| IsFriendRequestSent = snapshot.IsFriendRequestSent, | ||
| IsOnline = snapshot.IsOnline, | ||
| Guilds = snapshot | ||
| .Guilds.Select(x => new GuildInfo | ||
| { | ||
| GroupId = x.GroupId, | ||
| GroupName = x.GroupName, | ||
| BadgeCode = x.BadgeCode, | ||
| PrimaryColor = x.PrimaryColor, | ||
| SecondaryColor = x.SecondaryColor, | ||
| Favourite = x.Favourite, | ||
| OwnerId = x.OwnerId, | ||
| HasForum = x.HasForum | ||
| }) | ||
| .ToList(), | ||
| LastAccessSinceInSeconds = snapshot.LastAccessSinceInSeconds, | ||
| OpenProfileWindow = snapshot.OpenProfileWindow, | ||
| IsHidden = snapshot.IsHidden, | ||
| AccountLevel = snapshot.AccountLevel, | ||
| IntegerField24 = snapshot.IntegerField24, | ||
| StarGemCount = snapshot.StarGemCount, | ||
| BooleanField26 = snapshot.BooleanField26, | ||
| BooleanField27 = snapshot.BooleanField27 | ||
| }; |
There was a problem hiding this comment.
This handler duplicates the same snapshot->composer mapping logic as GetExtendedProfileMessageHandler. Consider factoring the mapping into a shared helper to reduce maintenance overhead and prevent the two responses from drifting when fields are added/renamed.
| [Id(15)] | ||
| public required int IntegerField24 { get; init; } | ||
| [Id(16)] | ||
| public required int StarGemCount { get; init; } | ||
| [Id(17)] | ||
| public required bool BooleanField26 { get; init; } | ||
| [Id(18)] | ||
| public required bool BooleanField27 { get; init; } |
There was a problem hiding this comment.
IntegerField24 / BooleanField26 / BooleanField27 are placeholder-style names in a public composer contract, which makes the message hard to understand and maintain. If their meaning is known, rename them to domain-specific terms; if not, consider adding clarifying comments or using a clearly-marked Unknown* naming scheme tied to the client/protocol field index.
| [Id(15)] | |
| public required int IntegerField24 { get; init; } | |
| [Id(16)] | |
| public required int StarGemCount { get; init; } | |
| [Id(17)] | |
| public required bool BooleanField26 { get; init; } | |
| [Id(18)] | |
| public required bool BooleanField27 { get; init; } | |
| /// <summary> | |
| /// Unknown profile-related integer mapped from client/protocol field index 24. | |
| /// TODO: Rename to a domain-specific name once the field's meaning is known. | |
| /// </summary> | |
| [Id(15)] | |
| public required int UnknownProfileIntegerField24 { get; init; } | |
| [Id(16)] | |
| public required int StarGemCount { get; init; } | |
| /// <summary> | |
| /// Unknown profile-related boolean mapped from client/protocol field index 26. | |
| /// TODO: Rename to a domain-specific name once the field's meaning is known. | |
| /// </summary> | |
| [Id(17)] | |
| public required bool UnknownProfileBooleanField26 { get; init; } | |
| /// <summary> | |
| /// Unknown profile-related boolean mapped from client/protocol field index 27. | |
| /// TODO: Rename to a domain-specific name once the field's meaning is known. | |
| /// </summary> | |
| [Id(18)] | |
| public required bool UnknownProfileBooleanField27 { get; init; } |
| [Id(15)] | ||
| public required int IntegerField24 { get; init; } | ||
|
|
||
| [Id(16)] | ||
| public required int StarGemCount { get; init; } | ||
|
|
||
| [Id(17)] | ||
| public required bool BooleanField26 { get; init; } | ||
|
|
||
| [Id(18)] | ||
| public required bool BooleanField27 { get; init; } |
There was a problem hiding this comment.
IntegerField24 / BooleanField26 / BooleanField27 are non-descriptive names in this snapshot type. If these are protocol-driven placeholders, consider documenting their meaning/source (or renaming to Unknown* with a reference) to avoid spreading ambiguous fields through the codebase.
| [Id(15)] | |
| public required int IntegerField24 { get; init; } | |
| [Id(16)] | |
| public required int StarGemCount { get; init; } | |
| [Id(17)] | |
| public required bool BooleanField26 { get; init; } | |
| [Id(18)] | |
| public required bool BooleanField27 { get; init; } | |
| /// <summary> | |
| /// Protocol-driven integer field mapped from the upstream extended profile payload. | |
| /// The exact semantic meaning is currently unknown; the <see cref="IdAttribute"/> value must not be changed. | |
| /// </summary> | |
| [Id(15)] | |
| public required int UnknownIntegerField24 { get; init; } | |
| [Id(16)] | |
| public required int StarGemCount { get; init; } | |
| /// <summary> | |
| /// Protocol-driven boolean field mapped from the upstream extended profile payload. | |
| /// The exact semantic meaning is currently unknown; the <see cref="IdAttribute"/> value must not be changed. | |
| /// </summary> | |
| [Id(17)] | |
| public required bool UnknownBooleanField26 { get; init; } | |
| /// <summary> | |
| /// Protocol-driven boolean field mapped from the upstream extended profile payload. | |
| /// The exact semantic meaning is currently unknown; the <see cref="IdAttribute"/> value must not be changed. | |
| /// </summary> | |
| [Id(18)] | |
| public required bool UnknownBooleanField27 { get; init; } |
|
Closed. Cherry-Picked into #2 |
No description provided.