A Core/NosSmooth.Game/Extensions/SkillsExtensions.cs => Core/NosSmooth.Game/Extensions/SkillsExtensions.cs +45 -0
@@ 0,0 1,45 @@
+//
+// SkillsExtensions.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.Characters;
+using Remora.Results;
+
+namespace NosSmooth.Game.Extensions;
+
+/// <summary>
+/// Contains extension methods for <see cref="Skills"/>.
+/// </summary>
+public static class SkillsExtensions
+{
+ /// <summary>
+ /// Tries to get the skill of the specified vnum.
+ /// </summary>
+ /// <param name="skills">The skills of the player.</param>
+ /// <param name="skillVNum">The vnum to search for.</param>
+ /// <returns>The skill, if found.</returns>
+ public static Result<Skill> TryGetSkill(this Skills skills, long skillVNum)
+ {
+ if (skills.PrimarySkill.SkillVNum == skillVNum)
+ {
+ return skills.PrimarySkill;
+ }
+
+ if (skills.SecondarySkill.SkillVNum == skillVNum)
+ {
+ return skills.SecondarySkill;
+ }
+
+ foreach (Skill skill in skills.OtherSkills)
+ {
+ if (skill.SkillVNum == skillVNum)
+ {
+ return skill;
+ }
+ }
+
+ return new NotFoundError();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs +138 -0
@@ 0,0 1,138 @@
+//
+// CharacterInitResponder.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 NosCore.Packets.ServerPackets.Player;
+using NosSmooth.Core.Packets;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Game.Data.Social;
+using NosSmooth.Game.Events.Characters;
+using NosSmooth.Game.Events.Core;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Characters;
+
+/// <summary>
+/// Responds to CInfoPacket by creating the character.
+/// </summary>
+public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResponder<LevPacket>, IPacketResponder<CModePacket>
+{
+ private readonly Game _game;
+ private readonly EventDispatcher _eventDispatcher;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CharacterInitResponder"/> class.
+ /// </summary>
+ /// <param name="game">The nostale game.</param>
+ /// <param name="eventDispatcher">The event dispatcher.</param>
+ public CharacterInitResponder(Game game, EventDispatcher eventDispatcher)
+ {
+ _game = game;
+ _eventDispatcher = eventDispatcher;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<CInfoPacket> packetArgs, CancellationToken ct = default)
+ {
+ var oldCharacter = _game.Character;
+ var character = oldCharacter;
+ var packet = packetArgs.Packet;
+ await _game.EnsureCharacterCreatedAsync(false, ct).ConfigureAwait(false);
+ if (character is not null)
+ {
+ _game.Character = character = character with
+ {
+ Id = packet.CharacterId,
+ AuthorityType = packet.Authority,
+ Gender = packet.Gender,
+ HairStyle = packet.HairStyle,
+ HairColor = packet.HairColor,
+ Class = packet.Class,
+ Icon = packet.Icon,
+ Compliment = packet.Compliment,
+ Morph = (character.Morph ?? new Morph(packet.Morph, packet.MorphUpgrade)) with
+ {
+ VNum = packet.Morph, Upgrade = packet.MorphUpgrade
+ },
+ ArenaWinner = packet.ArenaWinner,
+ Invisible = packet.Invisible,
+ Family = new Family(packet.FamilyId, packet.FamilyName, packet.FamilyLevel)
+ };
+ }
+
+ _game.SetSemaphore.Release();
+ if (character is not null && character != oldCharacter)
+ {
+ return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(oldCharacter, character), ct);
+ }
+
+ return Result.FromSuccess();
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<LevPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ await _game.EnsureCharacterCreatedAsync(false, ct).ConfigureAwait(false);
+ var oldCharacter = _game.Character;
+ var character = oldCharacter;
+
+ if (character is not null)
+ {
+ _game.Character = character = character with
+ {
+ SkillCp = packet.SkillCp,
+ Reputation = packet.Reputation,
+ Level = new Level(packet.Level, packet.LevelXp, packet.XpLoad),
+ JobLevel = new Level(packet.JobLevel, packet.JobLevelXp, packet.JobXpLoad),
+ HeroLevel = new Level(packet.HeroLevel, packet.HeroXp, packet.HeroXpLoad)
+ };
+ }
+
+ _game.SetSemaphore.Release();
+ if (character is not null && character != oldCharacter)
+ {
+ return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(oldCharacter, character), ct);
+ }
+
+ return Result.FromSuccess();
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<CModePacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ var oldCharacter = _game.Character;
+ var character = oldCharacter;
+
+ if (character is null || character.Id != packetArgs.Packet.VisualId)
+ { // Not the current character.
+ return Result.FromSuccess();
+ }
+
+ await _game.SetSemaphore.WaitAsync(ct);
+ character = character with
+ {
+ Morph = new Morph
+ (
+ packet.Morph,
+ packet.MorphUpgrade,
+ packet.MorphDesign,
+ packet.MorphBonus,
+ packet.MorphSkin
+ ),
+ Size = packet.Size
+ };
+
+ _game.SetSemaphore.Release();
+ if (oldCharacter != character)
+ {
+ return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(oldCharacter, character), ct);
+ }
+
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs +91 -0
@@ 0,0 1,91 @@
+//
+// SkillResponder.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 NosCore.Packets.ServerPackets.Battle;
+using NosSmooth.Core.Packets;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Events.Characters;
+using NosSmooth.Game.Events.Core;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Characters;
+
+/// <summary>
+/// Responds to SkiPacket to add skill to the character.
+/// </summary>
+public class SkillResponder : IPacketResponder<SkiPacket>
+{
+ private readonly Game _game;
+ private readonly EventDispatcher _eventDispatcher;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SkillResponder"/> class.
+ /// </summary>
+ /// <param name="game">The nostale game.</param>
+ /// <param name="eventDispatcher">The event dispatcher.</param>
+ public SkillResponder(Game game, EventDispatcher eventDispatcher)
+ {
+ _game = game;
+ _eventDispatcher = eventDispatcher;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<SkiPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+
+ Skill primarySkill, secondarySkill;
+
+ var character = await _game.EnsureCharacterCreatedAsync(false, ct);
+
+ if (packet.PrimarySkill == character.Skills?.PrimarySkill.SkillVNum)
+ {
+ primarySkill = character.Skills.PrimarySkill;
+ }
+ else
+ {
+ primarySkill = new Skill(packet.PrimarySkill);
+ }
+
+ if (packet.PrimarySkill == packet.SecondarySkill)
+ {
+ secondarySkill = primarySkill;
+ }
+ else if (packet.SecondarySkill == character.Skills?.SecondarySkill.SkillVNum)
+ {
+ secondarySkill = character.Skills.SecondarySkill;
+ }
+ else
+ {
+ secondarySkill = new Skill(packet.SecondarySkill);
+ }
+
+ var skillsFromPacket = packet.SkiSubPackets?.Select(x => x.SkillVNum).ToList() ?? new List<long>();
+ var skillsFromCharacter = character.Skills is null ? new List<long>() : character.Skills.OtherSkills.Select(x => x.SkillVNum).ToList();
+ var newSkills = skillsFromPacket.Except(skillsFromCharacter);
+ var oldSkills = skillsFromCharacter.Except(skillsFromPacket);
+
+ var otherSkillsFromCharacter = new List<Skill>(character.Skills?.OtherSkills ?? new Skill[] { });
+ otherSkillsFromCharacter.RemoveAll(x => oldSkills.Contains(x.SkillVNum));
+
+ foreach (var newSkill in newSkills)
+ {
+ otherSkillsFromCharacter.Add(new Skill(newSkill));
+ }
+
+ var skills = new Skills(primarySkill, secondarySkill, otherSkillsFromCharacter);
+
+ _game.Character = character with
+ {
+ Skills = skills
+ };
+
+ _game.SetSemaphore.Release();
+ await _eventDispatcher.DispatchEvent(new SkillsReceivedEvent(skills), ct);
+
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs +59 -0
@@ 0,0 1,59 @@
+//
+// WalkResponder.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 NosCore.Packets.ClientPackets.Movement;
+using NosSmooth.Core.Packets;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Game.Events.Core;
+using NosSmooth.Game.Events.Entities;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Characters;
+
+/// <summary>
+/// Responds to walk packet.
+/// </summary>
+public class WalkResponder : IPacketResponder<WalkPacket>
+{
+ private readonly Game _game;
+ private readonly EventDispatcher _eventDispatcher;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="WalkResponder"/> class.
+ /// </summary>
+ /// <param name="game">The nostale game.</param>
+ /// <param name="eventDispatcher">The event dispatcher.</param>
+ public WalkResponder(Game game, EventDispatcher eventDispatcher)
+ {
+ _game = game;
+ _eventDispatcher = eventDispatcher;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<WalkPacket> packetArgs, CancellationToken ct = default)
+ {
+ var character = _game.Character;
+ if (character is not null && character.Position is not null)
+ {
+ var oldPosition = new Position
+ {
+ X = character.Position.X,
+ Y = character.Position.Y
+ };
+
+ character.Position.X = packetArgs.Packet.XCoordinate;
+ character.Position.Y = packetArgs.Packet.YCoordinate;
+
+ return await _eventDispatcher.DispatchEvent
+ (
+ new MovedEvent(character, character.Id, oldPosition, character.Position),
+ ct
+ );
+ }
+
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs => Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs +118 -0
@@ 0,0 1,118 @@
+//
+// SkillUsedResponder.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 NosCore.Packets.ServerPackets.Battle;
+using NosCore.Shared.Enumerations;
+using NosSmooth.Core.Packets;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Game.Events.Characters;
+using NosSmooth.Game.Events.Core;
+using NosSmooth.Game.Events.Players;
+using NosSmooth.Game.Extensions;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Entities;
+
+/// <summary>
+/// Responds to skill used packet.
+/// </summary>
+public class SkillUsedResponder : IPacketResponder<SuPacket>, IPacketResponder<SrPacket>
+{
+ private readonly Game _game;
+ private readonly EventDispatcher _eventDispatcher;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SkillUsedResponder"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ /// <param name="eventDispatcher">The event dispatcher.</param>
+ public SkillUsedResponder(Game game, EventDispatcher eventDispatcher)
+ {
+ _game = game;
+ _eventDispatcher = eventDispatcher;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<SuPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ var character = _game.Character;
+
+ if (packet.VisualType != VisualType.Player)
+ {
+ return Result.FromSuccess();
+ }
+
+ if (character is not null && character.Id != packet.VisualId && character.Skills is not null)
+ {
+ var skillResult = character.Skills.TryGetSkill(packet.SkillVnum);
+
+ if (skillResult.IsDefined(out var skillEntity))
+ {
+ skillEntity.LastUseTime = DateTimeOffset.Now;
+ skillEntity.Cooldown = TimeSpan.FromSeconds(packet.SkillCooldown / 10.0);
+ skillEntity.IsOnCooldown = true;
+ }
+
+ await _eventDispatcher.DispatchEvent(
+ new SkillUsedEvent
+ (
+ character,
+ character.Id,
+ skillResult.IsSuccess ? skillEntity : null,
+ packet.SkillVnum,
+ packet.TargetId,
+ new Position { X = packet.PositionX, Y = packet.PositionY }
+ ),
+ ct);
+ }
+ else
+ {
+ // TODO: add entity from the map, if exists.
+ await _eventDispatcher.DispatchEvent(
+ new SkillUsedEvent
+ (
+ null,
+ packet.VisualId,
+ null,
+ packet.SkillVnum,
+ packet.TargetId,
+ new Position
+ {
+ X = packet.PositionX, Y = packet.PositionY
+ }
+ ),
+ ct
+ );
+ }
+
+ return Result.FromSuccess();
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<SrPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ var character = _game.Character;
+
+ if (character is not null && character.Skills is not null)
+ {
+ var skillResult = character.Skills.TryGetSkill(packet.SkillVnum);
+
+ if (skillResult.IsDefined(out var skillEntity))
+ {
+ skillEntity.IsOnCooldown = false;
+ await _eventDispatcher.DispatchEvent(new SkillReadyEvent(skillEntity, skillEntity.SkillVNum), ct);
+ }
+ }
+ else
+ {
+ await _eventDispatcher.DispatchEvent(new SkillReadyEvent(null, packet.SkillVnum), ct);
+ }
+
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Login/CListPacketResponder.cs => Core/NosSmooth.Game/PacketHandlers/Login/CListPacketResponder.cs +52 -0
@@ 0,0 1,52 @@
+//
+// CListPacketResponder.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 NosCore.Packets.ClientPackets.Warehouse;
+using NosCore.Packets.ServerPackets.CharacterSelectionScreen;
+using NosSmooth.Core.Packets;
+using NosSmooth.Game.Events.Core;
+using NosSmooth.Game.Events.Login;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Login;
+
+/// <summary>
+/// Handles FStashEnd packet to remove game data.
+/// </summary>
+public class CListPacketResponder : IPacketResponder<ClistPacket>
+{
+ private readonly EventDispatcher _eventDispatcher;
+ private readonly Game _game;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CListPacketResponder"/> class.
+ /// </summary>
+ /// <param name="eventDispatcher">The events dispatcher.</param>
+ /// <param name="game">The nostale game.</param>
+ public CListPacketResponder(EventDispatcher eventDispatcher, Game game)
+ {
+ _eventDispatcher = eventDispatcher;
+ _game = game;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<ClistPacket> packetArgs, CancellationToken ct = default)
+ {
+ await _game.SetSemaphore.WaitAsync(ct);
+ bool logout = _game.Character is not null || _game.CurrentMap is not null;
+ _game.Character = null;
+ _game.CurrentMap = null;
+ _game.CurrentRaid = null;
+ _game.SetSemaphore.Release();
+
+ if (logout)
+ {
+ return await _eventDispatcher.DispatchEvent(new LogoutEvent(), ct);
+ }
+
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file