diff --git a/src/Persistence/Initialization/MUnique.OpenMU.Persistence.Initialization.csproj b/src/Persistence/Initialization/MUnique.OpenMU.Persistence.Initialization.csproj index 310b825..00da802 100644 --- a/src/Persistence/Initialization/MUnique.OpenMU.Persistence.Initialization.csproj +++ b/src/Persistence/Initialization/MUnique.OpenMU.Persistence.Initialization.csproj @@ -25,6 +25,10 @@ + + + + @@ -105,6 +109,10 @@ + + + + diff --git a/src/Persistence/Initialization/Resources/Terrain84.att b/src/Persistence/Initialization/Resources/Terrain84.att new file mode 100644 index 0000000..26fe415 Binary files /dev/null and b/src/Persistence/Initialization/Resources/Terrain84.att differ diff --git a/src/Persistence/Initialization/Resources/Terrain85.att b/src/Persistence/Initialization/Resources/Terrain85.att new file mode 100644 index 0000000..d124c84 Binary files /dev/null and b/src/Persistence/Initialization/Resources/Terrain85.att differ diff --git a/src/Persistence/Initialization/Resources/Terrain86.att b/src/Persistence/Initialization/Resources/Terrain86.att new file mode 100644 index 0000000..c0bcb3a Binary files /dev/null and b/src/Persistence/Initialization/Resources/Terrain86.att differ diff --git a/src/Persistence/Initialization/Resources/Terrain87.att b/src/Persistence/Initialization/Resources/Terrain87.att new file mode 100644 index 0000000..a67a76f Binary files /dev/null and b/src/Persistence/Initialization/Resources/Terrain87.att differ diff --git a/src/Persistence/Initialization/Updates/AddArkaniaMapUpdateSeason6.cs b/src/Persistence/Initialization/Updates/AddArkaniaMapUpdateSeason6.cs index afd6b3e..fe7372f 100644 --- a/src/Persistence/Initialization/Updates/AddArkaniaMapUpdateSeason6.cs +++ b/src/Persistence/Initialization/Updates/AddArkaniaMapUpdateSeason6.cs @@ -41,20 +41,31 @@ public class AddArkaniaMapUpdateSeason6 : UpdatePlugInBase public override DateTime CreatedAt => new(2026, 07, 19, 12, 0, 0, DateTimeKind.Utc); /// - protected override ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration) + protected override async ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration) { - // Idempotency guard: skip if Arkania (83) already exists. + // Idempotency guard: skip if Arkania (82) already exists. if (gameConfiguration.Maps.Any(m => m.Number == Arkania.Number)) { - return default; + return; } var initializer = new Arkania(context, gameConfiguration); - initializer.Initialize(); // creates map 83 + loads terrain from Terrain84.att + initializer.Initialize(); // creates map 82 + loads terrain from Terrain83.att initializer.SetSafezoneMap(); var arkania = gameConfiguration.Maps.First(m => m.Number == Arkania.Number); + // Host the map on every game server configuration (fresh seeds do this + // automatically, but existing databases need it added explicitly). + var serverConfigs = await context.GetAsync().ConfigureAwait(false); + foreach (var serverConfig in serverConfigs) + { + if (serverConfig.Maps.All(m => m.Number != Arkania.Number)) + { + serverConfig.Maps.Add(arkania); + } + } + // Spawn gate on a walkable tile (center of the map; attr 0x00, verified walkable). var spawnGate = context.CreateNew(); arkania.ExitGates.Add(spawnGate); @@ -76,7 +87,5 @@ public class AddArkaniaMapUpdateSeason6 : UpdatePlugInBase warp.LevelRequirement = 10; warp.Gate = spawnGate; } - - return default; } } diff --git a/src/Persistence/Initialization/Updates/AddImportedMapsUpdateSeason6.cs b/src/Persistence/Initialization/Updates/AddImportedMapsUpdateSeason6.cs new file mode 100644 index 0000000..13f05a0 --- /dev/null +++ b/src/Persistence/Initialization/Updates/AddImportedMapsUpdateSeason6.cs @@ -0,0 +1,100 @@ +// +// 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; + +/// +/// Adds the four imported maps Acheron (83), Debenter (84), Uruk (85) and Ferea (86): +/// the map definitions with their terrain, a spawn gate and a /move warp each, and +/// assigns each to the game server configuration so it is hosted and its spawns run. +/// Monsters and NPCs are added afterwards via the admin panel. +/// +[PlugIn] +[Display(Name = PlugInName, Description = PlugInDescription)] +[Guid("3D9C1B72-7C3F-4E8B-AF14-E29D47A8B012")] +public class AddImportedMapsUpdateSeason6 : UpdatePlugInBase +{ + internal const string PlugInName = "Add imported maps (Acheron, Debenter, Uruk, Ferea)"; + internal const string PlugInDescription = "Adds maps 83-86 (Acheron, Debenter, Uruk, Ferea) with terrain, spawn gate and /move warp, hosted by the game server. Monsters/NPCs are added via the admin panel."; + + /// + public override UpdateVersion Version => UpdateVersion.AddImportedMapsSeason6; + + /// + 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, 07, 19, 15, 0, 0, DateTimeKind.Utc); + + /// + protected override async ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration) + { + var serverConfigs = (await context.GetAsync().ConfigureAwait(false)).ToList(); + + AddMap(context, gameConfiguration, serverConfigs, new Acheron(context, gameConfiguration), Acheron.Number, Acheron.Name, 84, 127, 126); + AddMap(context, gameConfiguration, serverConfigs, new Debenter(context, gameConfiguration), Debenter.Number, Debenter.Name, 85, 132, 105); + AddMap(context, gameConfiguration, serverConfigs, new Uruk(context, gameConfiguration), Uruk.Number, Uruk.Name, 86, 128, 128); + AddMap(context, gameConfiguration, serverConfigs, new Ferea(context, gameConfiguration), Ferea.Number, Ferea.Name, 87, 128, 128); + } + + private static void AddMap(IContext context, GameConfiguration gameConfiguration, IList serverConfigs, IMapInitializer initializer, byte number, string name, short warpIndex, byte x, byte y) + { + // Idempotency guard: skip if the map already exists. + if (gameConfiguration.Maps.Any(m => m.Number == number)) + { + return; + } + + initializer.Initialize(); // creates the map + loads terrain from Terrain{number+1}.att + initializer.SetSafezoneMap(); + + var map = gameConfiguration.Maps.First(m => m.Number == number); + + // Spawn gate on a walkable tile. + var spawnGate = context.CreateNew(); + map.ExitGates.Add(spawnGate); + spawnGate.Map = map; + spawnGate.X1 = x; + spawnGate.Y1 = y; + spawnGate.X2 = x; + spawnGate.Y2 = y; + spawnGate.IsSpawnGate = true; + + // /move warp entry. + if (gameConfiguration.WarpList.All(w => w.Name != name)) + { + var warp = context.CreateNew(); + gameConfiguration.WarpList.Add(warp); + warp.Index = warpIndex; + warp.Name = name; + warp.Costs = 5000; + warp.LevelRequirement = 10; + warp.Gate = spawnGate; + } + + // Host the map on every game server configuration (fresh seeds do this + // automatically, but existing databases need it added explicitly). + foreach (var serverConfig in serverConfigs) + { + if (serverConfig.Maps.All(m => m.Number != number)) + { + serverConfig.Maps.Add(map); + } + } + } +} diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs index 891f5fe..4ef0224 100644 --- a/src/Persistence/Initialization/Updates/UpdateVersion.cs +++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs @@ -489,4 +489,9 @@ public enum UpdateVersion /// The version of the . /// AddArkaniaMapSeason6 = 96, + + /// + /// The version of the (Acheron, Debenter, Uruk, Ferea). + /// + AddImportedMapsSeason6 = 97, } \ No newline at end of file diff --git a/src/Persistence/Initialization/VersionSeasonSix/GameMapsInitializer.cs b/src/Persistence/Initialization/VersionSeasonSix/GameMapsInitializer.cs index d9b0b40..d85548d 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/GameMapsInitializer.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/GameMapsInitializer.cs @@ -101,6 +101,10 @@ public class GameMapsInitializer : GameMapsInitializerBase yield return typeof(FortressOfImperialGuardian3); yield return typeof(FortressOfImperialGuardian4); yield return typeof(Arkania); + yield return typeof(Acheron); + yield return typeof(Debenter); + yield return typeof(Uruk); + yield return typeof(Ferea); } } } \ No newline at end of file diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/Acheron.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/Acheron.cs new file mode 100644 index 0000000..1e4b67e --- /dev/null +++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/Acheron.cs @@ -0,0 +1,40 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps; + +using MUnique.OpenMU.DataModel.Configuration; + +/// +/// The initialization for the Acheron map (imported map). Seeded empty; monster/NPC +/// spawns and gates are added via the admin panel. +/// +internal class Acheron : BaseMapInitializer +{ + /// + /// The Number of the Map. + /// + internal const byte Number = 83; + + /// + /// The Name of the Map. + /// + internal const string Name = "Acheron"; + + /// + /// Initializes a new instance of the class. + /// + /// The context. + /// The game configuration. + public Acheron(IContext context, GameConfiguration gameConfiguration) + : base(context, gameConfiguration) + { + } + + /// + protected override byte MapNumber => Number; + + /// + protected override string MapName => Name; +} diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/Debenter.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/Debenter.cs new file mode 100644 index 0000000..36454d5 --- /dev/null +++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/Debenter.cs @@ -0,0 +1,40 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps; + +using MUnique.OpenMU.DataModel.Configuration; + +/// +/// The initialization for the Debenter map (imported map). Seeded empty; monster/NPC +/// spawns and gates are added via the admin panel. +/// +internal class Debenter : BaseMapInitializer +{ + /// + /// The Number of the Map. + /// + internal const byte Number = 84; + + /// + /// The Name of the Map. + /// + internal const string Name = "Debenter"; + + /// + /// Initializes a new instance of the class. + /// + /// The context. + /// The game configuration. + public Debenter(IContext context, GameConfiguration gameConfiguration) + : base(context, gameConfiguration) + { + } + + /// + protected override byte MapNumber => Number; + + /// + protected override string MapName => Name; +} diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/Ferea.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/Ferea.cs new file mode 100644 index 0000000..9bba6fc --- /dev/null +++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/Ferea.cs @@ -0,0 +1,40 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps; + +using MUnique.OpenMU.DataModel.Configuration; + +/// +/// The initialization for the Ferea map (imported map). Seeded empty; monster/NPC +/// spawns and gates are added via the admin panel. +/// +internal class Ferea : BaseMapInitializer +{ + /// + /// The Number of the Map. + /// + internal const byte Number = 86; + + /// + /// The Name of the Map. + /// + internal const string Name = "Ferea"; + + /// + /// Initializes a new instance of the class. + /// + /// The context. + /// The game configuration. + public Ferea(IContext context, GameConfiguration gameConfiguration) + : base(context, gameConfiguration) + { + } + + /// + protected override byte MapNumber => Number; + + /// + protected override string MapName => Name; +} diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/Uruk.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/Uruk.cs new file mode 100644 index 0000000..05cd401 --- /dev/null +++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/Uruk.cs @@ -0,0 +1,40 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps; + +using MUnique.OpenMU.DataModel.Configuration; + +/// +/// The initialization for the Uruk map (imported map). Seeded empty; monster/NPC +/// spawns and gates are added via the admin panel. +/// +internal class Uruk : BaseMapInitializer +{ + /// + /// The Number of the Map. + /// + internal const byte Number = 85; + + /// + /// The Name of the Map. + /// + internal const string Name = "Uruk"; + + /// + /// Initializes a new instance of the class. + /// + /// The context. + /// The game configuration. + public Uruk(IContext context, GameConfiguration gameConfiguration) + : base(context, gameConfiguration) + { + } + + /// + protected override byte MapNumber => Number; + + /// + protected override string MapName => Name; +}