diff --git a/src/Persistence/Initialization/Updates/RepairImportedMapWarpsSeason6.cs b/src/Persistence/Initialization/Updates/RepairImportedMapWarpsSeason6.cs new file mode 100644 index 0000000..aad25c1 --- /dev/null +++ b/src/Persistence/Initialization/Updates/RepairImportedMapWarpsSeason6.cs @@ -0,0 +1,177 @@ +// +// 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; + +/// +/// Repairs the five imported season 6 maps (Arkania 82, Acheron 83, Debenter 84, +/// Uruk 85, Ferea 86) on databases where the map rows exist but their spawn gate, +/// /move warp entry or game-server assignment do not. +/// +/// +/// Why this is needed: and +/// guard on the map already existing and +/// return immediately when it does. That guard also skips the spawn gate, the +/// and the assignment +/// that follow it. A database whose maps were created any other way therefore ends up +/// with maps but no warps, and /move answers "Unknown warp index". +/// +/// +/// +/// Every step below is guarded on its own, so this update repairs whatever is missing +/// and leaves whatever is already correct untouched. It is safe to run repeatedly. +/// +/// +[PlugIn] +[Display(Name = PlugInName, Description = PlugInDescription)] +[Guid("6E4A57C3-1D82-4B90-A5F7-3C08E1D6B742")] +public class RepairImportedMapWarpsSeason6 : UpdatePlugInBase +{ + internal const string PlugInName = "Repair imported map warps (Arkania, Acheron, Debenter, Uruk, Ferea)"; + internal const string PlugInDescription = "Adds the missing spawn gates, /move warp entries (indexes 83-87) and game-server assignments for maps 82-86 on databases where the maps exist but the warps do not."; + + /// + /// Spawn point per map, taken from a working database rather than from the + /// original add-updates: those hardcoded map centres, which were later moved to + /// walkable tiles. Order: map number, map name, warp index, spawn X, spawn Y. + /// + private static readonly (byte MapNumber, string MapName, short WarpIndex, byte X, byte Y)[] Repairs = + { + (Arkania.Number, Arkania.Name, 83, 213, 44), + (Acheron.Number, Acheron.Name, 84, 59, 202), + (Debenter.Number, Debenter.Name, 85, 18, 100), + (Uruk.Number, Uruk.Name, 86, 115, 103), + (Ferea.Number, Ferea.Name, 87, 237, 146), + }; + + /// + public override UpdateVersion Version => UpdateVersion.RepairImportedMapWarpsSeason6; + + /// + 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, 31, 12, 0, 0, DateTimeKind.Utc); + + /// + protected override async ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration) + { + var serverConfigs = (await context.GetAsync().ConfigureAwait(false)).ToList(); + + foreach (var (mapNumber, mapName, warpIndex, x, y) in Repairs) + { + var map = gameConfiguration.Maps.FirstOrDefault(m => m.Number == mapNumber); + if (map is null) + { + // The map itself is missing, so the original add-update never ran here. + // Create it (definition + terrain) before repairing the rest. + CreateMap(context, gameConfiguration, mapNumber); + map = gameConfiguration.Maps.FirstOrDefault(m => m.Number == mapNumber); + if (map is null) + { + continue; // unknown map number — nothing sensible to do + } + } + + var spawnGate = EnsureSpawnGate(context, map, x, y); + EnsureWarp(context, gameConfiguration, warpIndex, mapName, spawnGate); + EnsureHostedByServers(serverConfigs, map, mapNumber); + } + } + + private static void CreateMap(IContext context, GameConfiguration gameConfiguration, byte mapNumber) + { + IMapInitializer? initializer = mapNumber switch + { + Arkania.Number => new Arkania(context, gameConfiguration), + Acheron.Number => new Acheron(context, gameConfiguration), + Debenter.Number => new Debenter(context, gameConfiguration), + Uruk.Number => new Uruk(context, gameConfiguration), + Ferea.Number => new Ferea(context, gameConfiguration), + _ => null, + }; + + if (initializer is null) + { + return; + } + + initializer.Initialize(); + initializer.SetSafezoneMap(); + } + + /// + /// Returns the map's spawn gate, creating it at the given tile when absent. + /// A warp whose is null still fails at /move time, + /// so the gate has to exist before the warp is wired to it. + /// + private static ExitGate EnsureSpawnGate(IContext context, GameMapDefinition map, byte x, byte y) + { + var existing = map.ExitGates.FirstOrDefault(g => g.IsSpawnGate); + if (existing is not null) + { + return existing; + } + + 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; + return spawnGate; + } + + /// + /// Ensures a exists for the index and points at the gate. + /// Matches on index OR name so a half-created entry is repaired instead of + /// duplicated — WarpHandlerPlugIn looks the entry up by index alone. + /// + private static void EnsureWarp(IContext context, GameConfiguration gameConfiguration, short warpIndex, string mapName, ExitGate spawnGate) + { + var warp = gameConfiguration.WarpList.FirstOrDefault(w => w.Index == warpIndex || w.Name == mapName); + if (warp is null) + { + warp = context.CreateNew(); + gameConfiguration.WarpList.Add(warp); + warp.Costs = 5000; + warp.LevelRequirement = 10; + } + + warp.Index = warpIndex; + warp.Name = mapName; + warp.Gate ??= spawnGate; + } + + /// + /// Adds the map to every game server configuration that does not host it yet. + /// Without this the warp resolves but the target map is not running. + /// + private static void EnsureHostedByServers(IList serverConfigs, GameMapDefinition map, byte mapNumber) + { + foreach (var serverConfig in serverConfigs) + { + if (serverConfig.Maps.All(m => m.Number != mapNumber)) + { + serverConfig.Maps.Add(map); + } + } + } +} diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs index a580f42..09aea6f 100644 --- a/src/Persistence/Initialization/Updates/UpdateVersion.cs +++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs @@ -524,4 +524,9 @@ public enum UpdateVersion /// The version of the . /// FinishElfMasterTree = 103, + + /// + /// The version of the . + /// + RepairImportedMapWarpsSeason6 = 104, }