From 3c5749d5ce91a55c1c42dd6e07e8be458bc50085 Mon Sep 17 00:00:00 2001 From: Oleksii Lisikh Date: Fri, 13 Mar 2026 12:56:12 +0100 Subject: [PATCH] feat(persistence): add Crest of Monarch drop group in Icarus --- ...AddCrestOfMonarchDropGroupUpdateSeason6.cs | 81 +++++++++++++++++++ .../Initialization/Updates/UpdateVersion.cs | 7 +- .../VersionSeasonSix/Maps/Icarus.cs | 16 +++- .../TestInitializationWithEfCore.cs | 74 ++++++++++++++++- 4 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 src/Persistence/Initialization/Updates/AddCrestOfMonarchDropGroupUpdateSeason6.cs diff --git a/src/Persistence/Initialization/Updates/AddCrestOfMonarchDropGroupUpdateSeason6.cs b/src/Persistence/Initialization/Updates/AddCrestOfMonarchDropGroupUpdateSeason6.cs new file mode 100644 index 000000000..7021f2420 --- /dev/null +++ b/src/Persistence/Initialization/Updates/AddCrestOfMonarchDropGroupUpdateSeason6.cs @@ -0,0 +1,81 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.Updates; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps; +using MUnique.OpenMU.PlugIns; + +/// +/// This update adds the Crest of Monarch drop item group for the Icarus map. +/// +[PlugIn] +[Display(Name = PlugInName, Description = PlugInDescription)] +[Guid("FF14A478-3EA8-4C41-A298-8E6698D5973D")] +public class AddCrestOfMonarchDropGroupUpdateSeason6 : UpdatePlugInBase +{ + /// + /// The plug in name. + /// + internal const string PlugInName = "Add Crest of Monarch Drop Group"; + + /// + /// The plug in description. + /// + internal const string PlugInDescription = "Adds the Crest of Monarch (Loch's Feather +1) drop item group to Icarus."; + + /// + public override UpdateVersion Version => UpdateVersion.AddCrestOfMonarchDropGroupSeason6; + + /// + public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id; + + /// + public override string Name => PlugInName; + + /// + public override string Description => PlugInDescription; + + /// + public override bool IsMandatory => true; + + /// + public override DateTime CreatedAt => new(2026, 03, 13, 12, 0, 0, DateTimeKind.Utc); + + /// +#pragma warning disable CS1998 + protected override async ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration) +#pragma warning restore CS1998 + { + var map = gameConfiguration.Maps.First(m => m.Number == Icarus.Number && m.Discriminator == 0); + var lochsFeather = gameConfiguration.Items.First(item => item.Group == 13 && item.Number == 14); + var crestId = GuidHelper.CreateGuid(Icarus.Number, 2); + + var crestGroup = gameConfiguration.DropItemGroups.FirstOrDefault(group => group.GetId() == crestId); + if (crestGroup is null) + { + crestGroup = context.CreateNew(); + crestGroup.SetGuid(Icarus.Number, 2); + gameConfiguration.DropItemGroups.Add(crestGroup); + } + + crestGroup.Description = "Crest of Monarch"; + crestGroup.Chance = 0.001; + crestGroup.MinimumMonsterLevel = 82; + crestGroup.MaximumMonsterLevel = null; + crestGroup.ItemLevel = 1; + if (crestGroup.PossibleItems.Count != 1 || crestGroup.PossibleItems.First().GetItemId() != lochsFeather.GetItemId()) + { + crestGroup.PossibleItems.Clear(); + crestGroup.PossibleItems.Add(lochsFeather); + } + + if (!map.DropItemGroups.Any(group => group.GetId() == crestGroup.GetId())) + { + map.DropItemGroups.Add(crestGroup); + } + } +} diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs index 1e771cc39..d3d81f020 100644 --- a/src/Persistence/Initialization/Updates/UpdateVersion.cs +++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs @@ -344,4 +344,9 @@ public enum UpdateVersion /// The version of the . /// AddProjectileCountToTripleShot = 67, -} \ No newline at end of file + + /// + /// The version of the . + /// + AddCrestOfMonarchDropGroupSeason6 = 68, +} diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/Icarus.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/Icarus.cs index 607b1e842..9995c8f7b 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Maps/Icarus.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/Icarus.cs @@ -25,13 +25,25 @@ public Icarus(IContext context, GameConfiguration gameConfiguration) protected override void InitializeDropItemGroups() { base.InitializeDropItemGroups(); + var lochsFeather = this.GameConfiguration.Items.First(item => item.Group == 13 && item.Number == 14); + var feather = this.Context.CreateNew(); feather.SetGuid(this.MapNumber, 1); feather.Chance = 0.001; feather.Description = "Loch's Feather"; feather.MinimumMonsterLevel = 82; - feather.PossibleItems.Add(this.GameConfiguration.Items.First(item => item.Group == 13 && item.Number == 14)); + feather.PossibleItems.Add(lochsFeather); this.MapDefinition!.DropItemGroups.Add(feather); this.GameConfiguration.DropItemGroups.Add(feather); + + var crest = this.Context.CreateNew(); + crest.SetGuid(this.MapNumber, 2); + crest.Chance = 0.001; + crest.Description = "Crest of Monarch"; + crest.MinimumMonsterLevel = 82; + crest.ItemLevel = 1; + crest.PossibleItems.Add(lochsFeather); + this.MapDefinition.DropItemGroups.Add(crest); + this.GameConfiguration.DropItemGroups.Add(crest); } -} \ No newline at end of file +} diff --git a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/TestInitializationWithEfCore.cs b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/TestInitializationWithEfCore.cs index 383982414..b053d2a1d 100644 --- a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/TestInitializationWithEfCore.cs +++ b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/TestInitializationWithEfCore.cs @@ -5,10 +5,13 @@ namespace MUnique.OpenMU.Persistence.Initialization.Tests; using Microsoft.Extensions.Logging.Abstractions; +using MUnique.OpenMU.AttributeSystem; using MUnique.OpenMU.DataModel; using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.Persistence.EntityFramework; +using MUnique.OpenMU.Persistence.Initialization.Updates; +using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps; using MUnique.OpenMU.Persistence.InMemory; /// @@ -47,9 +50,47 @@ public async Task TestSeason6DataAsync() var contextProvider = new InMemoryPersistenceContextProvider(); var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory()); await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false); + await this.AssertIcarusFeatherAndCrestDropGroupsAsync(contextProvider).ConfigureAwait(false); await this.TestIfItemsFitIntoInventoriesAsync(contextProvider).ConfigureAwait(false); } + /// + /// Tests that applying the update for Crest of Monarch in Season 6 is idempotent. + /// + [Test] + public async Task TestSeason6CrestOfMonarchUpdatePlugInAsync() + { + var contextProvider = new InMemoryPersistenceContextProvider(); + var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory()); + await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false); + + using var context = contextProvider.CreateNewContext(); + var gameConfiguration = (await context.GetAsync().ConfigureAwait(false)).First(); + var map = gameConfiguration.Maps.First(m => m.Number == Icarus.Number && m.Discriminator == 0); + + var crestGroupId = GuidHelper.CreateGuid(Icarus.Number, 2); + if (gameConfiguration.DropItemGroups.FirstOrDefault(group => group.GetId() == crestGroupId) is { } existingCrestGroup) + { + map.DropItemGroups.Remove(existingCrestGroup); + gameConfiguration.DropItemGroups.Remove(existingCrestGroup); + } + + var update = new AddCrestOfMonarchDropGroupUpdateSeason6(); + await update.ApplyUpdateAsync(context, gameConfiguration).ConfigureAwait(false); + await update.ApplyUpdateAsync(context, gameConfiguration).ConfigureAwait(false); + + var groups = gameConfiguration.DropItemGroups.Where(group => group.GetId() == crestGroupId).ToList(); + Assert.That(groups, Has.Count.EqualTo(1)); + Assert.That(map.DropItemGroups.Count(group => group.GetId() == crestGroupId), Is.EqualTo(1)); + Assert.That(groups[0].Description.ToString(), Is.EqualTo("Crest of Monarch")); + Assert.That(groups[0].Chance, Is.EqualTo(0.001)); + Assert.That(groups[0].MinimumMonsterLevel, Is.EqualTo((byte)82)); + Assert.That(groups[0].ItemLevel, Is.EqualTo((byte)1)); + Assert.That(groups[0].PossibleItems, Has.Count.EqualTo(1)); + Assert.That(groups[0].PossibleItems.Single().Group, Is.EqualTo((byte)13)); + Assert.That(groups[0].PossibleItems.Single().Number, Is.EqualTo((short)14)); + } + /// /// Tests the data initialization using the in-memory persistence. /// @@ -114,4 +155,35 @@ private async Task TestIfItemsFitIntoInventoriesAsync(IPersistenceContextProvide } } } -} \ No newline at end of file + + private async Task AssertIcarusFeatherAndCrestDropGroupsAsync(IPersistenceContextProvider contextProvider) + { + using var context = contextProvider.CreateNewConfigurationContext(); + var gameConfiguration = (await context.GetAsync().ConfigureAwait(false)).First(); + var map = gameConfiguration.Maps.First(m => m.Number == Icarus.Number && m.Discriminator == 0); + + var lochsFeatherGroupId = GuidHelper.CreateGuid(Icarus.Number, 1); + var crestGroupId = GuidHelper.CreateGuid(Icarus.Number, 2); + var featherGroup = gameConfiguration.DropItemGroups.Single(group => group.GetId() == lochsFeatherGroupId); + var crestGroup = gameConfiguration.DropItemGroups.Single(group => group.GetId() == crestGroupId); + + Assert.That(map.DropItemGroups.Any(group => group.GetId() == lochsFeatherGroupId), Is.True); + Assert.That(map.DropItemGroups.Any(group => group.GetId() == crestGroupId), Is.True); + + Assert.That(featherGroup.Description.ToString(), Is.EqualTo("Loch's Feather")); + Assert.That(featherGroup.Chance, Is.EqualTo(0.001)); + Assert.That(featherGroup.MinimumMonsterLevel, Is.EqualTo((byte)82)); + Assert.That(featherGroup.ItemLevel, Is.Null); + Assert.That(featherGroup.PossibleItems, Has.Count.EqualTo(1)); + Assert.That(featherGroup.PossibleItems.Single().Group, Is.EqualTo((byte)13)); + Assert.That(featherGroup.PossibleItems.Single().Number, Is.EqualTo((short)14)); + + Assert.That(crestGroup.Description.ToString(), Is.EqualTo("Crest of Monarch")); + Assert.That(crestGroup.Chance, Is.EqualTo(0.001)); + Assert.That(crestGroup.MinimumMonsterLevel, Is.EqualTo((byte)82)); + Assert.That(crestGroup.ItemLevel, Is.EqualTo((byte)1)); + Assert.That(crestGroup.PossibleItems, Has.Count.EqualTo(1)); + Assert.That(crestGroup.PossibleItems.Single().Group, Is.EqualTo((byte)13)); + Assert.That(crestGroup.PossibleItems.Single().Number, Is.EqualTo((short)14)); + } +}