From f6e12d464e6469d88e0f5b41066d27378b2c763b Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:33:52 -0300 Subject: [PATCH 01/14] fix remaining low effort warnings not caught by dotnet format --- src/Persistence/DataSourceBase.cs | 2 +- .../CacheAwareRepositoryProvider.cs | 2 +- .../ConfigurationTypeRepository.cs | 1 + .../EntityFrameworkContextBase.cs | 30 +++++++++---------- .../Extensions/LocalizedStringConverter.cs | 11 ++++--- .../Extensions/ModelBuilderExtensions.cs | 4 +-- .../EntityFramework/GenericRepositoryBase.cs | 2 +- .../EntityFramework/Json/JsonObjectLoader.cs | 1 + ....OpenMU.Persistence.EntityFramework.csproj | 2 +- .../Migrations/00000000000000_Initial.cs | 3 ++ .../00000000000000_Initial.designer.cs | 1 + .../20221008183306_PetLevels.Designer.cs | 1 + .../Migrations/20221008183306_PetLevels.cs | 3 ++ .../20221018194652_Combo.Designer.cs | 1 + .../Migrations/20221018194652_Combo.cs | 3 ++ .../PersistenceContextProvider.cs | 12 ++++---- .../EntityFramework/RepositoryProvider.cs | 1 + ...loodCastleMonsterAttributesUpdatePlugIn.cs | 6 ++++ .../MUnique.OpenMU.Persistence.csproj | 2 +- src/SourceGenerators/CloneableGenerator.cs | 25 ++++++++-------- src/Web/AdminPanel/Pages/EditBase.cs | 2 +- 21 files changed, 68 insertions(+), 47 deletions(-) diff --git a/src/Persistence/DataSourceBase.cs b/src/Persistence/DataSourceBase.cs index afed853e3..741ba44b7 100644 --- a/src/Persistence/DataSourceBase.cs +++ b/src/Persistence/DataSourceBase.cs @@ -44,7 +44,7 @@ protected DataSourceBase(ILogger> logger, IPersistenceCon } /// - /// Gets the mapping of a to their of the . + /// Gets the mapping of a to their of the . /// protected abstract IReadOnlyDictionary> TypeToEnumerables { get; } diff --git a/src/Persistence/EntityFramework/CacheAwareRepositoryProvider.cs b/src/Persistence/EntityFramework/CacheAwareRepositoryProvider.cs index 1357bacc8..b31573a22 100644 --- a/src/Persistence/EntityFramework/CacheAwareRepositoryProvider.cs +++ b/src/Persistence/EntityFramework/CacheAwareRepositoryProvider.cs @@ -28,7 +28,7 @@ internal class CacheAwareRepositoryProvider : ICacheAwareRepositoryProvider, ICo /// Initializes a new instance of the class. /// /// The logger factory. - /// The configuration change publisher. + /// The configuration change listener. public CacheAwareRepositoryProvider(ILoggerFactory loggerFactory, IConfigurationChangeListener? configurationChangeListener) { this._loggerFactory = loggerFactory; diff --git a/src/Persistence/EntityFramework/ConfigurationTypeRepository.cs b/src/Persistence/EntityFramework/ConfigurationTypeRepository.cs index a3727fd48..52b283bb4 100644 --- a/src/Persistence/EntityFramework/ConfigurationTypeRepository.cs +++ b/src/Persistence/EntityFramework/ConfigurationTypeRepository.cs @@ -51,6 +51,7 @@ public ConfigurationTypeRepository(IContextAwareRepositoryProvider repositoryPro /// /// Gets all objects by using the to the current . /// + /// The cancellation token. /// All objects of the repository. public ValueTask> GetAllAsync(CancellationToken cancellationToken = default) { diff --git a/src/Persistence/EntityFramework/EntityFrameworkContextBase.cs b/src/Persistence/EntityFramework/EntityFrameworkContextBase.cs index 9339dbc6c..07c6ef8f0 100644 --- a/src/Persistence/EntityFramework/EntityFrameworkContextBase.cs +++ b/src/Persistence/EntityFramework/EntityFrameworkContextBase.cs @@ -127,21 +127,6 @@ public void Attach(object item) this.Context.Attach(item); } - private bool DetachInternal(object item) - { - var entry = this.Context.Entry(item); - if (entry is null) - { - return false; - } - - var previousState = entry.State; - entry.State = EntityState.Detached; - this.ForEachAggregate(item, obj => this.DetachInternal(obj)); - - return previousState != EntityState.Added; - } - /// public T CreateNew(params object?[] args) where T : class @@ -281,6 +266,21 @@ protected virtual void Dispose(bool dispose) this.Context.Dispose(); } + private bool DetachInternal(object item) + { + var entry = this.Context.Entry(item); + if (entry is null) + { + return false; + } + + var previousState = entry.State; + entry.State = EntityState.Detached; + this.ForEachAggregate(item, obj => this.DetachInternal(obj)); + + return previousState != EntityState.Added; + } + private IRepository GetRepository() where T : class { diff --git a/src/Persistence/EntityFramework/Extensions/LocalizedStringConverter.cs b/src/Persistence/EntityFramework/Extensions/LocalizedStringConverter.cs index 7a1ffdef4..809c554a3 100644 --- a/src/Persistence/EntityFramework/Extensions/LocalizedStringConverter.cs +++ b/src/Persistence/EntityFramework/Extensions/LocalizedStringConverter.cs @@ -12,17 +12,16 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Extensions; /// internal class LocalizedStringConverter : ValueConverter { - /// - /// Gets the singleton instance of the . - /// - public static LocalizedStringConverter Instance { get; } = new(); - /// /// Initializes a new instance of the class. /// private LocalizedStringConverter() - : base(value => value.Value ?? string.Empty, value => new LocalizedString(value)) { } + + /// + /// Gets the singleton instance of the . + /// + public static LocalizedStringConverter Instance { get; } = new(); } \ No newline at end of file diff --git a/src/Persistence/EntityFramework/Extensions/ModelBuilderExtensions.cs b/src/Persistence/EntityFramework/Extensions/ModelBuilderExtensions.cs index 2f4055ed7..ddc2fa440 100644 --- a/src/Persistence/EntityFramework/Extensions/ModelBuilderExtensions.cs +++ b/src/Persistence/EntityFramework/Extensions/ModelBuilderExtensions.cs @@ -12,8 +12,8 @@ internal static class ModelBuilderExtensions /// /// Configures the model builder to use UUID V7 as primary keys. /// - /// The model builder. - /// The model builder. + /// The model builder to configure. + /// The configured model builder with UUID V7 key generation applied. public static Microsoft.EntityFrameworkCore.ModelBuilder UseGuidV7Ids(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder) { var types = modelBuilder.Model.GetEntityTypes(); diff --git a/src/Persistence/EntityFramework/GenericRepositoryBase.cs b/src/Persistence/EntityFramework/GenericRepositoryBase.cs index f4b61d6b8..6230b9c4f 100644 --- a/src/Persistence/EntityFramework/GenericRepositoryBase.cs +++ b/src/Persistence/EntityFramework/GenericRepositoryBase.cs @@ -191,7 +191,7 @@ protected virtual async ValueTask LoadDependentDataAsync(object obj, DbContext c /// The loaded objects. /// The current context with which the objects got loaded. It is necessary to retrieve the foreign key ids. /// The cancellation token. - /// + /// A representing the asynchronous operation. protected virtual async ValueTask LoadDependentDataAsync(IEnumerable loadedObjects, DbContext currentContext, CancellationToken cancellationToken) { foreach (var obj in loadedObjects) diff --git a/src/Persistence/EntityFramework/Json/JsonObjectLoader.cs b/src/Persistence/EntityFramework/Json/JsonObjectLoader.cs index fbbdf0cc1..4b5675ede 100644 --- a/src/Persistence/EntityFramework/Json/JsonObjectLoader.cs +++ b/src/Persistence/EntityFramework/Json/JsonObjectLoader.cs @@ -39,6 +39,7 @@ public JsonObjectLoader(JsonQueryBuilder queryBuilder, JsonObjectDeserializer de /// /// The type of the object. /// The context. + /// The cancellation token. /// All objects of . public async ValueTask> LoadAllObjectsAsync(DbContext context, CancellationToken cancellationToken = default) where T : class, IIdentifiable diff --git a/src/Persistence/EntityFramework/MUnique.OpenMU.Persistence.EntityFramework.csproj b/src/Persistence/EntityFramework/MUnique.OpenMU.Persistence.EntityFramework.csproj index c22ab0180..4d32df071 100644 --- a/src/Persistence/EntityFramework/MUnique.OpenMU.Persistence.EntityFramework.csproj +++ b/src/Persistence/EntityFramework/MUnique.OpenMU.Persistence.EntityFramework.csproj @@ -49,7 +49,7 @@ - + diff --git a/src/Persistence/EntityFramework/Migrations/00000000000000_Initial.cs b/src/Persistence/EntityFramework/Migrations/00000000000000_Initial.cs index 03ab5c28a..9ac3b13fa 100644 --- a/src/Persistence/EntityFramework/Migrations/00000000000000_Initial.cs +++ b/src/Persistence/EntityFramework/Migrations/00000000000000_Initial.cs @@ -9,6 +9,9 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations using System; using Microsoft.EntityFrameworkCore.Migrations; + /// + /// Initial database schema creation. + /// public partial class Initial : Migration { /// diff --git a/src/Persistence/EntityFramework/Migrations/00000000000000_Initial.designer.cs b/src/Persistence/EntityFramework/Migrations/00000000000000_Initial.designer.cs index a7e25d4a0..fad5bcf56 100644 --- a/src/Persistence/EntityFramework/Migrations/00000000000000_Initial.designer.cs +++ b/src/Persistence/EntityFramework/Migrations/00000000000000_Initial.designer.cs @@ -15,6 +15,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations [Migration("00000000000000_Initial")] partial class Initial { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 diff --git a/src/Persistence/EntityFramework/Migrations/20221008183306_PetLevels.Designer.cs b/src/Persistence/EntityFramework/Migrations/20221008183306_PetLevels.Designer.cs index 6bf4d1324..e4487b800 100644 --- a/src/Persistence/EntityFramework/Migrations/20221008183306_PetLevels.Designer.cs +++ b/src/Persistence/EntityFramework/Migrations/20221008183306_PetLevels.Designer.cs @@ -15,6 +15,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations [Migration("20221008183306_PetLevels")] partial class PetLevels { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 diff --git a/src/Persistence/EntityFramework/Migrations/20221008183306_PetLevels.cs b/src/Persistence/EntityFramework/Migrations/20221008183306_PetLevels.cs index f7431fa7f..dcf3ab6b4 100644 --- a/src/Persistence/EntityFramework/Migrations/20221008183306_PetLevels.cs +++ b/src/Persistence/EntityFramework/Migrations/20221008183306_PetLevels.cs @@ -8,6 +8,9 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations { using Microsoft.EntityFrameworkCore.Migrations; + /// + /// Adds pet experience columns to Item and ItemDefinition. + /// public partial class PetLevels : Migration { /// diff --git a/src/Persistence/EntityFramework/Migrations/20221018194652_Combo.Designer.cs b/src/Persistence/EntityFramework/Migrations/20221018194652_Combo.Designer.cs index cc9558812..c42968c5e 100644 --- a/src/Persistence/EntityFramework/Migrations/20221018194652_Combo.Designer.cs +++ b/src/Persistence/EntityFramework/Migrations/20221018194652_Combo.Designer.cs @@ -15,6 +15,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations [Migration("20221018194652_Combo")] partial class Combo { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 diff --git a/src/Persistence/EntityFramework/Migrations/20221018194652_Combo.cs b/src/Persistence/EntityFramework/Migrations/20221018194652_Combo.cs index f699c5446..a1b4f617a 100644 --- a/src/Persistence/EntityFramework/Migrations/20221018194652_Combo.cs +++ b/src/Persistence/EntityFramework/Migrations/20221018194652_Combo.cs @@ -9,6 +9,9 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations using System; using Microsoft.EntityFrameworkCore.Migrations; + /// + /// Adds skill combo definitions and steps. + /// public partial class Combo : Migration { /// diff --git a/src/Persistence/EntityFramework/PersistenceContextProvider.cs b/src/Persistence/EntityFramework/PersistenceContextProvider.cs index 939aa23a1..56b55db2c 100644 --- a/src/Persistence/EntityFramework/PersistenceContextProvider.cs +++ b/src/Persistence/EntityFramework/PersistenceContextProvider.cs @@ -32,6 +32,9 @@ public PersistenceContextProvider(ILoggerFactory loggerFactory, IConfigurationCh this.RepositoryProvider = new CacheAwareRepositoryProvider(loggerFactory, changeListener); } + /// + IRepositoryProvider IPersistenceContextProvider.RepositoryProvider => this.RepositoryProvider; + /// /// Gets the repository provider. /// @@ -40,9 +43,6 @@ public PersistenceContextProvider(ILoggerFactory loggerFactory, IConfigurationCh /// internal CacheAwareRepositoryProvider RepositoryProvider { get; private set; } - /// - IRepositoryProvider IPersistenceContextProvider.RepositoryProvider => this.RepositoryProvider; - /// public async Task IsDatabaseUpToDateAsync(CancellationToken cancellationToken = default) { @@ -143,7 +143,7 @@ public async Task ShouldDoAutoSchemaUpdateAsync(CancellationToken cancella SELECT "AutoUpdateSchema" as "Value" FROM config."SystemConfiguration" """).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); } - catch (Exception ex) + catch (Exception) { return false; } @@ -152,7 +152,7 @@ public async Task ShouldDoAutoSchemaUpdateAsync(CancellationToken cancella /// /// Recreates the database by deleting and creating it again. /// - /// The disposable which should be disposed when the data creation process is finished. + /// The disposable that should be disposed of when the data creation process is finished. public async Task ReCreateDatabaseAsync() { var changePublisher = this._changeListener; @@ -171,7 +171,7 @@ public async Task ReCreateDatabaseAsync() await this.ApplyAllPendingUpdatesAsync().ConfigureAwait(false); - // We create a new repository provider, so that the previously loaded data is not effective anymore. + // We create a new repository provider so that the previously loaded data is not effective anymore. this.ResetCache(); } catch diff --git a/src/Persistence/EntityFramework/RepositoryProvider.cs b/src/Persistence/EntityFramework/RepositoryProvider.cs index a65a6d556..fb0600fe7 100644 --- a/src/Persistence/EntityFramework/RepositoryProvider.cs +++ b/src/Persistence/EntityFramework/RepositoryProvider.cs @@ -44,6 +44,7 @@ public RepositoryProvider(ILoggerFactory loggerFactory, IConfigurationChangeList /// Creates the generic repository for the specified type. /// /// Type of the entity. + /// The repository provider. /// The created repository. protected virtual IRepository CreateGenericRepository(Type entityType, IContextAwareRepositoryProvider repositoryProvider) { diff --git a/src/Persistence/Initialization/Updates/FixBloodCastleMonsterAttributesUpdatePlugIn.cs b/src/Persistence/Initialization/Updates/FixBloodCastleMonsterAttributesUpdatePlugIn.cs index 3b7934710..e39f24bbe 100644 --- a/src/Persistence/Initialization/Updates/FixBloodCastleMonsterAttributesUpdatePlugIn.cs +++ b/src/Persistence/Initialization/Updates/FixBloodCastleMonsterAttributesUpdatePlugIn.cs @@ -17,8 +17,14 @@ namespace MUnique.OpenMU.Persistence.Initialization.Updates; [Guid("D4E5F6A0-1B2C-3D4E-5F6A-7B8C9D0E1F2A")] public class FixBloodCastleMonsterAttributesUpdatePlugIn : UpdatePlugInBase { + /// + /// Gets the plugin name. + /// internal const string PlugInName = "Fix Blood Castle 7/8 Monster Attributes"; + /// + /// Gets the plugin description. + /// internal const string PlugInDescription = "Swaps attribute values between BC7 (monsters 138-143) and BC8 (monsters 428-433) so progression is correct: BC6 → BC7 → BC8."; private static readonly Guid LevelId = new("560931AD-0901-4342-B7F4-FD2E2FCC0563"); diff --git a/src/Persistence/MUnique.OpenMU.Persistence.csproj b/src/Persistence/MUnique.OpenMU.Persistence.csproj index 13b6f1f4b..3763d8f5b 100644 --- a/src/Persistence/MUnique.OpenMU.Persistence.csproj +++ b/src/Persistence/MUnique.OpenMU.Persistence.csproj @@ -42,7 +42,7 @@ - + diff --git a/src/SourceGenerators/CloneableGenerator.cs b/src/SourceGenerators/CloneableGenerator.cs index de254096e..604894b7e 100644 --- a/src/SourceGenerators/CloneableGenerator.cs +++ b/src/SourceGenerators/CloneableGenerator.cs @@ -120,18 +120,19 @@ public partial class {className} : IAssignable, IAssignable<{className}>, IClone sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine($$""" - /// - public {{(isInheritedClonable ? "override" : "virtual")}} void AssignValuesOf(object other, GameConfiguration gameConfiguration) - { - if (other is {{className}} typedOther) - { - AssignValuesOf(typedOther, gameConfiguration); - } - } - - /// - public virtual void AssignValuesOf({{className}} other, GameConfiguration gameConfiguration) - """); + /// + public {{(isInheritedClonable ? "override" : "virtual")}} void AssignValuesOf(object other, GameConfiguration gameConfiguration) + { + if (other is {{className}} typedOther) + { + AssignValuesOf(typedOther, gameConfiguration); + } + } + + /// + public virtual void AssignValuesOf({{className}} other, GameConfiguration gameConfiguration) + """); + sb.AppendLine(" {"); if (isInheritedClonable) { diff --git a/src/Web/AdminPanel/Pages/EditBase.cs b/src/Web/AdminPanel/Pages/EditBase.cs index 43411e76c..9b69cd10a 100644 --- a/src/Web/AdminPanel/Pages/EditBase.cs +++ b/src/Web/AdminPanel/Pages/EditBase.cs @@ -309,7 +309,7 @@ private async ValueTask OnBeforeInternalNavigationAsync(LocationChangingContext { var isConfirmed = await this.JavaScript.InvokeAsync( "window.confirm", - Resources.UnsavedChangesQuestion) + Resources.UnsavedChangesQuestion) .ConfigureAwait(true); if (!isConfirmed) From 468d396ded68f07ea088d20549760748ae88f1e7 Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Sat, 13 Jun 2026 17:38:49 -0300 Subject: [PATCH 02/14] fix remaining low effort warnings not caught by dotnet format --- src/ChatServer/ChatServer.cs | 69 ++-- src/GameLogic/AttackableExtensions.cs | 22 +- src/GameLogic/DefaultDropGenerator.cs | 4 +- src/GameLogic/ItemPowerUpFactory.cs | 4 +- src/GameLogic/Player.cs | 389 +++++++++--------- .../ChatCommands/ItemChatCommandPlugIn.cs | 3 +- .../MonsterAttributeScalerConfiguration.cs | 6 +- src/GameLogic/Views/Duel/DuelStartResult.cs | 21 + .../Views/Duel/IShowDuelRequestPlugIn.cs | 1 + .../IGuildRelationshipChangeResultPlugIn.cs | 62 ++- .../Character/AddExperienceExtendedPlugIn.cs | 4 +- .../RemoteView/World/ObjectMovedPlugIn.cs | 4 + src/GuildServer/GuildServer.cs | 4 +- .../EntityFramework/RepositoryProvider.cs | 6 +- .../GameConfigurationInitializerBase.cs | 97 ++--- .../Items/ArmorInitializerBase.cs | 162 ++++++++ .../FixCharStatsForceWavePlugInSeason6.cs | 11 +- .../Updates/FixDamageCalcsPlugInBase.cs | 45 +- .../Updates/FixDamageCalcsPlugInSeason6.cs | 5 +- .../Updates/FixDefenseCalcsPlugInBase.cs | 3 +- ...xItemOptionsAndAttackSpeedPlugInSeason6.cs | 15 +- .../Updates/FixItemRequirementsPlugIn.cs | 22 +- .../Updates/FixItemRequirementsPlugIn2.cs | 20 +- .../FixRageFighterMultipleHitSkillsPlugIn.cs | 3 +- .../Updates/FixSkillMultipliersPlugIn.cs | 3 +- .../Updates/UpdatePlugInBase.cs | 7 + .../Version075/MerchantStores.cs | 8 + .../TestAccounts/AccountInitializerBase.cs | 4 + .../VersionSeasonSix/Items/Weapons.cs | 6 +- .../VersionSeasonSix/SkillsInitializer.cs | 2 + .../TestAccounts/GameMaster2.cs | 4 + .../Initialization/WingsInitializerBase.cs | 40 ++ src/SourceGenerators/CloneableGenerator.cs | 9 +- .../MulticastConnectionServerStateObserver.cs | 3 + src/Web/AdminPanel/API/ServerController.cs | 9 +- .../AdminPanel/Components/Install.razor.cs | 6 +- .../Components/Layout/ConfigNavMenu.razor.cs | 6 +- src/Web/AdminPanel/Exports.cs | 18 +- .../AdminPanel/Pages/EditConfigGrid.razor.cs | 14 +- .../Components/Form/LookupField.razor.cs | 3 + .../Components/Form/ValueListWrapper.cs | 6 +- src/Web/Shared/Exports.cs | 10 +- src/Web/Shared/Services/NavigationHistory.cs | 12 +- src/Web/Shared/Services/ThemeController.cs | 24 +- .../ExperienceRateSplitTest.cs | 16 + tests/MUnique.OpenMU.Tests/GuildTestBase.cs | 3 + .../MoveItemActionTests.cs | 27 ++ 47 files changed, 812 insertions(+), 410 deletions(-) diff --git a/src/ChatServer/ChatServer.cs b/src/ChatServer/ChatServer.cs index a5596b9c2..d6fdf16f0 100644 --- a/src/ChatServer/ChatServer.cs +++ b/src/ChatServer/ChatServer.cs @@ -17,7 +17,7 @@ namespace MUnique.OpenMU.ChatServer; using Timer = System.Timers.Timer; /// -/// Chat Server Listener, accepts incoming connections. +/// Chat Server Listener that accepts incoming connections. /// public sealed class ChatServer : IChatServer, IDisposable { @@ -180,38 +180,6 @@ public void Initialize(ChatServerSettings settings) this._settings = settings; } - private void CreateCleanupTimers() - { - this._clientCleanupTimer = new Timer(this.Settings.ClientCleanUpInterval.TotalMilliseconds); - this._clientCleanupTimer.Elapsed += this.ClientCleanupInactiveClients; - this._clientCleanupTimer.Start(); - this._roomCleanupTimer = new Timer(this.Settings.RoomCleanUpInterval.TotalMilliseconds); - this._roomCleanupTimer.Elapsed += this.ClientCleanupUnusedRooms; - this._roomCleanupTimer.Start(); - } - - private void RemoveCleanupTimers() - { - this._clientCleanupTimer?.Stop(); - this._clientCleanupTimer?.Dispose(); - this._clientCleanupTimer = null; - - this._roomCleanupTimer?.Stop(); - this._roomCleanupTimer?.Dispose(); - this._roomCleanupTimer = null; - } - - private void CreateListeners() - { - foreach (var endpoint in this.Settings.Endpoints) - { - var listener = new ChatServerListener(endpoint, this._plugInManager, this._loggerFactory); - listener.ClientAccepted += this.ChatClientAcceptedAsync; - listener.ClientAccepting += this.ChatClientAcceptingAsync; - this._listeners.Add(listener); - } - } - /// public async ValueTask ShutdownAsync() { @@ -262,6 +230,38 @@ public void Dispose() } } + private void CreateCleanupTimers() + { + this._clientCleanupTimer = new Timer(this.Settings.ClientCleanUpInterval.TotalMilliseconds); + this._clientCleanupTimer.Elapsed += this.ClientCleanupInactiveClients; + this._clientCleanupTimer.Start(); + this._roomCleanupTimer = new Timer(this.Settings.RoomCleanUpInterval.TotalMilliseconds); + this._roomCleanupTimer.Elapsed += this.ClientCleanupUnusedRooms; + this._roomCleanupTimer.Start(); + } + + private void RemoveCleanupTimers() + { + this._clientCleanupTimer?.Stop(); + this._clientCleanupTimer?.Dispose(); + this._clientCleanupTimer = null; + + this._roomCleanupTimer?.Stop(); + this._roomCleanupTimer?.Dispose(); + this._roomCleanupTimer = null; + } + + private void CreateListeners() + { + foreach (var endpoint in this.Settings.Endpoints) + { + var listener = new ChatServerListener(endpoint, this._plugInManager, this._loggerFactory); + listener.ClientAccepted += this.ChatClientAcceptedAsync; + listener.ClientAccepting += this.ChatClientAcceptingAsync; + this._listeners.Add(listener); + } + } + /// /// Gets a random authentication token. /// @@ -322,8 +322,7 @@ private async void ClientCleanupInactiveClients(object? sender, ElapsedEventArgs } this._logger.LogDebug( - "Disconnecting client {Client}, because of activity timeout. LastActivity: {ClientLastActivity}", - client, client.LastActivity); + "Disconnecting client {Client}, because of activity timeout. LastActivity: {ClientLastActivity}", client, client.LastActivity); await client.LogOffAsync().ConfigureAwait(false); } diff --git a/src/GameLogic/AttackableExtensions.cs b/src/GameLogic/AttackableExtensions.cs index 705ed3a55..9d52326f8 100644 --- a/src/GameLogic/AttackableExtensions.cs +++ b/src/GameLogic/AttackableExtensions.cs @@ -112,7 +112,7 @@ public static async ValueTask CalculateDamageAsync(this IAttacker attac if (attacker.Attributes[Stats.HasDoubleWield] > 0) { - // double wield => 110% dmg (55% + 55%) + // Double wield => 110% dmg (55% + 55%). dmg += dmg; } @@ -136,7 +136,7 @@ public static async ValueTask CalculateDamageAsync(this IAttacker attac } else { - // Wizardry, Curse, and Fenrir + // Wizardry, Curse, and Fenrir. if (isExcellentHit) { dmg = (int)((baseMaxDamage * duelDmgDec) - defense); @@ -218,7 +218,8 @@ public static async ValueTask CalculateDamageAsync(this IAttacker attac { multiplier = skillMultiplier; - if (skill.Skill!.Number == 265 && !isPvp) // DragonSlasher + // DragonSlasher. + if (skill.Skill!.Number == 265 && !isPvp) { multiplier *= 3; } @@ -806,7 +807,7 @@ private static void GetBaseDmg(this IAttacker attacker, SkillEntry? skill, out i maximumBaseDamage = (int)attackerStats[Stats.FenrirBaseDmg] + skillMaximumDamage; break; default: - // the skill has some other damage type defined that is not applicable to this calculation + // The skill has some other damage type defined that is not applicable to this calculation. break; } @@ -925,15 +926,20 @@ private static int GetMasterSkillTreePhysicalPassiveDamageBonus(IAttacker attack { int bonusDamage = 0; - if (attacker.Attributes[Stats.IsSpearEquipped] > 0) // always two-handed + // Always two-handed. + if (attacker.Attributes[Stats.IsSpearEquipped] > 0) { bonusDamage = (int)attacker.Attributes[Stats.SpearBonusDamage]; } - else if (attacker.Attributes[Stats.IsScepterEquipped] > 0) // impossible to double wield + + // Impossible to double wield. + else if (attacker.Attributes[Stats.IsScepterEquipped] > 0) { bonusDamage = (int)attacker.Attributes[Stats.ScepterStrBonusDamage]; } - else if (attacker.Attributes[Stats.IsGloveWeaponEquipped] > 0) // impossible to double wield + + // Impossible to double wield. + else if (attacker.Attributes[Stats.IsGloveWeaponEquipped] > 0) { bonusDamage = (int)attacker.Attributes[Stats.GloveWeaponBonusDamage]; } @@ -946,7 +952,7 @@ private static int GetMasterSkillTreePhysicalPassiveDamageBonus(IAttacker attack if (attacker.Attributes[Stats.IsMaceEquipped] > 0) { - // In case of a double wield with different possible bonuses, take the average + // In case of a double wielding with different possible bonuses, take the average. bonusDamage = (int)(bonusDamage == 0 ? attacker.Attributes[Stats.MaceBonusDamage] : (bonusDamage + attacker.Attributes[Stats.MaceBonusDamage]) / 2); diff --git a/src/GameLogic/DefaultDropGenerator.cs b/src/GameLogic/DefaultDropGenerator.cs index 8818f4b70..4df00f4e1 100644 --- a/src/GameLogic/DefaultDropGenerator.cs +++ b/src/GameLogic/DefaultDropGenerator.cs @@ -448,7 +448,9 @@ private void ApplyRandomAncientOption(Item item) var itemOfSet = ancientSet.Items.First(i => object.Equals(i.ItemDefinition, item.Definition)); item.ItemSetGroups.Add(itemOfSet); - if (itemOfSet.BonusOption is { } bonusOption) // for example: +5str or +10str + + // For example: +5str or +10str. + if (itemOfSet.BonusOption is { } bonusOption) { var bonusOptionLink = new ItemOptionLink(); bonusOptionLink.ItemOption = bonusOption; diff --git a/src/GameLogic/ItemPowerUpFactory.cs b/src/GameLogic/ItemPowerUpFactory.cs index 1107bcef0..2be86909b 100644 --- a/src/GameLogic/ItemPowerUpFactory.cs +++ b/src/GameLogic/ItemPowerUpFactory.cs @@ -253,7 +253,9 @@ private IEnumerable GetPowerUpsOfItemOptions(Item item, Attribut var level = option.LevelType == LevelType.ItemLevel ? item.Level : optionLink.Level; var optionOfLevel = option.LevelDependentOptions?.FirstOrDefault(l => l.Level == level); - if (optionOfLevel is null && level > 1 && item.Definition!.Skill?.Number != 49) // Dinorant options are an exception + + // Dinorant options are an exception. + if (optionOfLevel is null && level > 1 && item.Definition!.Skill?.Number != 49) { this._logger.LogWarning("Item {item} (id {itemId}) has IncreasableItemOption ({option}, id {optionId}) with level {level}, but no definition in LevelDependentOptions.", item, item.GetId(), option, option.GetId(), level); continue; diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs index d30df10f7..99f44bf3d 100644 --- a/src/GameLogic/Player.cs +++ b/src/GameLogic/Player.cs @@ -410,7 +410,7 @@ public Point RandomPosition public uint CurrentHealth => (uint)(this.Attributes?[Stats.CurrentHealth] ?? 0); /// - /// Gets or sets a value indicating whether this player is online as friend, and shown as online in its friends friendlists. + /// Gets or sets a value indicating whether this player is online as a friend and shown as online in its friends friendlists. /// public bool OnlineAsFriend { get; set; } = true; @@ -480,7 +480,7 @@ public Point RandomPosition public Bucket? OldBucket { get; set; } /// - /// Gets or sets the mini game, which the player has currently entered. + /// Gets or sets the mini-game, which the player has currently entered. /// public MiniGameContext? CurrentMiniGame { get; set; } @@ -510,8 +510,8 @@ public IPetCommandManager? PetCommandManager if (this._petCommandManager is null && this.Inventory?.GetItem(InventoryConstants.RightHandSlot) is { } pet && pet.IsTrainablePet()) { - // Since the Raven is currently the only pet which can attack, we directly use it. - // However, in the future we might use a factory as strategy plugin here which creates the command manager + // Since the Raven is currently the only pet that can attack, we directly use it. + // However, in the future we might use a factory as a strategy plugin here which creates the command manager // depending on the actual pet. this._petCommandManager = new RavenCommandManager(this, pet); } @@ -603,9 +603,9 @@ public async ValueTask SetSelectedCharacterAsync(Character? character) } /// - /// Will be called when an item has been picked up by player. + /// Will be called when an item has been picked up by a player. /// - /// The item, which the player has picked up. + /// The item that the player has picked up. public async ValueTask OnPickedUpItemAsync(ILocateable item) { if (this.PlayerPickedUpItem is { } eventHandler) @@ -647,10 +647,10 @@ public bool IsSelfDefenseActive(Player attacker) } /// - /// Determines whether the self defense is active for any attacker. + /// Determines whether the self-defense is active for any attacker. /// /// - /// true if any self defense is active; otherwise, false. + /// true if any self-defense is active; otherwise, false. /// public bool IsAnySelfDefenseActive() { @@ -894,7 +894,7 @@ public async Task TeleportToMapAsync(GameMap targetMap, Point targetPoint) /// /// Is called after the player killed a . - /// Adds recovered mana and health to the players attributes. + /// Adds recovered mana and health to the player attributes. /// public async ValueTask AfterKilledMonsterAsync() { @@ -931,10 +931,10 @@ public bool CompliesRequirements(Item item) } /// - /// Tries to remove the money from the players inventory. + /// Tries to remove the money from the player inventory. /// - /// The value which should be removed. - /// True, if the players inventory had enough money to remove; Otherwise, false. + /// The value that should be removed. + /// True, if the player inventory had enough money to remove; Otherwise, false. public bool TryRemoveMoney(int value) { if (this.Money < value) @@ -947,10 +947,10 @@ public bool TryRemoveMoney(int value) } /// - /// Tries to deposit the money from the players inventory. + /// Tries to deposit the money from the player inventory. /// - /// The value which should be moved the the vault. - /// True, if the players inventory had enough money to move; Otherwise, false. + /// The value that should be retrieved from the vault. + /// True, if the player inventory had enough money to move; Otherwise, false. public bool TryDepositVaultMoney(int value) { if (this.Vault is null) @@ -972,9 +972,9 @@ public bool TryDepositVaultMoney(int value) } /// - /// Tries to take the money from the the vault. + /// Tries to take the money from the vault. /// - /// The value which should be retrieve the the vault. + /// The value that should be retrieved from the vault. /// True, if the vault had enough money to move and player inventory isn't maximum; Otherwise, false. public bool TryTakeVaultMoney(int value) { @@ -997,10 +997,10 @@ public bool TryTakeVaultMoney(int value) } /// - /// Tries to add the money from the players inventory. + /// Tries to add the money from the player inventory. /// - /// The value which should be added. - /// True, if the players inventory had space to add money; Otherwise, false. + /// The value that should be added. + /// True, if the player inventory had space to add money; Otherwise, false. public virtual bool TryAddMoney(int value) { if (this.Money + value > this.GameContext?.Configuration?.MaximumInventoryMoney) @@ -1115,7 +1115,7 @@ public virtual async ValueTask RespawnAtAsync(ExitGate gate) if (this.ViewPlugIns.GetPlugIn() is { } respawnPlugIn) { - // Older clients use separate packet for the respawn, while newer don't. + // Older clients use a separate packet for the respawn, while newer don't. // It requires a slightly different logic. this.CurrentMap = await this.GameContext.GetMapAsync(this.SelectedCharacter!.CurrentMap!.Number.ToUnsigned()).ConfigureAwait(false) ?? throw new InvalidOperationException("Current map not found."); await respawnPlugIn.RespawnAsync().ConfigureAwait(false); @@ -1135,11 +1135,11 @@ public virtual async ValueTask RespawnAtAsync(ExitGate gate) } /// - /// Signals that the game client of the player is ready after a map change (data has been loaded etc). - /// On this event, the player enters the game map on the server side, and interacts with the other objects. + /// Signals that the game client of the player is ready after a map change (data has been loaded etc.). + /// In this event, the player enters the game map on the server side and interacts with the other objects. /// /// - /// This method is called after the client sent us the F3 12 packet, of after + /// This method is called after the client sent us the F3 12 packet, or after /// the player entered the game. /// public async ValueTask ClientReadyAfterMapChangeAsync() @@ -1262,110 +1262,17 @@ public async ValueTask AddMasterExperienceAsync(int experience, IAttackable? kil await this.AddMasterExperienceCoreAsync(experience, killedObject).ConfigureAwait(false); } - private async ValueTask AddMasterExperienceCoreAsync(int experience, IAttackable? killedObject) - { - if (this.Attributes![Stats.MasterLevel] >= this.GameContext.Configuration.MaximumMasterLevel) - { - await this.InvokeViewPlugInAsync(p => p.AddExperienceAsync(0, killedObject, ExperienceType.MaxMasterLevelReached)).ConfigureAwait(false); - return; - } - - if (killedObject is not null && killedObject.Attributes[Stats.Level] < this.GameContext.Configuration.MinimumMonsterLevelForMasterExperience) - { - await this.InvokeViewPlugInAsync(p => p.AddExperienceAsync(0, killedObject, ExperienceType.MonsterLevelTooLowForMasterExperience)).ConfigureAwait(false); - return; - } - - long exp = experience; - - // Add the Exp - bool lvlup = false; - var expTable = this.GameContext.MasterExperienceTable; - if (expTable[(int)this.Attributes[Stats.MasterLevel] + 1] - this.SelectedCharacter!.MasterExperience < exp) - { - exp = expTable[(int)this.Attributes[Stats.MasterLevel] + 1] - this.SelectedCharacter.MasterExperience; - lvlup = true; - } - - this.SelectedCharacter.MasterExperience += exp; - - // Tell it to the Player - await this.InvokeViewPlugInAsync(p => p.AddExperienceAsync((int)exp, killedObject, ExperienceType.Master)).ConfigureAwait(false); - - // Check the lvl up - if (lvlup) - { - this.Attributes[Stats.MasterLevel]++; - this.SelectedCharacter.MasterLevelUpPoints += (int)this.Attributes[Stats.MasterPointsPerLevelUp]; - this.SetReclaimableAttributesToMaximum(); - this.Logger.LogDebug("Character {0} leveled up to master level {1}", this.SelectedCharacter.Name, this.Attributes[Stats.MasterLevel]); - await this.InvokeViewPlugInAsync(p => p.UpdateMasterLevelAsync()).ConfigureAwait(false); - await this.ForEachWorldObserverAsync(p => p.ShowEffectAsync(this, IShowEffectPlugIn.EffectType.LevelUp), true).ConfigureAwait(false); - } - } - /// /// Adds the experience to the current character. /// - /// The experience which should be added. - /// The killed object which caused the experience gain. + /// The experience that should be added. + /// The killed object that caused the experience gain. public async ValueTask AddExperienceAsync(int experience, IAttackable? killedObject) { using var d = await this._experienceLock.LockAsync().ConfigureAwait(false); await this.AddExperienceCoreAsync(experience, killedObject).ConfigureAwait(false); } - private async ValueTask AddExperienceCoreAsync(int experience, IAttackable? killedObject) - { - var remainingExperience = experience; - while (remainingExperience > 0) - { - if (this.Attributes![Stats.Level] >= this.GameContext.Configuration.MaximumLevel) - { - await this.InvokeViewPlugInAsync(p => p.AddExperienceAsync(0, killedObject, ExperienceType.MaxLevelReached)).ConfigureAwait(false); - return; - } - - long gainedExperience = remainingExperience; - bool isLevelUp = false; - var expTable = this.GameContext.ExperienceTable; - var expForNextLevel = expTable[(int)this.Attributes[Stats.Level] + 1]; - if (expForNextLevel - this.SelectedCharacter!.Experience < gainedExperience) - { - gainedExperience = expForNextLevel - this.SelectedCharacter.Experience; - isLevelUp = true; - } - - this.SelectedCharacter.Experience += gainedExperience; - - // Tell it to the Player - await this.InvokeViewPlugInAsync(p => p.AddExperienceAsync((int)gainedExperience, killedObject, ExperienceType.Normal)).ConfigureAwait(false); - - if (!isLevelUp) - { - return; - } - - this.Attributes[Stats.Level]++; - this.SelectedCharacter.LevelUpPoints += (int)this.Attributes[Stats.PointsPerLevelUp]; - this.SetReclaimableAttributesToMaximum(); - this.Logger.LogDebug("Character {0} leveled up to {1}", this.SelectedCharacter.Name, this.Attributes[Stats.Level]); - - this.GameContext.PlugInManager.GetPlugInPoint()?.CharacterLeveledUp(this); - - await this.InvokeViewPlugInAsync(p => p.UpdateLevelAsync()).ConfigureAwait(false); - await this.ForEachWorldObserverAsync(p => p.ShowEffectAsync(this, IShowEffectPlugIn.EffectType.LevelUp), true).ConfigureAwait(false); - - remainingExperience -= (int)gainedExperience; - if (remainingExperience <= 0 - || this.Attributes[Stats.Level] >= this.GameContext.Configuration.MaximumLevel - || this.GameContext.Configuration.PreventExperienceOverflow) - { - return; - } - } - } - /// /// Moves the player to the specified coordinate. /// @@ -1469,8 +1376,8 @@ public async Task RegenerateAsync() if (r.CurrentAttribute == Stats.CurrentShield && !this.IsAtSafezone() && attributes[Stats.ShieldRecoveryEverywhere] < 1) { - // Shield recovery is only possible at safe-zone, except the character has an specific attribute which has the effect that it's recovered everywhere. - // This attribute is usually provided by a level 380 armor and a Guardian Option. + // Shield recovery is only possible at safe-zone, except the character has a specific attribute which has the effect that it's recovered everywhere. + // This attribute is usually provided by level 380 armor and a Guardian Option. continue; } @@ -1485,7 +1392,7 @@ public async Task RegenerateAsync() } catch (InvalidOperationException) { - // may happen after a character disconnected in the mean time. + // May happen after a character disconnected in the meantime. } catch (Exception ex) { @@ -1913,6 +1820,63 @@ public async ValueTask SaveProgressAsync(CancellationToken cancellationTok return true; } + /// + /// Is called after the player killed a . + /// Increment PK Level. + /// + /// The player killed. + internal async ValueTask AfterKilledPlayerAsync(Player killedPlayer) + { + if (this.DuelRoom?.State == DuelState.DuelStarted) + { + return; + } + + var killedPlayerState = killedPlayer.SelectedCharacter?.State; + if (killedPlayerState is null) + { + return; + } + + if (killedPlayerState >= HeroState.PlayerKiller1stStage) + { + // Killing PKs is allowed. + return; + } + + if (killedPlayerState <= HeroState.PlayerKillWarning + && this.IsSelfDefenseActive(killedPlayer)) + { + // Self-defense is allowed. + return; + } + + // Killing a rival guild member (hostility) is allowed without PK penalty. + if (this.GuildStatus is { } killerStatus + && killedPlayer.GuildStatus is { } killedStatus + && this.GameContext is IGameServerContext serverContext + && serverContext.AreGuildsRival(killerStatus.GuildId, killedStatus.GuildId)) + { + return; + } + + if (this._selectedCharacter!.State != HeroState.PlayerKiller2ndStage) + { + if (this._selectedCharacter.State < HeroState.Normal) + { + this._selectedCharacter.State = HeroState.PlayerKillWarning; + } + else + { + this._selectedCharacter.State++; + } + } + + this._selectedCharacter.StateRemainingSeconds += (int)TimeSpan.FromHours(1).TotalSeconds; + this._selectedCharacter.PlayerKillCount += 1; + await this.ForEachWorldObserverAsync(o => o.UpdateCharacterHeroStateAsync(this), true).ConfigureAwait(false); + } + /// protected override async ValueTask DisposeAsyncCore() { @@ -1948,14 +1912,103 @@ protected virtual async ValueTask InternalDisconnectAsync() } /// - /// Creates the view plug in container. + /// Creates the view plugin container. /// - /// The created view plug in container. + /// The created view plugin container. protected virtual ICustomPlugInContainer CreateViewPlugInContainer() { throw new NotImplementedException("CreateViewPlugInContainer must be overwritten in derived classes."); } + private async ValueTask AddMasterExperienceCoreAsync(int experience, IAttackable? killedObject) + { + if (this.Attributes![Stats.MasterLevel] >= this.GameContext.Configuration.MaximumMasterLevel) + { + await this.InvokeViewPlugInAsync(p => p.AddExperienceAsync(0, killedObject, ExperienceType.MaxMasterLevelReached)).ConfigureAwait(false); + return; + } + + if (killedObject is not null && killedObject.Attributes[Stats.Level] < this.GameContext.Configuration.MinimumMonsterLevelForMasterExperience) + { + await this.InvokeViewPlugInAsync(p => p.AddExperienceAsync(0, killedObject, ExperienceType.MonsterLevelTooLowForMasterExperience)).ConfigureAwait(false); + return; + } + + long exp = experience; + + bool lvlup = false; + var expTable = this.GameContext.MasterExperienceTable; + if (expTable[(int)this.Attributes[Stats.MasterLevel] + 1] - this.SelectedCharacter!.MasterExperience < exp) + { + exp = expTable[(int)this.Attributes[Stats.MasterLevel] + 1] - this.SelectedCharacter.MasterExperience; + lvlup = true; + } + + this.SelectedCharacter.MasterExperience += exp; + + await this.InvokeViewPlugInAsync(p => p.AddExperienceAsync((int)exp, killedObject, ExperienceType.Master)).ConfigureAwait(false); + + if (lvlup) + { + this.Attributes[Stats.MasterLevel]++; + this.SelectedCharacter.MasterLevelUpPoints += (int)this.Attributes[Stats.MasterPointsPerLevelUp]; + this.SetReclaimableAttributesToMaximum(); + this.Logger.LogDebug("Character {0} leveled up to master level {1}", this.SelectedCharacter.Name, this.Attributes[Stats.MasterLevel]); + await this.InvokeViewPlugInAsync(p => p.UpdateMasterLevelAsync()).ConfigureAwait(false); + await this.ForEachWorldObserverAsync(p => p.ShowEffectAsync(this, IShowEffectPlugIn.EffectType.LevelUp), true).ConfigureAwait(false); + } + } + + private async ValueTask AddExperienceCoreAsync(int experience, IAttackable? killedObject) + { + var remainingExperience = experience; + while (remainingExperience > 0) + { + if (this.Attributes![Stats.Level] >= this.GameContext.Configuration.MaximumLevel) + { + await this.InvokeViewPlugInAsync(p => p.AddExperienceAsync(0, killedObject, ExperienceType.MaxLevelReached)).ConfigureAwait(false); + return; + } + + long gainedExperience = remainingExperience; + bool isLevelUp = false; + var expTable = this.GameContext.ExperienceTable; + var expForNextLevel = expTable[(int)this.Attributes[Stats.Level] + 1]; + if (expForNextLevel - this.SelectedCharacter!.Experience < gainedExperience) + { + gainedExperience = expForNextLevel - this.SelectedCharacter.Experience; + isLevelUp = true; + } + + this.SelectedCharacter.Experience += gainedExperience; + + await this.InvokeViewPlugInAsync(p => p.AddExperienceAsync((int)gainedExperience, killedObject, ExperienceType.Normal)).ConfigureAwait(false); + + if (!isLevelUp) + { + return; + } + + this.Attributes[Stats.Level]++; + this.SelectedCharacter.LevelUpPoints += (int)this.Attributes[Stats.PointsPerLevelUp]; + this.SetReclaimableAttributesToMaximum(); + this.Logger.LogDebug("Character {0} leveled up to {1}", this.SelectedCharacter.Name, this.Attributes[Stats.Level]); + + this.GameContext.PlugInManager.GetPlugInPoint()?.CharacterLeveledUp(this); + + await this.InvokeViewPlugInAsync(p => p.UpdateLevelAsync()).ConfigureAwait(false); + await this.ForEachWorldObserverAsync(p => p.ShowEffectAsync(this, IShowEffectPlugIn.EffectType.LevelUp), true).ConfigureAwait(false); + + remainingExperience -= (int)gainedExperience; + if (remainingExperience <= 0 + || this.Attributes[Stats.Level] >= this.GameContext.Configuration.MaximumLevel + || this.GameContext.Configuration.PreventExperienceOverflow) + { + return; + } + } + } + /// /// Handles the move to next safezone logic after death or disconnect. /// @@ -2077,7 +2130,7 @@ private async ValueTask RestoreTemporaryStorageItemsAsync() if (this.TemporaryStorage is not { ItemStorage.Items.Count: > 0 } temporaryStorage) { - // nothing to restore. + // Nothing to restore. return; } @@ -2126,7 +2179,7 @@ private async ValueTask RegenerateHeroStateAsync() currentCharacter.StateRemainingSeconds -= (int)Math.Round(secondsSinceLastRegenerate); if (currentCharacter.StateRemainingSeconds <= 0) { - // Change the status + // Change the status. if (currentCharacter.State > HeroState.Normal) { currentCharacter.State--; @@ -2137,7 +2190,7 @@ private async ValueTask RegenerateHeroStateAsync() } else { - // State is already Normal, no change needed + // State is already Normal, no change needed. } await this.ForEachWorldObserverAsync(p => p.UpdateCharacterHeroStateAsync(this), true).ConfigureAwait(false); @@ -2357,7 +2410,7 @@ async Task RespawnAsync(CancellationToken cancellationToken) /// /// Called when this player is in a duel and was killed. - /// Sets the player back to its starting position and reclaims the attributes, + /// Sets the player back to its starting position and reclaims the attributes /// so that they're ready for the next round. /// private async ValueTask RespawnOfDuelPartnerIfInDuelAsync() @@ -2373,62 +2426,6 @@ private async ValueTask RespawnOfDuelPartnerIfInDuelAsync() } } - /// - /// Is called after the player killed a . - /// Increment PK Level. - /// - internal async ValueTask AfterKilledPlayerAsync(Player killedPlayer) - { - if (this.DuelRoom?.State == DuelState.DuelStarted) - { - return; - } - - var killedPlayerState = killedPlayer.SelectedCharacter?.State; - if (killedPlayerState is null) - { - return; - } - - if (killedPlayerState >= HeroState.PlayerKiller1stStage) - { - // Killing PKs is allowed. - return; - } - - if (killedPlayerState <= HeroState.PlayerKillWarning - && this.IsSelfDefenseActive(killedPlayer)) - { - // Self defense is allowed. - return; - } - - // Killing a rival guild member (hostility) is allowed without PK penalty. - if (this.GuildStatus is { } killerStatus - && killedPlayer.GuildStatus is { } killedStatus - && this.GameContext is IGameServerContext serverContext - && serverContext.AreGuildsRival(killerStatus.GuildId, killedStatus.GuildId)) - { - return; - } - - if (this._selectedCharacter!.State != HeroState.PlayerKiller2ndStage) - { - if (this._selectedCharacter.State < HeroState.Normal) - { - this._selectedCharacter.State = HeroState.PlayerKillWarning; - } - else - { - this._selectedCharacter.State++; - } - } - - this._selectedCharacter.StateRemainingSeconds += (int)TimeSpan.FromHours(1).TotalSeconds; - this._selectedCharacter.PlayerKillCount += 1; - await this.ForEachWorldObserverAsync(o => o.UpdateCharacterHeroStateAsync(this), true).ConfigureAwait(false); - } - private SkillComboDefinition? DetermineComboDefinition() { var characterClass = this.SelectedCharacter!.CharacterClass; @@ -2440,7 +2437,7 @@ internal async ValueTask AfterKilledPlayerAsync(Player killedPlayer) return comboDefinition; } - // Check previous class + // Check previous class. characterClass = this.GameContext.Configuration.CharacterClasses.FirstOrDefault(c => c.NextGenerationClass == characterClass); } @@ -2482,7 +2479,7 @@ private void RaisePlayerLeftMap(GameMap map) } /// - /// Adds the missing stat attributes, e.g. after the character class has been changed outside of the game. + /// Adds the missing stat attributes, e.g., after the character class has been changed outside the game. /// private void AddMissingStatAttributes() { @@ -2560,7 +2557,7 @@ private async ValueTask OnPlayerEnteredWorldAsync() await this.InvokeViewPlugInAsync(p => p.UpdateMuHelperConfigurationAsync(muHelperConfiguration)).ConfigureAwait(false); } - // Add GM mark (mu logo above character's head) + // Add GM mark (mu logo above character's head). if (selectedCharacter.CharacterStatus == CharacterStatus.GameMaster) { await this.MagicEffectList.AddEffectAsync(new MagicEffect( @@ -2568,7 +2565,7 @@ await this.MagicEffectList.AddEffectAsync(new MagicEffect( GMEffect)).ConfigureAwait(false); } - // Restore previously opened Store + // Restore previously opened Store. if (selectedCharacter.IsStoreOpened && !string.IsNullOrWhiteSpace(selectedCharacter.StoreName) && this.IsPlayerStoreOpeningAfterEnterSupported) @@ -2626,7 +2623,7 @@ private async void OnTransformationSkinChanged(object? sender, EventArgs args) /// /// Sets the reclaimable attributes before a character enters the game. - /// Current shield and mana is set to their maximum values. + /// Current shield and mana are set to their maximum values. /// Current ability starts at the half of the maximum (as at the original server). /// The current health value was restored from the previous session and is not set to the maximum value - it's just limited by the maximum value. /// @@ -2792,18 +2789,16 @@ async ValueTask AddExpToPetAsync(Item pet, double experience) Item? GetTrainablePet(byte inventorySlot) { -#pragma warning disable SA1513 // Closing brace should be followed by blank line - if (this.Inventory?.GetItem(inventorySlot) is - { - Definition.PetExperienceFormula: not null, - Definition.MaximumItemLevel: > 0, - Durability: > 0 - } pet + var pet = this.Inventory?.GetItem(inventorySlot); + if (pet is not null + && pet.Definition is not null + && pet.Definition.PetExperienceFormula is not null + && pet.Definition.MaximumItemLevel > 0 + && pet.Durability > 0 && pet.Level < pet.Definition.MaximumItemLevel) { return pet; } -#pragma warning restore SA1513 // Closing brace should be followed by blank line return null; } diff --git a/src/GameLogic/PlugIns/ChatCommands/ItemChatCommandPlugIn.cs b/src/GameLogic/PlugIns/ChatCommands/ItemChatCommandPlugIn.cs index 2a2462ffe..4fcfeccee 100644 --- a/src/GameLogic/PlugIns/ChatCommands/ItemChatCommandPlugIn.cs +++ b/src/GameLogic/PlugIns/ChatCommands/ItemChatCommandPlugIn.cs @@ -92,7 +92,8 @@ private static void AddOption(TemporaryItem item, ItemChatCommandArgs arguments) .Where(o => o.OptionType == ItemOptionTypes.Option); IncreasableItemOption itemOption; - if (item.Definition.Skill?.Number == 49) // Dinorant + // Dinorant. + if (item.Definition.Skill?.Number == 49) { if ((arguments.Opt & 1) > 0) { diff --git a/src/GameLogic/PlugIns/MonsterAttributeScalerConfiguration.cs b/src/GameLogic/PlugIns/MonsterAttributeScalerConfiguration.cs index 2b6d3eafd..e7d48c71f 100644 --- a/src/GameLogic/PlugIns/MonsterAttributeScalerConfiguration.cs +++ b/src/GameLogic/PlugIns/MonsterAttributeScalerConfiguration.cs @@ -16,8 +16,6 @@ public class MonsterAttributeScalerConfiguration private float _defensePercentage = 25.0f; private float _healthPercentage = 25.0f; - private bool ScaleAllActive => this._scaleAllPercentage > 0; - /// /// Gets or sets the percentage applied to all stats at once. /// When set above 0, cascades to all individual fields. @@ -49,7 +47,7 @@ public float DamagePercentage } /// - /// Gets or sets the percentage by which monster attack rate is increased. + /// Gets or sets the percentage by which the monster attack rate is increased. /// public float AttackRatePercentage { @@ -84,6 +82,8 @@ public float HealthPercentage set => this.SetIndividualField(ref this._healthPercentage, value); } + private bool ScaleAllActive => this._scaleAllPercentage > 0; + private void SetIndividualField(ref float field, float value) { if (this.ScaleAllActive) diff --git a/src/GameLogic/Views/Duel/DuelStartResult.cs b/src/GameLogic/Views/Duel/DuelStartResult.cs index b69717628..06babc657 100644 --- a/src/GameLogic/Views/Duel/DuelStartResult.cs +++ b/src/GameLogic/Views/Duel/DuelStartResult.cs @@ -9,17 +9,38 @@ namespace MUnique.OpenMU.GameLogic.Views.Duel; /// public enum DuelStartResult { + /// + /// Undefined result. + /// Undefined, + /// + /// The duel was accepted. + /// Success, + /// + /// The duel was refused. + /// Refused, + /// + /// Failed by an error. + /// FailedByError, + /// + /// Failed because the player level is too low. + /// FailedByTooLowLevel, + /// + /// Failed because there is no free room. + /// FailedByNoFreeRoom, + /// + /// Failed because the player does not have enough money. + /// FailedByNotEnoughMoney, } \ No newline at end of file diff --git a/src/GameLogic/Views/Duel/IShowDuelRequestPlugIn.cs b/src/GameLogic/Views/Duel/IShowDuelRequestPlugIn.cs index a854b2523..f1718d606 100644 --- a/src/GameLogic/Views/Duel/IShowDuelRequestPlugIn.cs +++ b/src/GameLogic/Views/Duel/IShowDuelRequestPlugIn.cs @@ -12,5 +12,6 @@ public interface IShowDuelRequestPlugIn : IViewPlugIn /// /// A player has started a duel request which should be answered. /// + /// The requesting player. ValueTask ShowDuelRequestAsync(Player requester); } \ No newline at end of file diff --git a/src/GameLogic/Views/Guild/IGuildRelationshipChangeResultPlugIn.cs b/src/GameLogic/Views/Guild/IGuildRelationshipChangeResultPlugIn.cs index abed375b0..ba9875c93 100644 --- a/src/GameLogic/Views/Guild/IGuildRelationshipChangeResultPlugIn.cs +++ b/src/GameLogic/Views/Guild/IGuildRelationshipChangeResultPlugIn.cs @@ -51,59 +51,119 @@ public enum GuildRelationshipRequestType Disband, } +/// +/// The result of a guild relationship change request. +/// public enum GuildRelationshipChangeResultType { + /// + /// The request failed. + /// Failed, + /// + /// The request succeeded. + /// Success, + /// + /// The guild was not found. + /// GuildNotFound, - // GUILD_ANS_UNIONFAIL_BY_CASTLE: Alliance function will be restricted due to the Castle Siege. + /// + /// Failed because the alliance function is restricted due to the Castle Siege. + /// + // GUILD_ANS_UNIONFAIL_BY_CASTLE FailedDuringCastleSiege, + /// + /// No authorization for the request. + /// // GUILD_ANS_NOTEXIST_PERMISSION NoAuthorization, + /// + /// The guilds are already in an alliance. + /// // GUILD_ANS_EXIST_RELATIONSHIP_UNION AlreadyInAlliance, + /// + /// The guilds are already in a hostility relationship. + /// // GUILD_ANS_EXIST_RELATIONSHIP_RIVAL AlreadyInHostility, + /// + /// The guild alliance already exists. + /// // GUILD_ANS_EXIST_UNION GuildAllianceExists, + /// + /// The hostile guild already exists. + /// // GUILD_ANS_EXIST_RIVAL HostileGuildExists, + /// + /// The guild alliance does not exist. + /// // GUILD_ANS_NOTEXIST_UNION GuildAllianceDoesNotExist, + /// + /// The hostile guild does not exist. + /// // GUILD_ANS_NOTEXIST_RIVAL HostileGuildDoesNotExist, + /// + /// The requesting guild master is not the master of the alliance. + /// // GUILD_ANS_NOT_UNION_MASTER NotMasterOfGuildAlliance, + /// + /// The requesting guild is not a rival. + /// // GUILD_ANS_NOT_GUILD_RIVAL NotGuildRival, + /// + /// Incomplete requirements to create an alliance. + /// // GUILD_ANS_CANNOT_BE_UNION_MASTER_GUILD IncompleteRequirementsToCreateAlliance, + /// + /// The maximum number of guilds in an alliance was reached. + /// // GUILD_ANS_EXCEED_MAX_UNION_MEMBER MaximumNumberOfGuildsInAllianceReached, + /// + /// The request was cancelled. + /// // GUILD_ANS_CANCEL_REQUEST RequestCancelled, + /// + /// The alliance master is not in Gens. + /// // GUILD_ANS_UNION_MASTER_NOT_GENS AllianceMasterNotInGens, + /// + /// The guild master is not in Gens. + /// // GUILD_ANS_GUILD_MASTER_NOT_GENS GuildMasterNotInGens, + /// + /// The guilds are in different Gens factions. + /// // GUILD_ANS_UNION_MASTER_DISAGREE_GENS DifferentGens = 0xA3, } diff --git a/src/GameServer/RemoteView/Character/AddExperienceExtendedPlugIn.cs b/src/GameServer/RemoteView/Character/AddExperienceExtendedPlugIn.cs index 1965d318b..b03960c50 100644 --- a/src/GameServer/RemoteView/Character/AddExperienceExtendedPlugIn.cs +++ b/src/GameServer/RemoteView/Character/AddExperienceExtendedPlugIn.cs @@ -33,8 +33,10 @@ public class AddExperienceExtendedPlugIn : IAddExperiencePlugIn public async ValueTask AddExperienceAsync(int exp, IAttackable? obj, ExperienceType experienceType) { uint damage = 0; + + // Show damage only for party members. if (obj is not null - && this._player.Id != obj.LastDeath?.KillerId) // Show Damage only for party members. + && this._player.Id != obj.LastDeath?.KillerId) { damage = (uint)Math.Min(obj.LastDeath?.FinalHit.HealthDamage ?? 0, uint.MaxValue); } diff --git a/src/GameServer/RemoteView/World/ObjectMovedPlugIn.cs b/src/GameServer/RemoteView/World/ObjectMovedPlugIn.cs index 95072df92..4c28df859 100644 --- a/src/GameServer/RemoteView/World/ObjectMovedPlugIn.cs +++ b/src/GameServer/RemoteView/World/ObjectMovedPlugIn.cs @@ -200,6 +200,10 @@ private byte GetInstantMoveCode() } } + /// + /// Gets the walk code for the current client version. + /// + /// The walk code. protected byte GetWalkCode() { if (this._player.ClientVersion.Season == 0) diff --git a/src/GuildServer/GuildServer.cs b/src/GuildServer/GuildServer.cs index db17bb8fe..1b3b5a0cd 100644 --- a/src/GuildServer/GuildServer.cs +++ b/src/GuildServer/GuildServer.cs @@ -189,8 +189,10 @@ public ValueTask GuildMemberLeftGameAsync(uint guildId, Guid guildMemberId, byte if (this._guildDictionary.TryGetValue(guildId, out var guild)) { guild.SetServerId(guildMemberId, OfflineServerId); + + // Keep alliances in memory for simplicity. if (guild.Members.Values.All(member => member.ServerId == OfflineServerId) - && guild.Guild.AllianceGuild is null) // Keep alliances in memory for simplicity + && guild.Guild.AllianceGuild is null) { this.RemoveGuildContainer(guild); } diff --git a/src/Persistence/EntityFramework/RepositoryProvider.cs b/src/Persistence/EntityFramework/RepositoryProvider.cs index fb0600fe7..14b325968 100644 --- a/src/Persistence/EntityFramework/RepositoryProvider.cs +++ b/src/Persistence/EntityFramework/RepositoryProvider.cs @@ -8,7 +8,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework; using MUnique.OpenMU.Interfaces; /// -/// A repository provider which does not use caching. +/// A repository provider that does not use caching. /// internal class RepositoryProvider : BaseRepositoryProvider, IContextAwareRepositoryProvider { @@ -53,9 +53,9 @@ protected virtual IRepository CreateGenericRepository(Type entityType, IContextA } /// - /// Registers the repository. Adapts the type, so that the base type gets registered. + /// Registers the repository. Adapts the type so that the base type gets registered. /// - /// The generic type which the repository handles. + /// The generic type that the repository handles. /// The repository. protected override void RegisterRepository(Type type, IRepository repository) { diff --git a/src/Persistence/Initialization/GameConfigurationInitializerBase.cs b/src/Persistence/Initialization/GameConfigurationInitializerBase.cs index e5666910f..ce4f492f9 100644 --- a/src/Persistence/Initialization/GameConfigurationInitializerBase.cs +++ b/src/Persistence/Initialization/GameConfigurationInitializerBase.cs @@ -79,6 +79,34 @@ public override void Initialize() this.GameConfiguration.ItemOptions.Add(this.CreateOptionDefinition(Stats.DefenseRatePvm, ItemOptionDefinitionNumbers.DefenseRateOption, 5)); } + /// + /// Calculates the necessary experience for the specified character level. + /// + /// The character level. + /// The calculated necessary experience. + internal static long CalculateNeededExperience(long level) + { + if (level == 0) + { + return 0; + } + + if (level < 256) + { + return 10 * (level + 8) * (level - 1) * (level - 1); + } + + return (10 * (level + 8) * (level - 1) * (level - 1)) + + (1000 * (level - 247) * (level - 256) * (level - 256)); + } + + /// + /// Creates an item option definition for the specified attribute definition. + /// + /// The attribute definition. + /// The amount of the item option definition. + /// The base value for the item option. + /// The created item option definition. protected ItemOptionDefinition CreateOptionDefinition(AttributeDefinition attributeDefinition, short number, byte baseValue = 4) { var definition = this.Context.CreateNew(); @@ -115,24 +143,32 @@ protected ItemOptionDefinition CreateOptionDefinition(AttributeDefinition attrib } /// - /// Calculates the needed experience for the specified character level. + /// Assigns the character class home maps. + /// Needs to be called after the character classes and maps have been initialized. /// - /// The character level. - /// The calculated needed experience. - internal static long CalculateNeededExperience(long level) + protected void AssignCharacterClassHomeMaps() { - if (level == 0) + foreach (var characterClass in this.GameConfiguration.CharacterClasses) { - return 0; - } + byte mapNumber; + switch ((CharacterClassNumber)characterClass.Number) + { + case CharacterClassNumber.FairyElf: + case CharacterClassNumber.HighElf: + case CharacterClassNumber.MuseElf: + mapNumber = Noria.Number; + break; + case CharacterClassNumber.BloodySummoner: + case CharacterClassNumber.Summoner: + mapNumber = Elvenland.Number; + break; + default: + mapNumber = Lorencia.Number; + break; + } - if (level < 256) - { - return 10 * (level + 8) * (level - 1) * (level - 1); + characterClass.HomeMap = this.GameConfiguration.Maps.First(map => map.Number == mapNumber); } - - return (10 * (level + 8) * (level - 1) * (level - 1)) + - (1000 * (level - 247) * (level - 256) * (level - 256)); } private void AddItemDropGroups() @@ -245,39 +281,4 @@ private void AddGlobalBaseAttributeValues() var randomExperienceMaxMultiplier = this.Context.CreateNew(1.2f, Stats.RandomExperienceMaxMultiplier.GetPersistent(this.GameConfiguration)); this.GameConfiguration.GlobalBaseAttributeValues.Add(randomExperienceMaxMultiplier); } - - private long CalcNeededMasterExp(long lvl) - { - // f(x) = 505 * x^3 + 35278500 * x + 228045 * x^2 - return (505 * lvl * lvl * lvl) + (35278500 * lvl) + (228045 * lvl * lvl); - } - - /// - /// Assigns the character class home maps. - /// Needs to be called after the character classes and maps have been initialized. - /// - protected void AssignCharacterClassHomeMaps() - { - foreach (var characterClass in this.GameConfiguration.CharacterClasses) - { - byte mapNumber; - switch ((CharacterClassNumber)characterClass.Number) - { - case CharacterClassNumber.FairyElf: - case CharacterClassNumber.HighElf: - case CharacterClassNumber.MuseElf: - mapNumber = Noria.Number; - break; - case CharacterClassNumber.BloodySummoner: - case CharacterClassNumber.Summoner: - mapNumber = Elvenland.Number; - break; - default: - mapNumber = Lorencia.Number; - break; - } - - characterClass.HomeMap = this.GameConfiguration.Maps.First(map => map.Number == mapNumber); - } - } } diff --git a/src/Persistence/Initialization/Items/ArmorInitializerBase.cs b/src/Persistence/Initialization/Items/ArmorInitializerBase.cs index b0e4f1983..5cf13f41b 100644 --- a/src/Persistence/Initialization/Items/ArmorInitializerBase.cs +++ b/src/Persistence/Initialization/Items/ArmorInitializerBase.cs @@ -104,11 +104,55 @@ protected void BuildSets() } } + /// + /// Creates a shield. + /// + /// The item number. + /// The equipment slot. + /// The skill number. + /// The item width. + /// The item height. + /// The item name. + /// The item drop level. + /// The defense value. + /// The defense rate value. + /// The item durability. + /// The required strength attribute. + /// The required agility attribute. + /// The dark wizard class level. + /// The dark knight class level. + /// The elf class level. protected void CreateShield(byte number, byte slot, byte skill, byte width, byte height, string name, byte dropLevel, int defense, int defenseRate, byte durability, int strengthRequirement, int agilityRequirement, int darkWizardClassLevel, int darkKnightClassLevel, int elfClassLevel) { this.CreateShield(number, slot, skill, width, height, name, dropLevel, defense, defenseRate, durability, 0, strengthRequirement, agilityRequirement, 0, 0, 0, darkWizardClassLevel, darkKnightClassLevel, elfClassLevel, 0, 0, 0, 0); } + /// + /// Creates a shield. + /// + /// The item number. + /// The equipment slot. + /// The skill number. + /// The item width. + /// The item height. + /// The item name. + /// The item drop level. + /// The defense value. + /// The defense rate value. + /// The item durability. + /// The required character level. + /// The required strength attribute. + /// The required agility attribute. + /// The required energy attribute. + /// The required vitality attribute. + /// The required leadership attribute. + /// The dark wizard class level. + /// The dark knight class level. + /// The elf class level. + /// The magic gladiator class level. + /// The dark lord class level. + /// The summoner class level. + /// The rage fighter class level. protected void CreateShield(byte number, byte slot, byte skill, byte width, byte height, string name, byte dropLevel, int defense, int defenseRate, byte durability, int levelRequirement, int strengthRequirement, int agilityRequirement, int energyRequirement, int vitalityRequirement, int leadershipRequirement, int darkWizardClassLevel, int darkKnightClassLevel, int elfClassLevel, int magicGladiatorClassLevel, int darkLordClassLevel, int summonerClassLevel, int ragefighterClassLevel) { var shield = this.CreateArmor(number, slot, width, height, name, dropLevel, 0, durability, levelRequirement, strengthRequirement, agilityRequirement, energyRequirement, vitalityRequirement, leadershipRequirement, darkWizardClassLevel, darkKnightClassLevel, elfClassLevel, magicGladiatorClassLevel, darkLordClassLevel, summonerClassLevel, ragefighterClassLevel, true); @@ -139,6 +183,21 @@ protected void CreateShield(byte number, byte slot, byte skill, byte width, byte shield.PossibleItemOptions.Add(this.GameConfiguration.GetDefenseRateOption()); } + /// + /// Creates gloves. + /// + /// The item number. + /// The item name. + /// The item drop level. + /// The defense value. + /// The attack speed bonus. + /// The item durability. + /// The required strength attribute. + /// The required agility attribute. + /// The dark wizard class level. + /// The dark knight class level. + /// The elf class level. + /// The created item definition. protected ItemDefinition CreateGloves(byte number, string name, byte dropLevel, int defense, int attackSpeed, byte durability, int strengthRequirement, int agilityRequirement, int darkWizardClassLevel, int darkKnightClassLevel, int elfClassLevel) { var gloves = this.CreateArmor(number, 5, 2, 2, name, dropLevel, defense, durability, strengthRequirement, agilityRequirement, darkWizardClassLevel, darkKnightClassLevel, elfClassLevel); @@ -150,6 +209,25 @@ protected ItemDefinition CreateGloves(byte number, string name, byte dropLevel, return gloves; } + /// + /// Creates gloves. + /// + /// The item number. + /// The item name. + /// The item drop level. + /// The defense value. + /// The attack speed bonus. + /// The item durability. + /// The required character level. + /// The required strength attribute. + /// The required agility attribute. + /// The dark wizard class level. + /// The dark knight class level. + /// The elf class level. + /// The magic gladiator class level. + /// The dark lord class level. + /// The summoner class level. + /// The created item definition. protected ItemDefinition CreateGloves(byte number, string name, byte dropLevel, int defense, int attackSpeed, byte durability, int levelRequirement, int strengthRequirement, int agilityRequirement, int darkWizardClassLevel, int darkKnightClassLevel, int elfClassLevel, int magicGladiatorClassLevel, int darkLordClassLevel, int summonerClassLevel) { var gloves = this.CreateArmor(number, 5, 2, 2, name, dropLevel, defense, durability, levelRequirement, strengthRequirement, agilityRequirement, 0, 0, 0, darkWizardClassLevel, darkKnightClassLevel, elfClassLevel, magicGladiatorClassLevel, darkLordClassLevel, summonerClassLevel, 0); @@ -161,6 +239,24 @@ protected ItemDefinition CreateGloves(byte number, string name, byte dropLevel, return gloves; } + /// + /// Creates boots. + /// + /// The item number. + /// The equipment slot. + /// The item width. + /// The item height. + /// The item name. + /// The item drop level. + /// The defense value. + /// The walk speed bonus. + /// The item durability. + /// The required strength attribute. + /// The required agility attribute. + /// The dark wizard class level. + /// The dark knight class level. + /// The elf class level. + /// The created item definition. protected ItemDefinition CreateBoots(byte number, byte slot, byte width, byte height, string name, byte dropLevel, int defense, int walkSpeed, byte durability, int strengthRequirement, int agilityRequirement, int darkWizardClassLevel, int darkKnightClassLevel, int elfClassLevel) { var boots = this.CreateArmor(number, 6, 2, 2, name, dropLevel, defense, durability, strengthRequirement, agilityRequirement, darkWizardClassLevel, darkKnightClassLevel, elfClassLevel); @@ -172,6 +268,29 @@ protected ItemDefinition CreateBoots(byte number, byte slot, byte width, byte he return boots; } + /// + /// Creates boots. + /// + /// The item number. + /// The item name. + /// The item drop level. + /// The defense value. + /// The walk speed bonus. + /// The item durability. + /// The required character level. + /// The required strength attribute. + /// The required agility attribute. + /// The required energy attribute. + /// The required vitality attribute. + /// The required leadership attribute. + /// The dark wizard class level. + /// The dark knight class level. + /// The elf class level. + /// The magic gladiator class level. + /// The dark lord class level. + /// The summoner class level. + /// The rage fighter class level. + /// The created item definition. protected ItemDefinition CreateBoots(byte number, string name, byte dropLevel, int defense, int walkSpeed, byte durability, int levelRequirement, int strengthRequirement, int agilityRequirement, int energyRequirement, int vitalityRequirement, int leadershipRequirement, int darkWizardClassLevel, int darkKnightClassLevel, int elfClassLevel, int magicGladiatorClassLevel, int darkLordClassLevel, int summonerClassLevel, int ragefighterClassLevel) { var boots = this.CreateArmor(number, 6, 2, 2, name, dropLevel, defense, durability, levelRequirement, strengthRequirement, agilityRequirement, energyRequirement, vitalityRequirement, leadershipRequirement, darkWizardClassLevel, darkKnightClassLevel, elfClassLevel, magicGladiatorClassLevel, darkLordClassLevel, summonerClassLevel, ragefighterClassLevel); @@ -183,6 +302,23 @@ protected ItemDefinition CreateBoots(byte number, string name, byte dropLevel, i return boots; } + /// + /// Creates an armor. + /// + /// The item number. + /// The equipment slot. + /// The item width. + /// The item height. + /// The item name. + /// The item drop level. + /// The defense value. + /// The item durability. + /// The required strength attribute. + /// The required agility attribute. + /// The dark wizard class level. + /// The dark knight class level. + /// The elf class level. + /// The created item definition. protected ItemDefinition CreateArmor(byte number, byte slot, byte width, byte height, string name, byte dropLevel, int defense, byte durability, int strengthRequirement, int agilityRequirement, int darkWizardClassLevel, int darkKnightClassLevel, int elfClassLevel) { var magicGladiatorClassLevel = 0; @@ -196,6 +332,32 @@ protected ItemDefinition CreateArmor(byte number, byte slot, byte width, byte he return this.CreateArmor(number, slot, width, height, name, dropLevel, defense, durability, 0, strengthRequirement, agilityRequirement, 0, 0, 0, darkWizardClassLevel, darkKnightClassLevel, elfClassLevel, magicGladiatorClassLevel, 0, 0, 0); } + /// + /// Creates an armor. + /// + /// The item number. + /// The equipment slot. + /// The item width. + /// The item height. + /// The item name. + /// The item drop level. + /// The defense value. + /// The item durability. + /// The required character level. + /// The required strength attribute. + /// The required agility attribute. + /// The required energy attribute. + /// The required vitality attribute. + /// The required leadership attribute. + /// The dark wizard class level. + /// The dark knight class level. + /// The elf class level. + /// The magic gladiator class level. + /// The dark lord class level. + /// The summoner class level. + /// The rage fighter class level. + /// If set to true the armor is considered a shield. + /// The created item definition. protected ItemDefinition CreateArmor(byte number, byte slot, byte width, byte height, string name, byte dropLevel, int defense, byte durability, int levelRequirement, int strengthRequirement, int agilityRequirement, int energyRequirement, int vitalityRequirement, int leadershipRequirement, int darkWizardClassLevel, int darkKnightClassLevel, int elfClassLevel, int magicGladiatorClassLevel, int darkLordClassLevel, int summonerClassLevel, int ragefighterClassLevel, bool isShield = false) { var armor = this.Context.CreateNew(); diff --git a/src/Persistence/Initialization/Updates/FixCharStatsForceWavePlugInSeason6.cs b/src/Persistence/Initialization/Updates/FixCharStatsForceWavePlugInSeason6.cs index 9f0b95972..502e7232f 100644 --- a/src/Persistence/Initialization/Updates/FixCharStatsForceWavePlugInSeason6.cs +++ b/src/Persistence/Initialization/Updates/FixCharStatsForceWavePlugInSeason6.cs @@ -83,7 +83,8 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio } } - if (charClass.Number == 16 || charClass.Number == 17) // Lord classes + // Lord classes. + if (charClass.Number == 16 || charClass.Number == 17) { charClass.StatAttributes.First(attr => attr.Attribute == Stats.CurrentHealth).BaseValue = 90; charClass.StatAttributes.First(attr => attr.Attribute == Stats.CurrentMana).BaseValue = 40; @@ -103,7 +104,9 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio totalVitalityoMaximumHealth.InputOperand = 2; } } - else if (charClass.Number == 24 || charClass.Number == 25) // RF classes + + // RF classes. + else if (charClass.Number == 24 || charClass.Number == 25) { if (charClass.AttributeCombinations.FirstOrDefault(attrCombo => attrCombo.TargetAttribute == Stats.DefenseRatePvp && attrCombo.InputAttribute == Stats.TotalAgility) is { } totalAgilityToDefenseRatePvp) { @@ -150,7 +153,9 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio totalVitalityToFenrirBaseDmg.InputOperand = 1.0f / 3; } } - else if (charClass.Number == 20 || charClass.Number == 22 || charClass.Number == 23) // Summoner classes + + // Summoner classes. + else if (charClass.Number == 20 || charClass.Number == 22 || charClass.Number == 23) { charClass.StatAttributes.First(attr => attr.Attribute == Stats.CurrentMana).BaseValue = 40; diff --git a/src/Persistence/Initialization/Updates/FixDamageCalcsPlugInBase.cs b/src/Persistence/Initialization/Updates/FixDamageCalcsPlugInBase.cs index b2e140d87..79dade575 100644 --- a/src/Persistence/Initialization/Updates/FixDamageCalcsPlugInBase.cs +++ b/src/Persistence/Initialization/Updates/FixDamageCalcsPlugInBase.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -255,10 +255,11 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio charClass.BaseAttributeValues.Add(context.CreateNew(1, physicalBaseDmgIncrease)); charClass.BaseAttributeValues.Add(context.CreateNew(-1, hasDoubleWield)); - // Add double wield attribute combos - if (charClass.Number == 4 || charClass.Number == 6 || charClass.Number == 7 // DK classes - || charClass.Number == 12 || charClass.Number == 13 // MG classes - || charClass.Number == 24 || charClass.Number == 25) // RF classes + // Add double wield attribute combos. + // DK classes, MG classes, RF classes. + if (charClass.Number == 4 || charClass.Number == 6 || charClass.Number == 7 + || charClass.Number == 12 || charClass.Number == 13 + || charClass.Number == 24 || charClass.Number == 25) { var tempDoubleWield = context.CreateNew(Guid.NewGuid(), "Temp Double Wield multiplier", string.Empty); gameConfiguration.Attributes.Add(tempDoubleWield); @@ -306,10 +307,11 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio charClass.AttributeCombinations.Add(maxPhysBaseDmgByRightWeaponToMaxPhysBaseDmgByWeapon); } - // Add wizardry damage attribute combos - if (charClass.Number == 0 || charClass.Number == 2 || charClass.Number == 3 // DW classes - || charClass.Number == 12 || charClass.Number == 13 // MG classes - || charClass.Number == 20 || charClass.Number == 22 || charClass.Number == 23) // Summoner classes + // Add wizardry damage attribute combos. + // DW classes, MG classes, Summoner classes. + if (charClass.Number == 0 || charClass.Number == 2 || charClass.Number == 3 + || charClass.Number == 12 || charClass.Number == 13 + || charClass.Number == 20 || charClass.Number == 22 || charClass.Number == 23) { var baseMinDmgBonusToMinWizBaseDmg = context.CreateNew( minimumWizBaseDmg, @@ -360,7 +362,9 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio } var attrCombos = charClass.AttributeCombinations.ToList(); - if (charClass.Number == 4 || charClass.Number == 6 || charClass.Number == 7) // DK classes + + // DK classes. + if (charClass.Number == 4 || charClass.Number == 6 || charClass.Number == 7) { foreach (var attrCombo in attrCombos) { @@ -382,13 +386,15 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio charClass.AttributeCombinations.Add(weaponMasteryAttackSpeedToAttackSpeedAny); } - if (charClass.Number == 16 || charClass.Number == 17) // Lord classes + // Lord classes. + if (charClass.Number == 16 || charClass.Number == 17) { foreach (var attrCombo in attrCombos) { + // RavenBonusDamage is the old RavenBaseDamage. if ((attrCombo.TargetAttribute == Stats.RavenMinimumDamage || attrCombo.TargetAttribute == Stats.RavenMaximumDamage) && attrCombo.InputOperand == 1 - && attrCombo.InputAttribute == Stats.RavenBonusDamage) // RavenBonusDamage is the old RavenBaseDamage + && attrCombo.InputAttribute == Stats.RavenBonusDamage) { charClass.AttributeCombinations.Remove(attrCombo); } @@ -421,7 +427,8 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio charClass.BaseAttributeValues.Add(context.CreateNew(0.3f, ravenCriticalDamageChance)); } - if (charClass.Number == 0 || charClass.Number == 2 || charClass.Number == 3) // DW classes + // DW classes. + if (charClass.Number == 0 || charClass.Number == 2 || charClass.Number == 3) { var weaponMasteryAttackSpeedToAttackSpeedAny = context.CreateNew( attackSpeedAny, @@ -439,7 +446,8 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio charClass.BaseAttributeValues.Add(context.CreateNew(0, Stats.NovaStageDamage.GetPersistent(gameConfiguration))); } - if (charClass.Number == 8 || charClass.Number == 10 || charClass.Number == 11) // Elf classes + // Elf classes. + if (charClass.Number == 8 || charClass.Number == 10 || charClass.Number == 11) { var ammunitionDmgIncrease = context.CreateNew(Guid.NewGuid(), "Ammunition damage increase", string.Empty); gameConfiguration.Attributes.Add(ammunitionDmgIncrease); @@ -571,7 +579,8 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio charClass.AttributeCombinations.Add(weaponMasteryAttackSpeedToAttackSpeedAny); } - if (charClass.Number == 12 || charClass.Number == 13) // MG classes + // MG classes. + if (charClass.Number == 12 || charClass.Number == 13) { foreach (var attrCombo in attrCombos) { @@ -607,7 +616,8 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio charClass.BaseAttributeValues.Add(context.CreateNew(0, isOneHandedSwordEquipped)); } - if (charClass.Number == 24 || charClass.Number == 25) // RF classes + // RF classes. + if (charClass.Number == 24 || charClass.Number == 25) { foreach (var attrCombo in attrCombos) { @@ -623,7 +633,8 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio } } - if (charClass.Number == 20 || charClass.Number == 22 || charClass.Number == 23) // Summoner classes + // Summoner classes. + if (charClass.Number == 20 || charClass.Number == 22 || charClass.Number == 23) { var statsDefense = context.CreateNew(Guid.NewGuid(), "Stats defense", string.Empty); gameConfiguration.Attributes.Add(statsDefense); diff --git a/src/Persistence/Initialization/Updates/FixDamageCalcsPlugInSeason6.cs b/src/Persistence/Initialization/Updates/FixDamageCalcsPlugInSeason6.cs index f3c449bff..0f19599f4 100644 --- a/src/Persistence/Initialization/Updates/FixDamageCalcsPlugInSeason6.cs +++ b/src/Persistence/Initialization/Updates/FixDamageCalcsPlugInSeason6.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -145,7 +145,8 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio var powerUps = infiniteArrowEffect.PowerUpDefinitions.ToList(); foreach (var powerUp in powerUps) { - if (powerUp.TargetAttribute == Stats.SkillExtraManaCost || powerUp.TargetAttribute == Stats.BaseDamageBonus) // SkillExtraManaCost is the old ManaLossAfterHit + // SkillExtraManaCost is the old ManaLossAfterHit. + if (powerUp.TargetAttribute == Stats.SkillExtraManaCost || powerUp.TargetAttribute == Stats.BaseDamageBonus) { infiniteArrowEffect.PowerUpDefinitions.Remove(powerUp); } diff --git a/src/Persistence/Initialization/Updates/FixDefenseCalcsPlugInBase.cs b/src/Persistence/Initialization/Updates/FixDefenseCalcsPlugInBase.cs index 693d34fe3..6e6c5fa46 100644 --- a/src/Persistence/Initialization/Updates/FixDefenseCalcsPlugInBase.cs +++ b/src/Persistence/Initialization/Updates/FixDefenseCalcsPlugInBase.cs @@ -159,7 +159,8 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio charClass.AttributeCombinations.Add(bonusDefenseWithShieldToDefenseFinal); charClass.AttributeCombinations.Add(bonusDefenseRateWithShieldToDefenseRatePvm); - if (charClass.Number == 16 || charClass.Number == 17) // Lord classes + // Lord classes. + if (charClass.Number == 16 || charClass.Number == 17) { var bonusDefenseWithHorseToDefenseFinal = context.CreateNew( defenseFinal, diff --git a/src/Persistence/Initialization/Updates/FixItemOptionsAndAttackSpeedPlugInSeason6.cs b/src/Persistence/Initialization/Updates/FixItemOptionsAndAttackSpeedPlugInSeason6.cs index c28f78a52..287fa07a9 100644 --- a/src/Persistence/Initialization/Updates/FixItemOptionsAndAttackSpeedPlugInSeason6.cs +++ b/src/Persistence/Initialization/Updates/FixItemOptionsAndAttackSpeedPlugInSeason6.cs @@ -134,13 +134,14 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio staff.BasePowerUpAttributes.Remove(twoHandedWeapon); } - if (staff.Number == 9 // Dragon Soul Staff - || staff.Number == 11 // Kundun Staff - || staff.Number == 12 // Grand Viper Staff - || staff.Number == 13 // Platina Staff - || staff.Number == 30 // Deadly Staff - || staff.Number == 31 // Imperial Staff - || staff.Number == 33) // Chromatic Staff + // Dragon Soul Staff, Kundun Staff, Grand Viper Staff, Platina Staff, Deadly Staff, Imperial Staff, Chromatic Staff. + if (staff.Number == 9 + || staff.Number == 11 + || staff.Number == 12 + || staff.Number == 13 + || staff.Number == 30 + || staff.Number == 31 + || staff.Number == 33) { staff.BasePowerUpAttributes.Add(CreateNewBasePowerUpDefinition(Stats.IsOneHandedStaffEquipped)); } diff --git a/src/Persistence/Initialization/Updates/FixItemRequirementsPlugIn.cs b/src/Persistence/Initialization/Updates/FixItemRequirementsPlugIn.cs index eaa0876e8..d70182fac 100644 --- a/src/Persistence/Initialization/Updates/FixItemRequirementsPlugIn.cs +++ b/src/Persistence/Initialization/Updates/FixItemRequirementsPlugIn.cs @@ -12,13 +12,23 @@ namespace MUnique.OpenMU.Persistence.Initialization.Updates; using MUnique.OpenMU.PlugIns; /// -/// Updates some item requirements for elf bows which were intialized wrongly. +/// Updates some item requirements for elf bows that were intialized wrongly. /// [PlugIn] [Display(Name = PlugInName, Description = PlugInDescription)] [Guid("9E8DB2CB-1972-40D3-9129-6964ABFEB4DC")] public class FixItemRequirementsPlugIn : UpdatePlugInBase { + /// + /// The plugin name. + /// + internal const string PlugInName = "Fix Item Requirements (Elf Bows)"; + + /// + /// The plugin description. + /// + internal const string PlugInDescription = "Updates some item requirements for elf bows which were intialized wrongly."; + private static readonly List<(int Group, int Number, int StrengthRequirement, int AgilityRequirement, int EnergyRequirement, int VitalityRequirement)> RequirementCorrections = [ (4, 0, 20, 80, 0, 0), // Short Bow @@ -68,16 +78,6 @@ See ItemExtensions.GetRequirement. */ ]; - /// - /// The plug in name. - /// - internal const string PlugInName = "Fix Item Requirements (Elf Bows)"; - - /// - /// The plug in description. - /// - internal const string PlugInDescription = "Updates some item requirements for elf bows which were intialized wrongly."; - /// public override UpdateVersion Version => UpdateVersion.FixItemRequirements; diff --git a/src/Persistence/Initialization/Updates/FixItemRequirementsPlugIn2.cs b/src/Persistence/Initialization/Updates/FixItemRequirementsPlugIn2.cs index cb3a16177..e988defdb 100644 --- a/src/Persistence/Initialization/Updates/FixItemRequirementsPlugIn2.cs +++ b/src/Persistence/Initialization/Updates/FixItemRequirementsPlugIn2.cs @@ -20,6 +20,16 @@ namespace MUnique.OpenMU.Persistence.Initialization.Updates; [Guid("A7C9D4E1-8F2B-4A3C-9E6D-7B8F9A0E1C2D")] public class FixItemRequirementsPlugIn2 : UpdatePlugInBase { + /// + /// The plugin name. + /// + internal const string PlugInName = "Fix Item Requirements (Elf Bows) v2"; + + /// + /// The plugin description. + /// + internal const string PlugInDescription = "Updates some item requirements for elf bows which were initialized wrongly. This fixes configurations created after the initial fix but before the base data was corrected."; + private static readonly List<(int Group, int Number, int StrengthRequirement, int AgilityRequirement, int EnergyRequirement, int VitalityRequirement)> RequirementCorrections = [ (4, 0, 20, 80, 0, 0), // Short Bow @@ -31,16 +41,6 @@ public class FixItemRequirementsPlugIn2 : UpdatePlugInBase (4, 6, 40, 150, 0, 0), // Chaos Nature Bow ]; - /// - /// The plug in name. - /// - internal const string PlugInName = "Fix Item Requirements (Elf Bows) v2"; - - /// - /// The plug in description. - /// - internal const string PlugInDescription = "Updates some item requirements for elf bows which were initialized wrongly. This fixes configurations created after the initial fix but before the base data was corrected."; - /// public override UpdateVersion Version => UpdateVersion.FixItemRequirements2; diff --git a/src/Persistence/Initialization/Updates/FixRageFighterMultipleHitSkillsPlugIn.cs b/src/Persistence/Initialization/Updates/FixRageFighterMultipleHitSkillsPlugIn.cs index 810284765..e9f5563e1 100644 --- a/src/Persistence/Initialization/Updates/FixRageFighterMultipleHitSkillsPlugIn.cs +++ b/src/Persistence/Initialization/Updates/FixRageFighterMultipleHitSkillsPlugIn.cs @@ -61,7 +61,8 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio gameConfiguration.CharacterClasses.ForEach(charClass => { // Update summoner berserker defense reduction attributes - if (charClass.Number == 20 || charClass.Number == 22 || charClass.Number == 23) // Summoner classes + // Summoner classes. + if (charClass.Number == 20 || charClass.Number == 22 || charClass.Number == 23) { if (charClass.AttributeCombinations.FirstOrDefault(attr => attr.TargetAttribute == Stats.DefensePvm && attr.AggregateType == AggregateType.AddFinal) is { } finalBerserkerHealthDecrementToDefensePvm) { diff --git a/src/Persistence/Initialization/Updates/FixSkillMultipliersPlugIn.cs b/src/Persistence/Initialization/Updates/FixSkillMultipliersPlugIn.cs index 6885b90f0..929401439 100644 --- a/src/Persistence/Initialization/Updates/FixSkillMultipliersPlugIn.cs +++ b/src/Persistence/Initialization/Updates/FixSkillMultipliersPlugIn.cs @@ -81,7 +81,8 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio // Fix RF classes skill multiplier gameConfiguration.CharacterClasses.ForEach(charClass => { - if (charClass.Number == 24 || charClass.Number == 25) // RF classes + // RF classes. + if (charClass.Number == 24 || charClass.Number == 25) { if (charClass.BaseAttributeValues.FirstOrDefault(a => a.Definition == Stats.SkillMultiplier) is { } skillMult) { diff --git a/src/Persistence/Initialization/Updates/UpdatePlugInBase.cs b/src/Persistence/Initialization/Updates/UpdatePlugInBase.cs index 89c977a45..1dd53bf93 100644 --- a/src/Persistence/Initialization/Updates/UpdatePlugInBase.cs +++ b/src/Persistence/Initialization/Updates/UpdatePlugInBase.cs @@ -51,6 +51,13 @@ public async ValueTask ApplyUpdateAsync(IContext context, GameConfiguration game /// protected abstract ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration); + /// + /// Adds a stat attribute if it does not already exist in the game configuration. + /// + /// The persistence context. + /// The game configuration. + /// The attribute to add. + /// true if the attribute was added; otherwise, false. protected bool AddStatIfNotExists(IContext context, GameConfiguration gameConfiguration, AttributeDefinition attribute) { if (gameConfiguration.Attributes.Contains(attribute)) diff --git a/src/Persistence/Initialization/Version075/MerchantStores.cs b/src/Persistence/Initialization/Version075/MerchantStores.cs index 99ae27cb6..e714445a3 100644 --- a/src/Persistence/Initialization/Version075/MerchantStores.cs +++ b/src/Persistence/Initialization/Version075/MerchantStores.cs @@ -107,6 +107,7 @@ protected virtual ItemStorage CreateWanderingMerchant(short number) /// /// Creates the merchant store of 'Hanzo the Blacksmith'. /// + /// The number of the store. /// The created store. protected virtual ItemStorage CreateHanzoTheBlacksmith(short number) { @@ -151,6 +152,7 @@ protected virtual ItemStorage CreateHanzoTheBlacksmith(short number) /// /// Creates the merchant store of 'Pasi the Mage'. /// + /// The number of the store. /// The created store. protected virtual ItemStorage CreatePasiTheMageStore(short number) { @@ -196,6 +198,7 @@ protected virtual ItemStorage CreatePasiTheMageStore(short number) /// /// Creates the merchant store of 'Elf Lala'. /// + /// The number of the store. /// The created store. protected virtual ItemStorage CreateElfLalaStore(short number) { @@ -266,6 +269,7 @@ protected virtual ItemStorage CreateElfLalaStore(short number) /// /// Creates the merchant store of 'Izabel the Wizard'. /// + /// The number of the store. /// The created store. protected virtual ItemStorage CreateIzabelTheWizardStore(short number) { @@ -319,6 +323,7 @@ protected virtual ItemStorage CreateIzabelTheWizardStore(short number) /// /// Creates the merchant store of 'Eo the Craftsman'. /// + /// The number of the store. /// The created store. protected virtual ItemStorage CreateEoTheCraftsmanStore(short number) { @@ -359,6 +364,7 @@ protected virtual ItemStorage CreateEoTheCraftsmanStore(short number) /// /// Creates the merchant store of 'Zienna'. /// + /// The number of the store. /// The created store. protected virtual ItemStorage CreateZiennaStore(short number) { @@ -395,6 +401,7 @@ protected virtual ItemStorage CreateZiennaStore(short number) /// /// Creates the merchant store of 'Lumen the Barmaid'. /// + /// The number of the store. /// The created store. protected virtual ItemStorage CreateLumenTheBarmaidStore(short number) { @@ -412,6 +419,7 @@ protected virtual ItemStorage CreateLumenTheBarmaidStore(short number) /// /// Creates the merchant store of 'Carmen the Barmaid'. /// + /// The number of the store. /// The created store. protected virtual ItemStorage CreateCarenTheBarmaidStore(short number) { diff --git a/src/Persistence/Initialization/Version075/TestAccounts/AccountInitializerBase.cs b/src/Persistence/Initialization/Version075/TestAccounts/AccountInitializerBase.cs index f2daf806f..8493cc2a7 100644 --- a/src/Persistence/Initialization/Version075/TestAccounts/AccountInitializerBase.cs +++ b/src/Persistence/Initialization/Version075/TestAccounts/AccountInitializerBase.cs @@ -238,6 +238,10 @@ protected void AddTestJewelsAndPotions(ItemStorage inventory) inventory.Items.Add(this.CreateAlcohol(31)); } + /// + /// Adds scrolls to the inventory. + /// + /// The inventory. protected void AddScrolls(ItemStorage inventory) { inventory.Items.Add(this.CreateScroll(32, 0)); // Scroll of Poison diff --git a/src/Persistence/Initialization/VersionSeasonSix/Items/Weapons.cs b/src/Persistence/Initialization/VersionSeasonSix/Items/Weapons.cs index a95851ef2..2da54e86b 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Items/Weapons.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Items/Weapons.cs @@ -338,7 +338,8 @@ protected void CreateWeapon(byte @group, byte number, byte slot, int skillNumber var qualifiedCharacterClasses = this.GameConfiguration.DetermineCharacterClasses(wizardClass, knightClass, elfClass, magicGladiatorClass, darkLordClass, summonerClass, ragefighterClass); qualifiedCharacterClasses.ToList().ForEach(item.QualifiedCharacters.Add); - if (height == 1) // bolts and arrows + // Bolts and arrows. + if (height == 1) { var damagePowerUp = this.CreateItemBasePowerUpDefinition(Stats.AmmunitionDamageBonus, 0f, AggregateType.AddRaw); damagePowerUp.BonusPerLevelTable = this._ammunitionDamageIncreaseTable; @@ -441,7 +442,8 @@ protected void CreateWeapon(byte @group, byte number, byte slot, int skillNumber item.BasePowerUpAttributes.Add(this.CreateItemBasePowerUpDefinition(Stats.IsTwoHandedWeaponEquipped, 1, AggregateType.AddRaw)); } - if (group == (int)ItemGroups.Swords || (group == (int)ItemGroups.Scepters && number == 5)) // Crystal Sword + // Crystal Sword. + if (group == (int)ItemGroups.Swords || (group == (int)ItemGroups.Scepters && number == 5)) { if (ragefighterClass == 0 || number < 2) { diff --git a/src/Persistence/Initialization/VersionSeasonSix/SkillsInitializer.cs b/src/Persistence/Initialization/VersionSeasonSix/SkillsInitializer.cs index c38a260e0..15697ef28 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/SkillsInitializer.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/SkillsInitializer.cs @@ -16,6 +16,7 @@ namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix; /// internal class SkillsInitializer : SkillsInitializerBase { +#pragma warning disable SA1600 // formula constants are documented by their name and inline skill number comment internal const string Formula1204 = "(1 + (((((((level - 30) ^ 3) + 25000) / 499) / 6)))) * 10"; // 17 internal const string Formula61408 = "(1 + (((((((level - 30) ^ 3) + 25000) / 499) / 50) * 100) / 12)) * 85 * 6"; // 12 internal const string Formula51173 = "(1 + (((((((level - 30) ^ 3) + 25000) / 499) / 50) * 100) / 12)) * 85 * 5"; // 13 @@ -48,6 +49,7 @@ internal class SkillsInitializer : SkillsInitializerBase internal const string Formula1806 = "(1 + (((((((level - 30) ^ 3) + 25000) / 499) / 6)))) * 15"; // 15 internal const string Formula32751 = "(1 + ( ( ( ( ( ( (level - 30) ^ 3) + 25000) / 499) / 50) * 100) / 12)) * 85 * 3.2"; // 31 internal const string Formula5418 = "(1 + ( ( ( ( ( ( (level - 30) ^ 3) + 25000) / 499) / 50) * 100) / 12)) * 45"; // 34 +#pragma warning restore SA1600 private static readonly IDictionary EffectsOfSkills = new Dictionary { diff --git a/src/Persistence/Initialization/VersionSeasonSix/TestAccounts/GameMaster2.cs b/src/Persistence/Initialization/VersionSeasonSix/TestAccounts/GameMaster2.cs index 5b2a96090..6a09572d7 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/TestAccounts/GameMaster2.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/TestAccounts/GameMaster2.cs @@ -44,6 +44,10 @@ protected override Account CreateAccount() return account; } + /// + /// Creates the summoner character. + /// + /// The created summoner character. protected Character CreateSummoner() { var character = this.CreateCharacter(this.AccountName + "Sum", CharacterClassNumber.DimensionMaster, this.Level, 0); diff --git a/src/Persistence/Initialization/WingsInitializerBase.cs b/src/Persistence/Initialization/WingsInitializerBase.cs index f5d9f0f25..99c22e3e6 100644 --- a/src/Persistence/Initialization/WingsInitializerBase.cs +++ b/src/Persistence/Initialization/WingsInitializerBase.cs @@ -28,15 +28,40 @@ protected WingsInitializerBase(IContext context, GameConfiguration gameConfigura { } + /// + /// The option type for wings. + /// protected enum OptionType { + /// + /// Health recover option. + /// HealthRecover, + + /// + /// Physical damage option. + /// PhysDamage, + + /// + /// Wizardry damage option. + /// WizDamage, + + /// + /// Curse damage option. + /// CurseDamage, + + /// + /// Defense option. + /// Defense, } + /// + /// Gets the maximum item level. + /// protected abstract int MaximumItemLevel { get; } /// @@ -77,6 +102,9 @@ protected IEnumerable BuildOptions(params (int, OptionTyp } } + /// + /// Creates the absorb bonus per level table. + /// protected ItemLevelBonusTable CreateAbsorbBonusPerLevel() { IEnumerable Generate() @@ -90,6 +118,9 @@ IEnumerable Generate() return this.CreateItemBonusTable(Generate().ToArray(), "Wing absorb", "The damage absorb of wings per item level, 2 % less damage per level."); } + /// + /// Creates the damage increase bonus per level table for first and third wings. + /// protected ItemLevelBonusTable CreateDamageIncreaseBonusPerLevelFirstAndThirdWings() { IEnumerable Generate() @@ -103,6 +134,9 @@ IEnumerable Generate() return this.CreateItemBonusTable(Generate().ToArray(), "Damage Increase (1st and 3rd Wings)", "Defines the damage increase multiplier for first and third level wings. It's 2 % per wing level."); } + /// + /// Creates the damage increase bonus per level table for second wings. + /// protected ItemLevelBonusTable CreateDamageIncreaseBonusPerLevelSecondWings() { IEnumerable Generate() @@ -116,11 +150,17 @@ IEnumerable Generate() return this.CreateItemBonusTable(Generate().ToArray(), "Damage Increase (2nd Wings)", "Defines the damage increase multiplier for second level wings. It's 1 % per wing level."); } + /// + /// Creates the bonus defense per level table. + /// protected ItemLevelBonusTable CreateBonusDefensePerLevel() { return this.CreateItemBonusTable(DefenseIncreaseByLevel, "Defense Bonus (1st and 2nd Wings)", "Defines the defense bonus per level for 1st and 2nd level wings."); } + /// + /// Creates the bonus defense per level table for third wings. + /// protected ItemLevelBonusTable CreateBonusDefensePerLevelThirdWings() { return this.CreateItemBonusTable(DefenseIncreaseByLevelThirdWings, "Defense Bonus (3rd Wings)", "Defines the defense bonus per level for 3rd level wings."); diff --git a/src/SourceGenerators/CloneableGenerator.cs b/src/SourceGenerators/CloneableGenerator.cs index 604894b7e..ed07b4388 100644 --- a/src/SourceGenerators/CloneableGenerator.cs +++ b/src/SourceGenerators/CloneableGenerator.cs @@ -97,6 +97,10 @@ private static StringBuilder GeneratePartialClass(ClassDeclarationSyntax annotat var isInheritedClonable = declaredClassSymbol.BaseType?.GetAttributes().Any(a => a.AttributeClass?.Name == CloneableAttributeName) ?? false; sb.AppendLine($""" + // + // Licensed under the MIT License. See LICENSE file in the project root for full license information. + // + namespace {ns}; using System; @@ -104,12 +108,13 @@ namespace {ns}; using MUnique.OpenMU.DataModel; using MUnique.OpenMU.DataModel.Configuration; + /// public partial class {className} : IAssignable, IAssignable<{className}>, ICloneable<{className}> """); sb.AppendLine("{"); sb.AppendLine($""" /// - public virtual {className} Clone(GameConfiguration gameConfiguration) + public {(isInheritedClonable ? "override" : "virtual")} {className} Clone(GameConfiguration gameConfiguration) """); sb.AppendLine(" {"); sb.AppendLine($""" @@ -125,7 +130,7 @@ public partial class {className} : IAssignable, IAssignable<{className}>, IClone { if (other is {{className}} typedOther) { - AssignValuesOf(typedOther, gameConfiguration); + this.AssignValuesOf(typedOther, gameConfiguration); } } diff --git a/src/Startup/MulticastConnectionServerStateObserver.cs b/src/Startup/MulticastConnectionServerStateObserver.cs index 4efdaf7cb..e80d20faf 100644 --- a/src/Startup/MulticastConnectionServerStateObserver.cs +++ b/src/Startup/MulticastConnectionServerStateObserver.cs @@ -18,6 +18,9 @@ internal class MulticastConnectionServerStateObserver : IGameServerStateObserver private readonly MemorizingObserver _memorizingObserver = new(); private readonly List _observers = new(); + /// + /// Initializes a new instance of the class. + /// public MulticastConnectionServerStateObserver() { this._observers.Add(this._memorizingObserver); diff --git a/src/Web/AdminPanel/API/ServerController.cs b/src/Web/AdminPanel/API/ServerController.cs index 36d5df3b5..53adff031 100644 --- a/src/Web/AdminPanel/API/ServerController.cs +++ b/src/Web/AdminPanel/API/ServerController.cs @@ -23,14 +23,14 @@ public class ServerController : Controller /// /// Initializes a new instance of the class. /// - /// + /// The game servers. public ServerController(IDictionary gameServers) => this._gameServers = gameServers; /// /// Sends a global message to the specified server. /// - /// - /// + /// The server id. + /// The message. /// A representing the asynchronous operation. [Route("send/{id=0}")] public async Task SendGlobalMessage(int id, [FromQuery(Name = "msg")] string msg) @@ -70,9 +70,8 @@ public async Task GetIsOnlineAsync(string accountName) } /// - /// + /// Gets the server state. /// - /// [HttpGet] [Route("status")] public IActionResult ServerState() diff --git a/src/Web/AdminPanel/Components/Install.razor.cs b/src/Web/AdminPanel/Components/Install.razor.cs index 07aaac00c..11267d829 100644 --- a/src/Web/AdminPanel/Components/Install.razor.cs +++ b/src/Web/AdminPanel/Components/Install.razor.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -39,8 +39,6 @@ public sealed partial class Install /// public bool IsInstalled { get; private set; } - private int CurrentConnections => this.ServerProvider.Servers.Where(s => s.ServerState != ServerState.Timeout).Sum(s => s.CurrentConnections); - /// /// Gets or sets the installation finished callback. /// @@ -59,6 +57,8 @@ public sealed partial class Install [Inject] public IServerProvider ServerProvider { get; set; } = null!; + private int CurrentConnections => this.ServerProvider.Servers.Where(s => s.ServerState != ServerState.Timeout).Sum(s => s.CurrentConnections); + /// protected override void OnParametersSet() { diff --git a/src/Web/AdminPanel/Components/Layout/ConfigNavMenu.razor.cs b/src/Web/AdminPanel/Components/Layout/ConfigNavMenu.razor.cs index 987447ac8..57fd02d31 100644 --- a/src/Web/AdminPanel/Components/Layout/ConfigNavMenu.razor.cs +++ b/src/Web/AdminPanel/Components/Layout/ConfigNavMenu.razor.cs @@ -15,9 +15,6 @@ public partial class ConfigNavMenu { private bool _expandGameConfig; - [Inject] - private NavigationHistory NavigationHistory { get; set; } = null!; - /// /// Gets or sets the game configuration identifier. /// @@ -25,6 +22,9 @@ public partial class ConfigNavMenu [Required] public Guid GameConfigurationId { get; set; } + [Inject] + private NavigationHistory NavigationHistory { get; set; } = null!; + private void ToggleGameConfig() { this._expandGameConfig = !this._expandGameConfig; diff --git a/src/Web/AdminPanel/Exports.cs b/src/Web/AdminPanel/Exports.cs index e74d55822..488761912 100644 --- a/src/Web/AdminPanel/Exports.cs +++ b/src/Web/AdminPanel/Exports.cs @@ -15,15 +15,6 @@ namespace MUnique.OpenMU.Web.AdminPanel; /// public static class Exports { - /// - /// Gets the url prefix to the scripts of this project. - /// - private static string Prefix { get; } = $"_content/{typeof(Exports).Namespace}"; - - private static IEnumerable AdminPanelScripts => []; - - private static IEnumerable AdminPanelStylesheets => []; - /// /// Gets the scripts. /// @@ -44,4 +35,13 @@ public static class Exports public static ImmutableList Stylesheets { get; } = AdminPanelEnvironment.IsHostingEmbedded ? Web.Map.Exports.Stylesheets.Concat(AdminPanelStylesheets).ToImmutableList() : AdminPanelStylesheets.ToImmutableList(); + + /// + /// Gets the url prefix to the scripts of this project. + /// + private static string Prefix { get; } = $"_content/{typeof(Exports).Namespace}"; + + private static IEnumerable AdminPanelScripts => []; + + private static IEnumerable AdminPanelStylesheets => []; } \ No newline at end of file diff --git a/src/Web/AdminPanel/Pages/EditConfigGrid.razor.cs b/src/Web/AdminPanel/Pages/EditConfigGrid.razor.cs index ca16e322b..1427243a6 100644 --- a/src/Web/AdminPanel/Pages/EditConfigGrid.razor.cs +++ b/src/Web/AdminPanel/Pages/EditConfigGrid.razor.cs @@ -102,13 +102,6 @@ private IQueryable? ViewModels private string? NameFilter { get; set; } - /// - protected override void OnInitialized() - { - base.OnInitialized(); - this.CreationPanelService.ItemCreated += this.OnItemCreatedAsync; - } - /// public async ValueTask DisposeAsync() { @@ -132,6 +125,13 @@ public async ValueTask DisposeAsync() } } + /// + protected override void OnInitialized() + { + base.OnInitialized(); + this.CreationPanelService.ItemCreated += this.OnItemCreatedAsync; + } + /// protected override async Task OnParametersSetAsync() { diff --git a/src/Web/Shared/Components/Form/LookupField.razor.cs b/src/Web/Shared/Components/Form/LookupField.razor.cs index e0f317015..83c878f2e 100644 --- a/src/Web/Shared/Components/Form/LookupField.razor.cs +++ b/src/Web/Shared/Components/Form/LookupField.razor.cs @@ -43,6 +43,9 @@ public partial class LookupField [Parameter] public ILookupController? ExplicitLookupController { get; set; } + /// + /// Gets or sets the caption factory. + /// [Parameter] public Func CaptionFactory { get; set; } = obj => obj.GetName(); diff --git a/src/Web/Shared/Components/Form/ValueListWrapper.cs b/src/Web/Shared/Components/Form/ValueListWrapper.cs index 242b7ef38..2b6bfb16c 100644 --- a/src/Web/Shared/Components/Form/ValueListWrapper.cs +++ b/src/Web/Shared/Components/Form/ValueListWrapper.cs @@ -25,6 +25,9 @@ public ValueListWrapper(IList innerList) this.AddRange(this._innerList.Select(this.CreateWrapper)); } + /// + public bool IsReadOnly => this._innerList.IsReadOnly; + /// TValue IList.this[int index] { @@ -86,9 +89,6 @@ public bool Remove(TValue item) } } - /// - public bool IsReadOnly => this._innerList.IsReadOnly; - /// public int IndexOf(TValue item) { diff --git a/src/Web/Shared/Exports.cs b/src/Web/Shared/Exports.cs index 8d544a776..fba3b8937 100644 --- a/src/Web/Shared/Exports.cs +++ b/src/Web/Shared/Exports.cs @@ -15,11 +15,6 @@ namespace MUnique.OpenMU.Web.Shared; /// public static class Exports { - /// - /// Gets the url prefix to the scripts of this project. - /// - private static string Prefix { get; } = $"_content/{typeof(Exports).Namespace}"; - /// /// Gets the scripts. /// @@ -35,6 +30,11 @@ public static class Exports /// public static ImmutableList Stylesheets { get; } = SharedStylesheets.ToImmutableList(); + /// + /// Gets the url prefix to the scripts of this project. + /// + private static string Prefix { get; } = $"_content/{typeof(Exports).Namespace}"; + private static IEnumerable SharedScripts { get diff --git a/src/Web/Shared/Services/NavigationHistory.cs b/src/Web/Shared/Services/NavigationHistory.cs index 0439c71f5..4645ed5b1 100644 --- a/src/Web/Shared/Services/NavigationHistory.cs +++ b/src/Web/Shared/Services/NavigationHistory.cs @@ -8,7 +8,7 @@ namespace MUnique.OpenMU.Web.Shared.Services; using Microsoft.AspNetCore.Components; /// -/// A service which keeps track of the navigation history to support breadcrumb navigation. +/// A service that keeps track of the navigation history to support breadcrumb navigation. /// public sealed class NavigationHistory { @@ -27,14 +27,14 @@ public NavigationHistory(NavigationManager navigationManager) } /// - /// Gets the navigation manager. + /// Event that is fired whenever the history changed. /// - public NavigationManager NavigationManager { get; } + public event EventHandler? HistoryChanged; /// - /// Event which is fired whenever the history changed. + /// Gets the navigation manager. /// - public event EventHandler? HistoryChanged; + public NavigationManager NavigationManager { get; } /// /// Gets the previous page entries. @@ -217,7 +217,7 @@ public void JumpTo(HistoryEntry entry) } else { - // should never happen :) + // Should never happen. return; } diff --git a/src/Web/Shared/Services/ThemeController.cs b/src/Web/Shared/Services/ThemeController.cs index 3bc30fabc..a5e23070c 100644 --- a/src/Web/Shared/Services/ThemeController.cs +++ b/src/Web/Shared/Services/ThemeController.cs @@ -7,7 +7,7 @@ namespace MUnique.OpenMU.Web.Shared.Services; using Microsoft.AspNetCore.Mvc; /// -/// A controller which writes the UI theme preference to a cookie and redirects to the specified uri. +/// A controller that writes the UI theme preference to a cookie and redirects to the specified uri. /// [Route("[controller]/[action]")] public class ThemeController : Controller @@ -22,10 +22,20 @@ public class ThemeController : Controller /// public static string DefaultTheme { get; } = "light"; + /// + /// Normalizes the supplied theme value to a known token. + /// + /// The raw value from the request. + /// Either dark or light. + public static string NormalizeTheme(string? theme) + { + return string.Equals(theme, "dark", StringComparison.OrdinalIgnoreCase) ? "dark" : "light"; + } + /// /// Sets the UI theme by writing the theme cookie and redirects to the specified URI. /// - /// The theme to set, e.g. "light" or "dark". + /// The theme to set, e.g. dark or light. /// The URI to redirect to after the cookie has been set. Must be a local URL; otherwise the user is sent to the application root. /// A local redirect to , or to "/" if the URI is missing or non-local. public IActionResult Set(string? theme, string? redirectUri) @@ -50,14 +60,4 @@ public IActionResult Set(string? theme, string? redirectUri) return this.LocalRedirect(redirectUri); } - - /// - /// Normalizes the supplied theme value to a known token. - /// - /// The raw value from the request. - /// Either "dark" or "light". - public static string NormalizeTheme(string? theme) - { - return string.Equals(theme, "dark", StringComparison.OrdinalIgnoreCase) ? "dark" : "light"; - } } diff --git a/tests/MUnique.OpenMU.Tests/ExperienceRateSplitTest.cs b/tests/MUnique.OpenMU.Tests/ExperienceRateSplitTest.cs index a2dca5d77..cac338205 100644 --- a/tests/MUnique.OpenMU.Tests/ExperienceRateSplitTest.cs +++ b/tests/MUnique.OpenMU.Tests/ExperienceRateSplitTest.cs @@ -113,6 +113,9 @@ public async ValueTask PartyDistributionUsesMasterExperienceRateForMasterMembers Assert.That(masterGained, Is.GreaterThan(normalGained * 3)); } + /// + /// Verifies that concurrent normal experience gains cannot exceed the maximum level. + /// [Test] public async ValueTask ConcurrentNormalExperienceCantExceedMaximumLevelAsync() { @@ -136,6 +139,9 @@ await Task.WhenAll( Assert.That(player.SelectedCharacter.LevelUpPoints, Is.EqualTo(initialLevelUpPoints + pointsPerLevelUp)); } + /// + /// Verifies that concurrent master experience stays within configured maximum bounds. + /// [Test] public async ValueTask ConcurrentMasterExperienceStaysWithinConfiguredMaximumBoundsAsync() { @@ -158,6 +164,9 @@ await Task.WhenAll( Assert.That(player.SelectedCharacter.MasterExperience, Is.LessThanOrEqualTo(maxMasterExperience)); } + /// + /// Verifies that experience overflow is applied below max when not prevented. + /// [Test] public async ValueTask OverflowIsAppliedBelowMaxWhenNotPreventedAsync() { @@ -176,6 +185,9 @@ public async ValueTask OverflowIsAppliedBelowMaxWhenNotPreventedAsync() Assert.That(player.SelectedCharacter.Experience, Is.EqualTo(context.ExperienceTable[2] + 10)); } + /// + /// Verifies that experience overflow is discarded below max when prevented. + /// [Test] public async ValueTask OverflowIsDiscardedBelowMaxWhenPreventedAsync() { @@ -195,6 +207,10 @@ public async ValueTask OverflowIsDiscardedBelowMaxWhenPreventedAsync() Assert.That(player.SelectedCharacter.Experience, Is.EqualTo(context.ExperienceTable[2])); } + /// + /// Verifies that experience always stops at the maximum level regardless of the overflow setting. + /// + /// Whether to prevent experience overflow. [TestCase(false)] [TestCase(true)] public async ValueTask ExperienceAlwaysStopsAtMaximumLevelRegardlessOfOverflowSettingAsync(bool preventExperienceOverflow) diff --git a/tests/MUnique.OpenMU.Tests/GuildTestBase.cs b/tests/MUnique.OpenMU.Tests/GuildTestBase.cs index 79a7e0646..cee3333cb 100644 --- a/tests/MUnique.OpenMU.Tests/GuildTestBase.cs +++ b/tests/MUnique.OpenMU.Tests/GuildTestBase.cs @@ -17,6 +17,9 @@ namespace MUnique.OpenMU.Tests; /// public class GuildTestBase { + /// + /// The default guild name used in tests. + /// protected const string GuildName = "Foobar"; /// diff --git a/tests/MUnique.OpenMU.Tests/MoveItemActionTests.cs b/tests/MUnique.OpenMU.Tests/MoveItemActionTests.cs index c5a87bb4b..0475a1950 100644 --- a/tests/MUnique.OpenMU.Tests/MoveItemActionTests.cs +++ b/tests/MUnique.OpenMU.Tests/MoveItemActionTests.cs @@ -26,6 +26,9 @@ namespace MUnique.OpenMU.Tests; [TestFixture] public class MoveItemActionTests { + /// + /// Verifies that a complete stack move consumes the source item. + /// [Test] public async ValueTask CompleteStackConsumesSourceItemAsync() { @@ -47,6 +50,9 @@ public async ValueTask CompleteStackConsumesSourceItemAsync() Assert.That(player.Inventory.ItemStorage.Items.Count(i => ReferenceEquals(i, target)), Is.EqualTo(1)); } + /// + /// Verifies that completing a stack and relogging keeps a single persisted item. + /// [Test] public async ValueTask CompleteStackAndRelogKeepsSinglePersistedItemAsync() { @@ -76,6 +82,9 @@ public async ValueTask CompleteStackAndRelogKeepsSinglePersistedItemAsync() Assert.That(player.Inventory.Items.Count(i => i.Definition == definition), Is.EqualTo(1)); } + /// + /// Verifies that a failed move to an occupied slot keeps the source at its original slot. + /// [Test] public async ValueTask FailedMoveToOccupiedSlotKeepsSourceAtOriginalSlotAsync() { @@ -94,6 +103,9 @@ public async ValueTask FailedMoveToOccupiedSlotKeepsSourceAtOriginalSlotAsync() Assert.That(player.Inventory.GetItem(21), Is.Null); } + /// + /// Verifies that a failed vault to inventory move keeps the item in the vault. + /// [Test] public async ValueTask FailedVaultToInventoryMoveKeepsItemInVaultAsync() { @@ -112,6 +124,9 @@ public async ValueTask FailedVaultToInventoryMoveKeepsItemInVaultAsync() Assert.That(player.Inventory!.GetItem(20), Is.Null); } + /// + /// Verifies that a move to a slot outside grid bounds is rejected without mutation. + /// [Test] public async ValueTask MoveToSlotOutsideGridBoundsIsRejectedWithoutMutationAsync() { @@ -128,6 +143,9 @@ public async ValueTask MoveToSlotOutsideGridBoundsIsRejectedWithoutMutationAsync Assert.That(source.ItemSlot, Is.EqualTo(20)); } + /// + /// Verifies that a move request in an invalid player state is rejected without mutation. + /// [Test] public async ValueTask MoveRequestInInvalidPlayerStateIsRejectedWithoutMutationAsync() { @@ -143,6 +161,9 @@ public async ValueTask MoveRequestInInvalidPlayerStateIsRejectedWithoutMutationA Assert.That(player.Inventory.GetItem(22), Is.Null); } + /// + /// Verifies that a move to trade storage outside of a trade is rejected without mutation. + /// [Test] public async ValueTask MoveToTradeStorageOutsideTradeIsRejectedWithoutMutationAsync() { @@ -157,6 +178,9 @@ public async ValueTask MoveToTradeStorageOutsideTradeIsRejectedWithoutMutationAs Assert.That(player.TemporaryStorage!.Items, Does.Not.Contain(source)); } + /// + /// Verifies that a move request when the trade button is pressed is rejected without mutation. + /// [Test] public async ValueTask MoveRequestInTradeButtonPressedStateIsRejectedWithoutMutationAsync() { @@ -186,6 +210,9 @@ public async ValueTask MoveRequestInTradeButtonPressedStateIsRejectedWithoutMuta Assert.That(trader1.TemporaryStorage!.GetItem(1), Is.Null); } + /// + /// Verifies that logging out and back in after an inventory to vault move keeps a single persisted copy. + /// [Test] public async ValueTask LogoutAndRelogAfterInventoryToVaultMoveKeepsSinglePersistedCopyAsync() { From b721e24858ecd74c89f87c84e5768952c1cf2cbb Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Sat, 13 Jun 2026 18:58:49 -0300 Subject: [PATCH 03/14] fix remaining low effort warnings not caught by dotnet format --- src/ChatServer/ChatRoom.cs | 15 ++- src/ChatServer/ChatServer.cs | 2 +- src/ClientLauncher/MainForm.cs | 4 +- .../PacketHandler/ServerInfoRequestHandler.cs | 4 +- src/Dapr/Common/Extensions.cs | 32 +++--- src/Dapr/GameServer.Host/_Imports.razor | 1 - src/DataModel/Entities/Item.cs | 6 +- src/DataModel/ModelResourceProvider.cs | 6 + src/GameLogic/GameContext.cs | 8 +- .../Views/World/IAppearanceChangedPlugIn.cs | 2 +- src/GuildServer/GuildServer.cs | 6 +- src/Interfaces/IGameServerInstanceManager.cs | 4 +- src/Network/Analyzer/PacketAnalyzer.cs | 20 ++-- src/Network/LoopbackIpResolver.cs | 10 +- src/Pathfinding/PathFinder.cs | 4 +- .../QuestDefinitionExtensions.cs | 30 +++-- .../Updates/IConfigurationUpdatePlugIn.cs | 4 +- .../Initialization/Updates/UpdateVersion.cs | 2 + .../Version075/Items/Weapons.cs | 31 +++++- .../Version095d/Items/Weapons.cs | 32 +++++- .../VersionSeasonSix/Items/Weapons.cs | 34 +++++- .../VersionSeasonSix/Maps/Elvenland.cs | 8 +- .../Initialization/VersionSeasonSix/Quests.cs | 13 ++- .../SourceGenerator/EfCoreModelGenerator.cs | 46 ++++---- .../SourceGenerator/ModelGeneratorHelper.cs | 10 +- src/PlugIns/IStrategyPlugInProvider.cs | 10 +- src/PlugIns/LocalizableString.cs | 24 ++-- src/PlugIns/PlugInManager.cs | 104 ++++++++---------- src/PlugIns/StrategyPlugInProvider.cs | 28 ++--- .../Pages/CreateGameServerConfig.razor.cs | 7 +- src/Web/AdminPanel/Pages/EditBase.cs | 6 +- src/Web/AdminPanel/_Imports.razor | 1 - .../Shared/Components/Form/AutoForm.razor.cs | 2 +- src/Web/Shared/Components/Form/InputByte.cs | 3 +- .../Modal/ModalObjectMultiSelection.razor.cs | 1 + .../Shared/Components/Form/ValueWrapper.cs | 4 +- .../ModelResourcesTest.cs | 5 +- 37 files changed, 320 insertions(+), 209 deletions(-) diff --git a/src/ChatServer/ChatRoom.cs b/src/ChatServer/ChatRoom.cs index b39875596..1736a85dd 100644 --- a/src/ChatServer/ChatRoom.cs +++ b/src/ChatServer/ChatRoom.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -164,9 +164,7 @@ internal async ValueTask TryJoinAsync(IChatClient chatClient) throw new ObjectDisposedException("Chat room is already disposed."); } - this._logger.LogDebug( - "Client {ChatClientIndex} is trying to join the room {RoomId} with token '{AuthenticationToken}'", - chatClient.Index, this.RoomId, chatClient.AuthenticationToken); + this._logger.LogDebug("Client {ChatClientIndex} is trying to join the room {RoomId} with token '{AuthenticationToken}'", chatClient.Index, this.RoomId, chatClient.AuthenticationToken); this._lockSlim?.EnterWriteLock(); try @@ -178,7 +176,10 @@ internal async ValueTask TryJoinAsync(IChatClient chatClient) { this._logger.LogInformation( "Client {ChatClientIndex} has tried to join the room {RoomId} with token '{AuthenticationToken}', but was too late. It was valid until {AuthenticationRequiredUntil}.", - chatClient.Index, this.RoomId, chatClient.AuthenticationToken, authenticationInformation.AuthenticationRequiredUntil); + chatClient.Index, + this.RoomId, + chatClient.AuthenticationToken, + authenticationInformation.AuthenticationRequiredUntil); } else { @@ -193,9 +194,7 @@ internal async ValueTask TryJoinAsync(IChatClient chatClient) } else { - this._logger.LogInformation( - "Client {ChatClientIndex} has tried to join the room {RoomId} with token '{AuthenticationToken}', but was not registered.", - chatClient.Index, this.RoomId, chatClient.AuthenticationToken); + this._logger.LogInformation("Client {ChatClientIndex} has tried to join the room {RoomId} with token '{AuthenticationToken}', but was not registered.", chatClient.Index, this.RoomId, chatClient.AuthenticationToken); } } finally diff --git a/src/ChatServer/ChatServer.cs b/src/ChatServer/ChatServer.cs index d6fdf16f0..bcc939953 100644 --- a/src/ChatServer/ChatServer.cs +++ b/src/ChatServer/ChatServer.cs @@ -266,7 +266,7 @@ private void CreateListeners() /// Gets a random authentication token. /// /// Index of the client. - /// The random authentication token as string. + /// The random authentication token as a string. /// /// This is the original way of generating the token - not especially secure, but to keep it simple, I leave it that way. /// diff --git a/src/ClientLauncher/MainForm.cs b/src/ClientLauncher/MainForm.cs index 1011ce535..84ce721fc 100644 --- a/src/ClientLauncher/MainForm.cs +++ b/src/ClientLauncher/MainForm.cs @@ -1,8 +1,8 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // -#pragma warning disable CA1416 This project is compiled for windows +#pragma warning disable CA1416 // This project is compiled for windows namespace MUnique.OpenMU.ClientLauncher; using System.ComponentModel; diff --git a/src/ConnectServer/PacketHandler/ServerInfoRequestHandler.cs b/src/ConnectServer/PacketHandler/ServerInfoRequestHandler.cs index 0a6c0ed84..c142945ac 100644 --- a/src/ConnectServer/PacketHandler/ServerInfoRequestHandler.cs +++ b/src/ConnectServer/PacketHandler/ServerInfoRequestHandler.cs @@ -47,9 +47,11 @@ public async ValueTask HandlePacketAsync(Client client, Memory packet) var isGameServerOnSameMachineAsConnectServer = (serverItem?.EndPoint.Address).IsOnSameHost(); var isClientConnectedOnNonRegisteredAddress = !object.Equals(serverItem?.EndPoint.Address, localIpEndPoint?.Address); bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"), out var isRunningOnDocker); + + // Only if we can't use the cached data. if (isGameServerOnSameMachineAsConnectServer && !isRunningOnDocker - && isClientConnectedOnNonRegisteredAddress) // only if we can't use the cached data + && isClientConnectedOnNonRegisteredAddress) { int WritePacket() { diff --git a/src/Dapr/Common/Extensions.cs b/src/Dapr/Common/Extensions.cs index 54b38cb29..efae0cb1a 100644 --- a/src/Dapr/Common/Extensions.cs +++ b/src/Dapr/Common/Extensions.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -34,9 +34,9 @@ public static class Extensions /// /// Adds the . /// - /// The services. + /// The service collection. /// If set to true, configuration changes are published to other Dapr services. - /// The services. + /// The modified service collection. public static IServiceCollection AddPeristenceProvider(this IServiceCollection services, bool publishConfigChanges = false) { services.AddSingleton(); @@ -59,9 +59,9 @@ public static IServiceCollection AddPeristenceProvider(this IServiceCollection s /// /// Adds the plug in manager. /// - /// The services. + /// The service collection. /// The plug in configurations. - /// The services. + /// The modified service collection. public static IServiceCollection AddPlugInManager(this IServiceCollection services, ICollection plugInConfigurations) { return services @@ -114,9 +114,9 @@ public static async ValueTask TryLoadPlugInConfigurationsAsync(this IServiceProv /// Adds a persistent object as singleton to the services. /// /// The base type of the persistent object. - /// The services. + /// The service collection. /// The predicate to select actual object. - /// The services. + /// The modified service collection. public static IServiceCollection AddPersistentSingleton(this IServiceCollection services, Func? predicate = null) where T : class { @@ -128,9 +128,9 @@ public static IServiceCollection AddPersistentSingleton(this IServiceCollecti /// /// The target, exposed type of the persistent object, usually an interface. /// The actual base type of the persistent object. - /// The services. - /// The predicate. - /// The services. + /// The service collection. + /// The predicate to select the actual object. + /// The modified service collection. public static IServiceCollection AddPersistentSingleton(this IServiceCollection services, Func? predicate = null) where TActual : class, TTarget where TTarget : class @@ -150,8 +150,8 @@ public static IServiceCollection AddPersistentSingleton(this I /// /// Adds the to the services. /// - /// The services. - /// The services. + /// The service collection. + /// The modified service collection. public static IServiceCollection AddManageableServerRegistry(this IServiceCollection services) { services.AddSingleton() @@ -163,8 +163,8 @@ public static IServiceCollection AddManageableServerRegistry(this IServiceCollec /// Publishes the server to other daprized services by registering a . /// /// The type of the server. - /// The services. - /// The services. + /// The service collection. + /// The modified service collection. public static IServiceCollection PublishManageableServer(this IServiceCollection services) where TServer : IManageableServer { @@ -180,7 +180,7 @@ public static IServiceCollection PublishManageableServer(this IServiceC /// /// The web application builder. /// Name of the service. - /// The web application builder. + /// The configured web application builder. public static WebApplicationBuilder UseLoki(this WebApplicationBuilder builder, string serviceName) { // We just want to transmit some static labels, as suggested in the best practice in the Loki documentation @@ -211,7 +211,7 @@ public static WebApplicationBuilder UseLoki(this WebApplicationBuilder builder, /// /// The web application builder. /// The registry. - /// The web application builder. + /// The configured web application builder. public static WebApplicationBuilder AddOpenTelemetryMetrics(this WebApplicationBuilder builder, MetricsRegistry registry) { builder.Services.AddOpenTelemetry() diff --git a/src/Dapr/GameServer.Host/_Imports.razor b/src/Dapr/GameServer.Host/_Imports.razor index 2f2d4aafa..63976a9a0 100644 --- a/src/Dapr/GameServer.Host/_Imports.razor +++ b/src/Dapr/GameServer.Host/_Imports.razor @@ -8,7 +8,6 @@ @using static Microsoft.AspNetCore.Components.Web.RenderMode @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop -@using System.Net.Http @using Blazored @using Blazored.Modal diff --git a/src/DataModel/Entities/Item.cs b/src/DataModel/Entities/Item.cs index 6f11bf2ce..d822fc736 100644 --- a/src/DataModel/Entities/Item.cs +++ b/src/DataModel/Entities/Item.cs @@ -154,7 +154,11 @@ public override string ToString() return stringBuilder.ToString(); } - /// + /// + /// Returns a string that represents this item, using the specified culture for localization. + /// + /// The culture to use for localization. + /// The localized string. public string ToString(CultureInfo culture) { using var cultureHelper = CultureHelper.SetTemporaryCulture(culture); diff --git a/src/DataModel/ModelResourceProvider.cs b/src/DataModel/ModelResourceProvider.cs index 698989788..50bf3726e 100644 --- a/src/DataModel/ModelResourceProvider.cs +++ b/src/DataModel/ModelResourceProvider.cs @@ -20,6 +20,12 @@ public static class ModelResourceProvider private static readonly ConcurrentDictionary ResourceManagersByAssembly = new(); + /// + /// Gets the localized caption for the specified model type . + /// + /// Optional culture to use. If null, is used. + /// The model type for which to get the caption. + /// The localized caption or a spaced fallback type name. public static string GetTypeCaption(CultureInfo? cultureInfo = null) { return typeof(TModel).GetTypeCaption(cultureInfo); diff --git a/src/GameLogic/GameContext.cs b/src/GameLogic/GameContext.cs index e37e47c8f..73d62b111 100644 --- a/src/GameLogic/GameContext.cs +++ b/src/GameLogic/GameContext.cs @@ -384,7 +384,13 @@ public async ValueTask ForEachPlayerAsync(Func action) await playerList.Select(action).WhenAll().ConfigureAwait(false); } - /// + /// + /// Executes the specified action for each player, grouped by their culture. + /// + /// The state factory which creates a state for each culture group. + /// The action to execute for each player and culture state. + /// The type of the culture state. + /// A task representing the asynchronous operation. public async ValueTask ForEachPlayerGroupedByCultureAsync(Func stateFactory, Func action) { if (this._playerList.Count == 0) diff --git a/src/GameLogic/Views/World/IAppearanceChangedPlugIn.cs b/src/GameLogic/Views/World/IAppearanceChangedPlugIn.cs index a7dd4aec7..acc9e0186 100644 --- a/src/GameLogic/Views/World/IAppearanceChangedPlugIn.cs +++ b/src/GameLogic/Views/World/IAppearanceChangedPlugIn.cs @@ -14,6 +14,6 @@ public interface IAppearanceChangedPlugIn : IViewPlugIn /// /// The changed player. /// The changed item. - /// + /// True if the item is equipped, false otherwise. ValueTask AppearanceChangedAsync(Player changedPlayer, Item changedItem, bool isEquipped); } \ No newline at end of file diff --git a/src/GuildServer/GuildServer.cs b/src/GuildServer/GuildServer.cs index 1b3b5a0cd..40ec99c75 100644 --- a/src/GuildServer/GuildServer.cs +++ b/src/GuildServer/GuildServer.cs @@ -355,7 +355,11 @@ public async ValueTask RemoveAllianceAsync(uint targetGuildId) } } - /// + /// + /// Disbands the alliance of the specified master guild, removing all alliance relationships for all members of the alliance. + /// + /// The ID of the master guild. + /// True if the alliance was disbanded, false otherwise. public async ValueTask DisbandAllianceAsync(uint masterGuildId) { if (!this._guildDictionary.TryGetValue(masterGuildId, out var masterContainer)) diff --git a/src/Interfaces/IGameServerInstanceManager.cs b/src/Interfaces/IGameServerInstanceManager.cs index 06eca3142..34aac4ef4 100644 --- a/src/Interfaces/IGameServerInstanceManager.cs +++ b/src/Interfaces/IGameServerInstanceManager.cs @@ -13,18 +13,20 @@ public interface IGameServerInstanceManager /// Restarts all servers of this container. /// /// If set to true, this method is called during a database initialization. - /// + /// A task representing the restart all operation. ValueTask RestartAllAsync(bool onDatabaseInit); /// /// Initializes a game server. /// /// The server identifier. + /// A task representing the initialize game operation. ValueTask InitializeGameServerAsync(byte serverId); /// /// Removes the game server instance. /// /// The server identifier. + /// A task representing the remove game server operation. ValueTask RemoveGameServerAsync(byte serverId); } \ No newline at end of file diff --git a/src/Network/Analyzer/PacketAnalyzer.cs b/src/Network/Analyzer/PacketAnalyzer.cs index 55412c5c7..f8f0aed78 100644 --- a/src/Network/Analyzer/PacketAnalyzer.cs +++ b/src/Network/Analyzer/PacketAnalyzer.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -12,7 +12,7 @@ namespace MUnique.OpenMU.Network.Analyzer; using static System.Buffers.Binary.BinaryPrimitives; /// -/// Analyzer which analyzes data packets by considering the configuration files. +/// Analyzer that analyzes data packets by considering the configuration files. /// public sealed class PacketAnalyzer : IDisposable { @@ -455,27 +455,33 @@ private int DetermineItemSize(Span restData, Field binaryField) var itemData = restData.Slice(binaryField.Index); var size = 5; var options = itemData[4]; - if ((options & 1) == 1) // Option + + // Option + if ((options & 1) == 1) { size++; } - if ((options & 8) == 8) // Excellent + // Excellent + if ((options & 8) == 8) { size++; } - if ((options & 0x10) == 0x10) // Ancient + // Ancient + if ((options & 0x10) == 0x10) { size++; } - if ((options & 0x20) == 0x20) // Harmony + // Harmony + if ((options & 0x20) == 0x20) { size++; } - if ((options & 0x80) == 0x80) // Sockets + // Sockets + if ((options & 0x80) == 0x80) { size++; var socketCount = itemData[size] & 0xF; diff --git a/src/Network/LoopbackIpResolver.cs b/src/Network/LoopbackIpResolver.cs index cd506d0f4..198cce9ba 100644 --- a/src/Network/LoopbackIpResolver.cs +++ b/src/Network/LoopbackIpResolver.cs @@ -12,11 +12,6 @@ namespace MUnique.OpenMU.Network; /// public class LoopbackIpResolver : CustomIpResolver { - /// - /// Gets the local address. - /// - internal static IPAddress LoopbackAddress { get; } = new IPAddress(0x7F7F7F7F); - /// /// Initializes a new instance of the class. /// @@ -24,4 +19,9 @@ public LoopbackIpResolver() : base(LoopbackAddress) { } + + /// + /// Gets the local address. + /// + internal static IPAddress LoopbackAddress { get; } = new IPAddress(0x7F7F7F7F); } \ No newline at end of file diff --git a/src/Pathfinding/PathFinder.cs b/src/Pathfinding/PathFinder.cs index 6f62affa7..7aa46e4e8 100644 --- a/src/Pathfinding/PathFinder.cs +++ b/src/Pathfinding/PathFinder.cs @@ -98,7 +98,9 @@ public PathFinder(INetwork network, IPriorityQueue openList) } } - /// + /// + /// Resets the pathfinder. + /// public void ResetPathFinder() { this._openList.Clear(); diff --git a/src/Persistence/Initialization/QuestDefinitionExtensions.cs b/src/Persistence/Initialization/QuestDefinitionExtensions.cs index cd6658043..ad9af9744 100644 --- a/src/Persistence/Initialization/QuestDefinitionExtensions.cs +++ b/src/Persistence/Initialization/QuestDefinitionExtensions.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -81,9 +81,16 @@ public static QuestDefinition WithExperienceReward(this QuestDefinition questDef /// if set to true [has luck]. /// if set to true [has skill]. /// This quest definition. - public static QuestDefinition WithItemReward(this QuestDefinition questDefinition, int itemGroup, - int itemNumber, IContext context, GameConfiguration gameConfiguration, - byte itemLevel = 0, int itemOption = 0, bool hasLuck = false, bool hasSkill = false) + public static QuestDefinition WithItemReward( + this QuestDefinition questDefinition, + int itemGroup, + int itemNumber, + IContext context, + GameConfiguration gameConfiguration, + byte itemLevel = 0, + int itemOption = 0, + bool hasLuck = false, + bool hasSkill = false) { var reward = context.CreateNew(); reward.RewardType = QuestRewardType.Item; @@ -131,9 +138,18 @@ public static QuestDefinition WithItemReward(this QuestDefinition questDefinitio /// If set to true, the item must have skill. /// The item count, default 1. /// This quest definition. - public static QuestDefinition WithItemRequirement(this QuestDefinition questDefinition, int itemGroup, - int itemNumber, IContext context, GameConfiguration gameConfiguration, byte durability = 0, byte itemLevel = 0, - int itemOption = 0, bool hasLuck = false, bool hasSkill = false, byte itemCount = 1) + public static QuestDefinition WithItemRequirement( + this QuestDefinition questDefinition, + int itemGroup, + int itemNumber, + IContext context, + GameConfiguration gameConfiguration, + byte durability = 0, + byte itemLevel = 0, + int itemOption = 0, + bool hasLuck = false, + bool hasSkill = false, + byte itemCount = 1) { var requirement = context.CreateNew(); requirement.MinimumNumber = itemCount; diff --git a/src/Persistence/Initialization/Updates/IConfigurationUpdatePlugIn.cs b/src/Persistence/Initialization/Updates/IConfigurationUpdatePlugIn.cs index a7c96329f..6c4f98835 100644 --- a/src/Persistence/Initialization/Updates/IConfigurationUpdatePlugIn.cs +++ b/src/Persistence/Initialization/Updates/IConfigurationUpdatePlugIn.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -21,7 +21,7 @@ public interface IConfigurationUpdatePlugIn : IStrategyPlugIn UpdateVersion Version { get; } /// - /// Gets the to which this update belongs to. + /// Gets the data initialization key to which this update belongs. /// string DataInitializationKey { get; } diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs index a1e209a93..9581d3b71 100644 --- a/src/Persistence/Initialization/Updates/UpdateVersion.cs +++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs @@ -345,6 +345,7 @@ public enum UpdateVersion /// AddProjectileCountToTripleShot = 67, + /// /// The version of the . /// RemoveJewelDropLevelGap075 = 68, @@ -359,6 +360,7 @@ public enum UpdateVersion /// RemoveJewelDropLevelGapSeason6 = 70, + /// /// The version of the . /// FixRageFighterMultipleHitSkills = 71, diff --git a/src/Persistence/Initialization/Version075/Items/Weapons.cs b/src/Persistence/Initialization/Version075/Items/Weapons.cs index bd63f01b4..c3e2622b7 100644 --- a/src/Persistence/Initialization/Version075/Items/Weapons.cs +++ b/src/Persistence/Initialization/Version075/Items/Weapons.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -229,11 +229,30 @@ protected void CreateAmmunition(byte @group, byte number, byte slot, byte width, /// The knight class. /// The elf class. /// If set to true, the item is ammunition for a weapon. - protected void CreateWeapon(byte @group, byte number, byte slot, int skillNumber, byte width, byte height, - bool dropsFromMonsters, string name, byte dropLevel, int minimumDamage, int maximumDamage, int attackSpeed, - byte durability, int magicPower, int levelRequirement, int strengthRequirement, int agilityRequirement, - int energyRequirement, int vitalityRequirement, - int wizardClass, int knightClass, int elfClass, bool isAmmunition = false) + protected void CreateWeapon( + byte @group, + byte number, + byte slot, + int skillNumber, + byte width, + byte height, + bool dropsFromMonsters, + string name, + byte dropLevel, + int minimumDamage, + int maximumDamage, + int attackSpeed, + byte durability, + int magicPower, + int levelRequirement, + int strengthRequirement, + int agilityRequirement, + int energyRequirement, + int vitalityRequirement, + int wizardClass, + int knightClass, + int elfClass, + bool isAmmunition = false) { var item = this.Context.CreateNew(); this.GameConfiguration.Items.Add(item); diff --git a/src/Persistence/Initialization/Version095d/Items/Weapons.cs b/src/Persistence/Initialization/Version095d/Items/Weapons.cs index 4a98459e2..581cda50f 100644 --- a/src/Persistence/Initialization/Version095d/Items/Weapons.cs +++ b/src/Persistence/Initialization/Version095d/Items/Weapons.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -244,11 +244,31 @@ protected void CreateAmmunition(byte @group, byte number, byte slot, byte width, /// The elf class. /// The magic gladiator class. /// If set to true, the item is ammunition for a weapon. - protected void CreateWeapon(byte @group, byte number, byte slot, int skillNumber, byte width, byte height, - bool dropsFromMonsters, string name, byte dropLevel, int minimumDamage, int maximumDamage, int attackSpeed, - byte durability, int magicPower, int levelRequirement, int strengthRequirement, int agilityRequirement, - int energyRequirement, int vitalityRequirement, - int wizardClass, int knightClass, int elfClass, int magicGladiatorClass = 0, bool isAmmunition = false) + protected void CreateWeapon( + byte @group, + byte number, + byte slot, + int skillNumber, + byte width, + byte height, + bool dropsFromMonsters, + string name, + byte dropLevel, + int minimumDamage, + int maximumDamage, + int attackSpeed, + byte durability, + int magicPower, + int levelRequirement, + int strengthRequirement, + int agilityRequirement, + int energyRequirement, + int vitalityRequirement, + int wizardClass, + int knightClass, + int elfClass, + int magicGladiatorClass = 0, + bool isAmmunition = false) { var item = this.Context.CreateNew(); this.GameConfiguration.Items.Add(item); diff --git a/src/Persistence/Initialization/VersionSeasonSix/Items/Weapons.cs b/src/Persistence/Initialization/VersionSeasonSix/Items/Weapons.cs index 2da54e86b..8979d3aad 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Items/Weapons.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Items/Weapons.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -301,11 +301,33 @@ public override void Initialize() /// The dark lord class. /// The summoner class. /// The ragefighter class. - protected void CreateWeapon(byte @group, byte number, byte slot, int skillNumber, byte width, byte height, - bool dropsFromMonsters, string name, byte dropLevel, int minimumDamage, int maximumDamage, int attackSpeed, - byte durability, int magicPower, int levelRequirement, int strengthRequirement, int agilityRequirement, - int energyRequirement, int vitalityRequirement, - int wizardClass, int knightClass, int elfClass, int magicGladiatorClass, int darkLordClass, int summonerClass, int ragefighterClass) + protected void CreateWeapon( + byte @group, + byte number, + byte slot, + int skillNumber, + byte width, + byte height, + bool dropsFromMonsters, + string name, + byte dropLevel, + int minimumDamage, + int maximumDamage, + int attackSpeed, + byte durability, + int magicPower, + int levelRequirement, + int strengthRequirement, + int agilityRequirement, + int energyRequirement, + int vitalityRequirement, + int wizardClass, + int knightClass, + int elfClass, + int magicGladiatorClass, + int darkLordClass, + int summonerClass, + int ragefighterClass) { var item = this.Context.CreateNew(); this.GameConfiguration.Items.Add(item); diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/Elvenland.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/Elvenland.cs index 181b7c171..2c762262c 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Maps/Elvenland.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/Elvenland.cs @@ -14,10 +14,14 @@ namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps; /// internal class Elvenland : BaseMapInitializer { - /// + /// + /// The Number of the Map. + /// public static readonly byte Number = 51; - /// + /// + /// The Name of the Map. + /// private static readonly string Name = "Elvenland"; /// diff --git a/src/Persistence/Initialization/VersionSeasonSix/Quests.cs b/src/Persistence/Initialization/VersionSeasonSix/Quests.cs index 83799b3b6..75b1b8312 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Quests.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Quests.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -57,8 +57,15 @@ public override void Initialize() this.CreateNewQuests(); } - private QuestDefinition CreateQuest(string name, short @group, short startingNumber, short number, - short refuseNumber, int minimumLevel, int maximumLevel, short npcNumber, + private QuestDefinition CreateQuest( + string name, + short @group, + short startingNumber, + short number, + short refuseNumber, + int minimumLevel, + int maximumLevel, + short npcNumber, CharacterClassNumber? qualifiedCharacter = null) { var npc = this.GameConfiguration.Monsters.First(m => m.Number == npcNumber); diff --git a/src/Persistence/SourceGenerator/EfCoreModelGenerator.cs b/src/Persistence/SourceGenerator/EfCoreModelGenerator.cs index 40459bf2d..9f975b0d7 100644 --- a/src/Persistence/SourceGenerator/EfCoreModelGenerator.cs +++ b/src/Persistence/SourceGenerator/EfCoreModelGenerator.cs @@ -143,6 +143,29 @@ public override int GetHashCode() } } + private static bool IsMemberOfAggregate(PropertyInfo propertyInfo) + { + if (propertyInfo?.Name.StartsWith("Raw") ?? false) + { + propertyInfo = propertyInfo.DeclaringType?.GetProperty(propertyInfo.Name.Substring(3), BindingFlags.Instance | BindingFlags.Public); + } + + return propertyInfo?.GetCustomAttribute() is { }; + } + + private static bool IsStandaloneType(string typeName, Type referencingType) + { + return StandaloneTypes.Any(st => + { + if (st.TypeName != typeName) + { + return false; + } + + return !st.StandaloneForEntityOnly || !ModelGeneratorHelper.IsConfigurationType(referencingType); + }); + } + private IEnumerable<(string Name, string Source)> GenerateJoinEntities() { var standaloneCollectionProperties = ModelGeneratorHelper.CustomTypes.SelectMany(this.GetStandaloneCollectionProperties).ToList(); @@ -224,16 +247,6 @@ public static void EnsureConfigured() return source; } - private static bool IsMemberOfAggregate(PropertyInfo propertyInfo) - { - if (propertyInfo?.Name.StartsWith("Raw") ?? false) - { - propertyInfo = propertyInfo.DeclaringType?.GetProperty(propertyInfo.Name.Substring(3), BindingFlags.Instance | BindingFlags.Public); - } - - return propertyInfo?.GetCustomAttribute() is { }; - } - private string GenerateDbContext() { var ignores = new StringBuilder(); @@ -479,19 +492,6 @@ private string CreateIdPropertyIfRequired(Type type) return string.Empty; } - private static bool IsStandaloneType(string typeName, Type referencingType) - { - return StandaloneTypes.Any(st => - { - if (st.TypeName != typeName) - { - return false; - } - - return !st.StandaloneForEntityOnly || !ModelGeneratorHelper.IsConfigurationType(referencingType); - }); - } - private IEnumerable GetStandaloneCollectionProperties(Type type) { return type.FullName != GameConfigurationFullName ? diff --git a/src/Persistence/SourceGenerator/ModelGeneratorHelper.cs b/src/Persistence/SourceGenerator/ModelGeneratorHelper.cs index a42c0b067..f74611e51 100644 --- a/src/Persistence/SourceGenerator/ModelGeneratorHelper.cs +++ b/src/Persistence/SourceGenerator/ModelGeneratorHelper.cs @@ -12,6 +12,8 @@ namespace MUnique.OpenMU.Persistence.SourceGenerator; /// internal static class ModelGeneratorHelper { + private static IList _customTypes; + /// /// Gets a header template for a generated file. /// @@ -32,15 +34,13 @@ internal static class ModelGeneratorHelper /// public static string ConfigurationNamespace => "MUnique.OpenMU.DataModel.Configuration"; - private static IList _customTypes; - /// /// Gets the types which need to be customized for persistence. /// public static IEnumerable CustomTypes => _customTypes ??= GetCustomTypes(); /// - /// Determines whether the given type is a is configuration type. + /// Determines whether the given type is a configuration type. /// /// The type. /// true if the given type is a configuration type; otherwise, false. @@ -115,7 +115,7 @@ public static string GetParameters(ICollection parameters) /// /// Overrides the implementation, so that the correct class instance - /// is created and the Id is assigned. + /// is created and the id is assigned. /// /// The type. /// Name of the class. @@ -144,7 +144,7 @@ public override void AssignValuesOf({{type.Namespace}}.{{className}} other, MUni /// /// Determines the types which require customization. /// - /// The types which require customization. + /// The types that require customization. private static List GetCustomTypes() { var result = new List(); diff --git a/src/PlugIns/IStrategyPlugInProvider.cs b/src/PlugIns/IStrategyPlugInProvider.cs index af5fc2fc1..8ca2e5945 100644 --- a/src/PlugIns/IStrategyPlugInProvider.cs +++ b/src/PlugIns/IStrategyPlugInProvider.cs @@ -14,6 +14,11 @@ namespace MUnique.OpenMU.PlugIns; public interface IStrategyPlugInProvider where TStrategy : class, IStrategyPlugIn { + /// + /// Gets the available s. + /// + IEnumerable AvailableStrategies { get; } + /// /// Gets the with the specified key. /// @@ -23,9 +28,4 @@ public interface IStrategyPlugInProvider /// The key. /// The with the specified key, if available; Otherwise, null. TStrategy? this[TKey key] { get; } - - /// - /// Gets the available s. - /// - IEnumerable AvailableStrategies { get; } } \ No newline at end of file diff --git a/src/PlugIns/LocalizableString.cs b/src/PlugIns/LocalizableString.cs index c3624a526..ca0a7c498 100644 --- a/src/PlugIns/LocalizableString.cs +++ b/src/PlugIns/LocalizableString.cs @@ -12,7 +12,7 @@ namespace MUnique.OpenMU.PlugIns; /// This class is currently compiled in both System.Web.dll and System.ComponentModel.DataAnnotations.dll. /// /// -/// See internal class of the same name in System.ComponentModel.DataAnnotations for reference. +/// See the internal class of the same name in System.ComponentModel.DataAnnotations for reference. /// internal sealed class LocalizableString { @@ -72,15 +72,6 @@ public Type? ResourceType } } - /// - /// Clears any cached values, forcing to - /// perform evaluation. - /// - private void ClearCache() - { - this._cachedResult = null; - } - /// /// Gets the potentially localized value. /// @@ -105,8 +96,8 @@ private void ClearCache() { if (this._cachedResult == null) { - // If the property value is null, then just cache that value - // If the resource type is null, then property value is literal, so cache it + // If the property value is null, then just cache that value. + // If the resource type is null, then the property value is literal, so cache it. if (this._propertyValue == null || this._resourceType == null) { this._cachedResult = () => this._propertyValue; @@ -154,4 +145,13 @@ private void ClearCache() // Return the cached result return this._cachedResult(); } + + /// + /// Clears any cached values, forcing to + /// perform evaluation. + /// + private void ClearCache() + { + this._cachedResult = null; + } } \ No newline at end of file diff --git a/src/PlugIns/PlugInManager.cs b/src/PlugIns/PlugInManager.cs index 523f14f0e..ef87e5b5f 100644 --- a/src/PlugIns/PlugInManager.cs +++ b/src/PlugIns/PlugInManager.cs @@ -33,6 +33,7 @@ public class PlugInManager /// The configurations. /// The logger factory. /// The service provider. + /// The reference handler for references in custom plugin configurations. public PlugInManager(ICollection? configurations, ILoggerFactory loggerFactory, IServiceProvider? serviceProvider, ReferenceHandler? customConfigReferenceHandler) { _ = typeof(Nito.AsyncEx.AsyncReaderWriterLock); // Ensure Nito.AsyncEx.Coordination is loaded so it will be available in proxy generation. @@ -56,12 +57,12 @@ public PlugInManager(ICollection? configurations, ILoggerFa } /// - /// Occurs when a plug in got deactivated. + /// Occurs when a plugin got deactivated. /// public event EventHandler? PlugInDeactivated; /// - /// Occurs when a plug in got activated. + /// Occurs when a plugin got activated. /// public event EventHandler? PlugInActivated; @@ -71,10 +72,10 @@ public PlugInManager(ICollection? configurations, ILoggerFa public event EventHandler? PlugInConfigurationChanged; /// - /// Gets the known plug in types. + /// Gets the known plugin types. /// /// - /// The known plug in types. + /// The known plugin types. /// public IEnumerable KnownPlugInTypes => this._knownPlugIns.Values; @@ -84,7 +85,7 @@ public PlugInManager(ICollection? configurations, ILoggerFa public ReferenceHandler? CustomConfigReferenceHandler { get; } /// - /// Discovers and registers all plug ins of all loaded assemblies. + /// Discovers and registers all plugins of all loaded assemblies. /// public void DiscoverAndRegisterPlugIns() { @@ -93,7 +94,7 @@ public void DiscoverAndRegisterPlugIns() } /// - /// Discovers and registers plug ins of the specified assembly. + /// Discovers and registers plugins of the specified assembly. /// /// The assembly. public void DiscoverAndRegisterPlugIns(Assembly assembly) @@ -103,9 +104,9 @@ public void DiscoverAndRegisterPlugIns(Assembly assembly) } /// - /// Discovers the and register plug ins of type . + /// Discovers and register plugins of type . /// - /// The type of the plugins which should be discovered. + /// The type of the plugins that should be discovered. public void DiscoverAndRegisterPlugInsOf() { var plugIns = this.DiscoverAllPlugIns().Where(type => typeof(T).IsAssignableFrom(type)); @@ -113,7 +114,7 @@ public void DiscoverAndRegisterPlugInsOf() } /// - /// Gets the known plug ins of the given interface type. + /// Gets the known plugins of the given interface type. /// /// The plugin interface type. Type parameter of a . /// The known plugins of the given interface type. @@ -128,9 +129,9 @@ public IEnumerable GetKnownPlugInsOf() } /// - /// Gets the active plug ins of the specified type. + /// Gets the active plugins of the specified type. /// - /// The type of the plug in. + /// The type of the plugin. /// The active plugins of the specified type. public IEnumerable GetActivePlugInsOf() { @@ -143,21 +144,21 @@ public IEnumerable GetActivePlugInsOf() } /// - /// Deactivates the plug in of type . + /// Deactivates the plugin of type . /// - /// The type of the plug in. + /// The type of the plugin. public void DeactivatePlugIn() => this.DeactivatePlugIn(typeof(TPlugIn)); /// - /// Deactivates the plug in of the specified type. + /// Deactivates the plugin of the specified type. /// - /// The plug in. + /// The plugin. public void DeactivatePlugIn(Type plugIn) => this.DeactivatePlugIn(plugIn.GUID); /// - /// Deactivates the plug in with the specified type id. + /// Deactivates the plugin with the specified type id. /// - /// The plug in identifier. + /// The plugin identifier. public void DeactivatePlugIn(Guid plugInId) { if (this._knownPlugIns.TryGetValue(plugInId, out var plugInType)) @@ -168,21 +169,21 @@ public void DeactivatePlugIn(Guid plugInId) } /// - /// Activates the plug in of type . + /// Activates the plugin of type . /// - /// The type of the plug in. + /// The type of the plugin. public void ActivatePlugIn() => this.ActivatePlugIn(typeof(TPlugIn)); /// - /// Activates the plug in of the specified type. + /// Activates the plugin of the specified type. /// - /// The plug in. + /// The plugin. public void ActivatePlugIn(Type plugIn) => this.ActivatePlugIn(plugIn.GUID); /// - /// Activates the plug in with the specified type id. + /// Activates the plugin with the specified type id. /// - /// The plug in identifier. + /// The plugin identifier. public void ActivatePlugIn(Guid plugInId) { if (this._knownPlugIns.TryGetValue(plugInId, out var plugInType)) @@ -193,10 +194,10 @@ public void ActivatePlugIn(Guid plugInId) } /// - /// Gets the plug in point which implements . + /// Gets the plugin point which implements . /// - /// The type of the plug in. - /// The plug in point which implements , if available; Otherwise, null. + /// The type of the plugin. + /// The plugin point which implements , if available; Otherwise, null. public TPlugIn? GetPlugInPoint() where TPlugIn : class { @@ -209,11 +210,11 @@ public void ActivatePlugIn(Guid plugInId) } /// - /// Gets the strategy plug in. + /// Gets the strategy plugin. /// /// The type of the key. /// The type of the strategy. - /// The strategy plug in. + /// The strategy plugin. public IStrategyPlugInProvider? GetStrategyProvider() where TStrategy : class, IStrategyPlugIn { @@ -226,12 +227,12 @@ public void ActivatePlugIn(Guid plugInId) } /// - /// Gets the strategy plug in. + /// Gets the strategy plugin. /// /// The type of the key. /// The type of the strategy. /// The key. - /// The strategy plug in of the specified key, if available; Otherwise, null. + /// The strategy plugin of the specified key, if available; Otherwise, null. public TStrategy? GetStrategy(TKey key) where TStrategy : class, IStrategyPlugIn { @@ -239,11 +240,11 @@ public void ActivatePlugIn(Guid plugInId) } /// - /// Gets the strategy plug in. + /// Gets the strategy plugin. /// /// The type of the strategy. /// The key. - /// The strategy plug in of the specified key, if available; Otherwise, null. + /// The strategy plugin of the specified key, if available; Otherwise, null. public TStrategy? GetStrategy(string key) where TStrategy : class, IStrategyPlugIn { @@ -251,11 +252,11 @@ public void ActivatePlugIn(Guid plugInId) } /// - /// Determines whether the specified plug in type is configured as active. + /// Determines whether the specified plugin type is configured as active. /// - /// Type of the plug in. + /// Type of the plugin. /// - /// true if the specified plug in type is configured as active; otherwise, false. + /// true if the specified plugin type is configured as active; otherwise, false. /// public bool IsPlugInActive(Type plugInType) { @@ -263,11 +264,11 @@ public bool IsPlugInActive(Type plugInType) } /// - /// Determines whether the specified plug in type is configured as active. + /// Determines whether the specified plugin type is configured as active. /// - /// Identifier of the type of the plug in. + /// Identifier of the type of the plugin. /// - /// true if the specified plug in type is configured as active; otherwise, false. + /// true if the specified plugin type is configured as active; otherwise, false. /// public bool IsPlugInActive(Guid plugInTypeId) { @@ -275,10 +276,10 @@ public bool IsPlugInActive(Guid plugInTypeId) } /// - /// Registers the plug in class for the specified plug in interface. + /// Registers the plugin class for the specified plugin interface. /// - /// The type of the plug in interface. - /// The type of the plug in class. + /// The type of the plugin interface. + /// The type of the plugin class. public void RegisterPlugIn() where TPlugInInterface : class where TPlugInClass : class, TPlugInInterface @@ -308,9 +309,9 @@ public void RegisterPlugIn() } /// - /// Registers the plug in instance for the specified plugin point interface. + /// Registers the plugin instance for the specified plugin point interface. /// - /// The type of the plug in interface. + /// The type of the plugin interface. /// The instance. /// Plugin Type {instance.GetType()} - instance. public void RegisterPlugInAtPlugInPoint(TPlugInInterface instance) @@ -340,9 +341,9 @@ public void RegisterPlugInAtPlugInPoint(TPlugInInterface insta } /// - /// Configures the plug in. + /// Configures the plugin. /// - /// The plug in identifier. + /// The plugin identifier. /// The configuration. public void ConfigurePlugIn(Guid plugInId, PlugInConfiguration configuration) { @@ -476,17 +477,6 @@ private void ConfigurePlugIn(Type plugInType, PlugInConfiguration configuration) this.PlugInConfigurationChanged?.Invoke(this, new PlugInConfigurationChangedEventArgs(plugInType, configuration)); } - private Assembly CompileCustomPlugInAssembly(PlugInConfiguration configuration) - { - if (string.IsNullOrEmpty(configuration.CustomPlugInSource)) - { - throw new ArgumentNullException(nameof(configuration)); - } - - var syntaxTree = SyntaxFactory.ParseSyntaxTree(configuration.CustomPlugInSource!); - return syntaxTree.CompileAndLoad($"CustomPlugIn_{configuration.TypeId}"); - } - private IEnumerable DiscoverNewPlugIns() { return this.DiscoverNewPlugIns(this.DiscoverAllPlugIns()); @@ -554,7 +544,7 @@ private void RegisterPlugIns(IEnumerable plugIns) } /// - /// Exception which occurs when the created proxy doesn't implement the expected interface . + /// Exception that occurs when the created proxy doesn't implement the expected interface . /// /// public class InvalidPlugInProxyException : Exception diff --git a/src/PlugIns/StrategyPlugInProvider.cs b/src/PlugIns/StrategyPlugInProvider.cs index c3f3616ee..b26fa297f 100644 --- a/src/PlugIns/StrategyPlugInProvider.cs +++ b/src/PlugIns/StrategyPlugInProvider.cs @@ -11,7 +11,7 @@ namespace MUnique.OpenMU.PlugIns; /// The implementation for the which provides plugins by their key. /// /// The type of the key. -/// The type of the plug in. +/// The type of the plugin. /// /// public class StrategyPlugInProvider : PlugInContainerBase, IStrategyPlugInProvider @@ -23,7 +23,7 @@ public class StrategyPlugInProvider : PlugInContainerBase /// Initializes a new instance of the class. /// - /// The plugin manager which manages this instance. + /// The plugin manager that manages this instance. /// The logger factory. public StrategyPlugInProvider(PlugInManager manager, ILoggerFactory loggerFactory) : base(manager) @@ -31,6 +31,16 @@ public StrategyPlugInProvider(PlugInManager manager, ILoggerFactory loggerFactor this.Logger = loggerFactory.CreateLogger(this.GetType()); } + /// + public IEnumerable AvailableStrategies + { + get + { + using var l = this.Lock.ReaderLock(); + return this._effectiveStrategies.Values.ToList(); + } + } + /// /// Gets the logger. /// @@ -54,21 +64,11 @@ public TPlugIn? this[TKey key] } } - /// - public IEnumerable AvailableStrategies - { - get - { - using var l = this.Lock.ReaderLock(); - return this._effectiveStrategies.Values.ToList(); - } - } - /// /// Tries the get the plug in with the specified key. /// /// The key. - /// The plug in. + /// The plugin. /// True, if the plugin has been found and returned; Otherwise, false. protected bool TryGetPlugIn(TKey key, [MaybeNullWhen(false)] out TPlugIn plugIn) => this._effectiveStrategies.TryGetValue(key, out plugIn); @@ -99,7 +99,7 @@ protected override void DeactivatePlugIn(TPlugIn plugIn) /// /// Sets the effective plugin. /// - /// The plug in. + /// The plugin. protected void SetEffectivePlugin(TPlugIn plugIn) { this._effectiveStrategies.Remove(plugIn.Key); diff --git a/src/Web/AdminPanel/Pages/CreateGameServerConfig.razor.cs b/src/Web/AdminPanel/Pages/CreateGameServerConfig.razor.cs index 20665c37e..605dbf9a2 100644 --- a/src/Web/AdminPanel/Pages/CreateGameServerConfig.razor.cs +++ b/src/Web/AdminPanel/Pages/CreateGameServerConfig.razor.cs @@ -15,7 +15,7 @@ namespace MUnique.OpenMU.Web.AdminPanel.Pages; using MUnique.OpenMU.Web.AdminPanel.Properties; /// -/// Razor page which shows objects of the specified type in a grid. +/// Razor page that shows objects of the specified type in a grid. /// public partial class CreateGameServerConfig : ComponentBase, IAsyncDisposable { @@ -74,11 +74,11 @@ public async ValueTask DisposeAsync() } catch (OperationCanceledException) { - // we can ignore that ... + // We can ignore that. } catch { - // and we should not throw exceptions in the dispose method ... + // And we should not throw exceptions in the dispose method. } } @@ -148,7 +148,6 @@ private async ValueTask CreateDefinitionByViewModelAsync(I private async Task OnSaveButtonClickAsync() { - string text; try { var gameConfiguration = await this.DataSource.GetOwnerAsync().ConfigureAwait(false); diff --git a/src/Web/AdminPanel/Pages/EditBase.cs b/src/Web/AdminPanel/Pages/EditBase.cs index 9b69cd10a..c15c28743 100644 --- a/src/Web/AdminPanel/Pages/EditBase.cs +++ b/src/Web/AdminPanel/Pages/EditBase.cs @@ -191,11 +191,13 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.OpenComponent>(11); builder.AddAttribute(12, nameof(CascadingValue.Value), this._persistenceContext); builder.AddAttribute(13, nameof(CascadingValue.IsFixed), this._isOwningContext); - builder.AddAttribute(14, nameof(CascadingValue.ChildContent), (RenderFragment)(builder2 => + RenderFragment childContent = builder2 => { var sequence = 14; this.AddFormToRenderTree(builder2, ref sequence); - })); + }; + + builder.AddAttribute(14, nameof(CascadingValue.ChildContent), childContent); builder.CloseComponent(); } diff --git a/src/Web/AdminPanel/_Imports.razor b/src/Web/AdminPanel/_Imports.razor index 372482e58..79d083e9b 100644 --- a/src/Web/AdminPanel/_Imports.razor +++ b/src/Web/AdminPanel/_Imports.razor @@ -9,7 +9,6 @@ @using static Microsoft.AspNetCore.Components.Web.RenderMode @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop -@using System.Net.Http @using Blazored @using Blazored.Modal diff --git a/src/Web/Shared/Components/Form/AutoForm.razor.cs b/src/Web/Shared/Components/Form/AutoForm.razor.cs index bfd70cbd6..122426e8e 100644 --- a/src/Web/Shared/Components/Form/AutoForm.razor.cs +++ b/src/Web/Shared/Components/Form/AutoForm.razor.cs @@ -18,7 +18,7 @@ public partial class AutoForm private string? _lastIncomingSearchTerm; /// - /// Gets or sets the model of the form, . + /// Gets or sets the model of the form. /// [Parameter] public T Model { get; set; } = default!; diff --git a/src/Web/Shared/Components/Form/InputByte.cs b/src/Web/Shared/Components/Form/InputByte.cs index 5870084bb..234439b87 100644 --- a/src/Web/Shared/Components/Form/InputByte.cs +++ b/src/Web/Shared/Components/Form/InputByte.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -11,7 +11,6 @@ namespace MUnique.OpenMU.Web.Shared.Components.Form; /// /// An input component for editing numeric byte values. /// -/// The type of the byte. public class InputByte : InputByteBase { /// diff --git a/src/Web/Shared/Components/Form/Modal/ModalObjectMultiSelection.razor.cs b/src/Web/Shared/Components/Form/Modal/ModalObjectMultiSelection.razor.cs index c66a7fe40..41d59e498 100644 --- a/src/Web/Shared/Components/Form/Modal/ModalObjectMultiSelection.razor.cs +++ b/src/Web/Shared/Components/Form/Modal/ModalObjectMultiSelection.razor.cs @@ -9,6 +9,7 @@ namespace MUnique.OpenMU.Web.Shared.Components.Form.Modal; using Blazored.Modal.Services; using Microsoft.AspNetCore.Components; using MUnique.OpenMU.Persistence; +using MUnique.OpenMU.Web.Shared.Services; /// /// A component which allows to select multiple instances of through the . diff --git a/src/Web/Shared/Components/Form/ValueWrapper.cs b/src/Web/Shared/Components/Form/ValueWrapper.cs index cfd58bbaa..5c2c72258 100644 --- a/src/Web/Shared/Components/Form/ValueWrapper.cs +++ b/src/Web/Shared/Components/Form/ValueWrapper.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -9,7 +9,7 @@ namespace MUnique.OpenMU.Web.Shared.Components.Form; using System.Runtime.CompilerServices; /// -/// A wrapper for which allows to be bound to edit components. +/// A wrapper for which allows to be bound to edit components. /// /// The type of the value. public sealed class ValueWrapper : INotifyPropertyChanged diff --git a/tests/MUnique.OpenMU.Tests/ModelResourcesTest.cs b/tests/MUnique.OpenMU.Tests/ModelResourcesTest.cs index 736d9e360..4cf4958fd 100644 --- a/tests/MUnique.OpenMU.Tests/ModelResourcesTest.cs +++ b/tests/MUnique.OpenMU.Tests/ModelResourcesTest.cs @@ -1,9 +1,10 @@ -// // +// // // // Licensed under the MIT License. See LICENSE file in the project root for full license information. // // namespace MUnique.OpenMU.Tests; +using System; using System.Globalization; using MUnique.OpenMU.DataModel; using MUnique.OpenMU.DataModel.Configuration; @@ -62,7 +63,7 @@ public void PropertyCaption() /// /// Verifies that a caption can be retrieved for a property inherited from a base type, - /// here . + /// here . /// [Test] public void InheritedPropertyCaption() From f55bf3cf52a298f92706398a8c52db09d7bb7261 Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Sat, 13 Jun 2026 19:10:51 -0300 Subject: [PATCH 04/14] fix doc reference --- src/Web/AdminPanel/API/ServerController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Web/AdminPanel/API/ServerController.cs b/src/Web/AdminPanel/API/ServerController.cs index 53adff031..3bf22bddd 100644 --- a/src/Web/AdminPanel/API/ServerController.cs +++ b/src/Web/AdminPanel/API/ServerController.cs @@ -29,7 +29,7 @@ public class ServerController : Controller /// /// Sends a global message to the specified server. /// - /// The server id. + /// The server id. /// The message. /// A representing the asynchronous operation. [Route("send/{id=0}")] From 75210fe6fe5020e803656b56305868dcd452f7f0 Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:19:46 -0300 Subject: [PATCH 05/14] fix remaining low effort warnings not caught by dotnet format --- ...ntroller.cs => ConnectServerController.cs} | 0 .../ServerInfoController.cs | 2 +- .../GameServer.Host/GameServerController.cs | 2 +- src/GameLogic/NPC/NonPlayerCharacter.cs | 12 ++--- src/GameLogic/ObservableExtensions.cs | 2 +- src/GameLogic/Party.cs | 8 +-- src/GameLogic/Player.cs | 6 +-- src/GameLogic/Resets/ResetProgression.cs | 20 +++++++ .../Resets/ResetProgressionCalculator.cs | 17 +----- .../Views/IConsumeSpecialItemPlugIn.cs | 2 +- .../Character/UpdateStatsBasePlugIn.cs | 22 ++++---- .../World/AppearanceChangedExtendedPlugIn.cs | 52 +++++++++++++++++++ .../World/AppearanceChangedPlugIn.cs | 39 -------------- .../RemoteView/World/ObjectMovedPlugIn.cs | 52 +++++++++---------- src/Network/Analyzer/MainForm.cs | 6 ++- .../Events/BloodCastleInitializer.cs | 3 +- src/Startup/ServerContainerBase.cs | 4 +- .../Layout/ConfigurationSearch.razor.cs | 4 +- src/Web/AdminPanel/Pages/Updates.razor.cs | 22 ++++---- src/Web/AdminPanel/Services/SetupService.cs | 24 ++++----- .../ComponentBuilders/DateOnlyFieldBuilder.cs | 22 ++++++++ .../ComponentBuilders/DateTimeFieldBuilder.cs | 36 ------------- .../ComponentBuilders/TimeOnlyFieldBuilder.cs | 22 ++++++++ .../ComponentBuilders/TimeSpanFieldBuilder.cs | 22 ++++++++ .../{ViewModel.cs => ItemViewModel.cs} | 0 .../MapEditor/MapCoordinateService.cs | 4 +- .../ModelResourcesTest.cs | 2 +- 27 files changed, 229 insertions(+), 178 deletions(-) rename src/Dapr/ConnectServer.Host/{ConnectionServerController.cs => ConnectServerController.cs} (100%) create mode 100644 src/GameLogic/Resets/ResetProgression.cs create mode 100644 src/GameServer/RemoteView/World/AppearanceChangedExtendedPlugIn.cs create mode 100644 src/Web/Shared/ComponentBuilders/DateOnlyFieldBuilder.cs create mode 100644 src/Web/Shared/ComponentBuilders/TimeOnlyFieldBuilder.cs create mode 100644 src/Web/Shared/ComponentBuilders/TimeSpanFieldBuilder.cs rename src/Web/Shared/Components/ItemEdit/{ViewModel.cs => ItemViewModel.cs} (100%) diff --git a/src/Dapr/ConnectServer.Host/ConnectionServerController.cs b/src/Dapr/ConnectServer.Host/ConnectServerController.cs similarity index 100% rename from src/Dapr/ConnectServer.Host/ConnectionServerController.cs rename to src/Dapr/ConnectServer.Host/ConnectServerController.cs diff --git a/src/Dapr/ConnectServer.Host/ServerInfoController.cs b/src/Dapr/ConnectServer.Host/ServerInfoController.cs index 66021cc91..54e59fb66 100644 --- a/src/Dapr/ConnectServer.Host/ServerInfoController.cs +++ b/src/Dapr/ConnectServer.Host/ServerInfoController.cs @@ -55,7 +55,7 @@ public object GetCompleteInfo() /// /// Gets the connection count of all game servers. /// - /// + /// The overall count of current connections. [HttpGet("playerCount")] public int GetOverallConnectionCount() { diff --git a/src/Dapr/GameServer.Host/GameServerController.cs b/src/Dapr/GameServer.Host/GameServerController.cs index e5338a90d..7366ec41a 100644 --- a/src/Dapr/GameServer.Host/GameServerController.cs +++ b/src/Dapr/GameServer.Host/GameServerController.cs @@ -30,7 +30,7 @@ public GameServerController(GameServer gameServer) /// /// Shuts down the server gracefully. /// - /// + /// A representing the asynchronous operation. [HttpPost(nameof(IGameServer.ShutdownAsync))] public async ValueTask ShutdownAsync() { diff --git a/src/GameLogic/NPC/NonPlayerCharacter.cs b/src/GameLogic/NPC/NonPlayerCharacter.cs index 60dc9cc4c..f73177df7 100644 --- a/src/GameLogic/NPC/NonPlayerCharacter.cs +++ b/src/GameLogic/NPC/NonPlayerCharacter.cs @@ -9,7 +9,7 @@ namespace MUnique.OpenMU.GameLogic.NPC; using Nito.AsyncEx; /// -/// The implementation of a non-player-character (Monster) which can not be attacked or attack. +/// The implementation of a non-player-character (Monster) which cannot be attacked or attack. /// public class NonPlayerCharacter : AsyncDisposable, IObservable, IRotatable, ILocateable, IHasBucketInformation { @@ -67,6 +67,11 @@ public NonPlayerCharacter(MonsterSpawnArea spawnInfo, MonsterDefinition stats, G /// public Bucket? OldBucket { get; set; } + /// + /// Gets a value indicating whether this instance can spawn in a safe zone. + /// + protected virtual bool CanSpawnInSafezone => this.Definition.ObjectKind != NpcObjectKind.Monster && this.Definition.ObjectKind != NpcObjectKind.Trap; + /// /// Initializes this instance. /// @@ -184,11 +189,6 @@ protected virtual ValueTask MoveAsync(Point target, MoveType type) throw new NotSupportedException("NPCs can't be moved"); } - /// - /// Gets a value indicating whether this instance can spawn in a safe zone. - /// - protected virtual bool CanSpawnInSafezone => this.Definition.ObjectKind != NpcObjectKind.Monster && this.Definition.ObjectKind != NpcObjectKind.Trap; - /// /// Gets the spawn direction. /// diff --git a/src/GameLogic/ObservableExtensions.cs b/src/GameLogic/ObservableExtensions.cs index 27cc0f87a..cab16bf4c 100644 --- a/src/GameLogic/ObservableExtensions.cs +++ b/src/GameLogic/ObservableExtensions.cs @@ -82,7 +82,7 @@ public static async ValueTask InvokeViewPlugInAsync(this IWorldObse /// The observable. /// The action. /// if set to true the should be done for too. - /// + /// A representing the asynchronous operation. public static async ValueTask ForEachObservingAsync(this IObservable observable, Func action, bool includeThis) where T : class, IWorldObserver { diff --git a/src/GameLogic/Party.cs b/src/GameLogic/Party.cs index e3476f15f..ef341bd93 100644 --- a/src/GameLogic/Party.cs +++ b/src/GameLogic/Party.cs @@ -204,7 +204,7 @@ await member.InvokeViewPlugInAsync( /// The total experience distributed. public async ValueTask DistributeExperienceAfterKillAsync(IAttackable killedObject, IObservable killer) { - using var _ = await this._distributionLock.LockAsync(); + using var l = await this._distributionLock.LockAsync(); try { return await this.InternalDistributeExperienceAfterKillAsync(killedObject, killer).ConfigureAwait(false); @@ -223,7 +223,7 @@ public async ValueTask DistributeExperienceAfterKillAsync(IAttackable kille /// The amount of money to distribute. public async ValueTask DistributeMoneyAfterKillAsync(IAttackable killed, IPartyMember killer, uint amount) { - using var _ = await this._distributionLock.LockAsync(); + using var l = await this._distributionLock.LockAsync(); try { this._logger.LogDebug("Distributing money after killing {name}", killed.GetName()); @@ -257,7 +257,7 @@ public async ValueTask DistributeMoneyAfterKillAsync(IAttackable killed, IPartyM /// A list of drop item groups from nearby party members' active quests. public async ValueTask> GetQuestDropItemGroupsAsync(IPartyMember killer) { - using var _ = await this._distributionLock.LockAsync(); + using var l = await this._distributionLock.LockAsync(); try { using (await killer.ObserverLock.ReaderLockAsync().ConfigureAwait(false)) @@ -501,7 +501,7 @@ private async ValueTask UpdateNearbyCountAsync() try { - using var _ = await player.ObserverLock.ReaderLockAsync().ConfigureAwait(false); + using var l = await player.ObserverLock.ReaderLockAsync().ConfigureAwait(false); attributes[Stats.NearbyPartyMemberCount] = this._partyMembers.Count(player.Observers.Contains); } catch (Exception ex) diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs index 99f44bf3d..e04347568 100644 --- a/src/GameLogic/Player.cs +++ b/src/GameLogic/Player.cs @@ -1447,7 +1447,7 @@ public async ValueTask AddObserverAsync(IWorldObserver observer) return; } - using var _ = await this.ObserverLock.WriterLockAsync(); + using var l = await this.ObserverLock.WriterLockAsync(); this.Observers.Add(observer); if (this.Party is not null && observer is Player observingPlayer @@ -1461,7 +1461,7 @@ public async ValueTask AddObserverAsync(IWorldObserver observer) /// public async ValueTask RemoveObserverAsync(IWorldObserver observer) { - using var _ = await this.ObserverLock.WriterLockAsync(); + using var l = await this.ObserverLock.WriterLockAsync(); this.Observers.Remove(observer); if (this.Party is not null && observer is Player observingPlayer @@ -1640,7 +1640,7 @@ IElement AppedMasterSkillPowerUp(SkillEntry masterSkillEntry, PowerUpDefinition /// Creates the magic effect power up for the given definition. /// /// The definition for a magic effect. - /// + /// A tuple containing the duration element and the power-up elements. public (IElement DurationInSeconds, (AttributeDefinition Target, IElement BuffPowerUp)[] PowerUps) CreateMagicEffectPowerUp(MagicEffectDefinition magicEffectDefinition) { ArgumentNullException.ThrowIfNull(magicEffectDefinition); diff --git a/src/GameLogic/Resets/ResetProgression.cs b/src/GameLogic/Resets/ResetProgression.cs new file mode 100644 index 000000000..822113dca --- /dev/null +++ b/src/GameLogic/Resets/ResetProgression.cs @@ -0,0 +1,20 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Resets; + +/// +/// A value object that contains costs and rewards for the next reset. +/// +/// The resulting reset count after a successful reset. +/// The required zen for the reset. +/// The required number of configured reset items. +/// The number of points granted for the reset. +/// The total number of points after the reset when replacement mode is active. +public readonly record struct ResetProgression( + int NextResetCount, + int RequiredZen, + int RequiredItemAmount, + int PointsForReset, + int TotalPointsAfterReset); diff --git a/src/GameLogic/Resets/ResetProgressionCalculator.cs b/src/GameLogic/Resets/ResetProgressionCalculator.cs index 529a47f15..59a7416ef 100644 --- a/src/GameLogic/Resets/ResetProgressionCalculator.cs +++ b/src/GameLogic/Resets/ResetProgressionCalculator.cs @@ -13,7 +13,7 @@ public static class ResetProgressionCalculator /// Calculates the reset progression for the next reset. /// /// The current reset count. - /// The player specific points per reset override (0 means not configured). + /// The player-specific points per reset override (0 means not configured). /// The reset configuration. /// The calculated progression. public static ResetProgression Calculate(int currentResetCount, int pointsPerResetOverride, ResetConfiguration configuration) @@ -91,18 +91,3 @@ private static int GetTotalPointsAfterReset(ResetConfiguration configuration, in .FirstOrDefault(tier => getMinimumResetCount(tier) <= resetCount); } } - -/// -/// A value object which contains costs and rewards for the next reset. -/// -/// The resulting reset count after a successful reset. -/// The required zen for the reset. -/// The required amount of configured reset items. -/// The amount of points granted for the reset. -/// The total amount of points after the reset when replacement mode is active. -public readonly record struct ResetProgression( - int NextResetCount, - int RequiredZen, - int RequiredItemAmount, - int PointsForReset, - int TotalPointsAfterReset); diff --git a/src/GameLogic/Views/IConsumeSpecialItemPlugIn.cs b/src/GameLogic/Views/IConsumeSpecialItemPlugIn.cs index 3dcdd8cf8..6e9050168 100644 --- a/src/GameLogic/Views/IConsumeSpecialItemPlugIn.cs +++ b/src/GameLogic/Views/IConsumeSpecialItemPlugIn.cs @@ -14,6 +14,6 @@ public interface IConsumeSpecialItemPlugIn : IViewPlugIn /// /// The item. /// The effect time in seconds. - /// + /// A representing the asynchronous operation. ValueTask ConsumeSpecialItemAsync(Item item, ushort effectTimeInSeconds); } \ No newline at end of file diff --git a/src/GameServer/RemoteView/Character/UpdateStatsBasePlugIn.cs b/src/GameServer/RemoteView/Character/UpdateStatsBasePlugIn.cs index f22f44bfd..03c3eba75 100644 --- a/src/GameServer/RemoteView/Character/UpdateStatsBasePlugIn.cs +++ b/src/GameServer/RemoteView/Character/UpdateStatsBasePlugIn.cs @@ -74,12 +74,22 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + private static FrozenDictionary GetActionIndexMapping(FrozenDictionary changeActions) + { + return ActionIndexMappings.GetOrAdd(changeActions, CreateNewIndexDictionary); + + FrozenDictionary CreateNewIndexDictionary(FrozenDictionary dict) + { + return dict.Values.Distinct().Index().ToFrozenDictionary(tuple => tuple.Item, tuple => tuple.Index); + } + } + private async ValueTask SendDelayedUpdateAsync(UpdateAction action) { var autoResetEvent = this._resetEvents[this._actionIndexMapping[action]]; if (!autoResetEvent.WaitOne(0)) { - // we're sending an update already. + // We're sending an update already. return; } @@ -93,14 +103,4 @@ private async ValueTask SendDelayedUpdateAsync(UpdateAction action) autoResetEvent.Set(); } } - - private static FrozenDictionary GetActionIndexMapping(FrozenDictionary changeActions) - { - return ActionIndexMappings.GetOrAdd(changeActions, CreateNewIndexDictionary); - - FrozenDictionary CreateNewIndexDictionary(FrozenDictionary dict) - { - return dict.Values.Distinct().Index().ToFrozenDictionary(tuple => tuple.Item, tuple => tuple.Index); - } - } } \ No newline at end of file diff --git a/src/GameServer/RemoteView/World/AppearanceChangedExtendedPlugIn.cs b/src/GameServer/RemoteView/World/AppearanceChangedExtendedPlugIn.cs new file mode 100644 index 000000000..b73814e84 --- /dev/null +++ b/src/GameServer/RemoteView/World/AppearanceChangedExtendedPlugIn.cs @@ -0,0 +1,52 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameServer.RemoteView.World; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.Views.World; +using MUnique.OpenMU.Network; +using MUnique.OpenMU.Network.PlugIns; +using MUnique.OpenMU.PlugIns; + +/// +/// The extended implementation of the which is forwarding appearance changes of other players to the game client with specific data packets. +/// +[PlugIn] +[Display(Name = nameof(PlugInResources.AppearanceChangedExtendedPlugIn_Name), Description = nameof(PlugInResources.AppearanceChangedExtendedPlugIn_Description), ResourceType = typeof(PlugInResources))] +[Guid("A2F298E4-9F48-402A-B30D-9BC2BA8DEB2E")] +[MinimumClient(106, 3, ClientLanguage.Invariant)] +public class AppearanceChangedExtendedPlugIn : IAppearanceChangedPlugIn +{ + private readonly RemotePlayer _player; + + /// + /// Initializes a new instance of the class. + /// + /// The player. + public AppearanceChangedExtendedPlugIn(RemotePlayer player) => this._player = player; + + /// + public async ValueTask AppearanceChangedAsync(Player changedPlayer, Item item, bool isEquipped) + { + var connection = this._player.Connection; + if (connection is null || changedPlayer.Inventory is null) + { + return; + } + + await connection.SendAppearanceChangedExtendedAsync( + changedPlayer.GetId(this._player), + item.ItemSlot, + (byte)((isEquipped ? item.Definition?.Group : 0xFF) ?? 0xFF), + (ushort)(item.Definition?.Number ?? 0xFFFF), + item.Level, + (byte)(ItemSerializerHelper.GetExcellentByte(item) | ItemSerializerHelper.GetFenrirByte(item)), + (byte)(item.ItemSetGroups.FirstOrDefault(set => set.AncientSetDiscriminator != 0)?.AncientSetDiscriminator ?? 0), + changedPlayer.SelectedCharacter?.HasFullAncientSetEquipped() is true) + .ConfigureAwait(false); + } +} diff --git a/src/GameServer/RemoteView/World/AppearanceChangedPlugIn.cs b/src/GameServer/RemoteView/World/AppearanceChangedPlugIn.cs index 5459874bd..7b3fa05cf 100644 --- a/src/GameServer/RemoteView/World/AppearanceChangedPlugIn.cs +++ b/src/GameServer/RemoteView/World/AppearanceChangedPlugIn.cs @@ -76,43 +76,4 @@ int Write() await connection.SendAsync(Write).ConfigureAwait(false); } -} - -/// -/// The extended implementation of the which is forwarding appearance changes of other players to the game client with specific data packets. -/// -[PlugIn] -[Display(Name = nameof(PlugInResources.AppearanceChangedExtendedPlugIn_Name), Description = nameof(PlugInResources.AppearanceChangedExtendedPlugIn_Description), ResourceType = typeof(PlugInResources))] -[Guid("A2F298E4-9F48-402A-B30D-9BC2BA8DEB2E")] -[MinimumClient(106, 3, ClientLanguage.Invariant)] -public class AppearanceChangedExtendedPlugIn : IAppearanceChangedPlugIn -{ - private readonly RemotePlayer _player; - - /// - /// Initializes a new instance of the class. - /// - /// The player. - public AppearanceChangedExtendedPlugIn(RemotePlayer player) => this._player = player; - - /// - public async ValueTask AppearanceChangedAsync(Player changedPlayer, Item item, bool isEquipped) - { - var connection = this._player.Connection; - if (connection is null || changedPlayer.Inventory is null) - { - return; - } - - await connection.SendAppearanceChangedExtendedAsync( - changedPlayer.GetId(this._player), - item.ItemSlot, - (byte)((isEquipped ? item.Definition?.Group : 0xFF) ?? 0xFF), - (ushort)(item.Definition?.Number ?? 0xFFFF), - item.Level, - (byte)(ItemSerializerHelper.GetExcellentByte(item) | ItemSerializerHelper.GetFenrirByte(item)), - (byte)(item.ItemSetGroups.FirstOrDefault(set => set.AncientSetDiscriminator != 0)?.AncientSetDiscriminator ?? 0), - changedPlayer.SelectedCharacter?.HasFullAncientSetEquipped() is true) - .ConfigureAwait(false); - } } \ No newline at end of file diff --git a/src/GameServer/RemoteView/World/ObjectMovedPlugIn.cs b/src/GameServer/RemoteView/World/ObjectMovedPlugIn.cs index 4c28df859..c51f0045a 100644 --- a/src/GameServer/RemoteView/World/ObjectMovedPlugIn.cs +++ b/src/GameServer/RemoteView/World/ObjectMovedPlugIn.cs @@ -116,6 +116,32 @@ int Write() await connection.SendAsync(Write).ConfigureAwait(false); } + /// + /// Gets the walk code for the current client version. + /// + /// The walk code. + protected byte GetWalkCode() + { + if (this._player.ClientVersion.Season == 0) + { + return 0x10; + } + + switch (this._player.ClientVersion.Language) + { + case ClientLanguage.English: return 0xD4; + case ClientLanguage.Japanese: return 0x1D; + case ClientLanguage.Chinese: + case ClientLanguage.Vietnamese: + return 0xD9; + case ClientLanguage.Filipino: return 0xDD; + case ClientLanguage.Korean: return 0xD3; + case ClientLanguage.Thai: return 0xD7; + default: + return (byte)PacketType.Walk; + } + } + private async ValueTask ObjectWalkedAsync(ILocateable obj) { var connection = this._player.Connection; @@ -199,30 +225,4 @@ private byte GetInstantMoveCode() return (byte)PacketType.Teleport; } } - - /// - /// Gets the walk code for the current client version. - /// - /// The walk code. - protected byte GetWalkCode() - { - if (this._player.ClientVersion.Season == 0) - { - return 0x10; - } - - switch (this._player.ClientVersion.Language) - { - case ClientLanguage.English: return 0xD4; - case ClientLanguage.Japanese: return 0x1D; - case ClientLanguage.Chinese: - case ClientLanguage.Vietnamese: - return 0xD9; - case ClientLanguage.Filipino: return 0xDD; - case ClientLanguage.Korean: return 0xD3; - case ClientLanguage.Thai: return 0xD7; - default: - return (byte)PacketType.Walk; - } - } } \ No newline at end of file diff --git a/src/Network/Analyzer/MainForm.cs b/src/Network/Analyzer/MainForm.cs index f177be88a..22876a317 100644 --- a/src/Network/Analyzer/MainForm.cs +++ b/src/Network/Analyzer/MainForm.cs @@ -9,6 +9,7 @@ namespace MUnique.OpenMU.Network.Analyzer; using System.Diagnostics; using System.Linq.Dynamic.Core; using System.Linq.Expressions; +using System.Runtime.Versioning; using System.Windows.Forms; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -19,6 +20,7 @@ namespace MUnique.OpenMU.Network.Analyzer; /// /// The main form of the analyzer. /// +[SupportedOSPlatform("windows")] public partial class MainForm : Form { private readonly BindingList _proxiedConnections = new(); @@ -116,7 +118,7 @@ private ICapturedConnection? SelectedConnection } /// - protected override void OnClosed(EventArgs e) + protected override void OnFormClosed(FormClosedEventArgs e) { if (this._clientListener != null) { @@ -124,7 +126,7 @@ protected override void OnClosed(EventArgs e) this._clientListener = null; } - base.OnClosed(e); + base.OnFormClosed(e); } private static string ConvertFilterStringToExpressionString(string filter) diff --git a/src/Persistence/Initialization/VersionSeasonSix/Events/BloodCastleInitializer.cs b/src/Persistence/Initialization/VersionSeasonSix/Events/BloodCastleInitializer.cs index d727292a4..97b92d400 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Events/BloodCastleInitializer.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Events/BloodCastleInitializer.cs @@ -18,13 +18,14 @@ internal class BloodCastleInitializer : InitializerBase private const short RequiredKillsBeforeBridgePerPlayer = 40; private const short RequiredKillsAfterGatePerPlayer = 2; - private static readonly Point StatusOfSaintSpawnPoint = new(14, 95); /// /// The score penalty which gets applied when the event wasn't won by any participating player. /// private const int ScorePenaltyAtLoss = -300; + private static readonly Point StatusOfSaintSpawnPoint = new(14, 95); + /// /// Gets the rewards based on game level and rank, in case the event was won (even by another player). /// diff --git a/src/Startup/ServerContainerBase.cs b/src/Startup/ServerContainerBase.cs index 94d941ce0..d6b7e2e54 100644 --- a/src/Startup/ServerContainerBase.cs +++ b/src/Startup/ServerContainerBase.cs @@ -46,7 +46,7 @@ public async Task StopAsync(CancellationToken cancellationToken) /// Restarts all servers of this container. /// /// If set to true, this method is called during a database initialization. - /// + /// A representing the asynchronous operation. public virtual async ValueTask RestartAllAsync(bool onDatabaseInit) { await this.StopAsync(default).ConfigureAwait(false); @@ -78,7 +78,7 @@ public virtual async ValueTask RestartAllAsync(bool onDatabaseInit) /// /// If set to true, this method is called during a database initialization. /// The cancellation token. - /// + /// A representing the asynchronous operation. protected virtual async ValueTask BeforeStartAsync(bool onDatabaseInit, CancellationToken cancellationToken) { // can be overwritten diff --git a/src/Web/AdminPanel/Components/Layout/ConfigurationSearch.razor.cs b/src/Web/AdminPanel/Components/Layout/ConfigurationSearch.razor.cs index 4da7496cb..98d61be4a 100644 --- a/src/Web/AdminPanel/Components/Layout/ConfigurationSearch.razor.cs +++ b/src/Web/AdminPanel/Components/Layout/ConfigurationSearch.razor.cs @@ -123,7 +123,7 @@ private static string Normalize(string value) value.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); } - private void OnSearchFocus(FocusEventArgs _) + private void OnSearchFocus(FocusEventArgs e) { this.UpdateSearchResults(); } @@ -145,7 +145,7 @@ await this.InvokeAsync(() => return Task.CompletedTask; } - private async Task OnSearchBlurAsync(FocusEventArgs _) + private async Task OnSearchBlurAsync(FocusEventArgs e) { await Task.Delay(100).ConfigureAwait(true); this._searchResults.Clear(); diff --git a/src/Web/AdminPanel/Pages/Updates.razor.cs b/src/Web/AdminPanel/Pages/Updates.razor.cs index 346ea849e..4c6a4dffd 100644 --- a/src/Web/AdminPanel/Pages/Updates.razor.cs +++ b/src/Web/AdminPanel/Pages/Updates.razor.cs @@ -10,10 +10,18 @@ namespace MUnique.OpenMU.Web.AdminPanel.Pages; using MUnique.OpenMU.Web.AdminPanel.Services; /// -/// The set up page. +/// The set-up page. /// public partial class Updates { + private bool _isDataInitialized; + + private Exception? _exception; + + private UpdateState _overallState; + + private List _availableUpdates = new(); + private enum UpdateState { NotStarted, @@ -25,14 +33,6 @@ private enum UpdateState Failed, } - private bool _isDataInitialized; - - private Exception? _exception; - - private UpdateState _overallState; - - private List _availableUpdates = new(); - /// /// Gets or sets the setup service. /// @@ -46,7 +46,7 @@ private enum UpdateState public DataUpdateService UpdateService { get; set; } = null!; /// - /// Gets or sets the javascript runtime. + /// Gets or sets the JavaScript runtime. /// [Inject] public IJSRuntime JsRuntime { get; set; } = null!; @@ -122,7 +122,7 @@ private class UpdateViewModel /// /// Initializes a new instance of the class. /// - /// The update plug in. + /// The update plugin. public UpdateViewModel(IConfigurationUpdatePlugIn updatePlugIn) { this._updatePlugIn = updatePlugIn; diff --git a/src/Web/AdminPanel/Services/SetupService.cs b/src/Web/AdminPanel/Services/SetupService.cs index 141ed0adb..ab1a5af2d 100644 --- a/src/Web/AdminPanel/Services/SetupService.cs +++ b/src/Web/AdminPanel/Services/SetupService.cs @@ -13,7 +13,7 @@ namespace MUnique.OpenMU.Web.AdminPanel.Services; using Nito.AsyncEx.Synchronous; /// -/// Service which allows to set the servers database up. +/// Service that allows set the server database up. /// public class SetupService { @@ -27,7 +27,7 @@ public class SetupService /// Initializes a new instance of the class. /// /// The context provider. - /// The plug in manager. + /// The plugin manager. public SetupService(IMigratableDatabaseContextProvider contextProvider, PlugInManager plugInManager) { this._contextProvider = contextProvider; @@ -40,7 +40,16 @@ public SetupService(IMigratableDatabaseContextProvider contextProvider, PlugInMa public event AsyncEventHandler? DatabaseInitialized; /// - /// Gets a value indicating whether this application can connect to database. + /// Gets the versions. + /// + public ICollection Versions => this._availableInitializationPlugIns + ??= (this._plugInManager.GetStrategyProvider() ?? throw new InvalidOperationException("No data initialization plugins were found.")) + .AvailableStrategies + .OrderByDescending(s => s.Caption) + .ToList(); + + /// + /// Gets a value indicating whether this application can connect to a database. /// public bool CanConnectToDatabase { @@ -103,15 +112,6 @@ public async ValueTask IsDataInitializedAsync() return definition is { } ? new ClientVersion(definition.Season, definition.Episode, definition.Language) : null; } - /// - /// Gets the versions. - /// - public ICollection Versions => this._availableInitializationPlugIns - ??= (this._plugInManager.GetStrategyProvider() ?? throw new InvalidOperationException("No data initialization plugins were found.")) - .AvailableStrategies - .OrderByDescending(s => s.Caption) - .ToList(); - /// /// Installs the updates asynchronous. /// diff --git a/src/Web/Shared/ComponentBuilders/DateOnlyFieldBuilder.cs b/src/Web/Shared/ComponentBuilders/DateOnlyFieldBuilder.cs new file mode 100644 index 000000000..26b1bf3dc --- /dev/null +++ b/src/Web/Shared/ComponentBuilders/DateOnlyFieldBuilder.cs @@ -0,0 +1,22 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Web.Shared.ComponentBuilders; + +using System.Reflection; +using Microsoft.AspNetCore.Components.Rendering; +using MUnique.OpenMU.Web.Shared.Components.Form; +using MUnique.OpenMU.Web.Shared.Services; + +/// +/// A for date fields. +/// +public class DateOnlyFieldBuilder : BaseComponentBuilder, IComponentBuilder +{ + /// + public int BuildComponent(object model, PropertyInfo propertyInfo, RenderTreeBuilder builder, int currentIndex, IChangeNotificationService notificationService) => this.BuildField(model, propertyInfo, builder, currentIndex, notificationService); + + /// + public bool CanBuildComponent(PropertyInfo propertyInfo) => propertyInfo.PropertyType == typeof(DateOnly); +} diff --git a/src/Web/Shared/ComponentBuilders/DateTimeFieldBuilder.cs b/src/Web/Shared/ComponentBuilders/DateTimeFieldBuilder.cs index b2c22d98e..48a757835 100644 --- a/src/Web/Shared/ComponentBuilders/DateTimeFieldBuilder.cs +++ b/src/Web/Shared/ComponentBuilders/DateTimeFieldBuilder.cs @@ -19,40 +19,4 @@ public class DateTimeFieldBuilder : BaseComponentBuilder, IComponentBuilder /// public bool CanBuildComponent(PropertyInfo propertyInfo) => propertyInfo.PropertyType == typeof(DateTime); -} - -/// -/// A for date fields. -/// -public class DateOnlyFieldBuilder : BaseComponentBuilder, IComponentBuilder -{ - /// - public int BuildComponent(object model, PropertyInfo propertyInfo, RenderTreeBuilder builder, int currentIndex, IChangeNotificationService notificationService) => this.BuildField(model, propertyInfo, builder, currentIndex, notificationService); - - /// - public bool CanBuildComponent(PropertyInfo propertyInfo) => propertyInfo.PropertyType == typeof(DateOnly); -} - -/// -/// A for date fields. -/// -public class TimeOnlyFieldBuilder : BaseComponentBuilder, IComponentBuilder -{ - /// - public int BuildComponent(object model, PropertyInfo propertyInfo, RenderTreeBuilder builder, int currentIndex, IChangeNotificationService notificationService) => this.BuildField(model, propertyInfo, builder, currentIndex, notificationService); - - /// - public bool CanBuildComponent(PropertyInfo propertyInfo) => propertyInfo.PropertyType == typeof(TimeOnly); -} - -/// -/// A for time span fields. -/// -public class TimeSpanFieldBuilder : BaseComponentBuilder, IComponentBuilder -{ - /// - public int BuildComponent(object model, PropertyInfo propertyInfo, RenderTreeBuilder builder, int currentIndex, IChangeNotificationService notificationService) => this.BuildField(model, propertyInfo, builder, currentIndex, notificationService); - - /// - public bool CanBuildComponent(PropertyInfo propertyInfo) => propertyInfo.PropertyType == typeof(TimeSpan); } \ No newline at end of file diff --git a/src/Web/Shared/ComponentBuilders/TimeOnlyFieldBuilder.cs b/src/Web/Shared/ComponentBuilders/TimeOnlyFieldBuilder.cs new file mode 100644 index 000000000..8e84493c3 --- /dev/null +++ b/src/Web/Shared/ComponentBuilders/TimeOnlyFieldBuilder.cs @@ -0,0 +1,22 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Web.Shared.ComponentBuilders; + +using System.Reflection; +using Microsoft.AspNetCore.Components.Rendering; +using MUnique.OpenMU.Web.Shared.Components.Form; +using MUnique.OpenMU.Web.Shared.Services; + +/// +/// A for date fields. +/// +public class TimeOnlyFieldBuilder : BaseComponentBuilder, IComponentBuilder +{ + /// + public int BuildComponent(object model, PropertyInfo propertyInfo, RenderTreeBuilder builder, int currentIndex, IChangeNotificationService notificationService) => this.BuildField(model, propertyInfo, builder, currentIndex, notificationService); + + /// + public bool CanBuildComponent(PropertyInfo propertyInfo) => propertyInfo.PropertyType == typeof(TimeOnly); +} diff --git a/src/Web/Shared/ComponentBuilders/TimeSpanFieldBuilder.cs b/src/Web/Shared/ComponentBuilders/TimeSpanFieldBuilder.cs new file mode 100644 index 000000000..beeff2181 --- /dev/null +++ b/src/Web/Shared/ComponentBuilders/TimeSpanFieldBuilder.cs @@ -0,0 +1,22 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Web.Shared.ComponentBuilders; + +using System.Reflection; +using Microsoft.AspNetCore.Components.Rendering; +using MUnique.OpenMU.Web.Shared.Components.Form; +using MUnique.OpenMU.Web.Shared.Services; + +/// +/// A for time span fields. +/// +public class TimeSpanFieldBuilder : BaseComponentBuilder, IComponentBuilder +{ + /// + public int BuildComponent(object model, PropertyInfo propertyInfo, RenderTreeBuilder builder, int currentIndex, IChangeNotificationService notificationService) => this.BuildField(model, propertyInfo, builder, currentIndex, notificationService); + + /// + public bool CanBuildComponent(PropertyInfo propertyInfo) => propertyInfo.PropertyType == typeof(TimeSpan); +} diff --git a/src/Web/Shared/Components/ItemEdit/ViewModel.cs b/src/Web/Shared/Components/ItemEdit/ItemViewModel.cs similarity index 100% rename from src/Web/Shared/Components/ItemEdit/ViewModel.cs rename to src/Web/Shared/Components/ItemEdit/ItemViewModel.cs diff --git a/src/Web/Shared/Components/MapEditor/MapCoordinateService.cs b/src/Web/Shared/Components/MapEditor/MapCoordinateService.cs index 4d0c143ca..2e851ed20 100644 --- a/src/Web/Shared/Components/MapEditor/MapCoordinateService.cs +++ b/src/Web/Shared/Components/MapEditor/MapCoordinateService.cs @@ -12,13 +12,13 @@ namespace MUnique.OpenMU.Web.Shared.Components.MapEditor; /// public sealed class MapCoordinateService { + private const int MapSize = 256; + /// /// Gets the base pixel scale factor before zoom is applied. /// public static int BaseScale => 3; - private const int MapSize = 256; - /// /// Converts client mouse coordinates to map tile coordinates. /// diff --git a/tests/MUnique.OpenMU.Tests/ModelResourcesTest.cs b/tests/MUnique.OpenMU.Tests/ModelResourcesTest.cs index 4cf4958fd..0046ad77d 100644 --- a/tests/MUnique.OpenMU.Tests/ModelResourcesTest.cs +++ b/tests/MUnique.OpenMU.Tests/ModelResourcesTest.cs @@ -125,7 +125,7 @@ public void EnumCaptionGeneric() } /// - /// Verifies that the non-generic overload of + /// Verifies that the non-generic overload of /// returns the expected caption for an enum value. /// [Test] From e8c440be26b231a5fb022dabac86165951cd6cef Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:49:32 -0300 Subject: [PATCH 06/14] fix imports on extracted class --- .../RemoteView/World/AppearanceChangedExtendedPlugIn.cs | 3 +++ src/GameServer/RemoteView/World/AppearanceChangedPlugIn.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/GameServer/RemoteView/World/AppearanceChangedExtendedPlugIn.cs b/src/GameServer/RemoteView/World/AppearanceChangedExtendedPlugIn.cs index b73814e84..1780667a3 100644 --- a/src/GameServer/RemoteView/World/AppearanceChangedExtendedPlugIn.cs +++ b/src/GameServer/RemoteView/World/AppearanceChangedExtendedPlugIn.cs @@ -7,8 +7,11 @@ namespace MUnique.OpenMU.GameServer.RemoteView.World; using System.Runtime.InteropServices; using MUnique.OpenMU.DataModel.Entities; using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.GameLogic.Views.World; +using MUnique.OpenMU.GameServer.RemoteView; using MUnique.OpenMU.Network; +using MUnique.OpenMU.Network.Packets.ServerToClient; using MUnique.OpenMU.Network.PlugIns; using MUnique.OpenMU.PlugIns; diff --git a/src/GameServer/RemoteView/World/AppearanceChangedPlugIn.cs b/src/GameServer/RemoteView/World/AppearanceChangedPlugIn.cs index 7b3fa05cf..2ecb3433c 100644 --- a/src/GameServer/RemoteView/World/AppearanceChangedPlugIn.cs +++ b/src/GameServer/RemoteView/World/AppearanceChangedPlugIn.cs @@ -66,7 +66,7 @@ int Write() packet.ItemData[1] |= item.GetGlowLevel(); // We could also continue to dumb down information here as this packet reveals all of the options of an item to - // other players - something which is probably not in interest of the players. + // other players - something that is probably not in the interest of the players. // However, for now we keep this logic close to the original server, which doesn't do a thing about it. // Additionally, we could think of ignoring changes of rings and pendants, as they are usually not visible in the game client, except From 2040730a71deb640d6b19bf9850ae08d5513e9a0 Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Sat, 13 Jun 2026 23:23:23 -0300 Subject: [PATCH 07/14] fix: migrate tests to constraint-based Assert.That --- .../RemoteView/World/AppearanceChangedExtendedPlugIn.cs | 1 - tests/MUnique.OpenMU.ChatServer.Tests/ChatRoomTests.cs | 2 +- tests/MUnique.OpenMU.Network.Tests/ConnectionTests.cs | 4 +++- tests/MUnique.OpenMU.Web.Tests/DebouncerTest.cs | 8 +++----- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/GameServer/RemoteView/World/AppearanceChangedExtendedPlugIn.cs b/src/GameServer/RemoteView/World/AppearanceChangedExtendedPlugIn.cs index 1780667a3..ef15306bb 100644 --- a/src/GameServer/RemoteView/World/AppearanceChangedExtendedPlugIn.cs +++ b/src/GameServer/RemoteView/World/AppearanceChangedExtendedPlugIn.cs @@ -9,7 +9,6 @@ namespace MUnique.OpenMU.GameServer.RemoteView.World; using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.GameLogic.Views.World; -using MUnique.OpenMU.GameServer.RemoteView; using MUnique.OpenMU.Network; using MUnique.OpenMU.Network.Packets.ServerToClient; using MUnique.OpenMU.Network.PlugIns; diff --git a/tests/MUnique.OpenMU.ChatServer.Tests/ChatRoomTests.cs b/tests/MUnique.OpenMU.ChatServer.Tests/ChatRoomTests.cs index ac9602c60..1cce4c151 100644 --- a/tests/MUnique.OpenMU.ChatServer.Tests/ChatRoomTests.cs +++ b/tests/MUnique.OpenMU.ChatServer.Tests/ChatRoomTests.cs @@ -65,7 +65,7 @@ public async ValueTask TryJoinNullClientAsync() var clientId = room.GetNextClientIndex(); var authenticationInfo = new ChatServerAuthenticationInfo(clientId, roomId, "Bob", ChatServerHost, "123456789"); room.RegisterClient(authenticationInfo); - Assert.ThrowsAsync(() => room.TryJoinAsync(null!).AsTask()); + Assert.That(async () => await room.TryJoinAsync(null!), Throws.TypeOf()); } /// diff --git a/tests/MUnique.OpenMU.Network.Tests/ConnectionTests.cs b/tests/MUnique.OpenMU.Network.Tests/ConnectionTests.cs index 0d1adc4c8..c986831a1 100644 --- a/tests/MUnique.OpenMU.Network.Tests/ConnectionTests.cs +++ b/tests/MUnique.OpenMU.Network.Tests/ConnectionTests.cs @@ -58,7 +58,9 @@ public async Task ExceptionWhenFailingToEncryptSentPacketAsync() _ = connection.BeginReceiveAsync(); await connection.Output.WriteAsync(malformedData).ConfigureAwait(false); - Assert.Throws(() => duplexPipe.SendPipe.Reader.ReadAsync().GetAwaiter().GetResult()); + Assert.That( + async () => await duplexPipe.SendPipe.Reader.ReadAsync().ConfigureAwait(false), + Throws.TypeOf()); } /// diff --git a/tests/MUnique.OpenMU.Web.Tests/DebouncerTest.cs b/tests/MUnique.OpenMU.Web.Tests/DebouncerTest.cs index 9ccc9f75c..901d0eee1 100644 --- a/tests/MUnique.OpenMU.Web.Tests/DebouncerTest.cs +++ b/tests/MUnique.OpenMU.Web.Tests/DebouncerTest.cs @@ -162,8 +162,7 @@ public void DebounceAsync_WithNullAction_ThrowsArgumentNullException() using var debouncer = new Debouncer(50); // Act & Assert - Assert.ThrowsAsync(() => - debouncer.DebounceAsync((Func)null!)); + Assert.That(async () => await debouncer.DebounceAsync((Func)null!), Throws.TypeOf()); } /// @@ -176,12 +175,11 @@ public void DebounceAsync_WithNullCancellableAction_ThrowsArgumentNullException( using var debouncer = new Debouncer(50); // Act & Assert - Assert.ThrowsAsync(() => - debouncer.DebounceAsync((Func)null!)); + Assert.That(async () => await debouncer.DebounceAsync((Func)null!), Throws.TypeOf()); } /// - /// Tests that actions spaced beyond the debounce window each execute independently. + /// Tests that actions spaced beyond the debounced window each execute independently. /// [Test] public async Task DebounceAsync_SpacedCalls_EachExecutes() From 735be0c5fd111edab1fe8ca3088293b45c43b341 Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Sun, 14 Jun 2026 10:25:16 -0300 Subject: [PATCH 08/14] remove return Task and ValueTask docs --- src/Dapr/GameServer.Host/GameServerController.cs | 1 - src/GameLogic/GameContext.cs | 1 - src/GameLogic/ObservableExtensions.cs | 1 - src/GameLogic/Offline/CombatHandler.cs | 1 - src/GameLogic/Offline/HealingHandler.cs | 1 - src/GameLogic/Views/IConsumeSpecialItemPlugIn.cs | 1 - src/Interfaces/IGameServerInstanceManager.cs | 3 --- src/Persistence/EntityFramework/GenericRepositoryBase.cs | 1 - src/Startup/ServerContainerBase.cs | 2 -- src/Web/AdminPanel/API/ServerController.cs | 1 - 10 files changed, 13 deletions(-) diff --git a/src/Dapr/GameServer.Host/GameServerController.cs b/src/Dapr/GameServer.Host/GameServerController.cs index 7366ec41a..ddd8909e3 100644 --- a/src/Dapr/GameServer.Host/GameServerController.cs +++ b/src/Dapr/GameServer.Host/GameServerController.cs @@ -30,7 +30,6 @@ public GameServerController(GameServer gameServer) /// /// Shuts down the server gracefully. /// - /// A representing the asynchronous operation. [HttpPost(nameof(IGameServer.ShutdownAsync))] public async ValueTask ShutdownAsync() { diff --git a/src/GameLogic/GameContext.cs b/src/GameLogic/GameContext.cs index 73d62b111..abbbb0955 100644 --- a/src/GameLogic/GameContext.cs +++ b/src/GameLogic/GameContext.cs @@ -390,7 +390,6 @@ public async ValueTask ForEachPlayerAsync(Func action) /// The state factory which creates a state for each culture group. /// The action to execute for each player and culture state. /// The type of the culture state. - /// A task representing the asynchronous operation. public async ValueTask ForEachPlayerGroupedByCultureAsync(Func stateFactory, Func action) { if (this._playerList.Count == 0) diff --git a/src/GameLogic/ObservableExtensions.cs b/src/GameLogic/ObservableExtensions.cs index cab16bf4c..bad6753d2 100644 --- a/src/GameLogic/ObservableExtensions.cs +++ b/src/GameLogic/ObservableExtensions.cs @@ -82,7 +82,6 @@ public static async ValueTask InvokeViewPlugInAsync(this IWorldObse /// The observable. /// The action. /// if set to true the should be done for too. - /// A representing the asynchronous operation. public static async ValueTask ForEachObservingAsync(this IObservable observable, Func action, bool includeThis) where T : class, IWorldObserver { diff --git a/src/GameLogic/Offline/CombatHandler.cs b/src/GameLogic/Offline/CombatHandler.cs index a2caeca5b..1ed9a3023 100644 --- a/src/GameLogic/Offline/CombatHandler.cs +++ b/src/GameLogic/Offline/CombatHandler.cs @@ -99,7 +99,6 @@ public void DecrementCooldown() /// /// Performs combat attacks on targets. /// - /// A value task representing the asynchronous operation. public async ValueTask PerformAttackAsync() { this.RefreshTarget(); diff --git a/src/GameLogic/Offline/HealingHandler.cs b/src/GameLogic/Offline/HealingHandler.cs index 5f0417345..480361433 100644 --- a/src/GameLogic/Offline/HealingHandler.cs +++ b/src/GameLogic/Offline/HealingHandler.cs @@ -45,7 +45,6 @@ public HealingHandler(OfflinePlayer player, IMuHelperSettings? config) /// /// Performs health recovery actions for the player and their party. /// - /// A value task representing the asynchronous operation. public async ValueTask PerformHealthRecoveryAsync() { if (this._config is null || this._player.Attributes is null) diff --git a/src/GameLogic/Views/IConsumeSpecialItemPlugIn.cs b/src/GameLogic/Views/IConsumeSpecialItemPlugIn.cs index 6e9050168..d9e31bc44 100644 --- a/src/GameLogic/Views/IConsumeSpecialItemPlugIn.cs +++ b/src/GameLogic/Views/IConsumeSpecialItemPlugIn.cs @@ -14,6 +14,5 @@ public interface IConsumeSpecialItemPlugIn : IViewPlugIn /// /// The item. /// The effect time in seconds. - /// A representing the asynchronous operation. ValueTask ConsumeSpecialItemAsync(Item item, ushort effectTimeInSeconds); } \ No newline at end of file diff --git a/src/Interfaces/IGameServerInstanceManager.cs b/src/Interfaces/IGameServerInstanceManager.cs index 34aac4ef4..9c1a65099 100644 --- a/src/Interfaces/IGameServerInstanceManager.cs +++ b/src/Interfaces/IGameServerInstanceManager.cs @@ -13,20 +13,17 @@ public interface IGameServerInstanceManager /// Restarts all servers of this container. /// /// If set to true, this method is called during a database initialization. - /// A task representing the restart all operation. ValueTask RestartAllAsync(bool onDatabaseInit); /// /// Initializes a game server. /// /// The server identifier. - /// A task representing the initialize game operation. ValueTask InitializeGameServerAsync(byte serverId); /// /// Removes the game server instance. /// /// The server identifier. - /// A task representing the remove game server operation. ValueTask RemoveGameServerAsync(byte serverId); } \ No newline at end of file diff --git a/src/Persistence/EntityFramework/GenericRepositoryBase.cs b/src/Persistence/EntityFramework/GenericRepositoryBase.cs index 6230b9c4f..aa94a875b 100644 --- a/src/Persistence/EntityFramework/GenericRepositoryBase.cs +++ b/src/Persistence/EntityFramework/GenericRepositoryBase.cs @@ -191,7 +191,6 @@ protected virtual async ValueTask LoadDependentDataAsync(object obj, DbContext c /// The loaded objects. /// The current context with which the objects got loaded. It is necessary to retrieve the foreign key ids. /// The cancellation token. - /// A representing the asynchronous operation. protected virtual async ValueTask LoadDependentDataAsync(IEnumerable loadedObjects, DbContext currentContext, CancellationToken cancellationToken) { foreach (var obj in loadedObjects) diff --git a/src/Startup/ServerContainerBase.cs b/src/Startup/ServerContainerBase.cs index d6b7e2e54..8cfdd6f67 100644 --- a/src/Startup/ServerContainerBase.cs +++ b/src/Startup/ServerContainerBase.cs @@ -46,7 +46,6 @@ public async Task StopAsync(CancellationToken cancellationToken) /// Restarts all servers of this container. /// /// If set to true, this method is called during a database initialization. - /// A representing the asynchronous operation. public virtual async ValueTask RestartAllAsync(bool onDatabaseInit) { await this.StopAsync(default).ConfigureAwait(false); @@ -78,7 +77,6 @@ public virtual async ValueTask RestartAllAsync(bool onDatabaseInit) /// /// If set to true, this method is called during a database initialization. /// The cancellation token. - /// A representing the asynchronous operation. protected virtual async ValueTask BeforeStartAsync(bool onDatabaseInit, CancellationToken cancellationToken) { // can be overwritten diff --git a/src/Web/AdminPanel/API/ServerController.cs b/src/Web/AdminPanel/API/ServerController.cs index 3bf22bddd..7b1e23152 100644 --- a/src/Web/AdminPanel/API/ServerController.cs +++ b/src/Web/AdminPanel/API/ServerController.cs @@ -31,7 +31,6 @@ public class ServerController : Controller /// /// The server id. /// The message. - /// A representing the asynchronous operation. [Route("send/{id=0}")] public async Task SendGlobalMessage(int id, [FromQuery(Name = "msg")] string msg) { From bc9bddde5b3fea16e133e86e62420aa1a0060350 Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Sun, 14 Jun 2026 10:38:04 -0300 Subject: [PATCH 09/14] fix remaining SA1638 --- src/Dapr/ConnectServer.Host/ConnectServerController.cs | 4 ++-- src/Web/Shared/Components/ItemEdit/ItemViewModel.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Dapr/ConnectServer.Host/ConnectServerController.cs b/src/Dapr/ConnectServer.Host/ConnectServerController.cs index 48f4f5fec..6da09fcd9 100644 --- a/src/Dapr/ConnectServer.Host/ConnectServerController.cs +++ b/src/Dapr/ConnectServer.Host/ConnectServerController.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -11,7 +11,7 @@ namespace MUnique.OpenMU.ConnectServer.Host; using MUnique.OpenMU.ServerClients; /// -/// API Controller which receives messages from other services. +/// API Controller that receives messages from other services. /// [ApiController] [Route("")] diff --git a/src/Web/Shared/Components/ItemEdit/ItemViewModel.cs b/src/Web/Shared/Components/ItemEdit/ItemViewModel.cs index f69a806db..097380eb5 100644 --- a/src/Web/Shared/Components/ItemEdit/ItemViewModel.cs +++ b/src/Web/Shared/Components/ItemEdit/ItemViewModel.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // From 215aea39272511f7eb55b3fc0270347d485b1ed2 Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:37:37 -0300 Subject: [PATCH 10/14] fix: replace static property with method to avoid init-order bug in Exports --- src/Web/AdminPanel/Exports.cs | 7 +------ src/Web/Map/Exports.cs | 21 +++++++++------------ src/Web/Shared/Exports.cs | 16 ++++++++-------- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/Web/AdminPanel/Exports.cs b/src/Web/AdminPanel/Exports.cs index 488761912..d90eea577 100644 --- a/src/Web/AdminPanel/Exports.cs +++ b/src/Web/AdminPanel/Exports.cs @@ -7,7 +7,7 @@ namespace MUnique.OpenMU.Web.AdminPanel; using System.Collections.Immutable; /// -/// Class which holds the script exports of this project. +/// Class that holds the script exports of this project. /// /// /// TODO: Instead of a static class, create an interface, so we can inject an instance into the layout. @@ -36,11 +36,6 @@ public static class Exports ? Web.Map.Exports.Stylesheets.Concat(AdminPanelStylesheets).ToImmutableList() : AdminPanelStylesheets.ToImmutableList(); - /// - /// Gets the url prefix to the scripts of this project. - /// - private static string Prefix { get; } = $"_content/{typeof(Exports).Namespace}"; - private static IEnumerable AdminPanelScripts => []; private static IEnumerable AdminPanelStylesheets => []; diff --git a/src/Web/Map/Exports.cs b/src/Web/Map/Exports.cs index 2f5e0ce95..58ff7c92f 100644 --- a/src/Web/Map/Exports.cs +++ b/src/Web/Map/Exports.cs @@ -7,24 +7,19 @@ namespace MUnique.OpenMU.Web.Map; using System.Collections.Immutable; /// -/// Class which holds the script exports of this project. +/// Class that holds the script exports of this project. /// public static class Exports { - /// - /// Gets the url prefix to the scripts of this project. - /// - public static string Prefix { get; } = $"_content/{typeof(Exports).Namespace}"; - /// /// Gets the scripts. /// public static ImmutableList Scripts { get; } = [ .. Web.Shared.Exports.Scripts, - $"{Prefix}/js/system-production.js", - $"{Prefix}/js/map-launcher.js", - $"{Prefix}/js/MUnique.OpenMU.Web.Map.js", - $"{Prefix}/js/Stats.js", + $"{GetPrefix()}/js/system-production.js", + $"{GetPrefix()}/js/map-launcher.js", + $"{GetPrefix()}/js/MUnique.OpenMU.Web.Map.js", + $"{GetPrefix()}/js/Stats.js", ]; /// @@ -32,12 +27,14 @@ public static class Exports /// public static ImmutableList<(string Key, string Path)> ScriptMappings { get; } = new (string Key, string Path)[] { - ("three", $"{Prefix}/js/three.min.js"), - ("tween", $"{Prefix}/js/tween.min.js"), + ("three", $"{GetPrefix()}/js/three.min.js"), + ("tween", $"{GetPrefix()}/js/tween.min.js"), }.ToImmutableList(); /// /// Gets the stylesheets. /// public static ImmutableList Stylesheets => Web.Shared.Exports.Stylesheets; + + private static string GetPrefix() => $"_content/{typeof(Exports).Namespace}"; } \ No newline at end of file diff --git a/src/Web/Shared/Exports.cs b/src/Web/Shared/Exports.cs index fba3b8937..1197bf07d 100644 --- a/src/Web/Shared/Exports.cs +++ b/src/Web/Shared/Exports.cs @@ -7,7 +7,7 @@ namespace MUnique.OpenMU.Web.Shared; using System.Collections.Immutable; /// -/// Class which holds the script exports of this project. +/// Class that holds the script exports of this project. /// /// /// TODO: Instead of a static class, create an interface, so we can inject an instance into the layout. @@ -30,11 +30,6 @@ public static class Exports /// public static ImmutableList Stylesheets { get; } = SharedStylesheets.ToImmutableList(); - /// - /// Gets the url prefix to the scripts of this project. - /// - private static string Prefix { get; } = $"_content/{typeof(Exports).Namespace}"; - private static IEnumerable SharedScripts { get @@ -47,8 +42,13 @@ private static IEnumerable SharedStylesheets { get { - yield return $"{Prefix}/css/shared.css"; - yield return $"{Prefix}/css/theme.css"; + yield return $"{GetPrefix()}/css/shared.css"; + yield return $"{GetPrefix()}/css/theme.css"; } } + + /// + /// Gets the url prefix to the scripts of this project. + /// + private static string GetPrefix() => $"_content/{typeof(Exports).Namespace}"; } \ No newline at end of file From 13b2d63f140e4f9b47bf83c3ce106b55969ac15b Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:44:20 -0300 Subject: [PATCH 11/14] fix typos --- .../Initialization/Updates/FixItemRequirementsPlugIn.cs | 4 ++-- src/Web/Shared/ComponentBuilders/TimeOnlyFieldBuilder.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Persistence/Initialization/Updates/FixItemRequirementsPlugIn.cs b/src/Persistence/Initialization/Updates/FixItemRequirementsPlugIn.cs index d70182fac..572769971 100644 --- a/src/Persistence/Initialization/Updates/FixItemRequirementsPlugIn.cs +++ b/src/Persistence/Initialization/Updates/FixItemRequirementsPlugIn.cs @@ -12,7 +12,7 @@ namespace MUnique.OpenMU.Persistence.Initialization.Updates; using MUnique.OpenMU.PlugIns; /// -/// Updates some item requirements for elf bows that were intialized wrongly. +/// Updates some item requirements for elf bows that were initialized wrongly. /// [PlugIn] [Display(Name = PlugInName, Description = PlugInDescription)] @@ -27,7 +27,7 @@ public class FixItemRequirementsPlugIn : UpdatePlugInBase /// /// The plugin description. /// - internal const string PlugInDescription = "Updates some item requirements for elf bows which were intialized wrongly."; + internal const string PlugInDescription = "Updates some item requirements for elf bows which were initialized wrongly."; private static readonly List<(int Group, int Number, int StrengthRequirement, int AgilityRequirement, int EnergyRequirement, int VitalityRequirement)> RequirementCorrections = [ diff --git a/src/Web/Shared/ComponentBuilders/TimeOnlyFieldBuilder.cs b/src/Web/Shared/ComponentBuilders/TimeOnlyFieldBuilder.cs index 8e84493c3..7399e6b25 100644 --- a/src/Web/Shared/ComponentBuilders/TimeOnlyFieldBuilder.cs +++ b/src/Web/Shared/ComponentBuilders/TimeOnlyFieldBuilder.cs @@ -10,7 +10,7 @@ namespace MUnique.OpenMU.Web.Shared.ComponentBuilders; using MUnique.OpenMU.Web.Shared.Services; /// -/// A for date fields. +/// A for time fields. /// public class TimeOnlyFieldBuilder : BaseComponentBuilder, IComponentBuilder { From 3ec35555a9bf37e97a12db9b041b5b4a3d8dc665 Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Mon, 15 Jun 2026 12:52:36 -0300 Subject: [PATCH 12/14] add log to avoid codacy warnings --- .../Components/Layout/ConfigurationSearch.razor.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Web/AdminPanel/Components/Layout/ConfigurationSearch.razor.cs b/src/Web/AdminPanel/Components/Layout/ConfigurationSearch.razor.cs index 98d61be4a..7b304991a 100644 --- a/src/Web/AdminPanel/Components/Layout/ConfigurationSearch.razor.cs +++ b/src/Web/AdminPanel/Components/Layout/ConfigurationSearch.razor.cs @@ -7,6 +7,7 @@ namespace MUnique.OpenMU.Web.AdminPanel.Components.Layout; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using MUnique.OpenMU.Web.AdminPanel.Services; +using Microsoft.Extensions.Logging; using MUnique.OpenMU.Web.Shared.Components; using MUnique.OpenMU.Web.Shared.Services; @@ -28,6 +29,9 @@ public partial class ConfigurationSearch : IDisposable [Inject] private ConfigurationSearchIndexCache SearchIndexCache { get; set; } = null!; + [Inject] + private ILogger Logger { get; set; } = null!; + [Inject] private NavigationManager NavigationManager { get; set; } = null!; @@ -125,6 +129,7 @@ private static string Normalize(string value) private void OnSearchFocus(FocusEventArgs e) { + this.Logger.LogDebug("Search input focused, event type: {EventType}", e.Type); this.UpdateSearchResults(); } @@ -147,6 +152,7 @@ await this.InvokeAsync(() => private async Task OnSearchBlurAsync(FocusEventArgs e) { + this.Logger.LogDebug("Search input blurred, event type: {EventType}", e.Type); await Task.Delay(100).ConfigureAwait(true); this._searchResults.Clear(); } From f3c43ed940843917d24115f418c705c6288fe374 Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:37:22 -0300 Subject: [PATCH 13/14] fix offlevel command reconnecting due to client and player count --- src/GameLogic/IGameContext.cs | 6 ++++++ src/GameLogic/Offline/OfflinePlayerManager.cs | 10 +++++++++- src/GameLogic/Player.cs | 19 ++++++++----------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/GameLogic/IGameContext.cs b/src/GameLogic/IGameContext.cs index 0fb984f48..57283e4c5 100644 --- a/src/GameLogic/IGameContext.cs +++ b/src/GameLogic/IGameContext.cs @@ -138,6 +138,12 @@ public interface IGameContext /// The player. ValueTask AddPlayerAsync(Player player); + /// + /// Removes the player from the game. + /// + /// The player. + ValueTask RemovePlayerAsync(Player player); + /// /// Gets the maps which is meant to be hosted by the game. /// diff --git a/src/GameLogic/Offline/OfflinePlayerManager.cs b/src/GameLogic/Offline/OfflinePlayerManager.cs index a5a3db2d9..2a8c7dfe9 100644 --- a/src/GameLogic/Offline/OfflinePlayerManager.cs +++ b/src/GameLogic/Offline/OfflinePlayerManager.cs @@ -6,6 +6,7 @@ namespace MUnique.OpenMU.GameLogic.Offline; using MUnique.OpenMU.GameLogic.Attributes; using MUnique.OpenMU.GameLogic.MuHelper; +using MUnique.OpenMU.GameLogic.Views.Login; /// /// Manages active sessions. @@ -102,8 +103,15 @@ private async ValueTask TransitionToOfflineAsync(Player realPlayer, string login { await this.LogOffFromLoginServerAsync(realPlayer, loginName).ConfigureAwait(false); - realPlayer.SuppressDisconnectedEvent(); + // Send a close-game packet so the client sees a clean exit. + await realPlayer.InvokeViewPlugInAsync(p => p.LogoutAsync(LogoutType.CloseGame)).ConfigureAwait(false); + + realPlayer.ClearDisconnectedEventSubscribers(); await realPlayer.DisconnectAsync().ConfigureAwait(false); + + // Explicitly remove the real player so _playerList stay consistent. + await realPlayer.GameContext.RemovePlayerAsync(realPlayer).ConfigureAwait(false); + realPlayer.PersistenceContext.Dispose(); } diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs index e04347568..fa09d06b8 100644 --- a/src/GameLogic/Player.cs +++ b/src/GameLogic/Player.cs @@ -1404,17 +1404,6 @@ public async Task RegenerateAsync() } } - /// - /// Clears all subscribers from the event so that - /// will not raise it. Used by offline player to prevent - /// GameServer.OnPlayerDisconnectedAsync from double-saving and double-logging off - /// after the real client disconnects. - /// - public void SuppressDisconnectedEvent() - { - this.PlayerDisconnected = null; - } - /// /// Disconnects the player from the game. Remote connections will be closed and data will be saved. /// @@ -1877,6 +1866,14 @@ internal async ValueTask AfterKilledPlayerAsync(Player killedPlayer) await this.ForEachWorldObserverAsync(o => o.UpdateCharacterHeroStateAsync(this), true).ConfigureAwait(false); } + /// + /// Clears all subscribers from the event. + /// + internal void ClearDisconnectedEventSubscribers() + { + this.PlayerDisconnected = null; + } + /// protected override async ValueTask DisposeAsyncCore() { From ed0982d60217244c4d2f45a9980340320393e630 Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:20:45 -0300 Subject: [PATCH 14/14] fix(/offlevel): send close-game packet and let PlayerDisconnected event clean up naturally --- src/GameLogic/Offline/OfflinePlayerManager.cs | 10 +++------- src/GameLogic/Player.cs | 8 -------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/GameLogic/Offline/OfflinePlayerManager.cs b/src/GameLogic/Offline/OfflinePlayerManager.cs index 2a8c7dfe9..1b02a0fa0 100644 --- a/src/GameLogic/Offline/OfflinePlayerManager.cs +++ b/src/GameLogic/Offline/OfflinePlayerManager.cs @@ -103,16 +103,12 @@ private async ValueTask TransitionToOfflineAsync(Player realPlayer, string login { await this.LogOffFromLoginServerAsync(realPlayer, loginName).ConfigureAwait(false); - // Send a close-game packet so the client sees a clean exit. + // Send a close-game packet so the client exits cleanly without auto-reconnecting. + // DisconnectAsync will then fire PlayerDisconnected, which triggers + // RemovePlayerAsync (player list cleanup) and OnPlayerDisconnectedAsync (dispose). await realPlayer.InvokeViewPlugInAsync(p => p.LogoutAsync(LogoutType.CloseGame)).ConfigureAwait(false); - realPlayer.ClearDisconnectedEventSubscribers(); await realPlayer.DisconnectAsync().ConfigureAwait(false); - - // Explicitly remove the real player so _playerList stay consistent. - await realPlayer.GameContext.RemovePlayerAsync(realPlayer).ConfigureAwait(false); - - realPlayer.PersistenceContext.Dispose(); } /// diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs index fa09d06b8..8579a5b5c 100644 --- a/src/GameLogic/Player.cs +++ b/src/GameLogic/Player.cs @@ -1866,14 +1866,6 @@ internal async ValueTask AfterKilledPlayerAsync(Player killedPlayer) await this.ForEachWorldObserverAsync(o => o.UpdateCharacterHeroStateAsync(this), true).ConfigureAwait(false); } - /// - /// Clears all subscribers from the event. - /// - internal void ClearDisconnectedEventSubscribers() - { - this.PlayerDisconnected = null; - } - /// protected override async ValueTask DisposeAsyncCore() {