Skip to content

Commit 8e9e9bf

Browse files
authored
feat(game): character select slot positions from DB query rownum (#334)
* feat(game): character select slot positions from DB Id rownum * refactor(game): populate Slot within LINQ * refactor(game): revert + tests + fix for potential GetPlayerAsync bug
1 parent 8f61e54 commit 8e9e9bf

7 files changed

Lines changed: 122 additions & 34 deletions

File tree

src/Data/Game.Persistence/DbPlayerRepository.cs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@ public DbPlayerRepository(GameDbContext db)
1717

1818
public async Task<PlayerData[]> GetPlayersAsync(Guid accountId)
1919
{
20-
return await _db.Players
20+
var playersWithUnpopulatedSlot = await _db.Players
2121
.AsNoTracking()
2222
.Where(x => x.AccountId == accountId)
23+
.OrderBy(x => x.Id)
2324
.SelectPlayerData()
2425
.ToArrayAsync();
26+
27+
return playersWithUnpopulatedSlot
28+
.AssignIncrementalSlots();
2529
}
2630

2731
public async Task<bool> IsNameInUseAsync(string name)
@@ -110,9 +114,29 @@ public async Task SetPlayerAsync(PlayerData data)
110114

111115
public async Task<PlayerData?> GetPlayerAsync(uint playerId)
112116
{
113-
return await _db.Players
117+
// TODO: optimize/refactor this part while maintaining Slot field correctness
118+
// (needed to avoid overwriting `players:..` cache keys when Slot is assigned default zero value for all players)
119+
// Note that currently this method is not used in production code
120+
var accountId = await _db.Players
121+
.AsNoTracking()
114122
.Where(x => x.Id == playerId)
123+
.Select(x => x.AccountId)
124+
.SingleOrDefaultAsync();
125+
126+
if (accountId == Guid.Empty)
127+
{
128+
return null;
129+
}
130+
131+
var playersWithUnpopulatedSlot = await _db.Players
132+
.AsNoTracking()
133+
.Where(x => x.AccountId == accountId)
134+
.OrderBy(x => x.Id)
115135
.SelectPlayerData()
116-
.FirstOrDefaultAsync();
136+
.ToArrayAsync();
137+
138+
return playersWithUnpopulatedSlot
139+
.AssignIncrementalSlots()
140+
.SingleOrDefault(x => x.Id == playerId);
117141
}
118142
}

src/Data/Game.Persistence/Extensions/QueryExtensions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ public static IQueryable<PlayerData> SelectPlayerData(this IQueryable<Player> qu
4141
GuildId = x.GuildId
4242
});
4343
}
44+
45+
public static PlayerData[] AssignIncrementalSlots(this PlayerData[] players)
46+
{
47+
for (var i = 0; i < players.Length; i++)
48+
{
49+
players[i].Slot = (byte)i;
50+
}
51+
52+
return players;
53+
}
4454

4555
public static IQueryable<Skill> SelectPlayerSkill(this IQueryable<PlayerSkill> query)
4656
{

src/Libraries/Game.Server/Commands/PhaseSelectCommand.cs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ public async Task ExecuteAsync(CommandContext context)
3636
context.Player.Connection.SetPhase(EPhases.Select);
3737

3838
var characters = new Characters();
39-
var i = 0;
4039
await using var scope = _serviceProvider.CreateAsyncScope();
4140
var playerManager = scope.ServiceProvider.GetRequiredService<IPlayerManager>();
4241
var guildManager = scope.ServiceProvider.GetRequiredService<IGuildManager>();
@@ -46,17 +45,15 @@ public async Task ExecuteAsync(CommandContext context)
4645
var host = _world.GetMapHost(player.PositionX, player.PositionY);
4746
var guild = await guildManager.GetGuildForPlayerAsync(player.Id);
4847

49-
// todo character slot position
50-
characters.CharacterList[i] = player.ToCharacter();
51-
characters.CharacterList[i].Ip = BitConverter.ToInt32(host.Ip.GetAddressBytes());
52-
characters.CharacterList[i].Port = host.Port;
53-
characters.GuildIds[i] = guild?.Id ?? 0;
54-
characters.GuildNames[i] = guild?.Name ?? "";
55-
56-
i++;
48+
var slot = (int)player.Slot;
49+
characters.CharacterList[slot] = player.ToCharacter();
50+
characters.CharacterList[slot].Ip = BitConverter.ToInt32(host.Ip.GetAddressBytes());
51+
characters.CharacterList[slot].Port = host.Port;
52+
characters.GuildIds[slot] = guild?.Id ?? 0;
53+
characters.GuildNames[slot] = guild?.Name ?? "";
5754
}
5855

5956
context.Player.Connection.Send(characters);
6057
}
6158
}
62-
}
59+
}

src/Libraries/Game.Server/Extensions/PaketExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public static Character ToCharacter(this PlayerData player)
99
{
1010
return new Character
1111
{
12-
Id = 1,
12+
Id = player.Id,
1313
Name = player.Name,
1414
Class = player.PlayerClass,
1515
Level = player.Level,
@@ -26,4 +26,4 @@ public static Character ToCharacter(this PlayerData player)
2626
SkillGroup = player.SkillGroup
2727
};
2828
}
29-
}
29+
}

