M Core/NosSmooth.Core/Packets/IPacketResponder.cs => Core/NosSmooth.Core/Packets/IPacketResponder.cs +4 -4
@@ 29,10 29,10 @@ public interface IPacketResponder<TPacket> : IPacketResponder
/// <summary>
/// Respond to the given packet.
/// </summary>
- /// <param name="packet">The packet to respond to.</param>
+ /// <param name="packetArgs">The packet to respond to.</param>
/// <param name="ct">The cancellation token for cancelling the operation.</param>
/// <returns>A result that may or may not have succeeded.</returns>
- public Task<Result> Respond(PacketEventArgs<TPacket> packet, CancellationToken ct = default);
+ public Task<Result> Respond(PacketEventArgs<TPacket> packetArgs, CancellationToken ct = default);
}
/// <summary>
@@ 43,10 43,10 @@ public interface IEveryPacketResponder : IPacketResponder
/// <summary>
/// Respond to the given packet.
/// </summary>
- /// <param name="packet">The packet to respond to.</param>
+ /// <param name="packetArgs">The packet to respond to.</param>
/// <param name="ct">The cancellation token for cancelling the operation.</param>
/// <typeparam name="TPacket">The type of the packet.</typeparam>
/// <returns>A result that may or may not have succeeded.</returns>
- public Task<Result> Respond<TPacket>(PacketEventArgs<TPacket> packet, CancellationToken ct = default)
+ public Task<Result> Respond<TPacket>(PacketEventArgs<TPacket> packetArgs, CancellationToken ct = default)
where TPacket : IPacket;
}
A Core/NosSmooth.Game/Apis/NostaleChatPacketApi.cs => Core/NosSmooth.Game/Apis/NostaleChatPacketApi.cs +91 -0
@@ 0,0 1,91 @@
+//
+// NostaleChatPacketApi.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.Core.Client;
+using NosSmooth.Packets.Enums;
+using NosSmooth.Packets.Enums.Chat;
+using NosSmooth.Packets.Server.Chat;
+using Remora.Results;
+
+namespace NosSmooth.Game.Apis;
+
+/// <summary>
+/// Packet api for sending and receiving messages.
+/// </summary>
+public class NostaleChatPacketApi
+{
+ // TODO: check length of the messages
+ private readonly INostaleClient _client;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NostaleChatPacketApi"/> class.
+ /// </summary>
+ /// <param name="client">The nostale client.</param>
+ public NostaleChatPacketApi(INostaleClient client)
+ {
+ _client = client;
+ }
+
+ /// <summary>
+ /// Receive the given system message on the client.
+ /// </summary>
+ /// <remarks>
+ /// Won't send anything to the server, it's just the client who will see the message.
+ /// </remarks>
+ /// <param name="content">The content of the message.</param>
+ /// <param name="color">The color of the message.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> ReceiveSystemMessageAsync(string content, SayColor color = SayColor.Yellow, CancellationToken ct = default)
+ => _client.ReceivePacketAsync(new SayPacket(EntityType.Map, 0, color, content), ct);
+
+ /// <summary>
+ /// Sends the given message to the public chat.
+ /// </summary>
+ /// <param name="content">The content of the message.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> SendMessageAsync(string content, CancellationToken ct = default)
+ => _client.SendPacketAsync(new Packets.Client.Chat.SayPacket(content), ct);
+
+ /// <summary>
+ /// Sends the given message to the family chat.
+ /// </summary>
+ /// <remarks>
+ /// Should be used only if the user is in a family.
+ /// </remarks>
+ /// <param name="content">The content of the message.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> SendFamilyMessage(string content, CancellationToken ct = default)
+ => _client.SendPacketAsync(":" + content, ct);
+
+ /// <summary>
+ /// Sends the given message to the group chat.
+ /// </summary>
+ /// <remarks>
+ /// Should be used only if the user is in a group. (with people, not only pets).
+ /// </remarks>
+ /// <param name="content">The content of the message.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> SendGroupMessage(string content, CancellationToken ct = default)
+ => _client.SendPacketAsync(";" + content, ct);
+
+ /// <summary>
+ /// Sends the given message to the target only.
+ /// </summary>
+ /// <remarks>
+ /// Won't return if the whisper has actually came through, event has to be hooked
+ /// up to know if the whisper has went through (and you can know only for messages that are sufficiently long).
+ /// </remarks>
+ /// <param name="targetName">The name of the user you want to whisper to.</param>
+ /// <param name="content">The content of the message.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> SendWhisper(string targetName, string content, CancellationToken ct = default)
+ => _client.SendPacketAsync($"/{targetName} {content}", ct);
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Apis/NostaleSkillsPacketApi.cs => Core/NosSmooth.Game/Apis/NostaleSkillsPacketApi.cs +248 -0
@@ 0,0 1,248 @@
+//
+// NostaleSkillsPacketApi.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.Core.Client;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Errors;
+using NosSmooth.Packets.Client.Battle;
+using NosSmooth.Packets.Enums;
+using Remora.Results;
+
+namespace NosSmooth.Game.Apis;
+
+/// <summary>
+/// Packet api for using character skills.
+/// </summary>
+public class NostaleSkillsPacketApi
+{
+ private readonly INostaleClient _client;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NostaleSkillsPacketApi"/> class.
+ /// </summary>
+ /// <param name="client">The nostale client.</param>
+ public NostaleSkillsPacketApi(INostaleClient client)
+ {
+ _client = client;
+ }
+
+ /// <summary>
+ /// Use the given (targetable) skill on specified entity.
+ /// </summary>
+ /// <remarks>
+ /// For skills that can be used only on self, use <paramref name="entityId"/> of the character.
+ /// For skills that cannot be targeted on an entity, proceed to <see cref="UseSkillAt"/>.
+ /// </remarks>
+ /// <param name="skillVNum">The id of the skill.</param>
+ /// <param name="entityId">The id of the entity to use the skill on.</param>
+ /// <param name="entityType">The type of the supplied entity.</param>
+ /// <param name="mapX">The x coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
+ /// <param name="mapY">The y coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> UseSkillOn
+ (
+ long skillVNum,
+ long entityId,
+ EntityType entityType,
+ short? mapX = default,
+ short? mapY = default,
+ CancellationToken ct = default
+ )
+ {
+ return _client.SendPacketAsync
+ (
+ new UseSkillPacket
+ (
+ skillVNum,
+ entityType,
+ entityId,
+ mapX,
+ mapY
+ ),
+ ct
+ );
+ }
+
+ /// <summary>
+ /// Use the given (targetable) skill on specified entity.
+ /// </summary>
+ /// <remarks>
+ /// For skills that can be used only on self, use <paramref name="entityId"/> of the character.
+ /// For skills that cannot be targeted on an entity, proceed to <see cref="UseSkillAt"/>.
+ /// </remarks>
+ /// <param name="skillVNum">The id of the skill.</param>
+ /// <param name="entity">The entity to use the skill on.</param>
+ /// <param name="mapX">The x coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
+ /// <param name="mapY">The y coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> UseSkillOn
+ (
+ long skillVNum,
+ ILivingEntity entity,
+ short? mapX = default,
+ short? mapY = default,
+ CancellationToken ct = default
+ )
+ {
+ return _client.SendPacketAsync
+ (
+ new UseSkillPacket
+ (
+ skillVNum,
+ entity.Type,
+ entity.Id,
+ mapX,
+ mapY
+ ),
+ ct
+ );
+ }
+
+ /// <summary>
+ /// Use the given (targetable) skill on specified entity.
+ /// </summary>
+ /// <remarks>
+ /// The skill won't be used if it is on cooldown.
+ /// For skills that can be used only on self, use <paramref name="entityId"/> of the character.
+ /// For skills that cannot be targeted on an entity, proceed to <see cref="UseSkillAt"/>.
+ /// </remarks>
+ /// <param name="skill">The skill to use.</param>
+ /// <param name="entity">The entity to use the skill on.</param>
+ /// <param name="mapX">The x coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
+ /// <param name="mapY">The y coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> UseSkillOn
+ (
+ Skill skill,
+ ILivingEntity entity,
+ short? mapX = default,
+ short? mapY = default,
+ CancellationToken ct = default
+ )
+ {
+ if (skill.IsOnCooldown)
+ {
+ return Task.FromResult<Result>(new SkillOnCooldownError(skill));
+ }
+
+ return _client.SendPacketAsync
+ (
+ new UseSkillPacket
+ (
+ skill.SkillVNum,
+ entity.Type,
+ entity.Id,
+ mapX,
+ mapY
+ ),
+ ct
+ );
+ }
+
+ /// <summary>
+ /// Use the given (targetable) skill on specified entity.
+ /// </summary>
+ /// <remarks>
+ /// For skills that can be used only on self, use <paramref name="entityId"/> of the character.
+ /// For skills that cannot be targeted on an entity, proceed to <see cref="UseSkillAt"/>.
+ /// </remarks>
+ /// <param name="skill">The skill to use.</param>
+ /// <param name="entityId">The id of the entity to use the skill on.</param>
+ /// <param name="entityType">The type of the supplied entity.</param>
+ /// <param name="mapX">The x coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
+ /// <param name="mapY">The y coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> UseSkillOn
+ (
+ Skill skill,
+ long entityId,
+ EntityType entityType,
+ short? mapX = default,
+ short? mapY = default,
+ CancellationToken ct = default
+ )
+ {
+ if (skill.IsOnCooldown)
+ {
+ return Task.FromResult<Result>(new SkillOnCooldownError(skill));
+ }
+
+ return _client.SendPacketAsync
+ (
+ new UseSkillPacket
+ (
+ skill.SkillVNum,
+ entityType,
+ entityId,
+ mapX,
+ mapY
+ ),
+ ct
+ );
+ }
+
+ /// <summary>
+ /// Use the given (aoe) skill on the specified place.
+ /// </summary>
+ /// <remarks>
+ /// For skills that can have targets, proceed to <see cref="UseSkillOn"/>.
+ /// </remarks>
+ /// <param name="skillVNum">The id of the skill.</param>
+ /// <param name="mapX">The x coordinate to use the skill at.</param>
+ /// <param name="mapY">The y coordinate to use the skill at.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> UseSkillAt
+ (
+ long skillVNum,
+ short mapX,
+ short mapY,
+ CancellationToken ct = default
+ )
+ {
+ return _client.SendPacketAsync
+ (
+ new UseAOESkillPacket(skillVNum, mapX, mapY),
+ ct
+ );
+ }
+
+ /// <summary>
+ /// Use the given (aoe) skill on the specified place.
+ /// </summary>
+ /// <remarks>
+ /// For skills that can have targets, proceed to <see cref="UseSkillOn"/>.
+ /// </remarks>
+ /// <param name="skill">The skill to use.</param>
+ /// <param name="mapX">The x coordinate to use the skill at.</param>
+ /// <param name="mapY">The y coordinate to use the skill at.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> UseSkillAt
+ (
+ Skill skill,
+ short mapX,
+ short mapY,
+ CancellationToken ct = default
+ )
+ {
+ if (skill.IsOnCooldown)
+ {
+ return Task.FromResult<Result>(new SkillOnCooldownError(skill));
+ }
+
+ return _client.SendPacketAsync
+ (
+ new UseAOESkillPacket(skill.SkillVNum, mapX, mapY),
+ ct
+ );
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Act4/Act4Raid.cs => Core/NosSmooth.Game/Data/Act4/Act4Raid.cs +33 -0
@@ 0,0 1,33 @@
+//
+// Act4Raid.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.
+
+namespace NosSmooth.Game.Data.Act4;
+
+/// <summary>
+/// Represents type of raid in act4.
+/// </summary>
+public enum Act4Raid
+{
+ /// <summary>
+ /// Fire element raid with the boss Morcos.
+ /// </summary>
+ Morcos,
+
+ /// <summary>
+ /// Shadow element raid with the boss Hatus.
+ /// </summary>
+ Hatus,
+
+ /// <summary>
+ /// Water element raid with the boss Calvina.
+ /// </summary>
+ Calvina,
+
+ /// <summary>
+ /// Light element raid with the boss Berios.
+ /// </summary>
+ Berios
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Act4/Act4Status.cs => Core/NosSmooth.Game/Data/Act4/Act4Status.cs +26 -0
@@ 0,0 1,26 @@
+//
+// Act4Status.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.Packets.Enums;
+
+namespace NosSmooth.Game.Data.Act4;
+
+/// <summary>
+/// Status of a faction in act4
+/// </summary>
+/// <param name="Percentage">The percentage to Mukraju.</param>
+/// <param name="Mode">The current mode.</param>
+/// <param name="CurrentTime">The current time of the raid.</param>
+/// <param name="TotalTime">The total time the raid will be for.</param>
+/// <param name="Raid">The type of the raid.</param>
+public record Act4FactionStatus
+(
+ short Percentage,
+ Act4Mode Mode,
+ long? CurrentTime,
+ long? TotalTime,
+ Act4Raid Raid
+);<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Characters/Character.cs => Core/NosSmooth.Game/Data/Characters/Character.cs +92 -0
@@ 0,0 1,92 @@
+//
+// Character.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.Chat;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Game.Data.Social;
+using NosSmooth.Packets.Enums;
+using NosSmooth.Packets.Enums.Players;
+
+namespace NosSmooth.Game.Data.Characters;
+
+/// <summary>
+/// Represents the client character.
+/// </summary>
+public class Character : Player
+{
+ /// <summary>
+ /// Gets or sets the inventory of the character.
+ /// </summary>
+ public Inventory.Inventory? Inventory { get; set; }
+
+ /// <summary>
+ /// Get or sets the friends of the character.
+ /// </summary>
+ public IReadOnlyList<Friend>? Friends { get; set; }
+
+ /// <summary>
+ /// Gets or sets the skills of the player.
+ /// </summary>
+ public Skills? Skills { get; set; }
+
+ /// <summary>
+ /// Gets or sets the group the player is in.
+ /// </summary>
+ public Group? Group { get; set; }
+
+ /// <summary>
+ /// Gets or sets the c skill points.
+ /// </summary>
+ public int? SkillCp { get; set; }
+
+ /// <summary>
+ /// Gets or sets the job level.
+ /// </summary>
+ public Level? JobLevel { get; set; }
+
+ /// <summary>
+ /// Gets or sets the player level.
+ /// </summary>
+ public Level? PlayerLevel { get; set; }
+
+ /// <summary>
+ /// Gets or sets the player level.
+ /// </summary>
+ public Level? HeroLevelStruct { get; set; }
+
+ /// <inheritdoc/>
+ public override short? HeroLevel
+ {
+ get => HeroLevelStruct?.Lvl;
+ set
+ {
+ if (HeroLevelStruct is not null && value is not null)
+ {
+ HeroLevelStruct = HeroLevelStruct with
+ {
+ Lvl = value.Value
+ };
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the sp points of the player.
+ /// </summary>
+ /// <remarks>
+ /// Resets every day, max 10 000.
+ /// </remarks>
+ public int SpPoints { get; set; }
+
+ /// <summary>
+ /// Gets or sets the additional sp points of the player.
+ /// </summary>
+ /// <remarks>
+ /// Used if <see cref="SpPoints"/> are 0. Max 1 000 000.
+ /// </remarks>
+ public int AdditionalSpPoints { get; set; }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Characters/Skill.cs => Core/NosSmooth.Game/Data/Characters/Skill.cs +31 -0
@@ 0,0 1,31 @@
+//
+// Skill.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.Data.Abstractions.Infos;
+
+namespace NosSmooth.Game.Data.Characters;
+
+/// <summary>
+/// Represents nostale skill entity.
+/// </summary>
+/// <param name="SkillVNum">The vnum of the skill.</param>
+/// <param name="Level">The level of the skill. Unknown feature.</param>
+public record Skill(int SkillVNum, int? Level = default, ISkillInfo? Info = default)
+{
+ /// <summary>
+ /// Gets the last time this skill was used.
+ /// </summary>
+ public DateTimeOffset LastUseTime { get; internal set; }
+
+ /// <summary>
+ /// Gets whether the skill is on cooldown.
+ /// </summary>
+ /// <remarks>
+ /// This is set when the server sends sr packet,
+ /// prefer to use this instead of checking the LastUseTime and Cooldown.
+ /// </remarks>
+ public bool IsOnCooldown { get; internal set; }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Characters/Skills.cs => Core/NosSmooth.Game/Data/Characters/Skills.cs +20 -0
@@ 0,0 1,20 @@
+//
+// Skills.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.
+
+namespace NosSmooth.Game.Data.Characters;
+
+/// <summary>
+/// Holds skill of a Character.
+/// </summary>
+/// <param name="PrimarySkill">The VNum of the primary skill. This skill is used with the primary weapon. (Could be different for sp cards.)</param>
+/// <param name="SecondarySkill">The VNum of the secondary skill. This skill is used with the secondary weapon. (Could be different for sp cards)</param>
+/// <param name="OtherSkills">The VNums of other skills.</param>
+public record Skills
+(
+ Skill PrimarySkill,
+ Skill SecondarySkill,
+ IReadOnlyList<Skill> OtherSkills
+);<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Chat/ChatMessage.cs => Core/NosSmooth.Game/Data/Chat/ChatMessage.cs +17 -0
@@ 0,0 1,17 @@
+//
+// ChatMessage.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.Entities;
+
+namespace NosSmooth.Game.Data.Chat;
+
+/// <summary>
+/// Message received from user in chat.
+/// </summary>
+/// <param name="CharacterId">The id of the character.</param>
+/// <param name="Player">The player </param>
+/// <param name="Message">The message sent from the friend.</param>
+public record ChatMessage(long CharacterId, Player? Player, string Message);<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Chat/DirectMessage.cs => Core/NosSmooth.Game/Data/Chat/DirectMessage.cs +15 -0
@@ 0,0 1,15 @@
+//
+// DirectMessage.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.
+
+namespace NosSmooth.Game.Data.Chat;
+
+/// <summary>
+/// Message received from a friend.
+/// </summary>
+/// <param name="CharacterId">The id of the character.</param>
+/// <param name="Friend">The friend from which the message is. May be null if the client did not receive friend packet.</param>
+/// <param name="Message">The message sent from the friend.</param>
+public record DirectMessage(long CharacterId, Friend? Friend, string? Message);<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Chat/Friend.cs => Core/NosSmooth.Game/Data/Chat/Friend.cs +33 -0
@@ 0,0 1,33 @@
+//
+// Friend.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.
+
+namespace NosSmooth.Game.Data.Chat;
+
+/// <summary>
+/// Represents character's friend.
+/// </summary>
+public class Friend
+{
+ /// <summary>
+ /// The id of the character.
+ /// </summary>
+ public long CharacterId { get; internal set; }
+
+ /// <summary>
+ /// The type of the relation.
+ /// </summary>
+ // public CharacterRelationType RelationType { get; internal set; }
+
+ /// <summary>
+ /// The name of the character.
+ /// </summary>
+ public string? CharacterName { get; internal set; }
+
+ /// <summary>
+ /// Whether the friend is connected to the server.
+ /// </summary>
+ public bool IsOnline { get; internal set; }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Dialogs/Dialog.cs => Core/NosSmooth.Game/Data/Dialogs/Dialog.cs +21 -0
@@ 0,0 1,21 @@
+//
+// Dialog.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.
+
+namespace NosSmooth.Game.Data.Dialogs;
+
+/// <summary>
+/// Represents dialog sent by the server
+/// </summary>
+/// <param name="AcceptCommand"></param>
+/// <param name="Message"></param>
+/// <param name="Parameters"></param>
+public record Dialog
+(
+ string AcceptCommand,
+
+ // OneOf<Game18NConstString, string> Message,
+ IReadOnlyList<string> Parameters
+);<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Entities/GroundItem.cs => Core/NosSmooth.Game/Data/Entities/GroundItem.cs +54 -0
@@ 0,0 1,54 @@
+//
+// GroundItem.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.Data.Abstractions.Infos;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Packets.Enums;
+
+namespace NosSmooth.Game.Data.Entities;
+
+/// <summary>
+/// The item on the ground.
+/// </summary>
+public class GroundItem : IEntity
+{
+ /// <summary>
+ /// Gets or sets the id of the owner, if any.
+ /// </summary>
+ public long? OwnerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the amount of the item on the ground.
+ /// </summary>
+ public int Amount { get; internal set; }
+
+ /// <summary>
+ /// Gets or sets whether the item is for a quest.
+ /// </summary>
+ public bool IsQuestRelated { get; internal set; }
+
+ /// <summary>
+ /// Gets or sets the info about the item, if available.
+ /// </summary>
+ public IItemInfo? ItemInfo { get; internal set; }
+
+ /// <summary>
+ /// Gets the VNum of the npc.
+ /// </summary>
+ public int VNum { get; internal set; }
+
+ /// <inheritdoc/>
+ public long Id { get; set; }
+
+ /// <inheritdoc/>
+ public Position? Position { get; set; }
+
+ /// <inheritdoc/>
+ public EntityType Type
+ {
+ get => EntityType.Object;
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Entities/IEntity.cs => Core/NosSmooth.Game/Data/Entities/IEntity.cs +31 -0
@@ 0,0 1,31 @@
+//
+// IEntity.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;
+
+namespace NosSmooth.Game.Data.Entities;
+
+/// <summary>
+/// Base type for entities.
+/// </summary>
+public interface IEntity
+{
+ /// <summary>
+ /// Gets the id of the entity.
+ /// </summary>
+ public long Id { get; set; }
+
+ /// <summary>
+ /// Gets the position of the entity.
+ /// </summary>
+ public Position? Position { get; set; }
+
+ /// <summary>
+ /// Gets the type of the entity.
+ /// </summary>
+ public EntityType Type { get; }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Entities/IPet.cs => Core/NosSmooth.Game/Data/Entities/IPet.cs +14 -0
@@ 0,0 1,14 @@
+//
+// IPet.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.
+
+namespace NosSmooth.Game.Data.Entities;
+
+/// <summary>
+/// Represents base type for a pet or a partner.
+/// </summary>
+public interface IPet
+{
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Entities/LivingEntity.cs => Core/NosSmooth.Game/Data/Entities/LivingEntity.cs +81 -0
@@ 0,0 1,81 @@
+//
+// LivingEntity.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;
+
+namespace NosSmooth.Game.Data.Entities;
+
+/// <summary>
+/// Represents any nostale living entity such as monster, npc, player.
+/// </summary>
+public interface ILivingEntity : IEntity
+{
+ /// <summary>
+ /// Gets the speed of the entity. May be null if unknown.
+ /// </summary>
+ public int? Speed { get; set; }
+
+ /// <summary>
+ /// Gets or sets whether the player is invisible.
+ /// </summary>
+ public bool? IsInvisible { get; set; }
+
+ /// <summary>
+ /// Gets the level of the entity. May be null if unknown.
+ /// </summary>
+ public ushort? Level { get; set; }
+
+ /// <summary>
+ /// Gets the direction the entity is looking. May be null if unknown.
+ /// </summary>
+ public byte? Direction { get; set; }
+
+ /// <summary>
+ /// Gets the percentage of the health points of the entity. May be null if unknown.
+ /// </summary>
+ public Health? Hp { get; set; }
+
+ /// <summary>
+ /// Gets the percentage of the mana points of the entity. May be null if unknown.
+ /// </summary>
+ public Health? Mp { get; set; }
+
+ /// <summary>
+ /// Gets the faction of the entity. May be null if unknown.
+ /// </summary>
+ public FactionType? Faction { get; set; }
+
+ /// <summary>
+ /// Gets the size of the entity.
+ /// </summary>
+ public short Size { get; set; }
+
+ /// <summary>
+ /// Gets the VNums of the effects the entity has.
+ /// </summary>
+ public IReadOnlyList<long>? EffectsVNums { get; set; }
+
+ /// <summary>
+ /// Gets the name of the entity. May be null if unknown.
+ /// </summary>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets whether the entity is sitting.
+ /// </summary>
+ public bool IsSitting { get; set; }
+
+ /// <summary>
+ /// Gets or sets whether the entity cannot move.
+ /// </summary>
+ public bool CantMove { get; set; }
+
+ /// <summary>
+ /// Gets or sets whether the entity cannot attack.
+ /// </summary>
+ public bool CantAttack { get; set; }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Entities/Monster.cs => Core/NosSmooth.Game/Data/Entities/Monster.cs +75 -0
@@ 0,0 1,75 @@
+//
+// Monster.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.Data.Abstractions.Infos;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Packets.Enums;
+
+namespace NosSmooth.Game.Data.Entities;
+
+/// <summary>
+/// Represents nostale monster entity.
+/// </summary>
+public class Monster : ILivingEntity
+{
+ /// <summary>
+ /// Gets or sets the monster info.
+ /// </summary>
+ public IMonsterInfo? MonsterInfo { get; set; }
+
+ /// <summary>
+ /// Gets the VNum of the monster.
+ /// </summary>
+ public int VNum { get; set; }
+
+ /// <inheritdoc/>
+ public long Id { get; set; }
+
+ /// <inheritdoc/>
+ public string? Name { get; set; }
+
+ /// <inheritdoc />
+ public bool IsSitting { get; set; }
+
+ /// <inheritdoc />
+ public bool CantMove { get; set; }
+
+ /// <inheritdoc />
+ public bool CantAttack { get; set; }
+
+ /// <inheritdoc/>
+ public Position? Position { get; set; }
+
+ /// <inheritdoc/>
+ public EntityType Type => EntityType.Monster;
+
+ /// <inheritdoc/>
+ public int? Speed { get; set; }
+
+ /// <inheritdoc />
+ public bool? IsInvisible { get; set; }
+
+ /// <inheritdoc/>
+ public ushort? Level { get; set; }
+
+ /// <inheritdoc/>
+ public byte? Direction { get; set; }
+
+ /// <inheritdoc/>
+ public Health? Hp { get; set; }
+
+ /// <inheritdoc/>
+ public Health? Mp { get; set; }
+
+ /// <inheritdoc/>
+ public FactionType? Faction { get; set; }
+
+ /// <inheritdoc/>
+ public short Size { get; set; }
+
+ /// <inheritdoc/>
+ public IReadOnlyList<long>? EffectsVNums { get; set; }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Entities/Npc.cs => Core/NosSmooth.Game/Data/Entities/Npc.cs +69 -0
@@ 0,0 1,69 @@
+//
+// Npc.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;
+
+namespace NosSmooth.Game.Data.Entities;
+
+/// <summary>
+/// Represents nostale npc entity.
+/// </summary>
+public class Npc : ILivingEntity
+{
+ /// <summary>
+ /// Gets the VNum of the npc.
+ /// </summary>
+ public int VNum { get; internal set; }
+
+ /// <inheritdoc/>
+ public long Id { get; set; }
+
+ /// <inheritdoc/>
+ public string? Name { get; set; }
+
+ /// <inheritdoc />
+ public bool IsSitting { get; set; }
+
+ /// <inheritdoc />
+ public bool CantMove { get; set; }
+
+ /// <inheritdoc />
+ public bool CantAttack { get; set; }
+
+ /// <inheritdoc/>
+ public Position? Position { get; set; }
+
+ /// <inheritdoc/>
+ public EntityType Type => EntityType.Npc;
+
+ /// <inheritdoc/>
+ public int? Speed { get; set; }
+
+ /// <inheritdoc />
+ public bool? IsInvisible { get; set; }
+
+ /// <inheritdoc/>
+ public ushort? Level { get; set; }
+
+ /// <inheritdoc/>
+ public byte? Direction { get; set; }
+
+ /// <inheritdoc/>
+ public Health? Hp { get; set; }
+
+ /// <inheritdoc/>
+ public Health? Mp { get; set; }
+
+ /// <inheritdoc/>
+ public FactionType? Faction { get; set; }
+
+ /// <inheritdoc/>
+ public short Size { get; set; }
+
+ /// <inheritdoc/>
+ public IReadOnlyList<long>? EffectsVNums { get; set; }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Entities/Partner.cs => Core/NosSmooth.Game/Data/Entities/Partner.cs +12 -0
@@ 0,0 1,12 @@
+//
+// Partner.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.
+
+namespace NosSmooth.Game.Data.Entities;
+
+/// <summary>
+/// Represents Partner of the Character.
+/// </summary>
+public record Partner() : IPet;<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Entities/Pet.cs => Core/NosSmooth.Game/Data/Entities/Pet.cs +12 -0
@@ 0,0 1,12 @@
+//
+// Pet.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.
+
+namespace NosSmooth.Game.Data.Entities;
+
+/// <summary>
+/// Represents pet of the character.
+/// </summary>
+public record Pet() : IPet;<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Entities/Player.cs => Core/NosSmooth.Game/Data/Entities/Player.cs +142 -0
@@ 0,0 1,142 @@
+//
+// Player.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.Game.Data.Items;
+using NosSmooth.Game.Data.Social;
+using NosSmooth.Packets.Enums;
+using NosSmooth.Packets.Enums.Players;
+
+namespace NosSmooth.Game.Data.Entities;
+
+/// <summary>
+/// Represents nostale player entity.
+/// </summary>
+public class Player : ILivingEntity
+{
+ /// <summary>
+ /// Gets or sets the authority of the player.
+ /// </summary>
+ public AuthorityType Authority { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sex of the player.
+ /// </summary>
+ public SexType Sex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the hairstyle of the player.
+ /// </summary>
+ public HairStyle HairStyle { get; set; }
+
+ /// <summary>
+ /// Gets or sets the hair color of the player.
+ /// </summary>
+ public HairColor HairColor { get; set; }
+
+ /// <summary>
+ /// Gets or sets the class of the player.
+ /// </summary>
+ public PlayerClass Class { get; set; }
+
+ /// <summary>
+ /// Gets or sets the reputation icon. UNKNOWN TODO.
+ /// </summary>
+ public byte? Icon { get; set; }
+
+ /// <summary>
+ /// UNKNOWN TODO.
+ /// </summary>
+ public short? Compliment { get; set; }
+
+ /// <summary>
+ /// Gets or sets the morph used for sps, vehicles and such.
+ /// </summary>
+ public Morph? Morph { get; set; }
+
+ /// <summary>
+ /// Gets or sets whether the player is a champion arena winner.
+ /// </summary>
+ public bool ArenaWinner { get; set; }
+
+ /// <summary>
+ /// Gets or sets the reputation number of the player.
+ /// </summary>
+ public long? Reputation { get; set; }
+
+ /// <summary>
+ /// Gets or sets the visible title of the player.
+ /// </summary>
+ public short Title { get; set; }
+
+ /// <summary>
+ /// Gets or sets the family.
+ /// </summary>
+ public Family? Family { get; set; }
+
+ /// <summary>
+ /// Gets the VNum of the npc.
+ /// </summary>
+ public int VNum { get; set; }
+
+ /// <inheritdoc/>
+ public long Id { get; set; }
+
+ /// <inheritdoc/>
+ public string? Name { get; set; }
+
+ /// <inheritdoc />
+ public bool IsSitting { get; set; }
+
+ /// <inheritdoc />
+ public bool CantMove { get; set; }
+
+ /// <inheritdoc />
+ public bool CantAttack { get; set; }
+
+ /// <inheritdoc/>
+ public Position? Position { get; set; }
+
+ /// <inheritdoc/>
+ public EntityType Type => EntityType.Player;
+
+ /// <inheritdoc/>
+ public int? Speed { get; set; }
+
+ /// <inheritdoc />
+ public bool? IsInvisible { get; set; }
+
+ /// <inheritdoc/>
+ public ushort? Level { get; set; }
+
+ /// <inheritdoc/>
+ public byte? Direction { get; set; }
+
+ /// <inheritdoc/>
+ public Health? Hp { get; set; }
+
+ /// <inheritdoc/>
+ public Health? Mp { get; set; }
+
+ /// <inheritdoc/>
+ public FactionType? Faction { get; set; }
+
+ /// <inheritdoc/>
+ public short Size { get; set; }
+
+ /// <inheritdoc/>
+ public IReadOnlyList<long>? EffectsVNums { get; set; }
+
+ /// <summary>
+ /// Gets or sets the hero level.
+ /// </summary>
+ public virtual short? HeroLevel { get; set; }
+
+ /// <summary>
+ /// Gets or sets the equipment.
+ /// </summary>
+ public Equipment? Equipment { get; set; }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Info/Health.cs => Core/NosSmooth.Game/Data/Info/Health.cs +112 -0
@@ 0,0 1,112 @@
+//
+// Health.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.
+
+namespace NosSmooth.Game.Data.Info;
+
+/// <summary>
+/// Represents the health or mana of an entity.
+/// </summary>
+public class Health
+{
+ private byte? _percentage;
+ private long? _amount;
+ private long? _maximum;
+
+ /// <summary>
+ /// Gets or sets the percentage of the health.
+ /// </summary>
+ public byte? Percentage
+ {
+ get => _percentage;
+ set
+ {
+ _percentage = value;
+ if (value is null)
+ {
+ return;
+ }
+
+ var maximum = _maximum;
+ if (maximum is not null)
+ {
+ _amount = (long)((value / 100.0) * maximum);
+ return;
+ }
+
+ var amount = _amount;
+ if (amount is not null)
+ {
+ _maximum = (long)(amount / (value / 100.0));
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the health amount.
+ /// </summary>
+ public long? Amount
+ {
+ get => _amount;
+ set
+ {
+ _amount = value;
+ if (value is null)
+ {
+ return;
+ }
+
+ var maximum = _maximum;
+ if (maximum is not null)
+ {
+ _percentage = (byte)(((double)value / maximum) * 100);
+ return;
+ }
+
+ var percentage = _percentage;
+ if (percentage is not null)
+ {
+ _maximum = (long)(value / (percentage / 100.0));
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum health.
+ /// </summary>
+ public long? Maximum
+ {
+ get => _maximum;
+ set
+ {
+ _maximum = value;
+ if (value is null)
+ {
+ return;
+ }
+
+ var amount = _amount;
+ var percentage = _percentage;
+
+ if (amount is not null)
+ {
+ if (amount > value)
+ {
+ amount = _amount = value;
+ _percentage = 100;
+ return;
+ }
+
+ _percentage = (byte)((amount / (double)value) * 100);
+ return;
+ }
+
+ if (percentage is not null)
+ { // ? would this be correct?
+ _amount = (long)((percentage / 100.0) * value);
+ }
+ }
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Info/Level.cs => Core/NosSmooth.Game/Data/Info/Level.cs +15 -0
@@ 0,0 1,15 @@
+//
+// Level.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.
+
+namespace NosSmooth.Game.Data.Info;
+
+/// <summary>
+/// Represents a level, such as job level, hero level, character level.
+/// </summary>
+/// <param name="Lvl">The level.</param>
+/// <param name="Xp">Current xp.</param>
+/// <param name="XpLoad">Maximum xp of the current level.</param>
+public record Level(short Lvl, long Xp, long XpLoad);<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Info/Morph.cs => Core/NosSmooth.Game/Data/Info/Morph.cs +28 -0
@@ 0,0 1,28 @@
+//
+// Morph.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.
+
+namespace NosSmooth.Game.Data.Info;
+
+/// <summary>
+/// Represents players morph.
+/// </summary>
+/// <remarks>
+/// Morphs are used mainly for special cards.
+/// The VNum will contain the vnum of the special card.
+/// </remarks>
+/// <param name="VNum">The vnum of the morph.</param>
+/// <param name="Upgrade">The upgrade to show wings.</param>
+/// <param name="Design">The design of the wings.</param>
+/// <param name="Bonus">Unknown.</param>
+/// <param name="Skin">The skin of the wings.</param>
+public record Morph
+(
+ long VNum,
+ byte Upgrade,
+ short? Design = default,
+ byte? Bonus = default,
+ short? Skin = default
+);<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Info/Position.cs => Core/NosSmooth.Game/Data/Info/Position.cs +26 -0
@@ 0,0 1,26 @@
+//
+// Position.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.CodeAnalysis;
+
+namespace NosSmooth.Game.Data.Info;
+
+/// <summary>
+/// Represents nostale position on map.
+/// </summary>
+[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313", MessageId = "Parameter names should begin with lower-case letter", Justification = "Standard.")]
+public record struct Position(long X, long Y)
+{
+ /// <summary>
+ /// Get the squared distance to the given position.
+ /// </summary>
+ /// <param name="position">The position.</param>
+ /// <returns>The distance squared.</returns>
+ public long DistanceSquared(Position position)
+ {
+ return ((position.X - X) * (position.X - X)) + ((position.Y - Y) * (position.Y - Y));
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Inventory/Bag.cs => Core/NosSmooth.Game/Data/Inventory/Bag.cs +12 -0
@@ 0,0 1,12 @@
+//
+// Bag.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.
+
+namespace NosSmooth.Game.Data.Inventory;
+
+/// <summary>
+/// Represents one bag in the inventory of the player.
+/// </summary>
+public record Bag();<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Inventory/Inventory.cs => Core/NosSmooth.Game/Data/Inventory/Inventory.cs +12 -0
@@ 0,0 1,12 @@
+//
+// Inventory.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.
+
+namespace NosSmooth.Game.Data.Inventory;
+
+/// <summary>
+/// Represents the whole inventory of the character.
+/// </summary>
+public record Inventory();<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Inventory/InventoryItem.cs => Core/NosSmooth.Game/Data/Inventory/InventoryItem.cs +12 -0
@@ 0,0 1,12 @@
+//
+// InventoryItem.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.
+
+namespace NosSmooth.Game.Data.Inventory;
+
+/// <summary>
+/// Represents item in bag inventory of the character.
+/// </summary>
+public record InventoryItem();<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Items/Equipment.cs => Core/NosSmooth.Game/Data/Items/Equipment.cs +21 -0
@@ 0,0 1,21 @@
+//
+// Equipment.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.
+
+namespace NosSmooth.Game.Data.Items;
+
+public record Equipment
+(
+ Item? Hat,
+ UpgradeableItem? Armor,
+ UpgradeableItem? MainWeapon,
+ UpgradeableItem? SecondaryWeapon,
+ Item? Mask,
+ Item? Fairy,
+ Item? CostumeSuit,
+ Item? CostumeHat,
+ short? WeaponSkin,
+ short? WingSkin
+);<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Items/Fairy.cs => Core/NosSmooth.Game/Data/Items/Fairy.cs +12 -0
@@ 0,0 1,12 @@
+//
+// Fairy.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.Data.Abstractions.Infos;
+using NosSmooth.Packets.Enums;
+
+namespace NosSmooth.Game.Data.Items;
+
+public record Fairy(int ItemVNum, Element Element, IItemInfo? Info) : Item(ItemVNum, Info);<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Items/Item.cs => Core/NosSmooth.Game/Data/Items/Item.cs +16 -0
@@ 0,0 1,16 @@
+//
+// Item.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.Data.Abstractions.Infos;
+
+namespace NosSmooth.Game.Data.Items;
+
+/// <summary>
+/// A NosTale item.
+/// </summary>
+/// <param name="ItemVNum"></param>
+/// <param name="Info"></param>
+public record Item(int ItemVNum, IItemInfo? Info);<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Items/UpgradeableItem.cs => Core/NosSmooth.Game/Data/Items/UpgradeableItem.cs +18 -0
@@ 0,0 1,18 @@
+//
+// UpgradeableItem.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.Data.Abstractions.Infos;
+
+namespace NosSmooth.Game.Data.Items;
+
+/// <summary>
+/// An item that can be upgraded and has rarity, ie. weapon or armor.
+/// </summary>
+/// <param name="ItemVNum">The vnum of the item.</param>
+/// <param name="Info">The information about the item.</param>
+/// <param name="Upgrade">The upgrade (0 - 10).</param>
+/// <param name="Rare">The rare nubmer (0 - 8).</param>
+public record UpgradeableItem(int ItemVNum, IItemInfo? Info, byte? Upgrade, sbyte? Rare) : Item(ItemVNum, Info);<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Login/Channel.cs => Core/NosSmooth.Game/Data/Login/Channel.cs +12 -0
@@ 0,0 1,12 @@
+//
+// Channel.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.
+
+namespace NosSmooth.Game.Data.Login;
+
+/// <summary>
+/// Channel of a nostale server.
+/// </summary>
+public record Channel();<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Login/WorldServer.cs => Core/NosSmooth.Game/Data/Login/WorldServer.cs +12 -0
@@ 0,0 1,12 @@
+//
+// WorldServer.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.
+
+namespace NosSmooth.Game.Data.Login;
+
+/// <summary>
+/// Represents nostale world server.
+/// </summary>
+public record WorldServer();<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Maps/Map.cs => Core/NosSmooth.Game/Data/Maps/Map.cs +46 -0
@@ 0,0 1,46 @@
+//
+// Map.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.CodeAnalysis;
+using NosSmooth.Data.Abstractions.Infos;
+using NosSmooth.Game.Data.Info;
+
+namespace NosSmooth.Game.Data.Maps;
+
+/// <summary>
+/// Represents nostale map.
+/// </summary>
+public record Map
+(
+ long Id,
+ byte Type,
+ IMapInfo? Info,
+ MapEntities Entities,
+ IReadOnlyList<Portal> Portals
+)
+{
+ /// <summary>
+ /// Gets whether the given position lies on a portal.
+ /// </summary>
+ /// <param name="position">The position.</param>
+ /// <param name="portal">The portal the position is on, if any.</param>
+ /// <returns>Whether there was a portal at the specified position.</returns>
+ public bool IsOnPortal(Position position, [NotNullWhen(true)] out Portal? portal)
+ {
+ foreach (var p in Portals)
+ {
+ // TODO: figure out the distance
+ if (p.Position.DistanceSquared(position) < 3)
+ {
+ portal = p;
+ return true;
+ }
+ }
+
+ portal = null;
+ return false;
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Maps/MapEntities.cs => Core/NosSmooth.Game/Data/Maps/MapEntities.cs +100 -0
@@ 0,0 1,100 @@
+//
+// MapEntities.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.Collections.Concurrent;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Packets.Enums;
+
+namespace NosSmooth.Game.Data.Maps;
+
+/// <summary>
+/// Thread-safe store for the entities on the map.
+/// </summary>
+public class MapEntities
+{
+ private readonly ConcurrentDictionary<long, IEntity> _entities;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MapEntities"/> class.
+ /// </summary>
+ public MapEntities()
+ {
+ _entities = new ConcurrentDictionary<long, IEntity>();
+ }
+
+ /// <summary>
+ /// Gets the given entity by id.
+ /// </summary>
+ /// <param name="id">The id of the entity.</param>
+ /// <returns>The entity, or null, if not found.</returns>
+ public IEntity? GetEntity(long id)
+ => _entities.GetValueOrDefault(id);
+
+ /// <summary>
+ /// Get the given entity by id.
+ /// </summary>
+ /// <param name="id">The id of the entity.</param>
+ /// <typeparam name="TEntity">The type of the entity.</typeparam>
+ /// <returns>The entity.</returns>
+ /// <exception cref="Exception">If the entity is not of the specified type.</exception>
+ public TEntity? GetEntity<TEntity>(long id)
+ {
+ var entity = GetEntity(id);
+ if (entity is null)
+ {
+ return default;
+ }
+
+ if (entity is TEntity tentity)
+ {
+ return tentity;
+ }
+
+ throw new Exception($"Could not find the entity with the given type {typeof(TEntity)}, was {entity.GetType()}");
+ }
+
+ /// <summary>
+ /// Add the given entity to the entities list.
+ /// </summary>
+ /// <param name="entity">The entity to add.</param>
+ internal void AddEntity(IEntity entity)
+ {
+ _entities.AddOrUpdate(entity.Id, _ => entity, (i, e) => entity);
+ }
+
+ /// <summary>
+ /// .
+ /// </summary>
+ /// <param name="entityId">The id of the entity.</param>
+ /// <param name="createAction">The action to execute on create.</param>
+ /// <param name="updateAction">The action to execute on update.</param>
+ /// <typeparam name="TEntity">The type of the entity.</typeparam>
+ internal void AddOrUpdateEntity<TEntity>
+ (long entityId, Func<long, TEntity> createAction, Func<long, TEntity, TEntity> updateAction)
+ where TEntity : IEntity
+ {
+ _entities.AddOrUpdate
+ (entityId, (key) => createAction(key), (key, entity) => updateAction(key, (TEntity)entity));
+ }
+
+ /// <summary>
+ /// Remove the given entity.
+ /// </summary>
+ /// <param name="entity">The entity to remove.</param>
+ internal void RemoveEntity(IEntity entity)
+ {
+ RemoveEntity(entity.Id);
+ }
+
+ /// <summary>
+ /// Remove the given entity.
+ /// </summary>
+ /// <param name="entityId">The id of the entity to remove.</param>
+ internal void RemoveEntity(long entityId)
+ {
+ _entities.TryRemove(entityId, out _);
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Maps/Miniland.cs => Core/NosSmooth.Game/Data/Maps/Miniland.cs +30 -0
@@ 0,0 1,30 @@
+//
+// Miniland.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.Data.Abstractions.Infos;
+
+namespace NosSmooth.Game.Data.Maps;
+
+/// <summary>
+/// Represents Miniland map that can contain miniland objects.
+/// </summary>
+/// <param name="Objects">The objects in the miniland.</param>
+public record Miniland
+(
+ long Id,
+ byte Type,
+ IMapInfo? Info,
+ MapEntities Entities,
+ IReadOnlyList<Portal> Portals,
+ IReadOnlyList<MinilandObject>? Objects
+) : Map
+(
+ Id,
+ Type,
+ Info,
+ Entities,
+ Portals
+);<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Maps/MinilandObject.cs => Core/NosSmooth.Game/Data/Maps/MinilandObject.cs +9 -0
@@ 0,0 1,9 @@
+//
+// MinilandObject.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.
+
+namespace NosSmooth.Game.Data.Maps;
+
+public record MinilandObject();<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Maps/Portal.cs => Core/NosSmooth.Game/Data/Maps/Portal.cs +25 -0
@@ 0,0 1,25 @@
+//
+// Portal.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;
+
+namespace NosSmooth.Game.Data.Maps;
+
+/// <summary>
+/// Represents map portal leading to another map.
+/// </summary>
+/// <param name="PortalId">The portal id.</param>
+/// <param name="Position">The position of the portal.</param>
+/// <param name="TargetMapId">The id of the target map.</param>
+public record Portal
+(
+ long PortalId,
+ Position Position,
+ long TargetMapId,
+ PortalType? PortalType,
+ bool IsDisabled
+);<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Maps/Timespace.cs => Core/NosSmooth.Game/Data/Maps/Timespace.cs +9 -0
@@ 0,0 1,9 @@
+//
+// Timespace.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.
+
+namespace NosSmooth.Game.Data.Maps;
+
+public record Timespace();<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Raids/Raid.cs => Core/NosSmooth.Game/Data/Raids/Raid.cs +12 -0
@@ 0,0 1,12 @@
+//
+// Raid.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.
+
+namespace NosSmooth.Game.Data.Raids;
+
+/// <summary>
+/// Represents nostale raid.
+/// </summary>
+public record Raid();<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Social/Family.cs => Core/NosSmooth.Game/Data/Social/Family.cs +22 -0
@@ 0,0 1,22 @@
+//
+// Family.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.
+
+namespace NosSmooth.Game.Data.Social;
+
+/// <summary>
+/// Represents nostale family entity.
+/// </summary>
+/// <param name="Id">The id of the family.</param>
+/// <param name="Name">The name of the family.</param>
+/// <param name="Level">The level of the entity.</param>
+public record Family
+(
+ string? Id,
+ short? Title,
+ string? Name,
+ byte? Level,
+ IReadOnlyList<bool>? Icons
+);<
\ No newline at end of file
A Core/NosSmooth.Game/Data/Social/Group.cs => Core/NosSmooth.Game/Data/Social/Group.cs +18 -0
@@ 0,0 1,18 @@
+//
+// Group.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.Entities;
+using OneOf;
+
+namespace NosSmooth.Game.Data.Social;
+
+/// <summary>
+/// Represents nostale group of players or pets and partners.
+/// </summary>
+/// <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
A Core/NosSmooth.Game/Errors/SkillOnCooldownError.cs => Core/NosSmooth.Game/Errors/SkillOnCooldownError.cs +15 -0
@@ 0,0 1,15 @@
+//
+// SkillOnCooldownError.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.Errors;
+
+/// <summary>
+/// Acts as an error specifying the skill is on cooldown.
+/// </summary>
+public record SkillOnCooldownError(Skill skill) : ResultError("The skill is on cooldown.");<
\ No newline at end of file
A Core/NosSmooth.Game/Events/Battle/AoESkillUsedEvent.cs => Core/NosSmooth.Game/Events/Battle/AoESkillUsedEvent.cs +26 -0
@@ 0,0 1,26 @@
+//
+// AoESkillUsedEvent.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 NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Packets.Enums.Battle;
+
+namespace NosSmooth.Game.Events.Battle;
+
+/// <summary>
+/// An AoE skill has been used. (bs packet)
+/// </summary>
+/// <remarks>
+/// The damage to various entities will be sent in respective Su packets.
+/// TODO find out connections between su and bs packets.
+/// </remarks>
+public record AoESkillUsedEvent
+(
+ ILivingEntity Caster,
+ Skill Skill,
+ Position TargetPosition
+) : IGameEvent;<
\ No newline at end of file
A Core/NosSmooth.Game/Events/Battle/SkillUsedEvent.cs => Core/NosSmooth.Game/Events/Battle/SkillUsedEvent.cs +32 -0
@@ 0,0 1,32 @@
+//
+// SkillUsedEvent.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 NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Packets.Enums.Battle;
+
+namespace NosSmooth.Game.Events.Battle;
+
+/// <summary>
+/// A skill has been used.
+/// </summary>
+/// <param name="Caster">The caster entity of the skill.</param>
+/// <param name="Target">The target entity of the skill.</param>
+/// <param name="Skill">The skill that has been used with the information about the skill.</param>
+/// <param name="SkillVNum">The vnum of the skill.</param>
+/// <param name="TargetPosition">The position of the target.</param>
+/// <param name="Hit"></param>
+/// <param name="Damage"></param>
+public record SkillUsedEvent
+(
+ ILivingEntity Caster,
+ ILivingEntity Target,
+ Skill Skill,
+ Position? TargetPosition,
+ HitMode? Hit,
+ uint Damage
+) : IGameEvent;<
\ No newline at end of file
A Core/NosSmooth.Game/Events/Characters/ReceivedCharacterDataEvent.cs => Core/NosSmooth.Game/Events/Characters/ReceivedCharacterDataEvent.cs +18 -0
@@ 0,0 1,18 @@
+//
+// ReceivedCharacterDataEvent.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;
+
+namespace NosSmooth.Game.Events.Characters;
+
+/// <summary>
+/// Represents received new updated character data.
+/// </summary>
+/// <param name="Character">The newly received data.</param>
+public record ReceivedCharacterDataEvent
+(
+ Character Character
+) : IGameEvent;<
\ No newline at end of file
A Core/NosSmooth.Game/Events/Characters/SkillReadyEvent.cs => Core/NosSmooth.Game/Events/Characters/SkillReadyEvent.cs +14 -0
@@ 0,0 1,14 @@
+//
+// SkillReadyEvent.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;
+
+namespace NosSmooth.Game.Events.Characters;
+
+/// <summary>
+/// The skill cooldown has been up.
+/// </summary>
+public record SkillReadyEvent(Skill? Skill, long SkillVNum) : IGameEvent;<
\ No newline at end of file
A Core/NosSmooth.Game/Events/Characters/SkillsReceivedEvent.cs => Core/NosSmooth.Game/Events/Characters/SkillsReceivedEvent.cs +15 -0
@@ 0,0 1,15 @@
+//
+// SkillsReceivedEvent.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;
+
+namespace NosSmooth.Game.Events.Characters;
+
+/// <summary>
+/// Received skills of the character.
+/// </summary>
+/// <param name="Skills">The skills.</param>
+public record SkillsReceivedEvent(Skills Skills) : IGameEvent;<
\ No newline at end of file
R Core/NosSmooth.Game/Events/Handlers/EventDispatcher.cs => Core/NosSmooth.Game/Events/Core/EventDispatcher.cs +52 -51
@@ 1,51 1,52 @@
-//
-// EventDispatcher.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 Microsoft.Extensions.DependencyInjection;
-using Remora.Results;
-
-namespace NosSmooth.Game.Events.Handlers;
-
-/// <summary>
-/// Dispatches <see cref="IGameResponder"/> with <see cref="IGameEvent"/>.
-/// </summary>
-public class EventDispatcher
-{
- private readonly IServiceProvider _provider;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="EventDispatcher"/> class.
- /// </summary>
- /// <param name="provider">The services provider.</param>
- public EventDispatcher(IServiceProvider provider)
- {
- _provider = provider;
- }
-
- /// <summary>
- /// Dispatches game responders that are registered in the service collection.
- /// </summary>
- /// <param name="event">The event to dispatch.</param>
- /// <param name="ct">The cancellation token for cancelling the operation.</param>
- /// <typeparam name="TEvent">The type of the event.</typeparam>
- /// <returns>A result that may or may not have succeeded.</returns>
- public async Task<Result> DispatchEvent<TEvent>(TEvent @event, CancellationToken ct = default)
- where TEvent : IGameEvent
- {
- var results = await Task.WhenAll(
- _provider
- .GetServices<IGameResponder<TEvent>>()
- .Select(responder => responder.Respond(@event, ct))
- );
-
- return results.Length switch
- {
- 0 => Result.FromSuccess(),
- 1 => results[0],
- _ => new AggregateError(results.Cast<IResult>().ToArray()),
- };
- }
-}>
\ No newline at end of file
+//
+// EventDispatcher.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 Microsoft.Extensions.DependencyInjection;
+using Remora.Results;
+
+namespace NosSmooth.Game.Events.Core;
+
+/// <summary>
+/// Dispatches <see cref="IGameResponder"/> with <see cref="IGameEvent"/>.
+/// </summary>
+public class EventDispatcher
+{
+ private readonly IServiceProvider _provider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EventDispatcher"/> class.
+ /// </summary>
+ /// <param name="provider">The services provider.</param>
+ public EventDispatcher(IServiceProvider provider)
+ {
+ _provider = provider;
+ }
+
+ /// <summary>
+ /// Dispatches game responders that are registered in the service collection.
+ /// </summary>
+ /// <param name="event">The event to dispatch.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <typeparam name="TEvent">The type of the event.</typeparam>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public async Task<Result> DispatchEvent<TEvent>(TEvent @event, CancellationToken ct = default)
+ where TEvent : IGameEvent
+ {
+ using var scope = _provider.CreateScope();
+ var results = await Task.WhenAll(
+ scope.ServiceProvider
+ .GetServices<IGameResponder<TEvent>>()
+ .Select(responder => responder.Respond(@event, ct))
+ );
+
+ return results.Length switch
+ {
+ 0 => Result.FromSuccess(),
+ 1 => results[0],
+ _ => new AggregateError(results.Cast<IResult>().ToArray()),
+ };
+ }
+}
R Core/NosSmooth.Game/Events/Handlers/IGameResponder.cs => Core/NosSmooth.Game/Events/Core/IGameResponder.cs +33 -34
@@ 1,34 1,33 @@
-//
-// IGameResponder.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 OneOf.Types;
-using Remora.Results;
-
-namespace NosSmooth.Game.Events.Handlers;
-
-/// <summary>
-/// Represents interface for classes that respond to <see cref="IGameEvent"/>.
-/// </summary>
-public interface IGameResponder
-{
-}
-
-/// <summary>
-/// Represents interface for classes that respond to game events.
-/// Responds to <typeparamref name="TPacket"/>.
-/// </summary>
-/// <typeparam name="TEvent">The event type this responder responds to.</typeparam>
-public interface IGameResponder<TEvent> : IGameResponder
- where TEvent : IGameEvent
-{
- /// <summary>
- /// Respond to the given packet.
- /// </summary>
- /// <param name="packet">The packet to respond to.</param>
- /// <param name="ct">The cancellation token for cancelling the operation.</param>
- /// <returns>A result that may or may not have succeeded.</returns>
- public Task<Result> Respond(TEvent packet, CancellationToken ct = default);
-}>
\ No newline at end of file
+//
+// IGameResponder.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 Remora.Results;
+
+namespace NosSmooth.Game.Events.Core;
+
+/// <summary>
+/// Represents interface for classes that respond to <see cref="IGameEvent"/>.
+/// </summary>
+public interface IGameResponder
+{
+}
+
+/// <summary>
+/// Represents interface for classes that respond to game events.
+/// Responds to <typeparamref name="TEvent"/>.
+/// </summary>
+/// <typeparam name="TEvent">The event type this responder responds to.</typeparam>
+public interface IGameResponder<TEvent> : IGameResponder
+ where TEvent : IGameEvent
+{
+ /// <summary>
+ /// Respond to the given packet.
+ /// </summary>
+ /// <param name="gameEvent">The packet to respond to.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> Respond(TEvent gameEvent, CancellationToken ct = default);
+}
A Core/NosSmooth.Game/Events/Entities/EntityDiedEvent.cs => Core/NosSmooth.Game/Events/Entities/EntityDiedEvent.cs +21 -0
@@ 0,0 1,21 @@
+//
+// EntityDiedEvent.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 NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Events.Characters;
+
+namespace NosSmooth.Game.Events.Entities;
+
+/// <summary>
+/// An entity has died.
+/// </summary>
+/// <remarks>
+/// Is not emitted for the character, see <see cref="CharacterDiedEvent"/>.
+/// </remarks>
+/// <param name="Entity">The entity that has died.</param>
+/// <param name="KillSkill">The skill that was used to kill the entity, if known.</param>
+public record EntityDiedEvent(ILivingEntity Entity, Skill? KillSkill) : IGameEvent;<
\ No newline at end of file
A Core/NosSmooth.Game/Events/Entities/EntityJoinedMapEvent.cs => Core/NosSmooth.Game/Events/Entities/EntityJoinedMapEvent.cs +15 -0
@@ 0,0 1,15 @@
+//
+// EntityJoinedMapEvent.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.Entities;
+
+namespace NosSmooth.Game.Events.Entities;
+
+/// <summary>
+/// The given entity has joined the map.
+/// </summary>
+/// <param name="Entity">The entity.</param>
+public record EntityJoinedMapEvent(IEntity Entity) : IGameEvent;<
\ No newline at end of file
A Core/NosSmooth.Game/Events/Entities/EntityLeftMapEvent.cs => Core/NosSmooth.Game/Events/Entities/EntityLeftMapEvent.cs +21 -0
@@ 0,0 1,21 @@
+//
+// EntityLeftMapEvent.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.Entities;
+using NosSmooth.Game.Data.Maps;
+
+namespace NosSmooth.Game.Events.Entities;
+
+/// <summary>
+/// An entity has left the map.
+/// </summary>
+/// <param name="Entity">The entity that has left.</param>
+public record EntityLeftMapEvent
+(
+ IEntity Entity,
+ Portal? Portal,
+ bool? Died
+) : IGameEvent;<
\ No newline at end of file
A Core/NosSmooth.Game/Events/Entities/EntityMovedEvent.cs => Core/NosSmooth.Game/Events/Entities/EntityMovedEvent.cs +23 -0
@@ 0,0 1,23 @@
+//
+// EntityMovedEvent.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.Entities;
+using NosSmooth.Game.Data.Info;
+
+namespace NosSmooth.Game.Events.Entities;
+
+/// <summary>
+/// An entity has moved.
+/// </summary>
+/// <param name="Entity">The entity.</param>
+/// <param name="OldPosition">The previous position of the entity.</param>
+/// <param name="NewPosition">The new position of the entity.</param>
+public record EntityMovedEvent
+(
+ IEntity Entity,
+ Position? OldPosition,
+ Position NewPosition
+) : IGameEvent;<
\ No newline at end of file
A Core/NosSmooth.Game/Events/Entities/EntityRevivedEvent.cs => Core/NosSmooth.Game/Events/Entities/EntityRevivedEvent.cs +15 -0
@@ 0,0 1,15 @@
+//
+// EntityRevivedEvent.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.Entities;
+
+namespace NosSmooth.Game.Events.Entities;
+
+/// <summary>
+/// The given entity has been revived.
+/// </summary>
+/// <param name="Entity"></param>
+public record EntityRevivedEvent(ILivingEntity Entity);<
\ No newline at end of file
A Core/NosSmooth.Game/Events/Entities/EntityStunnedEvent.cs => Core/NosSmooth.Game/Events/Entities/EntityStunnedEvent.cs +18 -0
@@ 0,0 1,18 @@
+//
+// EntityStunnedEvent.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.Entities;
+
+namespace NosSmooth.Game.Events.Entities;
+
+/// <summary>
+/// The given entity has been stunned or unstunned.
+/// </summary>
+/// <param name="Entity">The entity.</param>
+/// <param name="CantMove">Whether the entity cannot move.</param>
+/// <param name="CantAttack">Whether the entity cannot attack.</param>
+public record EntityStunnedEvent(ILivingEntity Entity, bool CantMove, bool CantAttack)
+ : IGameEvent;<
\ No newline at end of file
A Core/NosSmooth.Game/Events/Entities/ItemDroppedEvent.cs => Core/NosSmooth.Game/Events/Entities/ItemDroppedEvent.cs +15 -0
@@ 0,0 1,15 @@
+//
+// ItemDroppedEvent.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.Entities;
+
+namespace NosSmooth.Game.Events.Entities;
+
+/// <summary>
+/// An item has been dropped.
+/// </summary>
+/// <param name="Item">The item that has been dropped.</param>
+public record ItemDroppedEvent(IEntity Item) : IGameEvent;<
\ No newline at end of file
A Core/NosSmooth.Game/Events/Map/MapChangedEvent.cs => Core/NosSmooth.Game/Events/Map/MapChangedEvent.cs +18 -0
@@ 0,0 1,18 @@
+//
+// MapChangedEvent.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.
+
+namespace NosSmooth.Game.Events.Map;
+
+/// <summary>
+/// A map has been changed.
+/// </summary>
+/// <param name="PreviousMap">The previous map.</param>
+/// <param name="CurrentMap">The new map.</param>
+public record MapChangedEvent
+(
+ Data.Maps.Map? PreviousMap,
+ Data.Maps.Map CurrentMap
+) : IGameEvent;<
\ No newline at end of file
M Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs +105 -44
@@ 1,44 1,105 @@
-
- /// <summary>
- /// Adds the given game event responder.
- /// </summary>
- /// <param name="serviceCollection">The service collection.</param>
- /// <typeparam name="TGameResponder">The responder to add.</typeparam>
- /// <returns>The collection.</returns>
- public static IServiceCollection AddGameResponder<TGameResponder>(this IServiceCollection serviceCollection)
- where TGameResponder : IGameResponder
- {
- return serviceCollection.AddGameResponder(typeof(TGameResponder));
- }
-
- /// <summary>
- /// Adds the given game event responder.
- /// </summary>
- /// <param name="serviceCollection">The service collection.</param>
- /// <param name="gameResponder">The type of the event responder.</param>
- /// <returns>The collection.</returns>
- public static IServiceCollection AddGameResponder(this IServiceCollection serviceCollection, Type gameResponder)
- {
- if (!gameResponder.GetInterfaces().Any(
- i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IGameResponder<>)
- ))
- {
- throw new ArgumentException(
- $"{nameof(gameResponder)} should implement IGameResponder.",
- nameof(gameResponder));
- }
-
- var handlerTypeInterfaces = gameResponder.GetInterfaces();
- var handlerInterfaces = handlerTypeInterfaces.Where
- (
- r => r.IsGenericType && r.GetGenericTypeDefinition() == typeof(IGameResponder<>)
- );
-
- foreach (var handlerInterface in handlerInterfaces)
- {
- serviceCollection.AddScoped(handlerInterface, gameResponder);
- }
-
- return serviceCollection;
- }
-}>
\ No newline at end of file
+//
+// ServiceCollectionExtensions.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 Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Game.Apis;
+using NosSmooth.Game.Events.Core;
+using NosSmooth.Game.PacketHandlers.Characters;
+using NosSmooth.Game.PacketHandlers.Entities;
+using NosSmooth.Game.PacketHandlers.Map;
+using NosSmooth.Game.PacketHandlers.Specialists;
+
+namespace NosSmooth.Game.Extensions;
+
+/// <summary>
+/// Contains extension methods for <see cref="IServiceCollection"/>.
+/// </summary>
+public static class ServiceCollectionExtensions
+{
+ /// <summary>
+ /// Adds handling of nostale packets, registering <see cref="Game"/> singleton and dispatching of game events.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <returns>The collection.</returns>
+ public static IServiceCollection AddNostaleGame(this IServiceCollection serviceCollection)
+ {
+ serviceCollection
+ .AddNostaleCore()
+ .AddMemoryCache()
+ .TryAddSingleton<EventDispatcher>();
+ serviceCollection.TryAddSingleton<Game>();
+
+ serviceCollection
+ .AddPacketResponder<CharacterInitResponder>()
+ .AddPacketResponder<SkillResponder>()
+ .AddPacketResponder<WalkResponder>()
+ .AddPacketResponder<SkillUsedResponder>()
+ .AddPacketResponder<AoeSkillUsedResponder>()
+ .AddPacketResponder<AtResponder>()
+ .AddPacketResponder<CMapResponder>()
+ .AddPacketResponder<DropResponder>()
+ .AddPacketResponder<GpPacketResponder>()
+ .AddPacketResponder<InResponder>()
+ .AddPacketResponder<MoveResponder>()
+ .AddPacketResponder<OutResponder>()
+ .AddPacketResponder<StatPacketResponder>()
+ .AddPacketResponder<StPacketResponder>()
+ .AddPacketResponder<CondPacketResponder>()
+ .AddPacketResponder<SpResponder>()
+ .AddPacketResponder<EqResponder>();
+
+ serviceCollection
+ .AddTransient<NostaleChatPacketApi>()
+ .AddTransient<NostaleSkillsPacketApi>();
+
+ return serviceCollection;
+ }
+
+ /// <summary>
+ /// Adds the given game event responder.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <typeparam name="TGameResponder">The responder to add.</typeparam>
+ /// <returns>The collection.</returns>
+ public static IServiceCollection AddGameResponder<TGameResponder>(this IServiceCollection serviceCollection)
+ where TGameResponder : IGameResponder
+ {
+ return serviceCollection.AddGameResponder(typeof(TGameResponder));
+ }
+
+ /// <summary>
+ /// Adds the given game event responder.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <param name="gameResponder">The type of the event responder.</param>
+ /// <returns>The collection.</returns>
+ public static IServiceCollection AddGameResponder(this IServiceCollection serviceCollection, Type gameResponder)
+ {
+ if (!gameResponder.GetInterfaces().Any(
+ i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IGameResponder<>)
+ ))
+ {
+ throw new ArgumentException(
+ $"{nameof(gameResponder)} should implement IGameResponder.",
+ nameof(gameResponder));
+ }
+
+ var handlerTypeInterfaces = gameResponder.GetInterfaces();
+ var handlerInterfaces = handlerTypeInterfaces.Where
+ (
+ r => r.IsGenericType && r.GetGenericTypeDefinition() == typeof(IGameResponder<>)
+ );
+
+ foreach (var handlerInterface in handlerInterfaces)
+ {
+ serviceCollection.AddScoped(handlerInterface, gameResponder);
+ }
+
+ return serviceCollection;
+ }
+}
A Core/NosSmooth.Game/Extensions/SkillsExtensions.cs => Core/NosSmooth.Game/Extensions/SkillsExtensions.cs +74 -0
@@ 0,0 1,74 @@
+//
+// 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="castId">The cast id to search for.</param>
+ /// <returns>The skill, if found.</returns>
+ public static Result<Skill> TryGetSkillByCastId(this Skills skills, short castId)
+ {
+ if (skills.PrimarySkill.Info?.CastId == castId)
+ {
+ return skills.PrimarySkill;
+ }
+
+ if (skills.SecondarySkill.Info?.CastId == castId)
+ {
+ return skills.SecondarySkill;
+ }
+
+ foreach (Skill skill in skills.OtherSkills)
+ {
+ if (skill.Info?.CastId == castId)
+ {
+ return skill;
+ }
+ }
+
+ return new NotFoundError();
+ }
+
+ /// <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> TryGetSkillByVNum(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/Game.cs => Core/NosSmooth.Game/Game.cs +197 -0
@@ 0,0 1,197 @@
+//
+// Game.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 Microsoft.Extensions.Options;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Maps;
+using NosSmooth.Game.Data.Raids;
+
+namespace NosSmooth.Game;
+
+/// <summary>
+/// Represents base nostale game class with the character, current map, friends and current raid.
+/// </summary>
+public class Game
+{
+ private readonly GameOptions _options;
+ private Map? _currentMap;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Game"/> class.
+ /// </summary>
+ /// <param name="options">The options for the game.</param>
+ public Game(IOptions<GameOptions> options)
+ {
+ Semaphores = new GameSemaphores();
+ _options = options.Value;
+ }
+
+ /// <summary>
+ /// Gets the game semaphores.
+ /// </summary>
+ internal GameSemaphores Semaphores { get; }
+
+ /// <summary>
+ /// Gets the playing character of the client.
+ /// </summary>
+ public Character? Character { get; internal set; }
+
+ /// <summary>
+ /// Gets the current map of the client.
+ /// </summary>
+ /// <remarks>
+ /// Will be null until current map packet is received.
+ /// </remarks>
+ public Map? CurrentMap
+ {
+ get => _currentMap;
+ internal set
+ {
+ _currentMap = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the active raid the client is currently on.
+ /// </summary>
+ /// <remarks>
+ /// May be null if there is no raid in progress.
+ /// </remarks>
+ public Raid? CurrentRaid { get; internal set; }
+
+ /// <summary>
+ /// Creates the character if it is null, or updates the current character.
+ /// </summary>
+ /// <param name="create">The function for creating the character.</param>
+ /// <param name="update">The function for updating the character.</param>
+ /// <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
+ (
+ Func<Character> create,
+ Func<Character, Character> update,
+ bool releaseSemaphore = true,
+ CancellationToken ct = default
+ )
+ {
+ return await CreateOrUpdateAsync
+ (
+ GameSemaphoreType.Character,
+ () => Character,
+ c => Character = c,
+ create,
+ update,
+ releaseSemaphore,
+ ct
+ );
+ }
+
+ /// <summary>
+ /// Creates the map if it is null, or updates the current map.
+ /// </summary>
+ /// <param name="create">The function for creating the map.</param>
+ /// <param name="releaseSemaphore">Whether to release the semaphore used for changing the map.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>The updated character.</returns>
+ internal async Task<Map?> CreateMapAsync
+ (
+ Func<Map?> create,
+ bool releaseSemaphore = true,
+ CancellationToken ct = default
+ )
+ {
+ return await CreateAsync
+ (
+ GameSemaphoreType.Map,
+ m => CurrentMap = m,
+ create,
+ releaseSemaphore,
+ ct
+ );
+ }
+
+ /// <summary>
+ /// Creates the map if it is null, or updates the current map.
+ /// </summary>
+ /// <param name="create">The function for creating the map.</param>
+ /// <param name="update">The function for updating the map.</param>
+ /// <param name="releaseSemaphore">Whether to release the semaphore used for changing the map.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>The updated character.</returns>
+ internal async Task<Map?> CreateOrUpdateMapAsync
+ (
+ Func<Map?> create,
+ Func<Map?, Map?> update,
+ bool releaseSemaphore = true,
+ CancellationToken ct = default
+ )
+ {
+ return await CreateOrUpdateAsync<Map?>
+ (
+ GameSemaphoreType.Map,
+ () => CurrentMap,
+ m => CurrentMap = m,
+ create,
+ update,
+ releaseSemaphore,
+ ct
+ );
+ }
+
+ private async Task<T> CreateAsync<T>
+ (
+ GameSemaphoreType type,
+ Action<T> set,
+ Func<T> create,
+ bool releaseSemaphore = true,
+ CancellationToken ct = default
+ )
+ {
+ await Semaphores.AcquireSemaphore(type, ct);
+
+ var current = create();
+ set(current);
+ if (releaseSemaphore)
+ {
+ Semaphores.ReleaseSemaphore(type);
+ }
+
+ return current;
+ }
+
+ private async Task<T> CreateOrUpdateAsync<T>
+ (
+ GameSemaphoreType type,
+ Func<T?> get,
+ Action<T> set,
+ Func<T> create,
+ Func<T, T> update,
+ bool releaseSemaphore = true,
+ CancellationToken ct = default
+ )
+ {
+ await Semaphores.AcquireSemaphore(type, ct);
+
+ var current = get();
+ if (current is null)
+ {
+ current = create();
+ }
+ else
+ {
+ current = update(current);
+ }
+
+ set(current);
+ if (releaseSemaphore)
+ {
+ Semaphores.ReleaseSemaphore(type);
+ }
+
+ return current;
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/GameOptions.cs => Core/NosSmooth.Game/GameOptions.cs +18 -0
@@ 0,0 1,18 @@
+//
+// GameOptions.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.
+
+namespace NosSmooth.Game;
+
+/// <summary>
+/// Options for <see cref="Game"/>.
+/// </summary>
+public class GameOptions
+{
+ /// <summary>
+ /// Duration to cache entities for after changing maps in seconds.
+ /// </summary>
+ public ulong EntityCacheDuration { get; set; }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/GameSemaphoreType.cs => Core/NosSmooth.Game/GameSemaphoreType.cs +28 -0
@@ 0,0 1,28 @@
+//
+// GameSemaphoreType.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.
+
+namespace NosSmooth.Game;
+
+/// <summary>
+/// Type of game semaphore.
+/// </summary>
+public enum GameSemaphoreType
+{
+ /// <summary>
+ /// The semaphore for the character.
+ /// </summary>
+ Character,
+
+ /// <summary>
+ /// The semaphore for the map.
+ /// </summary>
+ Map,
+
+ /// <summary>
+ /// The semaphore for the raid.
+ /// </summary>
+ Raid
+}<
\ No newline at end of file
A Core/NosSmooth.Game/GameSemaphores.cs => Core/NosSmooth.Game/GameSemaphores.cs +47 -0
@@ 0,0 1,47 @@
+//
+// GameSemaphores.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.
+
+namespace NosSmooth.Game;
+
+/// <summary>
+/// Holds information about semaphores for synchornizing the game packet data.
+/// </summary>
+internal class GameSemaphores
+{
+ private Dictionary<GameSemaphoreType, SemaphoreSlim> _semaphores;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GameSemaphores"/> class.
+ /// </summary>
+ public GameSemaphores()
+ {
+ _semaphores = new Dictionary<GameSemaphoreType, SemaphoreSlim>();
+ foreach (var type in Enum.GetValues(typeof(GameSemaphoreType)).Cast<GameSemaphoreType>())
+ {
+ _semaphores[type] = new SemaphoreSlim(1, 1);
+ }
+ }
+
+ /// <summary>
+ /// Acquire the given semaphore.
+ /// </summary>
+ /// <param name="semaphoreType">The semaphore type.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A task that may or may not have succeeded.</returns>
+ public async Task AcquireSemaphore(GameSemaphoreType semaphoreType, CancellationToken ct = default)
+ {
+ await _semaphores[semaphoreType].WaitAsync(ct);
+ }
+
+ /// <summary>
+ /// Release the acquired semaphore.
+ /// </summary>
+ /// <param name="semaphoreType">The semaphore type.</param>
+ public void ReleaseSemaphore(GameSemaphoreType semaphoreType)
+ {
+ _semaphores[semaphoreType].Release();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Helpers/EntityHelpers.cs => Core/NosSmooth.Game/Helpers/EntityHelpers.cs +51 -0
@@ 0,0 1,51 @@
+//
+// EntityHelpers.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.Entities;
+using NosSmooth.Packets.Enums;
+
+namespace NosSmooth.Game.Helpers;
+
+/// <summary>
+/// Helper methods for various operations with entities.
+/// </summary>
+public static class EntityHelpers
+{
+ /// <summary>
+ /// Create an entity from the given type and id.
+ /// </summary>
+ /// <param name="type">The entity type.</param>
+ /// <param name="entityId">The entity id.</param>
+ /// <returns>The entity.</returns>
+ public static IEntity CreateEntity(EntityType type, long entityId)
+ {
+ switch (type)
+ {
+ case EntityType.Npc:
+ return new Npc
+ {
+ Id = entityId
+ };
+ case EntityType.Monster:
+ return new Monster
+ {
+ Id = entityId
+ };
+ case EntityType.Player:
+ return new Player
+ {
+ Id = entityId
+ };
+ case EntityType.Object:
+ return new GroundItem
+ {
+ Id = entityId
+ };
+ }
+
+ throw new Exception("Unknown entity type.");
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs => Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs +97 -0
@@ 0,0 1,97 @@
+//
+// EquipmentHelpers.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.Reflection;
+using NosSmooth.Data.Abstractions;
+using NosSmooth.Game.Data.Items;
+using NosSmooth.Packets.Server.Maps;
+using NosSmooth.Packets.Server.Weapons;
+using Remora.Results;
+
+namespace NosSmooth.Game.Helpers;
+
+/// <summary>
+/// Helpers for creating equipment from packets.
+/// </summary>
+public static class EquipmentHelpers
+{
+ /// <summary>
+ /// Create <see cref="Equipment"/> from the given in equipment subpacket.
+ /// </summary>
+ /// <param name="infoService">The info service.</param>
+ /// <param name="equipmentSubPacket">The subpacket.</param>
+ /// <param name="weaponUpgradeRare">The weapon upgrade.</param>
+ /// <param name="armorUpgradeRare">The armor upgrade.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>The equipment or an error.</returns>
+ public static async Task<Equipment> CreateEquipmentFromInSubpacketAsync
+ (
+ IInfoService infoService,
+ InEquipmentSubPacket equipmentSubPacket,
+ UpgradeRareSubPacket? weaponUpgradeRare = default,
+ UpgradeRareSubPacket? armorUpgradeRare = default,
+ CancellationToken ct = default
+ )
+ {
+ var fairy = await CreateItemAsync(infoService, equipmentSubPacket.FairyVNum, ct);
+ var mask = await CreateItemAsync(infoService, equipmentSubPacket.MaskVNum, ct);
+ var hat = await CreateItemAsync(infoService, equipmentSubPacket.HatVNum, ct);
+ var costumeSuit = await CreateItemAsync(infoService, equipmentSubPacket.CostumeSuitVNum, ct);
+ var costumeHat = await CreateItemAsync(infoService, equipmentSubPacket.CostumeHatVNum, ct);
+ var mainWeapon = await CreateItemAsync(infoService, equipmentSubPacket.MainWeaponVNum, weaponUpgradeRare, ct);
+ var secondaryWeapon = await CreateItemAsync(infoService, equipmentSubPacket.SecondaryWeaponVNum, null, ct);
+ var armor = await CreateItemAsync(infoService, equipmentSubPacket.ArmorVNum, armorUpgradeRare, ct);
+
+ return new Equipment
+ (
+ hat,
+ armor,
+ mainWeapon,
+ secondaryWeapon,
+ mask,
+ fairy,
+ costumeSuit,
+ costumeHat,
+ equipmentSubPacket.WeaponSkin,
+ equipmentSubPacket.WingSkin
+ );
+ }
+
+ private static async Task<Item?> CreateItemAsync(IInfoService infoService, int? itemVNum, CancellationToken ct = default)
+ {
+ if (itemVNum is null)
+ {
+ return null;
+ }
+
+ var itemInfo = await infoService.GetItemInfoAsync(itemVNum.Value, ct);
+
+ return new Item
+ (
+ itemVNum.Value,
+ itemInfo.IsSuccess ? itemInfo.Entity : null
+ );
+ }
+
+ private static async Task<UpgradeableItem?> CreateItemAsync
+ (IInfoService infoService, int? itemVNum, UpgradeRareSubPacket? upgradeRareSubPacket, CancellationToken ct = default)
+ {
+ if (itemVNum is null)
+ {
+ return null;
+ }
+
+ var itemInfo = await infoService.GetItemInfoAsync(itemVNum.Value, ct);
+
+ return new UpgradeableItem
+ (
+ itemVNum.Value,
+ itemInfo.IsSuccess ? itemInfo.Entity : null,
+ upgradeRareSubPacket?.Upgrade,
+ upgradeRareSubPacket?.Rare
+ );
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/IsExternalInit.cs => Core/NosSmooth.Game/IsExternalInit.cs +16 -0
@@ 0,0 1,16 @@
+//
+// IsExternalInit.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.
+
+// ReSharper disable once CheckNamespace
+namespace System.Runtime.CompilerServices
+{
+ /// <summary>
+ /// Dummy.
+ /// </summary>
+ public class IsExternalInit
+ {
+ }
+}<
\ No newline at end of file
M Core/NosSmooth.Game/NosSmooth.Game.csproj => Core/NosSmooth.Game/NosSmooth.Game.csproj +28 -10
@@ 1,10 1,28 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
- <PropertyGroup>
- <ImplicitUsings>enable</ImplicitUsings>
- <Nullable>enable</Nullable>
- <LangVersion>10</LangVersion>
- <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
- </PropertyGroup>
-
-</Project>
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <LangVersion>10</LangVersion>
+ <TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
+ <Description>NosSmooth Game library handling the current game state by responding to packets.</Description>
+ <RepositoryUrl>https://github.com/Rutherther/NosSmooth/</RepositoryUrl>
+ <PackageLicenseExpression>MIT</PackageLicenseExpression>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Folder Include="Apis" />
+ <Folder Include="Events\Players" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\Data\NosSmooth.Data.Abstractions\NosSmooth.Data.Abstractions.csproj" />
+ <ProjectReference Include="..\NosSmooth.Core\NosSmooth.Core.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" />
+ <PackageReference Include="OneOf" Version="3.0.205" />
+ </ItemGroup>
+
+</Project>
A Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs +170 -0
@@ 0,0 1,170 @@
+//
+// 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 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 NosSmooth.Packets.Server.Players;
+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 packet = packetArgs.Packet;
+ var character = await _game.CreateOrUpdateCharacterAsync
+ (
+ () => new Character
+ {
+ Family = new Family(packet.FamilyId, null, packet.FamilyName, packet.FamilyLevel, null),
+ Group = new Group(packet.GroupId, default, default),
+ Id = packet.CharacterId,
+ Name = packet.Name,
+ Authority = packet.Authority,
+ Sex = packet.Sex,
+ HairStyle = packet.HairStyle,
+ HairColor = packet.HairColor,
+ Class = packet.Class,
+ Icon = packet.Icon,
+ Compliment = packet.Compliment,
+ Morph = new Morph(packet.MorphVNum, packet.MorphUpgrade),
+ IsInvisible = packet.IsInvisible,
+ ArenaWinner = packet.ArenaWinner
+ },
+ (character) =>
+ {
+ character.Id = packet.CharacterId;
+ character.Authority = packet.Authority;
+ character.Sex = packet.Sex;
+ character.HairStyle = packet.HairStyle;
+ character.HairColor = packet.HairColor;
+ character.Class = packet.Class;
+ character.Icon = packet.Icon;
+ character.Compliment = packet.Compliment;
+ character.Group = (character.Group ?? new Group(packet.GroupId, null, null)) with
+ {
+ Id = packet.GroupId
+ };
+ character.Morph = (character.Morph ?? new Morph(packet.MorphVNum, packet.MorphUpgrade)) with
+ {
+ VNum = packet.MorphVNum, Upgrade = packet.MorphUpgrade
+ };
+ character.ArenaWinner = packet.ArenaWinner;
+ character.IsInvisible = packet.IsInvisible;
+ character.Family = new Family(packet.FamilyId, null, packet.FamilyName, packet.FamilyLevel, null);
+ return character;
+ },
+ ct: ct
+ );
+
+ if (character != oldCharacter)
+ {
+ return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(character), ct);
+ }
+
+ return Result.FromSuccess();
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<LevPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ var oldCharacter = _game.Character;
+
+ var character = await _game.CreateOrUpdateCharacterAsync
+ (
+ () => new Character
+ {
+ SkillCp = packet.SkillCp,
+ Reputation = packet.Reputation,
+ PlayerLevel = new Level(packet.Level, packet.LevelXp, packet.XpLoad),
+ JobLevel = new Level(packet.JobLevel, packet.JobLevelXp, packet.JobXpLoad),
+ HeroLevelStruct = new Level(packet.HeroLevel, packet.HeroLevelXp, packet.HeroXpLoad)
+ },
+ (character) =>
+ {
+ character.SkillCp = packet.SkillCp;
+ character.Reputation = packet.Reputation;
+ character.PlayerLevel = new Level(packet.Level, packet.LevelXp, packet.XpLoad);
+ character.JobLevel = new Level(packet.JobLevel, packet.JobLevelXp, packet.JobXpLoad);
+ character.HeroLevelStruct = new Level(packet.HeroLevel, packet.HeroLevelXp, packet.HeroXpLoad);
+ return character;
+ },
+ ct: ct
+ );
+
+ if (character != oldCharacter)
+ {
+ return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(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;
+
+ if (oldCharacter is null || oldCharacter.Id != packetArgs.Packet.EntityId)
+ { // Not the current character.
+ return Result.FromSuccess();
+ }
+
+ var character = await _game.CreateOrUpdateCharacterAsync
+ (
+ () => throw new NotImplementedException(),
+ (character) =>
+ {
+ character.Morph = new Morph
+ (
+ packet.MorphVNum,
+ packet.MorphUpgrade,
+ packet.MorphDesign,
+ packet.MorphBonus,
+ packet.MorphSkin
+ );
+
+ character.Size = packet.Size;
+ return character;
+ },
+ ct: ct
+ );
+
+ if (oldCharacter != character)
+ {
+ return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(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 +125 -0
@@ 0,0 1,125 @@
+//
+// 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 Microsoft.Extensions.Logging;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Core.Packets;
+using NosSmooth.Data.Abstractions;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Events.Characters;
+using NosSmooth.Game.Events.Core;
+using NosSmooth.Packets.Server.Skills;
+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;
+ private readonly IInfoService _infoService;
+ private readonly ILogger<SkillResponder> _logger;
+
+ /// <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>
+ /// <param name="infoService">The info service.</param>
+ /// <param name="logger">The logger.</param>
+ public SkillResponder
+ (
+ Game game,
+ EventDispatcher eventDispatcher,
+ IInfoService infoService,
+ ILogger<SkillResponder> logger
+ )
+ {
+ _game = game;
+ _eventDispatcher = eventDispatcher;
+ _infoService = infoService;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<SkiPacket> packetArgs, CancellationToken ct = default)
+ {
+ // TODO: put all code into CreateOrUpdate to avoid concurrent problems.
+ var packet = packetArgs.Packet;
+
+ Skill primarySkill, secondarySkill;
+
+ var character = _game.Character;
+
+ if (character is not null && packet.PrimarySkillVNum == character.Skills?.PrimarySkill.SkillVNum)
+ {
+ primarySkill = character.Skills.PrimarySkill;
+ }
+ else
+ {
+ primarySkill = await CreateSkill(packet.PrimarySkillVNum, default);
+ }
+
+ if (character is not null && packet.PrimarySkillVNum == packet.SecondarySkillVNum)
+ {
+ secondarySkill = primarySkill;
+ }
+ else if (character is not null && packet.SecondarySkillVNum == character.Skills?.SecondarySkill.SkillVNum)
+ {
+ secondarySkill = character.Skills.SecondarySkill;
+ }
+ else
+ {
+ secondarySkill = await CreateSkill(packet.SecondarySkillVNum, default);
+ }
+
+ var skillsFromPacket = packet.SkillSubPackets?.Select(x => x.SkillVNum).ToList() ?? new List<int>();
+ var skillsFromCharacter = character?.Skills is null
+ ? new List<int>()
+ : 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(await CreateSkill(newSkill, default));
+ }
+
+ var skills = new Skills(primarySkill, secondarySkill, otherSkillsFromCharacter);
+
+ await _game.CreateOrUpdateCharacterAsync
+ (
+ () => new Character { Skills = skills },
+ c =>
+ {
+ c.Skills = skills;
+ return c;
+ },
+ ct: ct
+ );
+
+ await _eventDispatcher.DispatchEvent(new SkillsReceivedEvent(skills), ct);
+
+ return Result.FromSuccess();
+ }
+
+ private async Task<Skill> CreateSkill(int vnum, int? level)
+ {
+ var infoResult = await _infoService.GetSkillInfoAsync(vnum);
+ if (!infoResult.IsSuccess)
+ {
+ _logger.LogWarning("Could not obtain a skill info for vnum {vnum}: {error}", vnum, infoResult.ToFullString());
+ }
+
+ return new Skill(vnum, level, infoResult.IsSuccess ? infoResult.Entity : null);
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Characters/StatPacketResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/StatPacketResponder.cs +70 -0
@@ 0,0 1,70 @@
+//
+// StatPacketResponder.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.Core.Packets;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Packets.Server.Entities;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Characters;
+
+/// <summary>
+/// Responder to stat packet.
+/// </summary>
+public class StatPacketResponder : IPacketResponder<StatPacket>
+{
+ private readonly Game _game;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StatPacketResponder"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ public StatPacketResponder(Game game)
+ {
+ _game = game;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(PacketEventArgs<StatPacket> packetArgs, CancellationToken ct = default)
+ {
+ var character = _game.Character;
+ if (character is null)
+ {
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ var packet = packetArgs.Packet;
+ if (character.Hp is null)
+ {
+ character.Hp = new Health
+ {
+ Amount = packet.Hp,
+ Maximum = packet.HpMaximum
+ };
+ }
+ else
+ {
+ character.Hp.Amount = packet.Hp;
+ character.Hp.Maximum = packet.HpMaximum;
+ }
+
+ if (character.Mp is null)
+ {
+ character.Mp = new Health
+ {
+ Amount = packet.Mp,
+ Maximum = packet.MpMaximum
+ };
+ }
+ else
+ {
+ character.Mp.Amount = packet.Mp;
+ character.Mp.Maximum = packet.MpMaximum;
+ }
+
+ return Task.FromResult(Result.FromSuccess());
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs +66 -0
@@ 0,0 1,66 @@
+//
+// 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 NosSmooth.Core.Packets;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Game.Events.Core;
+using NosSmooth.Game.Events.Entities;
+using NosSmooth.Packets.Client.Movement;
+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;
+ var packet = packetArgs.Packet;
+ var oldPosition = character?.Position;
+ var position = new Position(packet.PositionX, packet.PositionY);
+
+ character = await _game.CreateOrUpdateCharacterAsync
+ (
+ () => new Character
+ {
+ Position = position
+ },
+ (c) =>
+ {
+ c.Position = position;
+ return c;
+ },
+ ct: ct
+ );
+
+ return await _eventDispatcher.DispatchEvent
+ (
+ new EntityMovedEvent(character, oldPosition, character.Position!.Value),
+ ct
+ );
+
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Entities/AoeSkillUsedResponder.cs => Core/NosSmooth.Game/PacketHandlers/Entities/AoeSkillUsedResponder.cs +101 -0
@@ 0,0 1,101 @@
+//
+// AoeSkillUsedResponder.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 Microsoft.Extensions.Logging;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Core.Packets;
+using NosSmooth.Data.Abstractions;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Game.Events.Battle;
+using NosSmooth.Game.Events.Core;
+using NosSmooth.Game.Extensions;
+using NosSmooth.Game.Helpers;
+using NosSmooth.Packets.Server.Battle;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Entities;
+
+/// <summary>
+/// Responder to bs packet.
+/// </summary>
+public class AoeSkillUsedResponder : IPacketResponder<BsPacket>
+{
+ private readonly Game _game;
+ private readonly EventDispatcher _eventDispatcher;
+ private readonly IInfoService _infoService;
+ private readonly Logger<AoeSkillUsedResponder> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AoeSkillUsedResponder"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ /// <param name="eventDispatcher">The event dispatcher.</param>
+ /// <param name="infoService">The info service.</param>
+ /// <param name="logger">The logger.</param>
+ public AoeSkillUsedResponder
+ (
+ Game game,
+ EventDispatcher eventDispatcher,
+ IInfoService infoService,
+ Logger<AoeSkillUsedResponder> logger
+ )
+ {
+ _game = game;
+ _eventDispatcher = eventDispatcher;
+ _infoService = infoService;
+ _logger = logger;
+
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<BsPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ var caster = _game.CurrentMap?.Entities.GetEntity<ILivingEntity>
+ (packet.CasterEntityId) ?? (ILivingEntity)EntityHelpers.CreateEntity
+ (packet.CasterEntityType, packet.CasterEntityId);
+ Skill? skillEntity = null;
+
+ if (caster is Character character)
+ {
+ var skillResult = character.Skills?.TryGetSkillByVNum(packet.SkillVNum);
+ if (skillResult?.IsSuccess ?? false)
+ {
+ skillEntity = skillResult.Value.Entity;
+ }
+ }
+
+ if (skillEntity is null)
+ {
+ var skillInfoResult = await _infoService.GetSkillInfoAsync(packet.SkillVNum, ct);
+ _logger.LogWarning
+ (
+ "Could not obtain a skill info for vnum {vnum}: {error}",
+ packet.SkillVNum,
+ skillInfoResult.ToFullString()
+ );
+
+ skillEntity = new Skill(packet.SkillVNum, Info: skillInfoResult.IsSuccess ? skillInfoResult.Entity : null);
+ }
+
+ return await _eventDispatcher.DispatchEvent
+ (
+ new AoESkillUsedEvent
+ (
+ caster,
+ skillEntity,
+ new Position
+ (
+ packet.X,
+ packet.Y
+ )
+ ),
+ ct
+ );
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Entities/CondPacketResponder.cs => Core/NosSmooth.Game/PacketHandlers/Entities/CondPacketResponder.cs +66 -0
@@ 0,0 1,66 @@
+//
+// CondPacketResponder.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.Core.Packets;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Events.Core;
+using NosSmooth.Game.Events.Entities;
+using NosSmooth.Packets.Server.Entities;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Entities;
+
+/// <summary>
+/// Responder to cond packet.
+/// </summary>
+public class CondPacketResponder : IPacketResponder<CondPacket>
+{
+ private readonly Game _game;
+ private readonly EventDispatcher _eventDispatcher;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CondPacketResponder"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ /// <param name="eventDispatcher">The event dispatcher.</param>
+ public CondPacketResponder(Game game, EventDispatcher eventDispatcher)
+ {
+ _game = game;
+ _eventDispatcher = eventDispatcher;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<CondPacket> packetArgs, CancellationToken ct = default)
+ {
+ var map = _game.CurrentMap;
+ if (map is null)
+ {
+ return Result.FromSuccess();
+ }
+
+ var packet = packetArgs.Packet;
+ var entity = map.Entities.GetEntity<ILivingEntity>(packet.EntityId);
+
+ if (entity is null)
+ {
+ return Result.FromSuccess();
+ }
+
+ bool cantMove = entity.CantMove;
+ bool cantAttack = entity.CantAttack;
+
+ entity.Speed = packet.Speed;
+ entity.CantAttack = packet.CantAttack;
+ entity.CantMove = packet.CantMove;
+
+ if (cantMove != packet.CantMove || cantAttack != packet.CantAttack)
+ {
+ return await _eventDispatcher.DispatchEvent(new EntityStunnedEvent(entity, packet.CantMove, packet.CantAttack), ct);
+ }
+
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Entities/EqResponder.cs => Core/NosSmooth.Game/PacketHandlers/Entities/EqResponder.cs +71 -0
@@ 0,0 1,71 @@
+//
+// EqResponder.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.Core.Packets;
+using NosSmooth.Data.Abstractions;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Items;
+using NosSmooth.Game.Helpers;
+using NosSmooth.Packets.Server.Entities;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Entities;
+
+/// <summary>
+/// Responder to eq packet.
+/// </summary>
+public class EqResponder : IPacketResponder<EqPacket>
+{
+ private readonly Game _game;
+ private readonly IInfoService _infoService;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EqResponder"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ /// <param name="infoService">The info service.</param>
+ public EqResponder(Game game, IInfoService infoService)
+ {
+ _game = game;
+ _infoService = infoService;
+
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<EqPacket> packetArgs, CancellationToken ct = default)
+ {
+ var map = _game.CurrentMap;
+ if (map is null)
+ {
+ return Result.FromSuccess();
+ }
+
+ var packet = packetArgs.Packet;
+ var entity = map.Entities.GetEntity<Player>(packet.CharacterId);
+
+ if (entity is null)
+ {
+ return Result.FromSuccess();
+ }
+
+ entity.Sex = packet.Sex;
+ entity.Class = packet.Class;
+ entity.Size = packet.Size;
+ entity.Authority = packet.AuthorityType;
+ entity.HairColor = packet.HairColor;
+ entity.HairStyle = packet.HairStyle;
+ entity.Equipment = await EquipmentHelpers.CreateEquipmentFromInSubpacketAsync
+ (
+ _infoService,
+ packet.EquipmentSubPacket,
+ packet.WeaponUpgradeRareSubPacket,
+ packet.ArmorUpgradeRareSubPacket,
+ 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 +170 -0
@@ 0,0 1,170 @@
+//
+// 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 Microsoft.Extensions.Logging;
+using Microsoft.VisualBasic;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Core.Packets;
+using NosSmooth.Data.Abstractions;
+using NosSmooth.Data.Abstractions.Enums;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Game.Events.Battle;
+using NosSmooth.Game.Events.Characters;
+using NosSmooth.Game.Events.Core;
+using NosSmooth.Game.Events.Entities;
+using NosSmooth.Game.Extensions;
+using NosSmooth.Game.Helpers;
+using NosSmooth.Packets.Server.Battle;
+using NosSmooth.Packets.Server.Skills;
+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;
+ private readonly IInfoService _infoService;
+ private readonly ILogger<SkillUsedResponder> _logger;
+
+ /// <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>
+ /// <param name="infoService">The info service.</param>
+ /// <param name="logger">The logger.</param>
+ public SkillUsedResponder
+ (
+ Game game,
+ EventDispatcher eventDispatcher,
+ IInfoService infoService,
+ ILogger<SkillUsedResponder> logger
+ )
+ {
+ _game = game;
+ _eventDispatcher = eventDispatcher;
+ _infoService = infoService;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<SuPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ var map = _game.CurrentMap;
+
+ // TODO: add to map if the entity is created?
+ var caster = map?.Entities?.GetEntity<ILivingEntity>
+ (packet.CasterEntityId) ?? (ILivingEntity)EntityHelpers.CreateEntity
+ (packet.CasterEntityType, packet.CasterEntityId);
+ var target = map?.Entities?.GetEntity<ILivingEntity>
+ (packet.TargetEntityId) ?? (ILivingEntity)EntityHelpers.CreateEntity
+ (packet.TargetEntityType, packet.TargetEntityId);
+
+ if (target.Hp is null)
+ {
+ target.Hp = new Health
+ {
+ Percentage = packet.HpPercentage
+ };
+ }
+ else
+ {
+ target.Hp.Percentage = packet.HpPercentage;
+ }
+
+ Skill? skillEntity;
+ if (caster is Character character && character.Skills is not null)
+ {
+ var skillResult = character.Skills.TryGetSkillByVNum(packet.SkillVNum);
+
+ if (skillResult.IsDefined(out skillEntity))
+ {
+ skillEntity.LastUseTime = DateTimeOffset.Now;
+ skillEntity.IsOnCooldown = true;
+ }
+ else
+ {
+ var skillInfoResult = await _infoService.GetSkillInfoAsync(packet.SkillVNum, ct);
+ skillEntity = new Skill
+ (packet.SkillVNum, null, skillInfoResult.IsSuccess ? skillInfoResult.Entity : null);
+ }
+ }
+ else
+ {
+ var skillInfoResult = await _infoService.GetSkillInfoAsync(packet.SkillVNum, ct);
+ if (!skillInfoResult.IsSuccess)
+ {
+ _logger.LogWarning
+ (
+ "Could not obtain a skill info for vnum {vnum}: {error}",
+ packet.SkillVNum,
+ skillInfoResult.ToFullString()
+ );
+ }
+
+ skillEntity = new Skill
+ (packet.SkillVNum, null, skillInfoResult.IsSuccess ? skillInfoResult.Entity : null);
+ }
+
+ var dispatchResult = await _eventDispatcher.DispatchEvent
+ (
+ new SkillUsedEvent
+ (
+ caster,
+ target,
+ skillEntity,
+ new Position(packet.PositionX, packet.PositionY),
+ packet.HitMode,
+ packet.Damage
+ ),
+ ct
+ );
+
+ if (!packet.TargetIsAlive)
+ {
+ var diedResult = await _eventDispatcher.DispatchEvent(new EntityDiedEvent(target, skillEntity), ct);
+ if (!diedResult.IsSuccess)
+ {
+ return dispatchResult.IsSuccess
+ ? diedResult
+ : new AggregateError(diedResult, dispatchResult);
+ }
+ }
+
+ return dispatchResult;
+ }
+
+ /// <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.TryGetSkillByCastId(packet.SkillId);
+
+ 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.SkillId), ct);
+ }
+
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Entities/StPacketResponder.cs => Core/NosSmooth.Game/PacketHandlers/Entities/StPacketResponder.cs +85 -0
@@ 0,0 1,85 @@
+//
+// StPacketResponder.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.Core.Packets;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Packets.Server.Entities;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Entities;
+
+/// <summary>
+/// Responds to st packet.
+/// </summary>
+public class StPacketResponder : IPacketResponder<StPacket>
+{
+ private readonly Game _game;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StPacketResponder"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ public StPacketResponder(Game game)
+ {
+ _game = game;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(PacketEventArgs<StPacket> packetArgs, CancellationToken ct = default)
+ {
+ var map = _game.CurrentMap;
+
+ if (map is null)
+ {
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ var packet = packetArgs.Packet;
+ var entity = map.Entities.GetEntity<ILivingEntity>(packet.EntityId);
+ if (entity is null)
+ {
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ entity.EffectsVNums = packet.BuffVNums;
+ entity.Level = packet.Level;
+ if (entity is Player player)
+ {
+ player.HeroLevel = packet.HeroLevel;
+ }
+
+ if (entity.Hp is null)
+ {
+ entity.Hp = new Health
+ {
+ Amount = packet.Hp,
+ Percentage = packet.HpPercentage
+ };
+ }
+ else
+ {
+ entity.Hp.Amount = packet.Hp;
+ entity.Hp.Percentage = packet.HpPercentage;
+ }
+
+ if (entity.Mp is null)
+ {
+ entity.Mp = new Health
+ {
+ Amount = packet.Mp,
+ Percentage = packet.MpPercentage
+ };
+ }
+ else
+ {
+ entity.Mp.Amount = packet.Mp;
+ entity.Mp.Percentage = packet.MpPercentage;
+ }
+
+ return Task.FromResult(Result.FromSuccess());
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Map/AtResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/AtResponder.cs +49 -0
@@ 0,0 1,49 @@
+//
+// AtResponder.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.Core.Packets;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Packets.Server.Maps;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Map;
+
+/// <summary>
+/// Responds to at packet.
+/// </summary>
+public class AtResponder : IPacketResponder<AtPacket>
+{
+ private readonly Game _game;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AtResponder"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ public AtResponder(Game game)
+ {
+ _game = game;
+
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(PacketEventArgs<AtPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ var map = _game.CurrentMap;
+ if (map is null)
+ {
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ var entity = map.Entities.GetEntity(packet.CharacterId);
+ if (entity is not null)
+ {
+ entity.Position = new Position(packet.X, packet.Y);
+ }
+
+ return Task.FromResult(Result.FromSuccess());
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Map/CMapResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/CMapResponder.cs +91 -0
@@ 0,0 1,91 @@
+//
+// CMapResponder.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.Collections.Concurrent;
+using Microsoft.Extensions.Logging;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Core.Packets;
+using NosSmooth.Data.Abstractions;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Maps;
+using NosSmooth.Packets.Server.Maps;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Map;
+
+/// <summary>
+/// Responds to <see cref="CMapResponder"/> by creating a new map.
+/// </summary>
+public class CMapResponder : IPacketResponder<CMapPacket>
+{
+ private readonly Game _game;
+ private readonly IInfoService _infoService;
+ private readonly ILogger<CMapResponder> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CMapResponder"/> class.
+ /// </summary>
+ /// <param name="game">The nostale game.</param>
+ /// <param name="infoService">The info service.</param>
+ /// <param name="logger">The logger.</param>
+ public CMapResponder(Game game, IInfoService infoService, ILogger<CMapResponder> logger)
+ {
+ _game = game;
+ _infoService = infoService;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<CMapPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ var mapInfoResult = await _infoService.GetMapInfoAsync(packet.Id, ct);
+ if (!mapInfoResult.IsSuccess)
+ {
+ _logger.LogWarning
+ (
+ "Could not obtain a map info for id {id}: {error}",
+ packet.Id,
+ mapInfoResult.ToFullString()
+ );
+ }
+
+ await _game.CreateMapAsync
+ (
+ () =>
+ {
+ var map = packet.Id == 20001
+ ? new Miniland
+ (
+ packet.Id,
+ packet.Type,
+ mapInfoResult.IsSuccess ? mapInfoResult.Entity : null,
+ new MapEntities(),
+ Array.Empty<Portal>(),
+ Array.Empty<MinilandObject>()
+ )
+ : new Data.Maps.Map
+ (
+ packet.Id,
+ packet.Type,
+ mapInfoResult.IsSuccess ? mapInfoResult.Entity : null,
+ new MapEntities(),
+ Array.Empty<Portal>()
+ );
+
+ var character = _game.Character;
+ if (character is not null)
+ {
+ map.Entities.AddEntity(character);
+ }
+ return map;
+ },
+ ct: ct
+ );
+
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Map/DropResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/DropResponder.cs +86 -0
@@ 0,0 1,86 @@
+//
+// DropResponder.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 Microsoft.Extensions.Logging;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Core.Packets;
+using NosSmooth.Data.Abstractions;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Game.Events.Core;
+using NosSmooth.Game.Events.Entities;
+using NosSmooth.Packets.Server.Entities;
+using NosSmooth.Packets.Server.Maps;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Map;
+
+/// <summary>
+/// Responds to drop packet.
+/// </summary>
+public class DropResponder : IPacketResponder<DropPacket>
+{
+ private readonly Game _game;
+ private readonly EventDispatcher _eventDispatcher;
+ private readonly IInfoService _infoService;
+ private readonly ILogger<DropResponder> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DropResponder"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ /// <param name="eventDispatcher">The event dispatcher.</param>
+ /// <param name="infoService">The info service.</param>
+ /// <param name="logger">The logger.</param>
+ public DropResponder
+ (
+ Game game,
+ EventDispatcher eventDispatcher,
+ IInfoService infoService,
+ ILogger<DropResponder> logger
+ )
+ {
+ _game = game;
+ _eventDispatcher = eventDispatcher;
+ _infoService = infoService;
+ _logger = logger;
+
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<DropPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ var itemInfoResult = await _infoService.GetItemInfoAsync(packet.ItemVNum, ct);
+ if (!itemInfoResult.IsDefined(out var itemInfo))
+ {
+ _logger.LogWarning("Could not obtain item info for vnum {vnum}: {error}", packet.ItemVNum, itemInfoResult.ToFullString());
+ }
+
+ var entity = new GroundItem
+ {
+ Amount = packet.Amount,
+ Id = packet.DropId,
+ IsQuestRelated = packet.IsQuestRelated,
+ ItemInfo = itemInfo,
+ OwnerId = null,
+ Position = new Position
+ (
+ packet.X,
+ packet.Y
+ ),
+ VNum = packet.ItemVNum
+ };
+
+ var map = _game.CurrentMap;
+ if (map is not null)
+ {
+ map.Entities.AddEntity(entity);
+ }
+
+ return await _eventDispatcher.DispatchEvent(new ItemDroppedEvent(entity), ct);
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Map/GpPacketResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/GpPacketResponder.cs +60 -0
@@ 0,0 1,60 @@
+//
+// GpPacketResponder.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.Core.Packets;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Game.Data.Maps;
+using NosSmooth.Packets.Server.Portals;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Map;
+
+/// <summary>
+/// Responder to gp packet.
+/// </summary>
+public class GpPacketResponder : IPacketResponder<GpPacket>
+{
+ private readonly Game _game;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GpPacketResponder"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ public GpPacketResponder(Game game)
+ {
+ _game = game;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<GpPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ await _game.CreateOrUpdateMapAsync
+ (
+ () => null,
+ (map) => map! with
+ {
+ Portals = map.Portals.Concat
+ (
+ new[]
+ {
+ new Portal
+ (
+ packet.PortalId,
+ new Position(packet.X, packet.Y),
+ packet.TargetMapId,
+ packet.PortalType,
+ packet.IsDisabled
+ )
+ }
+ ).ToArray()
+ },
+ ct: ct
+ );
+
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Map/InResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/InResponder.cs +201 -0
@@ 0,0 1,201 @@
+//
+// InResponder.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 Microsoft.Extensions.Logging;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Core.Packets;
+using NosSmooth.Data.Abstractions;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Game.Data.Items;
+using NosSmooth.Game.Data.Social;
+using NosSmooth.Game.Events.Core;
+using NosSmooth.Game.Events.Entities;
+using NosSmooth.Game.Helpers;
+using NosSmooth.Packets.Server.Entities;
+using NosSmooth.Packets.Server.Maps;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Map;
+
+/// <summary>
+/// Responds to in packet.
+/// </summary>
+public class InResponder : IPacketResponder<InPacket>
+{
+ private readonly Game _game;
+ private readonly EventDispatcher _eventDispatcher;
+ private readonly IInfoService _infoService;
+ private readonly ILogger<InResponder> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InResponder"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ /// <param name="eventDispatcher">The event dispatcher.</param>
+ /// <param name="infoService">The info service.</param>
+ /// <param name="logger">The logger.</param>
+ public InResponder
+ (
+ Game game,
+ EventDispatcher eventDispatcher,
+ IInfoService infoService,
+ ILogger<InResponder> logger
+ )
+ {
+ _game = game;
+ _eventDispatcher = eventDispatcher;
+ _infoService = infoService;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<InPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ var map = _game.CurrentMap;
+ if (map is null)
+ {
+ return Result.FromSuccess();
+ }
+
+ var entities = map.Entities;
+
+ // add entity to the map
+ var entity = await CreateEntityFromInPacket(packet, ct);
+ entities.AddEntity(entity);
+
+ return await _eventDispatcher.DispatchEvent(new EntityJoinedMapEvent(entity), ct);
+ }
+
+ private async Task<IEntity> CreateEntityFromInPacket(InPacket packet, CancellationToken ct)
+ {
+ if (packet.ItemSubPacket is not null)
+ {
+ return await CreateGroundItem(packet, packet.ItemSubPacket, ct);
+ }
+ if (packet.PlayerSubPacket is not null)
+ {
+ return await CreatePlayer(packet, packet.PlayerSubPacket, ct);
+ }
+ if (packet.NonPlayerSubPacket is not null)
+ {
+ return await CreateMonster(packet, packet.NonPlayerSubPacket, ct);
+ }
+
+ throw new Exception("The in packet did not contain any subpacket. Bug?");
+ }
+
+ private async Task<GroundItem> CreateGroundItem
+ (InPacket packet, InItemSubPacket itemSubPacket, CancellationToken ct)
+ {
+ if (packet.VNum is null)
+ {
+ throw new Exception("The vnum from the in packet cannot be null for items.");
+ }
+ var itemInfoResult = await _infoService.GetItemInfoAsync(packet.VNum.Value, ct);
+ if (!itemInfoResult.IsDefined(out var itemInfo))
+ {
+ _logger.LogWarning
+ (
+ "Could not obtain an item info for vnum {vnum}: {error}",
+ packet.VNum.Value,
+ itemInfoResult.ToFullString()
+ );
+ }
+
+ return new GroundItem
+ {
+ Amount = itemSubPacket.Amount,
+ Id = packet.EntityId,
+ OwnerId = itemSubPacket.OwnerId,
+ IsQuestRelated = itemSubPacket.IsQuestRelative,
+ ItemInfo = itemInfo,
+ Position = new Position(packet.PositionX, packet.PositionY),
+ VNum = packet.VNum.Value,
+ };
+ }
+
+ private async Task<Player> CreatePlayer(InPacket packet, InPlayerSubPacket playerSubPacket, CancellationToken ct)
+ {
+ return new Player
+ {
+ Position = new Position(packet.PositionX, packet.PositionY),
+ Id = packet.EntityId,
+ Name = packet.Name?.Name,
+ ArenaWinner = playerSubPacket.ArenaWinner,
+ Class = playerSubPacket.Class,
+ Compliment = playerSubPacket.Compliment,
+ Direction = packet.Direction,
+ Equipment = await EquipmentHelpers.CreateEquipmentFromInSubpacketAsync
+ (
+ _infoService,
+ playerSubPacket.Equipment,
+ playerSubPacket.WeaponUpgradeRareSubPacket,
+ playerSubPacket.ArmorUpgradeRareSubPacket,
+ ct
+ ),
+ Faction = playerSubPacket.Faction,
+ Size = playerSubPacket.Size,
+ Authority = playerSubPacket.Authority,
+ Sex = playerSubPacket.Sex,
+ HairStyle = playerSubPacket.HairStyle,
+ HairColor = playerSubPacket.HairColor,
+ Icon = playerSubPacket.ReputationIcon,
+ IsInvisible = playerSubPacket.IsInvisible,
+ Title = playerSubPacket.Title,
+ Level = playerSubPacket.Level,
+ HeroLevel = playerSubPacket.HeroLevel,
+ Morph = new Morph(playerSubPacket.MorphVNum, playerSubPacket.MorphUpgrade),
+ Family = playerSubPacket.FamilySubPacket.FamilyId is null
+ ? null
+ : new Family
+ (
+ playerSubPacket.FamilySubPacket.FamilyId,
+ playerSubPacket.FamilySubPacket.Title,
+ playerSubPacket.FamilyName,
+ playerSubPacket.Level,
+ playerSubPacket.FamilyIcons
+ ),
+ };
+ }
+
+ private async Task<Monster> CreateMonster
+ (InPacket packet, InNonPlayerSubPacket nonPlayerSubPacket, CancellationToken ct)
+ {
+ if (packet.VNum is null)
+ {
+ throw new Exception("The vnum from the in packet cannot be null for monsters.");
+ }
+
+ var monsterInfoResult = await _infoService.GetMonsterInfoAsync(packet.VNum.Value, ct);
+ if (!monsterInfoResult.IsDefined(out var monsterInfo))
+ {
+ _logger.LogWarning
+ (
+ "Could not obtain a monster info for vnum {vnum}: {error}",
+ packet.VNum.Value,
+ monsterInfoResult.ToFullString()
+ );
+ }
+
+ return new Monster
+ {
+ VNum = packet.VNum.Value,
+ MonsterInfo = monsterInfo,
+ Id = packet.EntityId,
+ Direction = packet.Direction,
+ Faction = nonPlayerSubPacket.Faction,
+ Hp = new Health { Percentage = nonPlayerSubPacket.HpPercentage },
+ Mp = new Health { Percentage = nonPlayerSubPacket.MpPercentage },
+ Name = nonPlayerSubPacket.Name?.Name,
+ Position = new Position(packet.PositionX, packet.PositionY),
+ IsInvisible = nonPlayerSubPacket.IsInvisible,
+ Level = monsterInfo?.Level ?? null,
+ IsSitting = nonPlayerSubPacket.IsSitting
+ };
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Map/MoveResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/MoveResponder.cs +53 -0
@@ 0,0 1,53 @@
+//
+// MoveResponder.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.Collections.Concurrent;
+using NosSmooth.Core.Packets;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Packets.Server.Entities;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Map;
+
+/// <summary>
+/// Responds to move packet.
+/// </summary>
+public class MoveResponder : IPacketResponder<MovePacket>
+{
+ private readonly Game _game;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MoveResponder"/> class.
+ /// </summary>
+ /// <param name="game">The nostale game.</param>
+ public MoveResponder(Game game)
+ {
+ _game = game;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<MovePacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ var map = _game.CurrentMap;
+
+ // TODO: store entities somewhere else so we can store them even if the map is still null?
+ if (map is null)
+ {
+ return Result.FromSuccess();
+ }
+
+ var entity = map.Entities.GetEntity<ILivingEntity>(packet.EntityId);
+ if (entity is not null && entity.Position is not null)
+ {
+ entity.Position = new Position(packet.MapX, packet.MapY);
+ entity.Speed = packet.Speed;
+ }
+
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Map/OutResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/OutResponder.cs +67 -0
@@ 0,0 1,67 @@
+//
+// OutResponder.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.Core.Packets;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Maps;
+using NosSmooth.Game.Events.Core;
+using NosSmooth.Game.Events.Entities;
+using NosSmooth.Game.Helpers;
+using NosSmooth.Packets.Enums;
+using NosSmooth.Packets.Server.Maps;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Map;
+
+/// <summary>
+/// Responds to out packet.
+/// </summary>
+public class OutResponder : IPacketResponder<OutPacket>
+{
+ private readonly Game _game;
+ private readonly EventDispatcher _eventDispatcher;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OutResponder"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ /// <param name="eventDispatcher">The event dispatcher.</param>
+ public OutResponder(Game game, EventDispatcher eventDispatcher)
+ {
+ _game = game;
+ _eventDispatcher = eventDispatcher;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<OutPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ var map = _game.CurrentMap;
+ if (map is null)
+ {
+ return Result.FromSuccess();
+ }
+ map.Entities.RemoveEntity(packet.EntityId);
+ Portal? portal = null;
+
+ IEntity entity = map.Entities.GetEntity(packet.EntityId) ?? EntityHelpers.CreateEntity
+ (packet.EntityType, packet.EntityId);
+
+ var position = entity.Position;
+ if (position is not null)
+ {
+ map.IsOnPortal(position.Value, out portal);
+ }
+
+ bool? died = null;
+ if (entity is ILivingEntity livingEntity)
+ {
+ died = (livingEntity.Hp?.Amount ?? -1) == 0 || (livingEntity.Hp?.Percentage ?? -1) == 0;
+ }
+
+ return await _eventDispatcher.DispatchEvent(new EntityLeftMapEvent(entity, portal, died), ct);
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Game/PacketHandlers/Specialists/SpResponder.cs => Core/NosSmooth.Game/PacketHandlers/Specialists/SpResponder.cs +45 -0
@@ 0,0 1,45 @@
+//
+// SpResponder.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.Core.Packets;
+using NosSmooth.Packets.Server.Specialists;
+using Remora.Results;
+
+namespace NosSmooth.Game.PacketHandlers.Specialists;
+
+/// <summary>
+/// Responds to sp packet.
+/// </summary>
+public class SpResponder : IPacketResponder<SpPacket>
+{
+ private readonly Game _game;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SpResponder"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ public SpResponder(Game game)
+ {
+ _game = game;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(PacketEventArgs<SpPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+ var character = _game.Character;
+
+ if (character is null)
+ {
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ character.SpPoints = packet.SpPoints;
+ character.AdditionalSpPoints = packet.AdditionalSpPoints;
+
+ return Task.FromResult(Result.FromSuccess());
+ }
+}<
\ No newline at end of file
M NosSmooth.sln => NosSmooth.sln +30 -0
@@ 42,6 42,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{99E7
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataBrowser", "Samples\DataBrowser\DataBrowser.csproj", "{055C66A7-640C-49BB-81A7-28E630F51C37}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Game", "Core\NosSmooth.Game\NosSmooth.Game.csproj", "{7C9C7375-6FC0-4704-9332-1F74CDF41D11}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileClient", "Samples\FileClient\FileClient.csproj", "{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ 184,6 188,18 @@ Global
{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Release|x64.Build.0 = Release|Any CPU
{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Release|x86.ActiveCfg = Release|Any CPU
{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Release|x86.Build.0 = Release|Any CPU
+ {7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Debug|x64.Build.0 = Debug|Any CPU
+ {7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Debug|x86.Build.0 = Debug|Any CPU
+ {7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Release|x64.ActiveCfg = Release|Any CPU
+ {7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Release|x64.Build.0 = Release|Any CPU
+ {7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Release|x86.ActiveCfg = Release|Any CPU
+ {7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Release|x86.Build.0 = Release|Any CPU
{055C66A7-640C-49BB-81A7-28E630F51C37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{055C66A7-640C-49BB-81A7-28E630F51C37}.Debug|Any CPU.Build.0 = Debug|Any CPU
{055C66A7-640C-49BB-81A7-28E630F51C37}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ 196,6 212,18 @@ Global
{055C66A7-640C-49BB-81A7-28E630F51C37}.Release|x64.Build.0 = Release|Any CPU
{055C66A7-640C-49BB-81A7-28E630F51C37}.Release|x86.ActiveCfg = Release|Any CPU
{055C66A7-640C-49BB-81A7-28E630F51C37}.Release|x86.Build.0 = Release|Any CPU
+ {D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Debug|x64.Build.0 = Debug|Any CPU
+ {D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Debug|x86.Build.0 = Debug|Any CPU
+ {D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|x64.ActiveCfg = Release|Any CPU
+ {D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|x64.Build.0 = Release|Any CPU
+ {D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|x86.ActiveCfg = Release|Any CPU
+ {D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ 212,7 240,9 @@ Global
{C4114AC1-72E8-46DA-9B4B-A4C942004492} = {1C785A74-19B9-42D2-93B1-F4EC9D6A8CFD}
{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99} = {1C785A74-19B9-42D2-93B1-F4EC9D6A8CFD}
{F1884ADF-6412-4E9B-81FD-357DC5761ADF} = {1C785A74-19B9-42D2-93B1-F4EC9D6A8CFD}
+ {7C9C7375-6FC0-4704-9332-1F74CDF41D11} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
{055C66A7-640C-49BB-81A7-28E630F51C37} = {99E72557-BCE9-496A-B49C-79537B0E6063}
+ {D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B} = {99E72557-BCE9-496A-B49C-79537B0E6063}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C5F46653-4DEC-429B-8580-4ED18ED9B4CA}
M Packets/NosSmooth.Packets/Client/Battle/UseAOESkillPacket.cs => Packets/NosSmooth.Packets/Client/Battle/UseAOESkillPacket.cs +1 -1
@@ 24,4 24,4 @@ public record UseAOESkillPacket
short PositionX,
[PacketIndex(2)]
short PositionY
-);>
\ No newline at end of file
+) : IPacket;<
\ No newline at end of file
M Packets/NosSmooth.Packets/Client/Battle/UseSkillPacket.cs => Packets/NosSmooth.Packets/Client/Battle/UseSkillPacket.cs +1 -1
@@ 29,4 29,4 @@ public record UseSkillPacket
short? MapX,
[PacketIndex(4, IsOptional = true)]
short? MapY
-);>
\ No newline at end of file
+) : IPacket;<
\ No newline at end of file
A Packets/NosSmooth.Packets/Enums/MinilandState.cs => Packets/NosSmooth.Packets/Enums/MinilandState.cs +28 -0
@@ 0,0 1,28 @@
+//
+// MinilandState.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.
+
+namespace NosSmooth.Packets.Enums;
+
+/// <summary>
+/// State of a miniland.
+/// </summary>
+public enum MinilandState
+{
+ /// <summary>
+ /// The miniland is open for anybody.
+ /// </summary>
+ Open,
+
+ /// <summary>
+ /// The miniland is closed, cannot be accessed by anyone.
+ /// </summary>
+ Private,
+
+ /// <summary>
+ /// The miniland is locked, cannot be accessed and objects can be built.
+ /// </summary>
+ Lock,
+}<
\ No newline at end of file
M Packets/NosSmooth.Packets/NosSmooth.Packets.csproj => Packets/NosSmooth.Packets/NosSmooth.Packets.csproj +2 -3
@@ 7,10 7,9 @@
<Description>Contains default NosTale packets.</Description>
<RepositoryUrl>https://github.com/Rutherther/NosSmooth/</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
- <PackageVersion>2.0.0</PackageVersion>
+ <PackageVersion>2.1.0</PackageVersion>
<TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
- <PackageReleaseNotes>Add couple of packet types.
-Change namespace of some packets.</PackageReleaseNotes>
+ <PackageReleaseNotes>Add couple of packet types.</PackageReleaseNotes>
</PropertyGroup>
<ItemGroup>
M Packets/NosSmooth.Packets/Server/Maps/OutPacket.cs => Packets/NosSmooth.Packets/Server/Maps/OutPacket.cs +1 -1
@@ 14,7 14,7 @@ namespace NosSmooth.Packets.Server.Maps;
/// </summary>
/// <param name="EntityType">The entity type.</param>
/// <param name="EntityId">The entity id.</param>
-[PacketHeader("c_map", PacketSource.Server)]
+[PacketHeader("out", PacketSource.Server)]
[GenerateSerializer(true)]
public record OutPacket
(
A Packets/NosSmooth.Packets/Server/Miniland/MlInfoBrPacket.cs => Packets/NosSmooth.Packets/Server/Miniland/MlInfoBrPacket.cs +37 -0
@@ 0,0 1,37 @@
+//
+// MlInfoBrPacket.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.PacketSerializer.Abstractions.Attributes;
+using NosSmooth.PacketSerializer.Abstractions.Common;
+
+namespace NosSmooth.Packets.Server.Miniland;
+
+/// <summary>
+/// Miniland info packet. For minilands not owned by the playing character.
+/// </summary>
+/// <param name="MinilandMusicId">The id of the music. 3800 by default.</param>
+/// <param name="OwnerName">The name of the owner.</param>
+/// <param name="DailyVisitCount">The number of daily visits.</param>
+/// <param name="VisitCount">The number of total visits.</param>
+/// <param name="Unknown">Unknown TODO.</param>
+/// <param name="MinilandMessage">The welcome message.</param>
+[PacketHeader("mlinfobr", PacketSource.Server)]
+[GenerateSerializer(true)]
+public record MlInfoBrPacket
+(
+ [PacketIndex(0)]
+ short MinilandMusicId,
+ [PacketIndex(1)]
+ NameString OwnerName,
+ [PacketIndex(2)]
+ int DailyVisitCount,
+ [PacketIndex(3)]
+ int VisitCount,
+ [PacketIndex(4)]
+ byte Unknown,
+ [PacketGreedyIndex(5)]
+ string MinilandMessage
+) : IPacket;<
\ No newline at end of file
A Packets/NosSmooth.Packets/Server/Miniland/MlInfoPacket.cs => Packets/NosSmooth.Packets/Server/Miniland/MlInfoPacket.cs +47 -0
@@ 0,0 1,47 @@
+//
+// MlInfoPacket.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.Packets.Enums;
+using NosSmooth.PacketSerializer.Abstractions.Attributes;
+using NosSmooth.PacketSerializer.Abstractions.Common;
+
+namespace NosSmooth.Packets.Server.Miniland;
+
+/// <summary>
+/// Miniland info packet. For miniland owned by the playing character.
+/// </summary>
+/// <param name="MinilandMusicId">The id of the music. 3800 by default.</param>
+/// <param name="MinilandPoints">The points of the miniland.</param>
+/// <param name="Unknown">Unknown TODO.</param>
+/// <param name="DailyVisitCount">The number of daily visits.</param>
+/// <param name="VisitCount">The number of total visits.</param>
+/// <param name="Unknown1">Unknown TODO.</param>
+/// <param name="MinilandState">The state of the miniland.</param>
+/// <param name="MinilandMusicName">The name of the miniland music.</param>
+/// <param name="MinilandMessage">The welcome message.</param>
+[PacketHeader("mlinfo", PacketSource.Server)]
+[GenerateSerializer(true)]
+public record MlInfoPacket
+(
+ [PacketIndex(0)]
+ short MinilandMusicId,
+ [PacketIndex(1)]
+ long MinilandPoints,
+ [PacketIndex(2)]
+ byte Unknown,
+ [PacketIndex(3)]
+ int DailyVisitCount,
+ [PacketIndex(4)]
+ int VisitCount,
+ [PacketIndex(5)]
+ byte Unknown1,
+ [PacketIndex(6)]
+ MinilandState MinilandState,
+ [PacketIndex(7)]
+ NameString MinilandMusicName,
+ [PacketGreedyIndex(8)]
+ string MinilandMessage
+) : IPacket;<
\ No newline at end of file
A Packets/NosSmooth.Packets/Server/Miniland/MlObjLstPacket.cs => Packets/NosSmooth.Packets/Server/Miniland/MlObjLstPacket.cs +21 -0
@@ 0,0 1,21 @@
+//
+// MlObjLstPacket.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.PacketSerializer.Abstractions.Attributes;
+
+namespace NosSmooth.Packets.Server.Miniland;
+
+/// <summary>
+/// Miniland object list packet.
+/// </summary>
+/// <param name="Objects">The objects in the miniland or inventory.</param>
+[PacketHeader("mlobjlst", PacketSource.Server)]
+[GenerateSerializer(true)]
+public record MlObjLstPacket
+(
+ [PacketListIndex(0, ListSeparator = ' ', InnerSeparator = '.')]
+ IReadOnlyList<MlObjPacket> Objects
+) : IPacket;<
\ No newline at end of file
A Packets/NosSmooth.Packets/Server/Miniland/MlObjPacket.cs => Packets/NosSmooth.Packets/Server/Miniland/MlObjPacket.cs +48 -0
@@ 0,0 1,48 @@
+//
+// MlObjPacket.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.PacketSerializer.Abstractions.Attributes;
+
+namespace NosSmooth.Packets.Server.Miniland;
+
+/// <summary>
+/// Miniland object packet.
+/// </summary>
+/// <param name="Slot">The slot in the inventory.</param>
+/// <param name="InUse">Whether the item is placed in the miniland.</param>
+/// <param name="X">The x coordinate, if in use.</param>
+/// <param name="Y">The y coordinate, if in use.</param>
+/// <param name="Width">The width of the object.</param>
+/// <param name="Height">The height of the object.</param>
+/// <param name="Unknown">Unknown TODO.</param>
+/// <param name="DurabilityPoints">The durability points of a minigame.</param>
+/// <param name="Unknown1">Unknown TODO.</param>
+/// <param name="Unknown2">Unknown TODO.</param>
+[PacketHeader("mlobj", PacketSource.Server)]
+[GenerateSerializer(true)]
+public record MlObjPacket
+(
+ [PacketIndex(0)]
+ short Slot,
+ [PacketIndex(1)]
+ bool InUse,
+ [PacketIndex(2)]
+ short X,
+ [PacketIndex(3)]
+ short Y,
+ [PacketIndex(4)]
+ byte Width,
+ [PacketIndex(5)]
+ byte Height,
+ [PacketIndex(6)]
+ byte Unknown,
+ [PacketIndex(7)]
+ int DurabilityPoints,
+ [PacketIndex(8)]
+ bool Unknown1,
+ [PacketIndex(9)]
+ bool Unknown2
+) : IPacket;<
\ No newline at end of file
A Packets/NosSmooth.Packets/Server/Miniland/MltObjPacket.cs => Packets/NosSmooth.Packets/Server/Miniland/MltObjPacket.cs +24 -0
@@ 0,0 1,24 @@
+//
+// MltObjPacket.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.PacketSerializer.Abstractions.Attributes;
+
+namespace NosSmooth.Packets.Server.Miniland;
+
+/// <summary>
+/// Miniland objects list packet.
+/// </summary>
+/// <remarks>
+/// Used for minilands of different owners.
+/// </remarks>
+/// <param name="Objects">The miniland objects.</param>
+[PacketHeader("mltobj", PacketSource.Server)]
+[GenerateSerializer(true)]
+public record MltObjPacket
+(
+ [PacketIndex(0)]
+ IReadOnlyList<MltObjSubPacket> Objects
+) : IPacket;<
\ No newline at end of file
A Packets/NosSmooth.Packets/Server/Miniland/MltObjSubPacket.cs => Packets/NosSmooth.Packets/Server/Miniland/MltObjSubPacket.cs +30 -0
@@ 0,0 1,30 @@
+//
+// MltObjSubPacket.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.PacketSerializer.Abstractions.Attributes;
+
+namespace NosSmooth.Packets.Server.Miniland;
+
+/// <summary>
+/// Sub packet of <see cref="MltObjPacket"/>.
+/// </summary>
+/// <param name="VNum">The vnum of the item.</param>
+/// <param name="Slot">The slot.</param>
+/// <param name="X">The x coordinate.</param>
+/// <param name="Y">The y coordinate.</param>
+[PacketHeader("mltobjsub", PacketSource.Server)]
+[GenerateSerializer(true)]
+public record MltObjSubPacket
+(
+ [PacketIndex(0)]
+ int VNum,
+ [PacketIndex(1)]
+ int Slot,
+ [PacketIndex(2)]
+ short X,
+ [PacketIndex(3)]
+ short Y
+) : IPacket;<
\ No newline at end of file
M Packets/NosSmooth.Packets/Server/Players/CInfoPacket.cs => Packets/NosSmooth.Packets/Server/Players/CInfoPacket.cs +67 -4
@@ 4,11 4,74 @@
// 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.Packets.Enums;
+using NosSmooth.Packets.Enums.Players;
+using NosSmooth.PacketSerializer.Abstractions.Attributes;
+
namespace NosSmooth.Packets.Server.Players;
/// <summary>
-/// Character information.
+/// Information about the playing character.
/// </summary>
-// [PacketHeader()]
-// [GenerateSerializer(true)]
-public record CInfoPacket();>
\ No newline at end of file
+/// <remarks>
+/// Sent on login and when changing map.
+/// </remarks>
+/// <param name="Name">The name of the character.</param>
+/// <param name="Unknown">Unknown TODO</param>
+/// <param name="GroupId">The id of the group the player is in, if any.</param>
+/// <param name="FamilyId">The id of the family the player is in, if any.</param>
+/// <param name="FamilyName">The name of the family the player is in, if any.</param>
+/// <param name="CharacterId">The id of the character.</param>
+/// <param name="Authority">The authority of the character.</param>
+/// <param name="Sex">The sex of the character.</param>
+/// <param name="HairStyle">The hair style of the character.</param>
+/// <param name="HairColor">The hair color of the character.</param>
+/// <param name="Class">The class of the character.</param>
+/// <param name="Icon">Unknown TODO</param>
+/// <param name="Compliment">Unknown TODO</param>
+/// <param name="MorphVNum">The vnum of the morph (used for special cards, vehicles and such).</param>
+/// <param name="IsInvisible">Whether the character is invisible.</param>
+/// <param name="FamilyLevel">The level of the family, if any.</param>
+/// <param name="MorphUpgrade">The upgrade of the morph (wings)</param>
+/// <param name="ArenaWinner">Whether the character is an arena winner.</param>
+[PacketHeader("c_info", PacketSource.Server)]
+[GenerateSerializer(true)]
+public record CInfoPacket
+(
+ [PacketIndex(0)]
+ string Name,
+ [PacketIndex(1)]
+ string? Unknown,
+ [PacketIndex(2)]
+ short? GroupId,
+ [PacketIndex(3)]
+ string? FamilyId,
+ [PacketIndex(4)]
+ string? FamilyName,
+ [PacketIndex(5)]
+ long CharacterId,
+ [PacketIndex(6)]
+ AuthorityType Authority,
+ [PacketIndex(7)]
+ SexType Sex,
+ [PacketIndex(8)]
+ HairStyle HairStyle,
+ [PacketIndex(9)]
+ HairColor HairColor,
+ [PacketIndex(10)]
+ PlayerClass Class,
+ [PacketIndex(11)]
+ byte Icon,
+ [PacketIndex(12)]
+ short Compliment,
+ [PacketIndex(13)]
+ short MorphVNum,
+ [PacketIndex(14)]
+ bool IsInvisible,
+ [PacketIndex(15)]
+ byte? FamilyLevel,
+ [PacketIndex(16)]
+ byte MorphUpgrade,
+ [PacketIndex(17)]
+ bool ArenaWinner
+) : IPacket;
M Packets/NosSmooth.Packets/Server/Players/LevPacket.cs => Packets/NosSmooth.Packets/Server/Players/LevPacket.cs +2 -2
@@ 22,7 22,7 @@ namespace NosSmooth.Packets.Server.Players;
/// <param name="JobXpLoad">Unknown TODO</param>
/// <param name="Reputation">The reputation of the player.</param>
/// <param name="SkillCp">The skill cp. (Used for learning skills)</param>
-/// <param name="HeroXp">The xp in hero level. TODO</param>
+/// <param name="HeroLevelXp">The xp in hero level. TODO</param>
/// <param name="HeroLevel">The hero level. (shown as (+xx))</param>
/// <param name="HeroXpLoad">Unknown TODO</param>
[PacketHeader("lev", PacketSource.Server)]
@@ 46,7 46,7 @@ public record LevPacket
[PacketIndex(7)]
int SkillCp,
[PacketIndex(8)]
- long HeroXp,
+ long HeroLevelXp,
[PacketIndex(9)]
byte HeroLevel,
[PacketIndex(10)]
A Packets/NosSmooth.Packets/Server/Specialists/SdPacket.cs => Packets/NosSmooth.Packets/Server/Specialists/SdPacket.cs +24 -0
@@ 0,0 1,24 @@
+//
+// SdPacket.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.PacketSerializer.Abstractions.Attributes;
+
+namespace NosSmooth.Packets.Server.Specialists;
+
+/// <summary>
+/// Packet for sp cooldown.
+/// </summary>
+/// <remarks>
+/// Doesn't block putting on the sp. Just shows loading on the character icon.
+/// </remarks>
+/// <param name="Cooldown">The cooldown.</param>
+[PacketHeader("sd", PacketSource.Server)]
+[GenerateSerializer(true)]
+public record SdPacket
+(
+ [PacketIndex(0)]
+ short Cooldown
+) : IPacket;<
\ No newline at end of file
A Packets/NosSmooth.Packets/Server/Specialists/SpPacket.cs => Packets/NosSmooth.Packets/Server/Specialists/SpPacket.cs +33 -0
@@ 0,0 1,33 @@
+//
+// SpPacket.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.PacketSerializer.Abstractions.Attributes;
+
+namespace NosSmooth.Packets.Server.Specialists;
+
+/// <summary>
+/// Packet with information about sp points.
+/// </summary>
+/// <remarks>
+/// Sent on login, on sp change, on points change.
+/// </remarks>
+/// <param name="AdditionalSpPoints">The additional sp points used after sp points are 0.</param>
+/// <param name="MaxAdditionalSpPoints">The maximum of additional sp points.</param>
+/// <param name="SpPoints">The sp points that decrease upon using sp.</param>
+/// <param name="MaxSpPoints">The maximum of sp points.</param>
+[PacketHeader("sp", PacketSource.Server)]
+[GenerateSerializer(true)]
+public record SpPacket
+(
+ [PacketIndex(0)]
+ int AdditionalSpPoints,
+ [PacketIndex(1)]
+ int MaxAdditionalSpPoints,
+ [PacketIndex(2)]
+ int SpPoints,
+ [PacketIndex(3)]
+ int MaxSpPoints
+) : IPacket;<
\ No newline at end of file
A Samples/FileClient/App.cs => Samples/FileClient/App.cs +77 -0
@@ 0,0 1,77 @@
+//
+// App.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 Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Data.NOSFiles;
+using NosSmooth.Packets.Extensions;
+using NosSmooth.Packets.Packets;
+using Remora.Results;
+
+namespace FileClient;
+
+/// <summary>
+/// The application.
+/// </summary>
+public class App : BackgroundService
+{
+ private readonly INostaleClient _client;
+ private readonly IPacketTypesRepository _packetRepository;
+ private readonly NostaleDataFilesManager _filesManager;
+ private readonly ILogger<App> _logger;
+ private readonly IHostLifetime _lifetime;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="App"/> class.
+ /// </summary>
+ /// <param name="client">The client.</param>
+ /// <param name="packetRepository">The packet repository.</param>
+ /// <param name="filesManager">The file manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="lifetime">The lifetime.</param>
+ public App
+ (
+ INostaleClient client,
+ IPacketTypesRepository packetRepository,
+ NostaleDataFilesManager filesManager,
+ ILogger<App> logger,
+ IHostLifetime lifetime
+ )
+ {
+ _client = client;
+ _packetRepository = packetRepository;
+ _filesManager = filesManager;
+ _logger = logger;
+ _lifetime = lifetime;
+ }
+
+ /// <inheritdoc />
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ var packetResult = _packetRepository.AddDefaultPackets();
+ if (!packetResult.IsSuccess)
+ {
+ _logger.LogResultError(packetResult);
+ return;
+ }
+
+ var filesResult = _filesManager.Initialize();
+ if (!filesResult.IsSuccess)
+ {
+ _logger.LogResultError(filesResult);
+ return;
+ }
+
+ var runResult = await _client.RunAsync(stoppingToken);
+ if (!runResult.IsSuccess)
+ {
+ _logger.LogResultError(runResult);
+ await _lifetime.StopAsync(default);
+ }
+ }
+}<
\ No newline at end of file
A Samples/FileClient/Client.cs => Samples/FileClient/Client.cs +126 -0
@@ 0,0 1,126 @@
+//
+// Client.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.Text.RegularExpressions;
+using Microsoft.Extensions.Logging;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Core.Packets;
+using NosSmooth.Packets;
+using NosSmooth.Packets.Errors;
+using NosSmooth.PacketSerializer.Abstractions.Attributes;
+using Remora.Results;
+
+namespace FileClient;
+
+/// <summary>
+/// A NosTale client using stream to read lines.
+/// </summary>
+public class Client : BaseNostaleClient
+{
+ private const string LineRegex = ".*\\[(Recv|Send)\\]\t(.*)";
+ private readonly IPacketHandler _packetHandler;
+ private readonly IPacketSerializer _packetSerializer;
+ private readonly ILogger<Client> _logger;
+ private readonly Stream _stream;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Client"/> class.
+ /// </summary>
+ /// <param name="stream">The stream with packets.</param>
+ /// <param name="packetHandler">The packet handler.</param>
+ /// <param name="commandProcessor">The command processor.</param>
+ /// <param name="packetSerializer">The packet serializer.</param>
+ /// <param name="logger">The logger.</param>
+ public Client(
+ Stream stream,
+ IPacketHandler packetHandler,
+ CommandProcessor commandProcessor,
+ IPacketSerializer packetSerializer,
+ ILogger<Client> logger
+ )
+ : base(commandProcessor, packetSerializer)
+ {
+ _stream = stream;
+ _packetHandler = packetHandler;
+ _packetSerializer = packetSerializer;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public override async Task<Result> RunAsync(CancellationToken stopRequested = default)
+ {
+ using var reader = new StreamReader(_stream);
+ var regex = new Regex(LineRegex);
+ while (!reader.EndOfStream)
+ {
+ stopRequested.ThrowIfCancellationRequested();
+ var line = await reader.ReadLineAsync();
+ if (line is null)
+ {
+ continue;
+ }
+
+ var match = regex.Match(line);
+ if (!match.Success)
+ {
+ _logger.LogError("Could not find match on line {Line}", line);
+ continue;
+ }
+
+ var type = match.Groups[1].Value;
+ var packetStr = match.Groups[2].Value;
+
+ var source = type == "Recv" ? PacketSource.Server : PacketSource.Client;
+ var packet = CreatePacket(packetStr, source);
+ Result result;
+ if (source == PacketSource.Client)
+ {
+ result = await _packetHandler.HandleSentPacketAsync(packet, packetStr, stopRequested);
+ }
+ else
+ {
+ result = await _packetHandler.HandleReceivedPacketAsync(packet, packetStr, stopRequested);
+ }
+
+ if (!result.IsSuccess)
+ {
+ _logger.LogResultError(result);
+ }
+ }
+
+ return Result.FromSuccess();
+ }
+
+ /// <inheritdoc/>
+ public override async Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default)
+ {
+ return await _packetHandler.HandleReceivedPacketAsync(CreatePacket(packetString, PacketSource.Client), packetString, ct);
+ }
+
+ /// <inheritdoc/>
+ public override async Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default)
+ {
+ return await _packetHandler.HandleReceivedPacketAsync(CreatePacket(packetString, PacketSource.Server), packetString, ct);
+ }
+
+ private IPacket CreatePacket(string packetStr, PacketSource source)
+ {
+ var packetResult = _packetSerializer.Deserialize(packetStr, source);
+ if (!packetResult.IsSuccess)
+ {
+ if (packetResult.Error is PacketConverterNotFoundError err)
+ {
+ return new UnresolvedPacket(err.Header, packetStr);
+ }
+
+ return new ParsingFailedPacket(packetResult, packetStr);
+ }
+
+ return packetResult.Entity;
+ }
+}<
\ No newline at end of file
A Samples/FileClient/FileClient.csproj => Samples/FileClient/FileClient.csproj +27 -0
@@ 0,0 1,27 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Folder Include="Handlers" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\Core\NosSmooth.Core\NosSmooth.Core.csproj" />
+ <ProjectReference Include="..\..\Core\NosSmooth.Game\NosSmooth.Game.csproj" />
+ <ProjectReference Include="..\..\Data\NosSmooth.Data.Abstractions\NosSmooth.Data.Abstractions.csproj" />
+ <ProjectReference Include="..\..\Data\NosSmooth.Data.NOSFiles\NosSmooth.Data.NOSFiles.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
+ </ItemGroup>
+
+</Project>
A Samples/FileClient/Program.cs => Samples/FileClient/Program.cs +66 -0
@@ 0,0 1,66 @@
+//
+// Program.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 Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Core.Packets;
+using NosSmooth.Data.Abstractions.Language;
+using NosSmooth.Data.NOSFiles.Extensions;
+using NosSmooth.Data.NOSFiles.Options;
+using NosSmooth.Game.Extensions;
+using NosSmooth.Packets;
+
+namespace FileClient;
+
+/// <summary>
+/// An entrypoint class.
+/// </summary>
+public static class Program
+{
+ // TODO: create console hosting.
+
+ /// <summary>
+ /// An entrypoint method.
+ /// </summary>
+ /// <param name="args">The command line arguments.</param>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ public static async Task Main(string[] args)
+ {
+ await using FileStream stream = File.OpenRead(string.Join(' ', args));
+ await CreateHost(stream).StartAsync();
+ }
+
+ private static IHost CreateHost(Stream fileStream)
+ {
+ return Host.CreateDefaultBuilder()
+ .ConfigureServices(coll =>
+ {
+ coll.AddHostedService<App>();
+
+ coll.AddNostaleCore()
+ .AddNostaleGame()
+ .AddNostaleDataFiles()
+ .Configure<LanguageServiceOptions>(o => o.Language = Language.Cz)
+ .Configure<NostaleDataOptions>(o => o.SupportedLanguages = new[]
+ {
+ Language.Cz
+ });
+ coll.AddSingleton<INostaleClient>(p => new Client(
+ fileStream,
+ p.GetRequiredService<IPacketHandler>(),
+ p.GetRequiredService<CommandProcessor>(),
+ p.GetRequiredService<IPacketSerializer>(),
+ p.GetRequiredService<ILogger<Client>>()
+ ));
+ })
+ .UseConsoleLifetime()
+ .Build();
+ }
+}<
\ No newline at end of file
M Tests/NosSmooth.Packets.Tests/Converters/Packets/InPacketConverterTests.cs => Tests/NosSmooth.Packets.Tests/Converters/Packets/InPacketConverterTests.cs +3 -2
@@ 11,6 11,7 @@ using NosSmooth.Packets.Enums.Entities;
using NosSmooth.Packets.Enums.Players;
using NosSmooth.Packets.Extensions;
using NosSmooth.Packets.Server.Entities;
+using NosSmooth.Packets.Server.Maps;
using NosSmooth.Packets.Server.Players;
using NosSmooth.Packets.Server.Weapons;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
@@ 90,7 91,7 @@ public class InPacketConverterTests
new UpgradeRareSubPacket(10, 8),
new FamilySubPacket(null, null),
null,
- "26",
+ 26,
false,
0,
0,
@@ 161,7 162,7 @@ public class InPacketConverterTests
new UpgradeRareSubPacket(10, 8),
new FamilySubPacket("-1", null),
null,
- "26",
+ 26,
false,
0,
0,