M Core/NosSmooth.Game/Game.cs => Core/NosSmooth.Game/Game.cs +95 -12
@@ 25,10 25,16 @@ public class Game
/// <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; }
@@ 63,30 69,107 @@ public class Game
internal CancellationTokenSource? MapChanged { get; private set; }
/// <summary>
- /// Gets the set semaphore used for changing internal fields.
+ /// Creates the character if it is null, or updates the current character.
/// </summary>
- internal SemaphoreSlim SetSemaphore { get; } = new SemaphoreSlim(1, 1);
+ /// <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>
- /// Ensures that Character is not null.
+ /// Creates the map if it is null, or updates the current map.
/// </summary>
- /// <param name="releaseSemaphore">Whether to release the semaphore.</param>
+ /// <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>A Task that may or may not have succeeded.</returns>
- internal async Task<Character> EnsureCharacterCreatedAsync(bool releaseSemaphore, CancellationToken ct = default)
+ /// <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
+ );
+ }
+
+ 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 SetSemaphore.WaitAsync(ct);
- Character? character = Character;
- if (Character is null)
+ await Semaphores.AcquireSemaphore(type, ct);
+
+ var current = get();
+ if (current is null)
+ {
+ current = create();
+ }
+ else
{
- Character = character = new Character();
+ current = update(current);
}
+ set(current);
if (releaseSemaphore)
{
- SetSemaphore.Release();
+ Semaphores.ReleaseSemaphore(type);
}
- return character!;
+ return current;
}
}=
\ 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
M Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs +88 -55
@@ 4,7 4,9 @@
// 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.Data.Common;
using NosSmooth.Core.Packets;
+using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Info;
using NosSmooth.Game.Data.Social;
using NosSmooth.Game.Events.Characters;
@@ 17,7 19,8 @@ namespace NosSmooth.Game.PacketHandlers.Characters;
/// <summary>
/// Responds to CInfoPacket by creating the character.
/// </summary>
-public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResponder<LevPacket>, IPacketResponder<CModePacket>
+public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResponder<LevPacket>,
+ IPacketResponder<CModePacket>
{
private readonly Game _game;
private readonly EventDispatcher _eventDispatcher;
@@ 37,33 40,53 @@ public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResp
public async Task<Result> Respond(PacketEventArgs<CInfoPacket> packetArgs, CancellationToken ct = default)
{
var oldCharacter = _game.Character;
- var character = oldCharacter;
var packet = packetArgs.Packet;
- await _game.EnsureCharacterCreatedAsync(false, ct).ConfigureAwait(false);
- if (character is not null)
- {
- _game.Character = character = character with
- {
- Id = packet.CharacterId,
- AuthorityType = packet.Authority,
- Sex = packet.Sex,
- HairStyle = packet.HairStyle,
- HairColor = packet.HairColor,
- Class = packet.Class,
- Icon = packet.Icon,
- Compliment = packet.Compliment,
- Morph = (character.Morph ?? new Morph(packet.MorphVNum, packet.MorphUpgrade)) with
+ var character = await _game.CreateOrUpdateCharacterAsync
+ (
+ () => new Character
+ (
+ Family: new Family(packet.FamilyId, packet.FamilyName, packet.FamilyLevel),
+ Group: new Group(packet.GroupId, default, default),
+ Id: packet.CharacterId,
+ Name: packet.Name,
+ AuthorityType: 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),
+ Invisible: packet.IsInvisible,
+ ArenaWinner: packet.ArenaWinner
+ ),
+ (character)
+ => character with
{
- VNum = packet.MorphVNum, Upgrade = packet.MorphUpgrade
+ Id = packet.CharacterId,
+ AuthorityType = packet.Authority,
+ Sex = packet.Sex,
+ HairStyle = packet.HairStyle,
+ HairColor = packet.HairColor,
+ Class = packet.Class,
+ Icon = packet.Icon,
+ Compliment = packet.Compliment,
+ Group = (character.Group ?? new Group(packet.GroupId, null, null)) with
+ {
+ Id = packet.GroupId
+ },
+ Morph = (character.Morph ?? new Morph(packet.MorphVNum, packet.MorphUpgrade)) with
+ {
+ VNum = packet.MorphVNum, Upgrade = packet.MorphUpgrade
+ },
+ ArenaWinner = packet.ArenaWinner,
+ Invisible = packet.IsInvisible,
+ Family = new Family(packet.FamilyId, packet.FamilyName, packet.FamilyLevel)
},
- ArenaWinner = packet.ArenaWinner,
- Invisible = packet.IsInvisible,
- Family = new Family(packet.FamilyId, packet.FamilyName, packet.FamilyLevel)
- };
- }
+ ct: ct
+ );
- _game.SetSemaphore.Release();
- if (character is not null && character != oldCharacter)
+ if (character != oldCharacter)
{
return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(oldCharacter, character), ct);
}
@@ 75,24 98,31 @@ public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResp
public async Task<Result> Respond(PacketEventArgs<LevPacket> packetArgs, CancellationToken ct = default)
{
var packet = packetArgs.Packet;
- await _game.EnsureCharacterCreatedAsync(false, ct).ConfigureAwait(false);
var oldCharacter = _game.Character;
- var character = oldCharacter;
- if (character is not null)
- {
- _game.Character = character = character with
- {
- SkillCp = packet.SkillCp,
- Reputation = packet.Reputation,
- Level = new Level(packet.Level, packet.LevelXp, packet.XpLoad),
- JobLevel = new Level(packet.JobLevel, packet.JobLevelXp, packet.JobXpLoad),
- HeroLevel = new Level(packet.HeroLevel, packet.HeroXp, packet.HeroXpLoad)
- };
- }
+ var character = await _game.CreateOrUpdateCharacterAsync
+ (
+ () => new Character
+ (
+ SkillCp: packet.SkillCp,
+ Reputation: packet.Reputation,
+ Level: new Level(packet.Level, packet.LevelXp, packet.XpLoad),
+ JobLevel: new Level(packet.JobLevel, packet.JobLevelXp, packet.JobXpLoad),
+ HeroLevel: new Level(packet.HeroLevel, packet.HeroLevelXp, packet.HeroXpLoad)
+ ),
+ (character) =>
+ character with
+ {
+ SkillCp = packet.SkillCp,
+ Reputation = packet.Reputation,
+ Level = new Level(packet.Level, packet.LevelXp, packet.XpLoad),
+ JobLevel = new Level(packet.JobLevel, packet.JobLevelXp, packet.JobXpLoad),
+ HeroLevel = new Level(packet.HeroLevel, packet.HeroLevelXp, packet.HeroXpLoad)
+ },
+ ct: ct
+ );
- _game.SetSemaphore.Release();
- if (character is not null && character != oldCharacter)
+ if (character != oldCharacter)
{
return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(oldCharacter, character), ct);
}
@@ 105,28 135,31 @@ public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResp
{
var packet = packetArgs.Packet;
var oldCharacter = _game.Character;
- var character = oldCharacter;
- if (character is null || character.Id != packetArgs.Packet.EntityId)
+ if (oldCharacter is null || oldCharacter.Id != packetArgs.Packet.EntityId)
{ // Not the current character.
return Result.FromSuccess();
}
- await _game.SetSemaphore.WaitAsync(ct);
- character = character with
- {
- Morph = new Morph
- (
- packet.MorphVNum,
- packet.MorphUpgrade,
- packet.MorphDesign,
- packet.MorphBonus,
- packet.MorphSkin
- ),
- Size = packet.Size
- };
+ var character = await _game.CreateOrUpdateCharacterAsync
+ (
+ () => throw new NotImplementedException(),
+ (character) =>
+ character with
+ {
+ Morph = new Morph
+ (
+ packet.MorphVNum,
+ packet.MorphUpgrade,
+ packet.MorphDesign,
+ packet.MorphBonus,
+ packet.MorphSkin
+ ),
+ Size = packet.Size
+ },
+ ct: ct
+ );
- _game.SetSemaphore.Release();
if (oldCharacter != character)
{
return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(oldCharacter, character), ct);
M Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs +14 -11
@@ 39,9 39,9 @@ public class SkillResponder : IPacketResponder<SkiPacket>
Skill primarySkill, secondarySkill;
- var character = await _game.EnsureCharacterCreatedAsync(false, ct);
+ var character = _game.Character;
- if (packet.PrimarySkillId == character.Skills?.PrimarySkill.SkillVNum)
+ if (character is not null && packet.PrimarySkillId == character.Skills?.PrimarySkill.SkillVNum)
{
primarySkill = character.Skills.PrimarySkill;
}
@@ 50,11 50,11 @@ public class SkillResponder : IPacketResponder<SkiPacket>
primarySkill = new Skill(packet.PrimarySkillId);
}
- if (packet.PrimarySkillId == packet.SecondarySkillId)
+ if (character is not null && packet.PrimarySkillId == packet.SecondarySkillId)
{
secondarySkill = primarySkill;
}
- else if (packet.SecondarySkillId == character.Skills?.SecondarySkill.SkillVNum)
+ else if (character is not null && packet.SecondarySkillId == character.Skills?.SecondarySkill.SkillVNum)
{
secondarySkill = character.Skills.SecondarySkill;
}
@@ 64,11 64,13 @@ public class SkillResponder : IPacketResponder<SkiPacket>
}
var skillsFromPacket = packet.SkillSubPackets?.Select(x => x.SkillId).ToList() ?? new List<long>();
- var skillsFromCharacter = character.Skills is null ? new List<long>() : character.Skills.OtherSkills.Select(x => x.SkillVNum).ToList();
+ var skillsFromCharacter = character?.Skills is null
+ ? new List<long>()
+ : character.Skills.OtherSkills.Select(x => x.SkillVNum).ToList();
var newSkills = skillsFromPacket.Except(skillsFromCharacter);
var oldSkills = skillsFromCharacter.Except(skillsFromPacket);
- var otherSkillsFromCharacter = new List<Skill>(character.Skills?.OtherSkills ?? new Skill[] { });
+ var otherSkillsFromCharacter = new List<Skill>(character?.Skills?.OtherSkills ?? new Skill[] { });
otherSkillsFromCharacter.RemoveAll(x => oldSkills.Contains(x.SkillVNum));
foreach (var newSkill in newSkills)
@@ 78,12 80,13 @@ public class SkillResponder : IPacketResponder<SkiPacket>
var skills = new Skills(primarySkill, secondarySkill, otherSkillsFromCharacter);
- _game.Character = character with
- {
- Skills = skills
- };
+ await _game.CreateOrUpdateCharacterAsync
+ (
+ () => new Character(Skills: skills),
+ c => c with { Skills = skills },
+ ct: ct
+ );
- _game.SetSemaphore.Release();
await _eventDispatcher.DispatchEvent(new SkillsReceivedEvent(skills), ct);
return Result.FromSuccess();
M Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs +27 -2
@@ 5,6 5,7 @@
// 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;
@@ 36,6 37,7 @@ public class WalkResponder : IPacketResponder<WalkPacket>
public async Task<Result> Respond(PacketEventArgs<WalkPacket> packetArgs, CancellationToken ct = default)
{
var character = _game.Character;
+ var packet = packetArgs.Packet;
if (character is not null && character.Position is not null)
{
var oldPosition = new Position
@@ 44,8 46,8 @@ public class WalkResponder : IPacketResponder<WalkPacket>
Y = character.Position.Y
};
- character.Position.X = packetArgs.Packet.PositionX;
- character.Position.Y = packetArgs.Packet.PositionY;
+ character.Position.X = packet.PositionX;
+ character.Position.Y = packet.PositionY;
return await _eventDispatcher.DispatchEvent
(
@@ 53,6 55,29 @@ public class WalkResponder : IPacketResponder<WalkPacket>
ct
);
}
+ else if (character?.Position is null)
+ {
+ await _game.CreateOrUpdateCharacterAsync
+ (
+ () => new Character
+ (
+ Position: new Position()
+ {
+ X = packet.PositionX,
+ Y = packet.PositionY
+ }
+ ),
+ (c) => c with
+ {
+ Position = new Position()
+ {
+ X = packet.PositionX,
+ Y = packet.PositionY
+ }
+ },
+ ct: ct
+ );
+ }
return Result.FromSuccess();
}
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)]