src/Libraries/Game.Server/PacketHandlers/TokenLoginHandler.cs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,22 +81,18 @@ public async Task ExecuteAsync(GamePacketContext<TokenLogin> ctx, CancellationTo
8181

8282
// Load players of account
8383
var characters = new Characters();
84-
var i = 0;
8584
var charactersFromCacheOrDb = await _playerManager.GetPlayers(token.AccountId);
8685
foreach (var player in charactersFromCacheOrDb)
8786
{
8887
var host = _world.GetMapHost(player.PositionX, player.PositionY);
8988

9089
var guild = await _guildManager.GetGuildForPlayerAsync(player.Id, cancellationToken);
91-
// todo character slot position
92-
characters.CharacterList[i] = player.ToCharacter();
93-
characters.CharacterList[i].Ip = BitConverter.ToInt32(host.Ip.GetAddressBytes());
94-
characters.CharacterList[i].Port = host.Port;
95-
characters.GuildIds[i] = guild?.Id ?? 0;
96-
characters.GuildNames[i] = guild?.Name ?? "";
97-
// todo armor on character select
98-
99-
i++;
90+
var slot = (int)player.Slot;
91+
characters.CharacterList[slot] = player.ToCharacter();
92+
characters.CharacterList[slot].Ip = BitConverter.ToInt32(host.Ip.GetAddressBytes());
93+
characters.CharacterList[slot].Port = host.Port;
94+
characters.GuildIds[slot] = guild?.Id ?? 0;
95+
characters.GuildNames[slot] = guild?.Name ?? "";
10096
}
10197

10298
// When there are no characters belonging to the account, the empire status is stored in the cache.

src/Libraries/Game.Server/PlayerManager.cs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,11 @@ public PlayerManager(IDbPlayerRepository dbPlayerRepository, ICachePlayerReposit
3333
if (cachedPlayer is null)
3434
{
3535
var players = await _dbPlayerRepository.GetPlayersAsync(accountId);
36-
for (var i = 0; i < players.Length; i++)
36+
var player = players.FirstOrDefault(p => p.Slot == slot);
37+
if (player is not null)
3738
{
38-
var player = players[i];
39-
player.Slot = (byte)i;
40-
41-
if (i == slot)
42-
{
43-
await _cachePlayerRepository.SetPlayerAsync(player);
44-
return player;
45-
}
39+
await _cachePlayerRepository.SetPlayerAsync(player);
40+
return player;
4641
}
4742

4843
_logger.LogWarning("Could not find player for account {AccountId} at slot {Slot}", accountId, slot);

src/Tests/Game.Tests/PlayerManagerTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,31 @@ public async Task GetPlayerById_NotFound()
218218
output.Should().BeNull();
219219
}
220220

221+
[Fact]
222+
public async Task GetPlayerById_WithMultiplePlayers_CachesUnderCorrectSlot()
223+
{
224+
await _cacheManager.FlushAll();
225+
226+
var accountId = Guid.NewGuid();
227+
228+
const uint FirstId = 100u, SecondId = 200u, ThirdId = 300u;
229+
await _dbPlayerRepository.CreateAsync(new PlayerData { Id = FirstId, AccountId = accountId, Name = "PlayerA" });
230+
await _dbPlayerRepository.CreateAsync(new PlayerData { Id = SecondId, AccountId = accountId, Name = "PlayerB" });
231+
await _dbPlayerRepository.CreateAsync(new PlayerData { Id = ThirdId, AccountId = accountId, Name = "PlayerC" });
232+
233+
var fetched = await _playerManager.GetPlayer(SecondId);
234+
235+
fetched.Should().NotBeNull();
236+
fetched.Id.Should().Be(SecondId);
237+
238+
var keys = await _cacheManager.Server.Keys("*");
239+
keys.Should().HaveCount(2)
240+
.And.Contain($"player:{SecondId}")
241+
.And.Contain($"players:{accountId}:1");
242+
keys.Should().NotContain($"players:{accountId}:0");
243+
keys.Should().NotContain($"players:{accountId}:2");
244+
}
245+
221246
[Fact]
222247
public async Task GetPlayerByAccountIdAndSlot_NotFound()
223248
{
@@ -237,4 +262,45 @@ public async Task DeleteCharacter()
237262
(await _cacheManager.Server.Keys("*")).Should().BeEquivalentTo([$"temp:empire-selection:{accountId}"]);
238263
(await _dbPlayerRepository.GetPlayersAsync(accountId)).Should().BeEmpty();
239264
}
265+
266+
[Theory]
267+
[InlineData(0)]
268+
[InlineData(1)]
269+
[InlineData(2)]
270+
[InlineData(3)]
271+
[InlineData(4)]
272+
public async Task GetPlayers_ReturnsOrderedAndCachesWithSlots(int charactersCount)
273+
{
274+
await _cacheManager.FlushAll();
275+
await _db.Players.ExecuteDeleteAsync();
276+
277+
var accountId = Guid.NewGuid();
278+
var basePlayerId = 1000u;
279+
280+
for (uint i = 0; i < charactersCount; i++)
281+
{
282+
var id = basePlayerId + i;
283+
await _dbPlayerRepository.CreateAsync(new PlayerData { Id = id, AccountId = accountId, Name = $"Player{i}" });
284+
}
285+
286+
var players = await _playerManager.GetPlayers(accountId);
287+
288+
players.Should().HaveCount(charactersCount);
289+
for (var i = 0; i < charactersCount; i++)
290+
{
291+
players[i].Slot.Should().Be((byte)i);
292+
293+
var expectedId = basePlayerId + (uint)i;
294+
players[i].Id.Should().Be(expectedId);
295+
}
296+
297+
var keys = await _cacheManager.Server.Keys("*");
298+
keys.Should().HaveCount(charactersCount * 2);
299+
for (var i = 0; i < charactersCount; i++)
300+
{
301+
var expectedId = basePlayerId + i;
302+
keys.Should().Contain($"player:{expectedId}");
303+
keys.Should().Contain($"players:{accountId}:{i}");
304+
}
305+
}
240306
}

0 commit comments

Comments
 (0)