M Core/NosSmooth.Game/Data/Social/Group.cs => Core/NosSmooth.Game/Data/Social/Group.cs +42 -1
@@ 5,7 5,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Maps;
+using NosSmooth.Packets.Enums.Entities;
using OneOf;
+using Remora.Results;
namespace NosSmooth.Game.Data.Social;
@@ 15,4 18,42 @@ namespace NosSmooth.Game.Data.Social;
/// <param name="Id">The id of the group.</param>
/// <param name="Size">The size of the group.</param>
/// <param name="Members">The members of the group. (excluding the character)</param>
-public record Group(short? Id, byte? Size, IReadOnlyList<OneOf<Player, IPet>>? Members);>
\ No newline at end of file
+public record Group(short? Id, IReadOnlyList<GroupMember>? Members)
+{
+ /// <summary>
+ /// Gets the living entities from the map associated with group members.
+ /// </summary>
+ /// <remarks>
+ /// If a group member is not found on the map, null will be returned instead on its position.
+ /// </remarks>
+ /// <param name="game">The map to get map from.</param>
+ /// <returns>The living entities representing group members.</returns>
+ public IReadOnlyList<Player?> GetLivingEntities(Game game)
+ => GetLivingEntities(game.CurrentMap);
+
+ /// <summary>
+ /// Gets the living entities from the map associated with group members.
+ /// </summary>
+ /// <remarks>
+ /// If a group member is not found on the map, null will be returned instead on its position.
+ /// </remarks>
+ /// <param name="map">The map to get entities from.</param>
+ /// <returns>The living entities representing group members.</returns>
+ public IReadOnlyList<Player?> GetLivingEntities(Map? map)
+ => GetLivingEntities(map?.Entities);
+
+ /// <summary>
+ /// Gets the living entities from the map associated with group members.
+ /// </summary>
+ /// <remarks>
+ /// If a group member is not found on the map, null will be returned instead on its position.
+ /// </remarks>
+ /// <param name="entities">The entities to look at.</param>
+ /// <returns>The living entities representing group members.</returns>
+ public IReadOnlyList<Player?> GetLivingEntities(MapEntities? entities)
+ {
+ return (IReadOnlyList<Player?>?)Members?
+ .Select(x => entities?.GetEntity<Player>(x.PlayerId))
+ .ToList() ?? Array.Empty<Player?>();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Social/GroupMember.cs => Core/NosSmooth.Game/Data/Social/GroupMember.cs +58 -0
@@ 0,0 1,58 @@
+//
+// GroupMember.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Packets.Enums.Players;
+
+namespace NosSmooth.Game.Data.Social;
+
+public record GroupMember(long PlayerId)
+{
+ /// <summary>
+ /// Gets the level of the member.
+ /// </summary>
+ public byte Level { get; internal set; }
+
+ /// <summary>
+ /// Gets the hero level of the member.
+ /// </summary>
+ public byte? HeroLevel { get; internal set; }
+
+ /// <summary>
+ /// Gets the name of the member.
+ /// </summary>
+ public string? Name { get; internal set; }
+
+ /// <summary>
+ /// Gets the class of the member.
+ /// </summary>
+ public PlayerClass Class { get; internal set; }
+
+ /// <summary>
+ /// Gets the sex of the member.
+ /// </summary>
+ public SexType Sex { get; internal set; }
+
+ /// <summary>
+ /// Gets the morph vnum of the player.
+ /// </summary>
+ public long MorphVNum { get; internal set; }
+
+ /// <summary>
+ /// Gets the hp of the member.
+ /// </summary>
+ public Health? Hp { get; internal set; }
+
+ /// <summary>
+ /// Gets the mp of the member.
+ /// </summary>
+ public Health? Mp { get; internal set; }
+
+ /// <summary>
+ /// Gets the effects of the member.
+ /// </summary>
+ public IReadOnlyList<long>? EffectsVNums { get; internal set; }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Events/Groups/GroupInitializedEvent.cs => Core/NosSmooth.Game/Events/Groups/GroupInitializedEvent.cs +11 -0
@@ 0,0 1,11 @@
+//
+// GroupInitializedEvent.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Game.Data.Social;
+
+namespace NosSmooth.Game.Events.Groups;
+
+public record GroupInitializedEvent(Group Group) : IGameEvent;<
\ No newline at end of file
A Core/NosSmooth.Game/Events/Groups/GroupMemberStatEvent.cs => Core/NosSmooth.Game/Events/Groups/GroupMemberStatEvent.cs +11 -0
@@ 0,0 1,11 @@
+//
+// GroupMemberStatEvent.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Game.Data.Social;
+
+namespace NosSmooth.Game.Events.Groups;
+
+public record GroupMemberStatEvent(GroupMember Member) : IGameEvent;<
\ No newline at end of file
M Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs +3 -1
@@ 14,6 14,7 @@ using NosSmooth.Game.PacketHandlers.Characters;
using NosSmooth.Game.PacketHandlers.Entities;
using NosSmooth.Game.PacketHandlers.Inventory;
using NosSmooth.Game.PacketHandlers.Map;
+using NosSmooth.Game.PacketHandlers.Relations;
using NosSmooth.Game.PacketHandlers.Specialists;
namespace NosSmooth.Game.Extensions;
@@ 38,10 39,11 @@ public static class ServiceCollectionExtensions
serviceCollection
.AddPacketResponder<CharacterInitResponder>()
- .AddPacketResponder<SkillResponder>()
+ .AddPacketResponder<PlayerSkillResponder>()
.AddPacketResponder<WalkResponder>()
.AddPacketResponder<SkillUsedResponder>()
.AddPacketResponder<InventoryInitResponder>()
+ .AddPacketResponder<GroupInitResponder>()
.AddPacketResponder<AoeSkillUsedResponder>()
.AddPacketResponder<AtResponder>()
.AddPacketResponder<CMapResponder>()
M Core/NosSmooth.Game/Game.cs => Core/NosSmooth.Game/Game.cs +19 -19
@@ 96,10 96,10 @@ public class Game : IStatefulEntity
/// <param name="releaseSemaphore">Whether to release the semaphore used for changing the skills.</param>
/// <param name="ct">The cancellation token for cancelling the operation.</param>
/// <returns>The updated skills.</returns>
- internal async Task<Skills> CreateOrUpdateSkillsAsync
+ internal async Task<Skills?> CreateOrUpdateSkillsAsync
(
- Func<Skills> create,
- Func<Skills, Skills> update,
+ Func<Skills?> create,
+ Func<Skills, Skills?> update,
bool releaseSemaphore = true,
CancellationToken ct = default
)
@@ 124,10 124,10 @@ public class Game : IStatefulEntity
/// <param name="releaseSemaphore">Whether to release the semaphore used for changing the inventory.</param>
/// <param name="ct">The cancellation token for cancelling the operation.</param>
/// <returns>The updated inventory.</returns>
- internal async Task<Inventory> CreateOrUpdateInventoryAsync
+ internal async Task<Inventory?> CreateOrUpdateInventoryAsync
(
- Func<Inventory> create,
- Func<Inventory, Inventory> update,
+ Func<Inventory?> create,
+ Func<Inventory, Inventory?> update,
bool releaseSemaphore = true,
CancellationToken ct = default
)
@@ 152,10 152,10 @@ public class Game : IStatefulEntity
/// <param name="releaseSemaphore">Whether to release the semaphore used for changing the family.</param>
/// <param name="ct">The cancellation token for cancelling the operation.</param>
/// <returns>The updated family.</returns>
- internal async Task<Family> CreateOrUpdateFamilyAsync
+ internal async Task<Family?> CreateOrUpdateFamilyAsync
(
- Func<Family> create,
- Func<Family, Family> update,
+ Func<Family?> create,
+ Func<Family, Family?> update,
bool releaseSemaphore = true,
CancellationToken ct = default
)
@@ 196,10 196,10 @@ public class Game : IStatefulEntity
/// <param name="releaseSemaphore">Whether to release the semaphore used for changing the group.</param>
/// <param name="ct">The cancellation token for cancelling the operation.</param>
/// <returns>The updated group.</returns>
- internal async Task<Group> CreateOrUpdateGroupAsync
+ internal async Task<Group?> CreateOrUpdateGroupAsync
(
- Func<Group> create,
- Func<Group, Group> update,
+ Func<Group?> create,
+ Func<Group, Group?> update,
bool releaseSemaphore = true,
CancellationToken ct = default
)
@@ 224,10 224,10 @@ public class Game : IStatefulEntity
/// <param name="releaseSemaphore">Whether to release the semaphore used for changing the character.</param>
/// <param name="ct">The cancellation token for cancelling the operation.</param>
/// <returns>The updated character.</returns>
- internal async Task<Character> CreateOrUpdateCharacterAsync
+ internal async Task<Character?> CreateOrUpdateCharacterAsync
(
- Func<Character> create,
- Func<Character, Character> update,
+ Func<Character?> create,
+ Func<Character, Character?> update,
bool releaseSemaphore = true,
CancellationToken ct = default
)
@@ 322,13 322,13 @@ public class Game : IStatefulEntity
}
}
- private async Task<T> CreateOrUpdateAsync<T>
+ private async Task<T?> CreateOrUpdateAsync<T>
(
GameSemaphoreType type,
Func<T?> get,
- Action<T> set,
- Func<T> create,
- Func<T, T> update,
+ Action<T?> set,
+ Func<T?> create,
+ Func<T, T?> update,
bool releaseSemaphore = true,
CancellationToken ct = default
)
M Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs +17 -1
@@ 4,6 4,7 @@
// Copyright (c) František Boháček. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System.Diagnostics;
using NosSmooth.Core.Packets;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Info;
@@ 80,7 81,7 @@ public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResp
await _game.CreateOrUpdateGroupAsync
(
- () => new Group(packet.GroupId, null, null),
+ () => new Group(packet.GroupId, null),
g => g with
{
Id = packet.GroupId
@@ 107,6 108,11 @@ public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResp
ct: ct
);
+ if (character is null)
+ {
+ throw new UnreachableException();
+ }
+
if (character != oldCharacter)
{
return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(character), ct);
@@ 143,6 149,11 @@ public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResp
ct: ct
);
+ if (character is null)
+ {
+ throw new UnreachableException();
+ }
+
if (character != oldCharacter)
{
return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(character), ct);
@@ 182,6 193,11 @@ public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResp
ct: ct
);
+ if (character is null)
+ {
+ throw new UnreachableException();
+ }
+
if (oldCharacter != character)
{
return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(character), ct);
M Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs +6 -0
@@ 4,6 4,7 @@
// Copyright (c) František Boháček. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System.Diagnostics;
using NosSmooth.Core.Packets;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Info;
@@ 55,6 56,11 @@ public class WalkResponder : IPacketResponder<WalkPacket>
ct: ct
);
+ if (character is null)
+ {
+ throw new UnreachableException();
+ }
+
return await _eventDispatcher.DispatchEvent
(
new EntityMovedEvent(character, oldPosition, character.Position!.Value),
M Core/NosSmooth.Game/PacketHandlers/Inventory/InventoryInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Inventory/InventoryInitResponder.cs +11 -0
@@ 4,6 4,7 @@
// Copyright (c) František Boháček. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System.Diagnostics;
using Microsoft.Extensions.Logging;
using NosSmooth.Core.Extensions;
using NosSmooth.Core.Packets;
@@ 91,6 92,11 @@ public class InventoryInitResponder : IPacketResponder<InvPacket>, IPacketRespon
ct: ct
);
+ if (inventory is null)
+ {
+ throw new UnreachableException();
+ }
+
if (packet.Bag == Packets.Enums.Inventory.BagType.Costume)
{
// last bag initialized. TODO solve race condition.
@@ 161,6 167,11 @@ public class InventoryInitResponder : IPacketResponder<InvPacket>, IPacketRespon
ct: ct
);
+ if (inventory is null)
+ {
+ throw new UnreachableException();
+ }
+
return await _eventDispatcher.DispatchEvent
(
new InventorySlotUpdatedEvent
A Core/NosSmooth.Game/PacketHandlers/Relations/GroupInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Relations/GroupInitResponder.cs +132 -0
@@ 0,0 1,132 @@
+//
+// GroupInitResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using System.Linq.Expressions;
+using NosSmooth.Core.Packets;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Game.Data.Social;
+using NosSmooth.Game.Events.Core;
+using NosSmooth.Game.Events.Groups;
+using NosSmooth.Packets.Enums.Entities;
+using NosSmooth.Packets.Server.Groups;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Relations;
+
+/// <summary>
+/// A group initialization responder.
+/// </summary>
+public class GroupInitResponder : IPacketResponder<PinitPacket>, IPacketResponder<PstPacket>
+{
+ private readonly Game _game;
+ private readonly EventDispatcher _eventDispatcher;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GroupInitResponder"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ /// <param name="eventDispatcher">The event dispatcher.</param>
+ public GroupInitResponder(Game game, EventDispatcher eventDispatcher)
+ {
+ _game = game;
+ _eventDispatcher = eventDispatcher;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<PinitPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+
+ Group BuildGroup(Group? group)
+ {
+ var members = packet.PinitSubPackets?
+ .Where(x => x.PlayerSubPacket is not null)
+ .OrderBy(x => x.PlayerSubPacket!.GroupPosition)
+ .Select(e => CreateEntity(e, group?.Members))
+ .ToList() ?? new List<GroupMember>();
+
+ return new Group(null, members);
+ }
+
+ var group = await _game.CreateOrUpdateGroupAsync
+ (
+ () => BuildGroup(null),
+ BuildGroup,
+ ct: ct
+ );
+
+ if (group is null)
+ {
+ throw new UnreachableException();
+ }
+
+ return await _eventDispatcher.DispatchEvent(new GroupInitializedEvent(group), ct);
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<PstPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ if (packet.EntityType != EntityType.Player)
+ {
+ return Result.FromSuccess();
+ }
+
+ GroupMember? member = null;
+ await _game.CreateOrUpdateGroupAsync
+ (
+ () => null,
+ g =>
+ {
+ member = g.Members?.FirstOrDefault(x => x.PlayerId == packet.EntityId);
+
+ if (member is not null)
+ {
+ member.Hp = new Health { Amount = packet.Hp, Percentage = packet.HpPercentage };
+ member.Mp = new Health { Amount = packet.Mp, Percentage = packet.MpPercentage };
+ member.Class = packet.PlayerClass ?? member.Class;
+ member.Sex = packet.PlayerSex ?? member.Sex;
+ member.EffectsVNums = packet.Effects?.Select(x => x.CardId).ToList();
+ member.MorphVNum = packet.PlayerMorphVNum ?? member.MorphVNum;
+ }
+
+ return g;
+ },
+ ct: ct
+ );
+
+ if (member is not null)
+ {
+ await _eventDispatcher.DispatchEvent
+ (
+ new GroupMemberStatEvent(member),
+ ct
+ );
+ }
+
+ return Result.FromSuccess();
+ }
+
+ private GroupMember CreateEntity(PinitSubPacket packet, IReadOnlyList<GroupMember>? members)
+ {
+ var playerSubPacket = packet.PlayerSubPacket!;
+ var originalMember = members?.FirstOrDefault(x => x.PlayerId == packet.EntityId);
+
+ return new(packet.EntityId)
+ {
+ Level = playerSubPacket.Level,
+ HeroLevel = playerSubPacket.HeroLevel,
+ Name = playerSubPacket.Name?.Name,
+ Class = playerSubPacket.Class,
+ Sex = playerSubPacket.Sex,
+ MorphVNum = playerSubPacket.MorphVNum,
+ Hp = originalMember?.Hp,
+ Mp = originalMember?.Mp,
+ EffectsVNums = originalMember?.EffectsVNums
+ };
+ }
+}<
\ No newline at end of file
R Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs => Core/NosSmooth.Game/PacketHandlers/Skills/PlayerSkillResponder.cs +6 -6
@@ 1,5 1,5 @@
//
-// SkillResponder.cs
+// PlayerSkillResponder.cs
//
// Copyright (c) František Boháček. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
@@ 19,26 19,26 @@ namespace NosSmooth.Game.PacketHandlers.Characters;
/// <summary>
/// Responds to SkiPacket to add skill to the character.
/// </summary>
-public class SkillResponder : IPacketResponder<SkiPacket>
+public class PlayerSkillResponder : IPacketResponder<SkiPacket>
{
private readonly Game _game;
private readonly EventDispatcher _eventDispatcher;
private readonly IInfoService _infoService;
- private readonly ILogger<SkillResponder> _logger;
+ private readonly ILogger<PlayerSkillResponder> _logger;
/// <summary>
- /// Initializes a new instance of the <see cref="SkillResponder"/> class.
+ /// Initializes a new instance of the <see cref="PlayerSkillResponder"/> class.
/// </summary>
/// <param name="game">The nostale game.</param>
/// <param name="eventDispatcher">The event dispatcher.</param>
/// <param name="infoService">The info service.</param>
/// <param name="logger">The logger.</param>
- public SkillResponder
+ public PlayerSkillResponder
(
Game game,
EventDispatcher eventDispatcher,
IInfoService infoService,
- ILogger<SkillResponder> logger
+ ILogger<PlayerSkillResponder> logger
)
{
_game = game;