From 12a4ce334aee0f367afaf85a5e1c5562d571578c Mon Sep 17 00:00:00 2001 From: Dippys Date: Thu, 5 Feb 2026 19:56:59 +0400 Subject: [PATCH 1/5] Extended Profile Start --- .../GetExtendedProfileByNameMessageHandler.cs | 63 ++++++++++++- .../Users/GetExtendedProfileMessageHandler.cs | 64 ++++++++++++- .../Users/GetExtendedProfileByNameMessage.cs | 5 +- .../Users/GetExtendedProfileMessage.cs | 6 +- .../ExtendedProfileChangedMessageComposer.cs | 3 +- .../Users/ExtendedProfileMessageComposer.cs | 61 +++++++++++- .../Players/PlayerExtendedProfileSnapshot.cs | 94 +++++++++++++++++++ 7 files changed, 290 insertions(+), 6 deletions(-) create mode 100644 Turbo.Primitives/Orleans/Snapshots/Players/PlayerExtendedProfileSnapshot.cs diff --git a/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs b/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs index faa0ba41..f07a9cba 100644 --- a/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs +++ b/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs @@ -1,19 +1,80 @@ +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Orleans; +using Turbo.Database.Context; using Turbo.Messages.Registry; using Turbo.Primitives.Messages.Incoming.Users; +using Turbo.Primitives.Messages.Outgoing.Users; namespace Turbo.PacketHandlers.Users; public class GetExtendedProfileByNameMessageHandler : IMessageHandler { + private readonly IGrainFactory _grainFactory; + private readonly IDbContextFactory _dbContextFactory; + + public GetExtendedProfileByNameMessageHandler( + IGrainFactory grainFactory, + IDbContextFactory dbContextFactory + ) + { + _grainFactory = grainFactory; + _dbContextFactory = dbContextFactory; + } + public async ValueTask HandleAsync( GetExtendedProfileByNameMessage message, MessageContext ctx, CancellationToken ct ) { - await ValueTask.CompletedTask.ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(message.UserName)) + return; + + try + { + // Get player data from database by username + await using var dbCtx = await _dbContextFactory.CreateDbContextAsync(ct); + + var player = await dbCtx.Players + .AsNoTracking() + .Include(p => p.PlayerCurrencies) + .FirstOrDefaultAsync(p => p.Name == message.UserName, ct); + + if (player == null) + return; + + var response = new ExtendedProfileMessageComposer + { + UserId = player.Id, + UserName = player.Name ?? "Unknown", + Figure = player.Figure ?? "hr-115-42.hd-195-19.ch-3030-82.lg-275-1408.fa-1201.ca-1804-64", + Motto = player.Motto ?? "", + CreationDate = player.CreatedAt.ToString("yyyy-MM-dd"), + AchievementScore = 0, + FriendCount = 0, // TODO: Query from messenger_friends when available + IsFriend = false, // TODO: Check friendship when messenger_friends is available + IsFriendRequestSent = false, // TODO: Check messenger_requests when available + IsOnline = true, // TODO: Check if player has active room or session + Guilds = new List(), // TODO: Fetch guilds from database when guild system is implemented + LastAccessSinceInSeconds = 0, + OpenProfileWindow = true, + IsHidden = false, + AccountLevel = 1, // TODO: Get from database when account level is implemented + IntegerField24 = 0, + StarGemCount = 0, + BooleanField26 = false, + BooleanField27 = false + }; + + await ctx.SendComposerAsync(response, ct).ConfigureAwait(false); + } + catch + { + // TODO: Log error + } } } diff --git a/Turbo.PacketHandlers/Users/GetExtendedProfileMessageHandler.cs b/Turbo.PacketHandlers/Users/GetExtendedProfileMessageHandler.cs index 4567bdcf..c1e0a256 100644 --- a/Turbo.PacketHandlers/Users/GetExtendedProfileMessageHandler.cs +++ b/Turbo.PacketHandlers/Users/GetExtendedProfileMessageHandler.cs @@ -1,18 +1,80 @@ +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Orleans; +using Turbo.Database.Context; using Turbo.Messages.Registry; using Turbo.Primitives.Messages.Incoming.Users; +using Turbo.Primitives.Messages.Outgoing.Users; namespace Turbo.PacketHandlers.Users; public class GetExtendedProfileMessageHandler : IMessageHandler { + private readonly IGrainFactory _grainFactory; + private readonly IDbContextFactory _dbContextFactory; + + public GetExtendedProfileMessageHandler( + IGrainFactory grainFactory, + IDbContextFactory dbContextFactory + ) + { + _grainFactory = grainFactory; + _dbContextFactory = dbContextFactory; + } + public async ValueTask HandleAsync( GetExtendedProfileMessage message, MessageContext ctx, CancellationToken ct ) { - await ValueTask.CompletedTask.ConfigureAwait(false); + var targetUserId = message.UserId; + if (targetUserId <= 0) + return; + + try + { + // Get player data from database + await using var dbCtx = await _dbContextFactory.CreateDbContextAsync(ct); + + var player = await dbCtx.Players + .AsNoTracking() + .Include(p => p.PlayerCurrencies) + .FirstOrDefaultAsync(p => p.Id == (int)targetUserId, ct); + + if (player == null) + return; + + var response = new ExtendedProfileMessageComposer + { + UserId = player.Id, + UserName = player.Name ?? "Unknown", + Figure = player.Figure ?? "hr-115-42.hd-195-19.ch-3030-82.lg-275-1408.fa-1201.ca-1804-64", + Motto = player.Motto ?? "", + CreationDate = player.CreatedAt.ToString("yyyy-MM-dd"), + AchievementScore = 0, + FriendCount = 0, // TODO: Query from messenger_friends when available + IsFriend = false, // TODO: Check friendship when messenger_friends is available + IsFriendRequestSent = false, // TODO: Check messenger_requests when available + IsOnline = true, // TODO: Check if player has active room or session + Guilds = new List(), // TODO: Fetch guilds from database when guild system is implemented + LastAccessSinceInSeconds = 0, + OpenProfileWindow = true, + IsHidden = false, + AccountLevel = 1, // TODO: Get from database when account level is implemented + IntegerField24 = 0, + StarGemCount = 0, + BooleanField26 = false, + BooleanField27 = false + }; + + await ctx.SendComposerAsync(response, ct).ConfigureAwait(false); + } + catch + { + // TODO: Log error + } } } diff --git a/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileByNameMessage.cs b/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileByNameMessage.cs index dba00f00..88395f58 100644 --- a/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileByNameMessage.cs +++ b/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileByNameMessage.cs @@ -2,4 +2,7 @@ namespace Turbo.Primitives.Messages.Incoming.Users; -public record GetExtendedProfileByNameMessage : IMessageEvent { } +public record GetExtendedProfileByNameMessage : IMessageEvent +{ + public string UserName { get; init; } = string.Empty; +} diff --git a/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileMessage.cs b/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileMessage.cs index e3dad237..b1ca09e5 100644 --- a/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileMessage.cs +++ b/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileMessage.cs @@ -1,5 +1,9 @@ using Turbo.Primitives.Networking; +using Turbo.Primitives.Players; namespace Turbo.Primitives.Messages.Incoming.Users; -public record GetExtendedProfileMessage : IMessageEvent { } +public record GetExtendedProfileMessage : IMessageEvent +{ + public PlayerId UserId { get; init; } +} diff --git a/Turbo.Primitives/Messages/Outgoing/Users/ExtendedProfileChangedMessageComposer.cs b/Turbo.Primitives/Messages/Outgoing/Users/ExtendedProfileChangedMessageComposer.cs index ae71448e..203b1092 100644 --- a/Turbo.Primitives/Messages/Outgoing/Users/ExtendedProfileChangedMessageComposer.cs +++ b/Turbo.Primitives/Messages/Outgoing/Users/ExtendedProfileChangedMessageComposer.cs @@ -6,5 +6,6 @@ namespace Turbo.Primitives.Messages.Outgoing.Users; [GenerateSerializer, Immutable] public sealed record ExtendedProfileChangedMessageComposer : IComposer { - // TODO: add properties if/when identified + [Id(0)] + public required int UserId { get; init; } } diff --git a/Turbo.Primitives/Messages/Outgoing/Users/ExtendedProfileMessageComposer.cs b/Turbo.Primitives/Messages/Outgoing/Users/ExtendedProfileMessageComposer.cs index d5085374..0a9f9ae1 100644 --- a/Turbo.Primitives/Messages/Outgoing/Users/ExtendedProfileMessageComposer.cs +++ b/Turbo.Primitives/Messages/Outgoing/Users/ExtendedProfileMessageComposer.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Orleans; using Turbo.Primitives.Networking; @@ -6,5 +7,63 @@ namespace Turbo.Primitives.Messages.Outgoing.Users; [GenerateSerializer, Immutable] public sealed record ExtendedProfileMessageComposer : IComposer { - // TODO: add properties if/when identified + [Id(0)] + public required int UserId { get; init; } + [Id(1)] + public required string UserName { get; init; } + [Id(2)] + public required string Figure { get; init; } + [Id(3)] + public required string Motto { get; init; } + [Id(4)] + public required string CreationDate { get; init; } + [Id(5)] + public required int AchievementScore { get; init; } + [Id(6)] + public required int FriendCount { get; init; } + [Id(7)] + public required bool IsFriend { get; init; } + [Id(8)] + public required bool IsFriendRequestSent { get; init; } + [Id(9)] + public required bool IsOnline { get; init; } + [Id(10)] + public required List Guilds { get; init; } + [Id(11)] + public required int LastAccessSinceInSeconds { get; init; } + [Id(12)] + public required bool OpenProfileWindow { get; init; } + [Id(13)] + public required bool IsHidden { get; init; } + [Id(14)] + public required int AccountLevel { 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; } +} + +[GenerateSerializer, Immutable] +public sealed record GuildInfo +{ + [Id(0)] + public required int GroupId { get; init; } + [Id(1)] + public required string GroupName { get; init; } + [Id(2)] + public required string BadgeCode { get; init; } + [Id(3)] + public required string PrimaryColor { get; init; } + [Id(4)] + public required string SecondaryColor { get; init; } + [Id(5)] + public required bool Favourite { get; init; } + [Id(6)] + public required int OwnerId { get; init; } + [Id(7)] + public required bool HasForum { get; init; } } diff --git a/Turbo.Primitives/Orleans/Snapshots/Players/PlayerExtendedProfileSnapshot.cs b/Turbo.Primitives/Orleans/Snapshots/Players/PlayerExtendedProfileSnapshot.cs new file mode 100644 index 00000000..c90bf26b --- /dev/null +++ b/Turbo.Primitives/Orleans/Snapshots/Players/PlayerExtendedProfileSnapshot.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using Orleans; +using Turbo.Primitives.Players; + +namespace Turbo.Primitives.Orleans.Snapshots.Players; + +[GenerateSerializer, Immutable] +public sealed record PlayerExtendedProfileSnapshot +{ + [Id(0)] + public required PlayerId UserId { get; init; } + + [Id(1)] + public required string UserName { get; init; } + + [Id(2)] + public required string Figure { get; init; } + + [Id(3)] + public required string Motto { get; init; } + + [Id(4)] + public required string CreationDate { get; init; } + + [Id(5)] + public required int AchievementScore { get; init; } + + [Id(6)] + public required int FriendCount { get; init; } + + [Id(7)] + public required bool IsFriend { get; init; } + + [Id(8)] + public required bool IsFriendRequestSent { get; init; } + + [Id(9)] + public required bool IsOnline { get; init; } + + [Id(10)] + public required List Guilds { get; init; } + + [Id(11)] + public required int LastAccessSinceInSeconds { get; init; } + + [Id(12)] + public required bool OpenProfileWindow { get; init; } + + [Id(13)] + public required bool IsHidden { get; init; } + + [Id(14)] + public required int AccountLevel { 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; } + + [GenerateSerializer, Immutable] + public sealed record GuildInfo + { + [Id(0)] + public required int GroupId { get; init; } + + [Id(1)] + public required string GroupName { get; init; } + + [Id(2)] + public required string BadgeCode { get; init; } + + [Id(3)] + public required string PrimaryColor { get; init; } + + [Id(4)] + public required string SecondaryColor { get; init; } + + [Id(5)] + public required bool Favourite { get; init; } + + [Id(6)] + public required int OwnerId { get; init; } + + [Id(7)] + public required bool HasForum { get; init; } + } +} From ee294e65848dcb62b1bb7489bb73ed557dea7b26 Mon Sep 17 00:00:00 2001 From: Dippys Date: Thu, 5 Feb 2026 22:13:37 +0400 Subject: [PATCH 2/5] Use Grains --- .../Users/GetExtendedProfileMessageHandler.cs | 61 ++++++++----------- Turbo.Players/Grains/PlayerGrain.cs | 29 +++++++++ Turbo.Primitives/Players/IPlayerGrain.cs | 2 + 3 files changed, 55 insertions(+), 37 deletions(-) diff --git a/Turbo.PacketHandlers/Users/GetExtendedProfileMessageHandler.cs b/Turbo.PacketHandlers/Users/GetExtendedProfileMessageHandler.cs index c1e0a256..971bad2d 100644 --- a/Turbo.PacketHandlers/Users/GetExtendedProfileMessageHandler.cs +++ b/Turbo.PacketHandlers/Users/GetExtendedProfileMessageHandler.cs @@ -1,27 +1,21 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using Orleans; -using Turbo.Database.Context; using Turbo.Messages.Registry; using Turbo.Primitives.Messages.Incoming.Users; using Turbo.Primitives.Messages.Outgoing.Users; +using Turbo.Primitives.Players; namespace Turbo.PacketHandlers.Users; public class GetExtendedProfileMessageHandler : IMessageHandler { private readonly IGrainFactory _grainFactory; - private readonly IDbContextFactory _dbContextFactory; - public GetExtendedProfileMessageHandler( - IGrainFactory grainFactory, - IDbContextFactory dbContextFactory - ) + public GetExtendedProfileMessageHandler(IGrainFactory grainFactory) { _grainFactory = grainFactory; - _dbContextFactory = dbContextFactory; } public async ValueTask HandleAsync( @@ -36,38 +30,31 @@ CancellationToken ct try { - // Get player data from database - await using var dbCtx = await _dbContextFactory.CreateDbContextAsync(ct); - - var player = await dbCtx.Players - .AsNoTracking() - .Include(p => p.PlayerCurrencies) - .FirstOrDefaultAsync(p => p.Id == (int)targetUserId, ct); - - if (player == null) - return; + // Get player data from the grain + var grain = _grainFactory.GetGrain(targetUserId); + var snapshot = await grain.GetExtendedProfileSnapshotAsync(ct).ConfigureAwait(false); var response = new ExtendedProfileMessageComposer { - UserId = player.Id, - UserName = player.Name ?? "Unknown", - Figure = player.Figure ?? "hr-115-42.hd-195-19.ch-3030-82.lg-275-1408.fa-1201.ca-1804-64", - Motto = player.Motto ?? "", - CreationDate = player.CreatedAt.ToString("yyyy-MM-dd"), - AchievementScore = 0, - FriendCount = 0, // TODO: Query from messenger_friends when available - IsFriend = false, // TODO: Check friendship when messenger_friends is available - IsFriendRequestSent = false, // TODO: Check messenger_requests when available - IsOnline = true, // TODO: Check if player has active room or session - Guilds = new List(), // TODO: Fetch guilds from database when guild system is implemented - LastAccessSinceInSeconds = 0, - OpenProfileWindow = true, - IsHidden = false, - AccountLevel = 1, // TODO: Get from database when account level is implemented - IntegerField24 = 0, - StarGemCount = 0, - BooleanField26 = false, - BooleanField27 = false + 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 = new List(), // TODO: Convert snapshot.Guilds when guild system is ready + LastAccessSinceInSeconds = snapshot.LastAccessSinceInSeconds, + OpenProfileWindow = snapshot.OpenProfileWindow, + IsHidden = snapshot.IsHidden, + AccountLevel = snapshot.AccountLevel, + IntegerField24 = snapshot.IntegerField24, + StarGemCount = snapshot.StarGemCount, + BooleanField26 = snapshot.BooleanField26, + BooleanField27 = snapshot.BooleanField27 }; await ctx.SendComposerAsync(response, ct).ConfigureAwait(false); diff --git a/Turbo.Players/Grains/PlayerGrain.cs b/Turbo.Players/Grains/PlayerGrain.cs index 8b5379a5..34817f19 100644 --- a/Turbo.Players/Grains/PlayerGrain.cs +++ b/Turbo.Players/Grains/PlayerGrain.cs @@ -93,4 +93,33 @@ public Task GetSummaryAsync(CancellationToken ct) => CreatedAt = state.State.CreatedAt, } ); + + public Task GetExtendedProfileSnapshotAsync(CancellationToken ct) + { + var s = state.State; + return Task.FromResult( + new PlayerExtendedProfileSnapshot + { + UserId = (PlayerId)this.GetPrimaryKeyLong(), + UserName = s.Name, + Figure = s.Figure, + Motto = s.Motto, + CreationDate = s.CreatedAt.ToString("yyyy-MM-dd"), + AchievementScore = 0, + FriendCount = 0, + IsFriend = false, + IsFriendRequestSent = false, + IsOnline = true, + Guilds = new(), + LastAccessSinceInSeconds = 0, + OpenProfileWindow = true, + IsHidden = false, + AccountLevel = 1, + IntegerField24 = 0, + StarGemCount = 0, + BooleanField26 = false, + BooleanField27 = false + } + ); + } } diff --git a/Turbo.Primitives/Players/IPlayerGrain.cs b/Turbo.Primitives/Players/IPlayerGrain.cs index 918f7743..3328d65e 100644 --- a/Turbo.Primitives/Players/IPlayerGrain.cs +++ b/Turbo.Primitives/Players/IPlayerGrain.cs @@ -8,4 +8,6 @@ namespace Turbo.Primitives.Players; public interface IPlayerGrain : IGrainWithIntegerKey { public Task GetSummaryAsync(CancellationToken ct); + + public Task GetExtendedProfileSnapshotAsync(CancellationToken ct); } From ca02825337dd2ccd9a9984aa6220bad29d68c4f9 Mon Sep 17 00:00:00 2001 From: Dippys Date: Thu, 5 Feb 2026 22:23:21 +0400 Subject: [PATCH 3/5] grains impliment search via username. --- .../GetExtendedProfileByNameMessageHandler.cs | 68 +++++++++---------- Turbo.Players/Grains/PlayerDirectoryGrain.cs | 30 +++++++- .../Players/Grains/IPlayerDirectoryGrain.cs | 1 + 3 files changed, 63 insertions(+), 36 deletions(-) diff --git a/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs b/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs index f07a9cba..2247a1ff 100644 --- a/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs +++ b/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using Orleans; -using Turbo.Database.Context; using Turbo.Messages.Registry; using Turbo.Primitives.Messages.Incoming.Users; using Turbo.Primitives.Messages.Outgoing.Users; +using Turbo.Primitives.Players; +using Turbo.Primitives.Players.Grains; namespace Turbo.PacketHandlers.Users; @@ -14,15 +14,10 @@ public class GetExtendedProfileByNameMessageHandler : IMessageHandler { private readonly IGrainFactory _grainFactory; - private readonly IDbContextFactory _dbContextFactory; - public GetExtendedProfileByNameMessageHandler( - IGrainFactory grainFactory, - IDbContextFactory dbContextFactory - ) + public GetExtendedProfileByNameMessageHandler(IGrainFactory grainFactory) { _grainFactory = grainFactory; - _dbContextFactory = dbContextFactory; } public async ValueTask HandleAsync( @@ -36,38 +31,40 @@ CancellationToken ct try { - // Get player data from database by username - await using var dbCtx = await _dbContextFactory.CreateDbContextAsync(ct); - - var player = await dbCtx.Players - .AsNoTracking() - .Include(p => p.PlayerCurrencies) - .FirstOrDefaultAsync(p => p.Name == message.UserName, ct); + // Resolve username to player ID using the directory grain + var directoryGrain = _grainFactory.GetGrain( + "default" // PlayerDirectoryGrain uses a static key + ); + var playerId = await directoryGrain.GetPlayerIdAsync(message.UserName, ct).ConfigureAwait(false); - if (player == null) + if (playerId is null or <= 0) return; + // Get player data from the grain + var grain = _grainFactory.GetGrain(playerId.Value); + var snapshot = await grain.GetExtendedProfileSnapshotAsync(ct).ConfigureAwait(false); + var response = new ExtendedProfileMessageComposer { - UserId = player.Id, - UserName = player.Name ?? "Unknown", - Figure = player.Figure ?? "hr-115-42.hd-195-19.ch-3030-82.lg-275-1408.fa-1201.ca-1804-64", - Motto = player.Motto ?? "", - CreationDate = player.CreatedAt.ToString("yyyy-MM-dd"), - AchievementScore = 0, - FriendCount = 0, // TODO: Query from messenger_friends when available - IsFriend = false, // TODO: Check friendship when messenger_friends is available - IsFriendRequestSent = false, // TODO: Check messenger_requests when available - IsOnline = true, // TODO: Check if player has active room or session - Guilds = new List(), // TODO: Fetch guilds from database when guild system is implemented - LastAccessSinceInSeconds = 0, - OpenProfileWindow = true, - IsHidden = false, - AccountLevel = 1, // TODO: Get from database when account level is implemented - IntegerField24 = 0, - StarGemCount = 0, - BooleanField26 = false, - BooleanField27 = false + 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 = new List(), // TODO: Convert snapshot.Guilds when guild system is ready + LastAccessSinceInSeconds = snapshot.LastAccessSinceInSeconds, + OpenProfileWindow = snapshot.OpenProfileWindow, + IsHidden = snapshot.IsHidden, + AccountLevel = snapshot.AccountLevel, + IntegerField24 = snapshot.IntegerField24, + StarGemCount = snapshot.StarGemCount, + BooleanField26 = snapshot.BooleanField26, + BooleanField27 = snapshot.BooleanField27 }; await ctx.SendComposerAsync(response, ct).ConfigureAwait(false); @@ -78,3 +75,4 @@ CancellationToken ct } } } + diff --git a/Turbo.Players/Grains/PlayerDirectoryGrain.cs b/Turbo.Players/Grains/PlayerDirectoryGrain.cs index 99d6df66..a055c9b7 100644 --- a/Turbo.Players/Grains/PlayerDirectoryGrain.cs +++ b/Turbo.Players/Grains/PlayerDirectoryGrain.cs @@ -107,4 +107,32 @@ public Task InvalidatePlayerNameAsync(PlayerId playerId, CancellationToken ct) return Task.CompletedTask; } -} + + public async Task GetPlayerIdAsync(string userName, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(userName)) + return null; + + // Check cache first (reverse lookup) + var cached = _idToName.FirstOrDefault(x => x.Value.Equals(userName, System.StringComparison.OrdinalIgnoreCase)); + if (cached.Key != default) + return cached.Key; + + // Not in cache, query database + await using var dbCtx = await _dbCtxFactory.CreateDbContextAsync(ct); + + var playerId = await dbCtx + .Players.AsNoTracking() + .Where(x => x.Name == userName) + .Select(x => (long?)x.Id) + .FirstOrDefaultAsync(ct); + + if (playerId is null or <= 0) + return null; + + // Cache the name for future lookups + _idToName[(PlayerId)playerId.Value] = userName; + + return (PlayerId)playerId.Value; + } +} \ No newline at end of file diff --git a/Turbo.Primitives/Players/Grains/IPlayerDirectoryGrain.cs b/Turbo.Primitives/Players/Grains/IPlayerDirectoryGrain.cs index 5916058e..bd697df6 100644 --- a/Turbo.Primitives/Players/Grains/IPlayerDirectoryGrain.cs +++ b/Turbo.Primitives/Players/Grains/IPlayerDirectoryGrain.cs @@ -13,6 +13,7 @@ public Task> GetPlayerNamesAsync( List playerIds, CancellationToken ct ); + public Task GetPlayerIdAsync(string userName, CancellationToken ct); public Task SetPlayerNameAsync(PlayerId playerId, string name, CancellationToken ct); public Task InvalidatePlayerNameAsync(PlayerId playerId, CancellationToken ct); } From d83d4835ba6cf0d0e5864f23537574d66889d958 Mon Sep 17 00:00:00 2001 From: Dippys Date: Thu, 5 Feb 2026 22:25:19 +0400 Subject: [PATCH 4/5] small fix --- .../Users/GetExtendedProfileByNameMessageHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs b/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs index 2247a1ff..04298c66 100644 --- a/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs +++ b/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs @@ -37,7 +37,7 @@ CancellationToken ct ); var playerId = await directoryGrain.GetPlayerIdAsync(message.UserName, ct).ConfigureAwait(false); - if (playerId is null or <= 0) + if (playerId is null) return; // Get player data from the grain From 0df03e5e4a72a88385f0dfc050397fb7f2100d79 Mon Sep 17 00:00:00 2001 From: Diddyy Date: Fri, 6 Feb 2026 16:51:08 +0000 Subject: [PATCH 5/5] Refine extended profile flow to match grain architecture patterns - 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. --- .../GetExtendedProfileByNameMessageHandler.cs | 90 +++++++++---------- .../Users/GetExtendedProfileMessageHandler.cs | 75 +++++++++------- Turbo.Players/Grains/PlayerDirectoryGrain.cs | 46 ++++++---- .../Users/GetExtendedProfileByNameMessage.cs | 2 +- .../Users/GetExtendedProfileMessage.cs | 4 +- Turbo.Primitives/Players/IPlayerGrain.cs | 1 - 6 files changed, 118 insertions(+), 100 deletions(-) diff --git a/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs b/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs index 04298c66..1f370070 100644 --- a/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs +++ b/Turbo.PacketHandlers/Users/GetExtendedProfileByNameMessageHandler.cs @@ -1,12 +1,12 @@ -using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Orleans; using Turbo.Messages.Registry; using Turbo.Primitives.Messages.Incoming.Users; using Turbo.Primitives.Messages.Outgoing.Users; +using Turbo.Primitives.Orleans; using Turbo.Primitives.Players; -using Turbo.Primitives.Players.Grains; namespace Turbo.PacketHandlers.Users; @@ -29,50 +29,50 @@ CancellationToken ct if (string.IsNullOrWhiteSpace(message.UserName)) return; - try - { - // Resolve username to player ID using the directory grain - var directoryGrain = _grainFactory.GetGrain( - "default" // PlayerDirectoryGrain uses a static key - ); - var playerId = await directoryGrain.GetPlayerIdAsync(message.UserName, ct).ConfigureAwait(false); - - if (playerId is null) - return; - - // Get player data from the grain - var grain = _grainFactory.GetGrain(playerId.Value); - var snapshot = await grain.GetExtendedProfileSnapshotAsync(ct).ConfigureAwait(false); - - 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 = new List(), // TODO: Convert snapshot.Guilds when guild system is ready - LastAccessSinceInSeconds = snapshot.LastAccessSinceInSeconds, - OpenProfileWindow = snapshot.OpenProfileWindow, - IsHidden = snapshot.IsHidden, - AccountLevel = snapshot.AccountLevel, - IntegerField24 = snapshot.IntegerField24, - StarGemCount = snapshot.StarGemCount, - BooleanField26 = snapshot.BooleanField26, - BooleanField27 = snapshot.BooleanField27 - }; + var directoryGrain = _grainFactory.GetPlayerDirectoryGrain(); + var playerId = await directoryGrain.GetPlayerIdAsync(message.UserName, ct).ConfigureAwait(false); + if (playerId is null) + return; + var snapshot = await _grainFactory + .GetPlayerGrain(playerId.Value) + .GetExtendedProfileSnapshotAsync(ct) + .ConfigureAwait(false); - await ctx.SendComposerAsync(response, ct).ConfigureAwait(false); - } - catch + var response = new ExtendedProfileMessageComposer { - // TODO: Log error - } + 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 + }; + + await ctx.SendComposerAsync(response, ct).ConfigureAwait(false); } } - diff --git a/Turbo.PacketHandlers/Users/GetExtendedProfileMessageHandler.cs b/Turbo.PacketHandlers/Users/GetExtendedProfileMessageHandler.cs index 971bad2d..9b92cf00 100644 --- a/Turbo.PacketHandlers/Users/GetExtendedProfileMessageHandler.cs +++ b/Turbo.PacketHandlers/Users/GetExtendedProfileMessageHandler.cs @@ -1,10 +1,11 @@ -using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Orleans; using Turbo.Messages.Registry; using Turbo.Primitives.Messages.Incoming.Users; using Turbo.Primitives.Messages.Outgoing.Users; +using Turbo.Primitives.Orleans; using Turbo.Primitives.Players; namespace Turbo.PacketHandlers.Users; @@ -28,40 +29,46 @@ CancellationToken ct if (targetUserId <= 0) return; - try - { - // Get player data from the grain - var grain = _grainFactory.GetGrain(targetUserId); - var snapshot = await grain.GetExtendedProfileSnapshotAsync(ct).ConfigureAwait(false); - - 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 = new List(), // TODO: Convert snapshot.Guilds when guild system is ready - LastAccessSinceInSeconds = snapshot.LastAccessSinceInSeconds, - OpenProfileWindow = snapshot.OpenProfileWindow, - IsHidden = snapshot.IsHidden, - AccountLevel = snapshot.AccountLevel, - IntegerField24 = snapshot.IntegerField24, - StarGemCount = snapshot.StarGemCount, - BooleanField26 = snapshot.BooleanField26, - BooleanField27 = snapshot.BooleanField27 - }; + var snapshot = await _grainFactory + .GetPlayerGrain(targetUserId) + .GetExtendedProfileSnapshotAsync(ct) + .ConfigureAwait(false); - await ctx.SendComposerAsync(response, ct).ConfigureAwait(false); - } - catch + var response = new ExtendedProfileMessageComposer { - // TODO: Log error - } + 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 + }; + + await ctx.SendComposerAsync(response, ct).ConfigureAwait(false); } } diff --git a/Turbo.Players/Grains/PlayerDirectoryGrain.cs b/Turbo.Players/Grains/PlayerDirectoryGrain.cs index a055c9b7..20067c5a 100644 --- a/Turbo.Players/Grains/PlayerDirectoryGrain.cs +++ b/Turbo.Players/Grains/PlayerDirectoryGrain.cs @@ -19,6 +19,9 @@ internal class PlayerDirectoryGrain(IDbContextFactory dbCtxFacto private readonly IDbContextFactory _dbCtxFactory = dbCtxFactory; private readonly Dictionary _idToName = []; + private readonly Dictionary _nameToId = new( + System.StringComparer.OrdinalIgnoreCase + ); public async Task GetPlayerNameAsync(PlayerId playerId, CancellationToken ct) { @@ -36,7 +39,7 @@ public async Task GetPlayerNameAsync(PlayerId playerId, CancellationToke if (string.IsNullOrWhiteSpace(dbName)) return string.Empty; - _idToName[playerId] = dbName; + SetCache(playerId, dbName); return dbName; } @@ -84,7 +87,7 @@ CancellationToken ct foreach (var player in players) { - _idToName[player.Key] = player.Value; + SetCache(player.Key, player.Value); names.TryAdd(player.Key, player.Value); } @@ -96,14 +99,15 @@ CancellationToken ct public Task SetPlayerNameAsync(PlayerId playerId, string name, CancellationToken ct) { - _idToName[playerId] = name; + SetCache(playerId, name); return Task.CompletedTask; } public Task InvalidatePlayerNameAsync(PlayerId playerId, CancellationToken ct) { - _idToName.Remove(playerId); + if (_idToName.Remove(playerId, out var oldName)) + _nameToId.Remove(oldName); return Task.CompletedTask; } @@ -113,26 +117,34 @@ public Task InvalidatePlayerNameAsync(PlayerId playerId, CancellationToken ct) if (string.IsNullOrWhiteSpace(userName)) return null; - // Check cache first (reverse lookup) - var cached = _idToName.FirstOrDefault(x => x.Value.Equals(userName, System.StringComparison.OrdinalIgnoreCase)); - if (cached.Key != default) - return cached.Key; + var normalizedUserName = userName.Trim(); + if (_nameToId.TryGetValue(normalizedUserName, out var cachedPlayerId)) + return cachedPlayerId; - // Not in cache, query database await using var dbCtx = await _dbCtxFactory.CreateDbContextAsync(ct); - var playerId = await dbCtx + var loweredUserName = normalizedUserName.ToLowerInvariant(); + var player = await dbCtx .Players.AsNoTracking() - .Where(x => x.Name == userName) - .Select(x => (long?)x.Id) + .Where(x => x.Name != null && x.Name.ToLower() == loweredUserName) + .Select(x => new { x.Id, x.Name }) .FirstOrDefaultAsync(ct); - if (playerId is null or <= 0) + if (player is null or { Id: <= 0 }) return null; - // Cache the name for future lookups - _idToName[(PlayerId)playerId.Value] = userName; + var playerId = (PlayerId)player.Id; + SetCache(playerId, player.Name ?? normalizedUserName); + + return playerId; + } + + private void SetCache(PlayerId playerId, string name) + { + if (_idToName.TryGetValue(playerId, out var existingName)) + _nameToId.Remove(existingName); - return (PlayerId)playerId.Value; + _idToName[playerId] = name; + _nameToId[name] = playerId; } -} \ No newline at end of file +} diff --git a/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileByNameMessage.cs b/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileByNameMessage.cs index 88395f58..06180ae3 100644 --- a/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileByNameMessage.cs +++ b/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileByNameMessage.cs @@ -4,5 +4,5 @@ namespace Turbo.Primitives.Messages.Incoming.Users; public record GetExtendedProfileByNameMessage : IMessageEvent { - public string UserName { get; init; } = string.Empty; + public required string UserName { get; init; } } diff --git a/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileMessage.cs b/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileMessage.cs index b1ca09e5..96140d2e 100644 --- a/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileMessage.cs +++ b/Turbo.Primitives/Messages/Incoming/Users/GetExtendedProfileMessage.cs @@ -3,7 +3,7 @@ namespace Turbo.Primitives.Messages.Incoming.Users; -public record GetExtendedProfileMessage : IMessageEvent +public record GetExtendedProfileMessage : IMessageEvent { - public PlayerId UserId { get; init; } + public required PlayerId UserId { get; init; } } diff --git a/Turbo.Primitives/Players/IPlayerGrain.cs b/Turbo.Primitives/Players/IPlayerGrain.cs index 3328d65e..dbc16712 100644 --- a/Turbo.Primitives/Players/IPlayerGrain.cs +++ b/Turbo.Primitives/Players/IPlayerGrain.cs @@ -8,6 +8,5 @@ namespace Turbo.Primitives.Players; public interface IPlayerGrain : IGrainWithIntegerKey { public Task GetSummaryAsync(CancellationToken ct); - public Task GetExtendedProfileSnapshotAsync(CancellationToken ct); }