From b511848142c61966923573443eda00494cb70e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Tue, 4 Jan 2022 21:48:31 +0100 Subject: [PATCH] feat: extract game semaphores logic --- Core/NosSmooth.Game/Game.cs | 107 +++++++++++-- Core/NosSmooth.Game/GameSemaphoreType.cs | 28 ++++ Core/NosSmooth.Game/GameSemaphores.cs | 47 ++++++ .../Characters/CharacterInitResponder.cs | 143 +++++++++++------- .../Characters/SkillResponder.cs | 25 +-- .../Characters/WalkResponder.cs | 29 +++- .../Server/Players/LevPacket.cs | 4 +- 7 files changed, 301 insertions(+), 82 deletions(-) create mode 100644 Core/NosSmooth.Game/GameSemaphoreType.cs create mode 100644 Core/NosSmooth.Game/GameSemaphores.cs diff --git a/Core/NosSmooth.Game/Game.cs b/Core/NosSmooth.Game/Game.cs index baa4d49..888a2af 100644 --- a/Core/NosSmooth.Game/Game.cs +++ b/Core/NosSmooth.Game/Game.cs @@ -25,9 +25,15 @@ public class Game /// The options for the game. public Game(IOptions options) { + Semaphores = new GameSemaphores(); _options = options.Value; } + /// + /// Gets the game semaphores. + /// + internal GameSemaphores Semaphores { get; } + /// /// Gets the playing character of the client. /// @@ -63,30 +69,107 @@ public class Game internal CancellationTokenSource? MapChanged { get; private set; } /// - /// Gets the set semaphore used for changing internal fields. + /// Creates the character if it is null, or updates the current character. /// - internal SemaphoreSlim SetSemaphore { get; } = new SemaphoreSlim(1, 1); + /// The function for creating the character. + /// The function for updating the character. + /// Whether to release the semaphore used for changing the character. + /// The cancellation token for cancelling the operation. + /// The updated character. + internal async Task CreateOrUpdateCharacterAsync + ( + Func create, + Func update, + bool releaseSemaphore = true, + CancellationToken ct = default + ) + { + return await CreateOrUpdateAsync + ( + GameSemaphoreType.Character, + () => Character, + c => Character = c, + create, + update, + releaseSemaphore, + ct + ); + } /// - /// Ensures that Character is not null. + /// Creates the map if it is null, or updates the current map. /// - /// Whether to release the semaphore. + /// The function for creating the map. + /// Whether to release the semaphore used for changing the map. /// The cancellation token for cancelling the operation. - /// A Task that may or may not have succeeded. - internal async Task EnsureCharacterCreatedAsync(bool releaseSemaphore, CancellationToken ct = default) + /// The updated character. + internal async Task CreateMapAsync + ( + Func create, + bool releaseSemaphore = true, + CancellationToken ct = default + ) + { + return await CreateAsync + ( + GameSemaphoreType.Map, + m => CurrentMap = m, + create, + releaseSemaphore, + ct + ); + } + + private async Task CreateAsync + ( + GameSemaphoreType type, + Action set, + Func 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 CreateOrUpdateAsync + ( + GameSemaphoreType type, + Func get, + Action set, + Func create, + Func 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 diff --git a/Core/NosSmooth.Game/GameSemaphoreType.cs b/Core/NosSmooth.Game/GameSemaphoreType.cs new file mode 100644 index 0000000..672da35 --- /dev/null +++ b/Core/NosSmooth.Game/GameSemaphoreType.cs @@ -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; + +/// +/// Type of game semaphore. +/// +public enum GameSemaphoreType +{ + /// + /// The semaphore for the character. + /// + Character, + + /// + /// The semaphore for the map. + /// + Map, + + /// + /// The semaphore for the raid. + /// + Raid +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/GameSemaphores.cs b/Core/NosSmooth.Game/GameSemaphores.cs new file mode 100644 index 0000000..2303122 --- /dev/null +++ b/Core/NosSmooth.Game/GameSemaphores.cs @@ -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; + +/// +/// Holds information about semaphores for synchornizing the game packet data. +/// +internal class GameSemaphores +{ + private Dictionary _semaphores; + + /// + /// Initializes a new instance of the class. + /// + public GameSemaphores() + { + _semaphores = new Dictionary(); + foreach (var type in Enum.GetValues(typeof(GameSemaphoreType)).Cast()) + { + _semaphores[type] = new SemaphoreSlim(1, 1); + } + } + + /// + /// Acquire the given semaphore. + /// + /// The semaphore type. + /// The cancellation token for cancelling the operation. + /// A task that may or may not have succeeded. + public async Task AcquireSemaphore(GameSemaphoreType semaphoreType, CancellationToken ct = default) + { + await _semaphores[semaphoreType].WaitAsync(ct); + } + + /// + /// Release the acquired semaphore. + /// + /// The semaphore type. + public void ReleaseSemaphore(GameSemaphoreType semaphoreType) + { + _semaphores[semaphoreType].Release(); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs index 5ad9402..0ff28c0 100644 --- a/Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs +++ b/Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs @@ -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; /// /// Responds to CInfoPacket by creating the character. /// -public class CharacterInitResponder : IPacketResponder, IPacketResponder, IPacketResponder +public class CharacterInitResponder : IPacketResponder, IPacketResponder, + IPacketResponder { private readonly Game _game; private readonly EventDispatcher _eventDispatcher; @@ -37,33 +40,53 @@ public class CharacterInitResponder : IPacketResponder, IPacketResp public async Task Respond(PacketEventArgs 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, IPacketResp public async Task Respond(PacketEventArgs 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, 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); diff --git a/Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs index 6954eec..c128cb6 100644 --- a/Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs +++ b/Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs @@ -39,9 +39,9 @@ public class SkillResponder : IPacketResponder 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 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 } var skillsFromPacket = packet.SkillSubPackets?.Select(x => x.SkillId).ToList() ?? new List(); - var skillsFromCharacter = character.Skills is null ? new List() : character.Skills.OtherSkills.Select(x => x.SkillVNum).ToList(); + var skillsFromCharacter = character?.Skills is null + ? new List() + : character.Skills.OtherSkills.Select(x => x.SkillVNum).ToList(); var newSkills = skillsFromPacket.Except(skillsFromCharacter); var oldSkills = skillsFromCharacter.Except(skillsFromPacket); - var otherSkillsFromCharacter = new List(character.Skills?.OtherSkills ?? new Skill[] { }); + var otherSkillsFromCharacter = new List(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 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(); diff --git a/Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs index f776a38..1e6ea67 100644 --- a/Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs +++ b/Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs @@ -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 public async Task Respond(PacketEventArgs 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 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 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(); } diff --git a/Packets/NosSmooth.Packets/Server/Players/LevPacket.cs b/Packets/NosSmooth.Packets/Server/Players/LevPacket.cs index 28ca368..9c3e389 100644 --- a/Packets/NosSmooth.Packets/Server/Players/LevPacket.cs +++ b/Packets/NosSmooth.Packets/Server/Players/LevPacket.cs @@ -22,7 +22,7 @@ namespace NosSmooth.Packets.Server.Players; /// Unknown TODO /// The reputation of the player. /// The skill cp. (Used for learning skills) -/// The xp in hero level. TODO +/// The xp in hero level. TODO /// The hero level. (shown as (+xx)) /// Unknown TODO [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)] -- 2.48.1