Add RepairImportedMapWarpsSeason6 update (warp indexes 83-87)

Databases whose imported season 6 maps (Arkania 82, Acheron 83, Debenter 84,
Uruk 85, Ferea 86) were created by a fresh initialization end up with the map
rows and exit gates but without the /move WarpInfo entries, so /move answers
"Unknown warp index" for those maps.

AddArkaniaMapUpdateSeason6 and AddImportedMapsUpdateSeason6 return early when
the map already exists, which also skips the spawn gate, the WarpInfo and the
game-server assignment that follow. Update 104 repairs each of those pieces
independently and is safe to run repeatedly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Acentech Dev
2026-07-31 15:02:56 +03:00
parent 30b0fd1bc9
commit 38fd435ff6
2 changed files with 182 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
// <copyright file="RepairImportedMapWarpsSeason6.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
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;
/// <summary>
/// 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.
///
/// <para>
/// Why this is needed: <see cref="AddArkaniaMapUpdateSeason6"/> and
/// <see cref="AddImportedMapsUpdateSeason6"/> guard on the map already existing and
/// <c>return</c> immediately when it does. That guard also skips the spawn gate, the
/// <see cref="WarpInfo"/> and the <see cref="GameServerConfiguration"/> assignment
/// that follow it. A database whose maps were created any other way therefore ends up
/// with maps but no warps, and <c>/move</c> answers "Unknown warp index".
/// </para>
///
/// <para>
/// 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.
/// </para>
/// </summary>
[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.";
/// <summary>
/// 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.
/// </summary>
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),
};
/// <inheritdoc />
public override UpdateVersion Version => UpdateVersion.RepairImportedMapWarpsSeason6;
/// <inheritdoc />
public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id;
/// <inheritdoc />
public override string Name => PlugInName;
/// <inheritdoc />
public override string Description => PlugInDescription;
/// <inheritdoc />
public override bool IsMandatory => true;
/// <inheritdoc />
public override DateTime CreatedAt => new(2026, 07, 31, 12, 0, 0, DateTimeKind.Utc);
/// <inheritdoc />
protected override async ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration)
{
var serverConfigs = (await context.GetAsync<GameServerConfiguration>().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();
}
/// <summary>
/// Returns the map's spawn gate, creating it at the given tile when absent.
/// A warp whose <see cref="WarpInfo.Gate"/> is null still fails at /move time,
/// so the gate has to exist before the warp is wired to it.
/// </summary>
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<ExitGate>();
map.ExitGates.Add(spawnGate);
spawnGate.Map = map;
spawnGate.X1 = x;
spawnGate.Y1 = y;
spawnGate.X2 = x;
spawnGate.Y2 = y;
spawnGate.IsSpawnGate = true;
return spawnGate;
}
/// <summary>
/// Ensures a <see cref="WarpInfo"/> 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.
/// </summary>
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<WarpInfo>();
gameConfiguration.WarpList.Add(warp);
warp.Costs = 5000;
warp.LevelRequirement = 10;
}
warp.Index = warpIndex;
warp.Name = mapName;
warp.Gate ??= spawnGate;
}
/// <summary>
/// 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.
/// </summary>
private static void EnsureHostedByServers(IList<GameServerConfiguration> serverConfigs, GameMapDefinition map, byte mapNumber)
{
foreach (var serverConfig in serverConfigs)
{
if (serverConfig.Maps.All(m => m.Number != mapNumber))
{
serverConfig.Maps.Add(map);
}
}
}
}

View File

@@ -524,4 +524,9 @@ public enum UpdateVersion
/// The version of the <see cref="FinishElfMasterTreePlugIn"/>.
/// </summary>
FinishElfMasterTree = 103,
/// <summary>
/// The version of the <see cref="RepairImportedMapWarpsSeason6"/>.
/// </summary>
RepairImportedMapWarpsSeason6 = 104,
}