~ruther/NosSmooth

b511848142c61966923573443eda00494cb70e50 — František Boháček 3 years ago ee5e966
feat: extract game semaphores logic
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)]

Do not follow this link