From 02a3d388d5826f4b794b39129550ca08a5173277 Mon Sep 17 00:00:00 2001 From: nolt Date: Sat, 27 Jun 2026 22:25:05 +0200 Subject: [PATCH 1/2] Fix Golden Invasion announcement to point at the dragon's real map The Golden Invasion start/end broadcast ("[{mapName}] Golden invasion!") named a map that was picked independently from where the Golden Dragon actually spawned, so the announcement matched the boss only ~1/3 of the time (and never when the dragon rolled Atlans/Tarkan). This is a regression from the invasion-spawn-table refactor (PR 757). Before it, the Golden Dragon was the "mob on the selected map" and spawned on state.MapId, so the announced map always contained it. The refactor turned the dragon into an ordinary RandomMap mob and made the display map a separate random draw over all spawned maps, which the minions (always on Lorencia/Noria/Devias) dominate. Fix: a plugin now designates a featured monster (AnnouncedMonsterId) and the broadcast names that monster's actual spawn map(s): - RandomMap -> the single map it rolled (recorded in SelectedMaps) - AllMaps -> every configured map (comma-separated list) The spawn strategy and the global broadcast are unchanged; only the map name(s) put into the message changed. When no featured monster is set, the previous EventDisplayMapIds behaviour is kept as a fallback. Golden Invasion announces the Golden Dragon; Red Dragon announces the Red Dragon (it had no bug since it has a single mob, wired for robustness). The per-map event marker (IMapEventStateUpdatePlugIn) is left as-is. --- .../InvasionEvents/BaseInvasionPlugIn.cs | 75 ++++++++++++++++--- .../InvasionEvents/GoldenInvasionPlugIn.cs | 2 +- .../InvasionEvents/InvasionGameServerState.cs | 21 ++++++ .../InvasionEvents/RedDragonInvasionPlugIn.cs | 2 +- .../InvasionEvents/SimpleInvasionPlugIn.cs | 10 ++- 5 files changed, 97 insertions(+), 13 deletions(-) diff --git a/src/GameLogic/PlugIns/InvasionEvents/BaseInvasionPlugIn.cs b/src/GameLogic/PlugIns/InvasionEvents/BaseInvasionPlugIn.cs index 741eecd68..b96d06263 100644 --- a/src/GameLogic/PlugIns/InvasionEvents/BaseInvasionPlugIn.cs +++ b/src/GameLogic/PlugIns/InvasionEvents/BaseInvasionPlugIn.cs @@ -41,6 +41,16 @@ protected BaseInvasionPlugIn(MapEventType? mapEventType = null) /// protected virtual IReadOnlyList? EventDisplayMapIds => null; + /// + /// Gets the monster ID of the featured monster whose actual spawn map(s) are named in the + /// start/end broadcast (e.g. the Golden Dragon for the Golden Invasion). When set, the + /// announcement always points at the map(s) where this monster really spawned this run - + /// the single chosen map for , or all its maps for + /// . When null, the display map falls back to + /// . + /// + protected virtual ushort? AnnouncedMonsterId => null; + /// public virtual async ValueTask ObjectAddedToMapAsync(GameMap map, ILocateable addedObject) { @@ -172,10 +182,7 @@ protected async Task TrySendStartMessageAsync(Player player, InvasionGameServerS return; } - var mapName = state.Context.Configuration.Maps - .FirstOrDefault(m => m.Number == state.MapId) - ?.Name.GetTranslation(player.Culture) - ?? string.Empty; + var mapName = BuildAnnouncedMapNames(state, player); var message = (configuration.StartMessage.GetTranslation(player.Culture) ?? PlugInResources.BaseInvasionPlugIn_DefaultStartMessage) @@ -204,10 +211,7 @@ protected async Task TrySendEndMessageAsync(Player player, InvasionGameServerSta return; } - var mapName = state.Context.Configuration.Maps - .FirstOrDefault(m => m.Number == state.MapId) - ?.Name.GetTranslation(player.Culture) - ?? string.Empty; + var mapName = BuildAnnouncedMapNames(state, player); var message = (configuration.EndMessage.GetTranslation(player.Culture) ?? string.Empty) .Replace("{mapName}", mapName, StringComparison.InvariantCulture); @@ -342,23 +346,74 @@ private void SelectDisplayMap(InvasionGameServerState state) return; } + var announcedMaps = this.GetAnnouncedMaps(state); + if (announcedMaps.Count > 0) + { + state.SetAnnouncedMaps(announcedMaps); + return; + } + if (this.EventDisplayMapIds is { Count: > 0 } displayMaps) { var eligible = state.MapIds .Where(displayMaps.Contains) .ToList(); - state.MapId = eligible.Count switch + var mapId = eligible.Count switch { 0 => state.MapIds.Min(), 1 => eligible[0], _ => eligible[Rand.NextInt(0, eligible.Count)], }; + state.SetAnnouncedMaps([mapId]); } else { - state.MapId = state.MapIds.Min(); + state.SetAnnouncedMaps([state.MapIds.Min()]); + } + } + + /// + /// Gets the maps where the featured actually spawns this run, + /// so the broadcast names a map that really contains it. Returns an empty list when no announced + /// monster is configured or it did not spawn, in which case the caller falls back to + /// . + /// + /// The current invasion state. + private IReadOnlyList GetAnnouncedMaps(InvasionGameServerState state) + { + if (this.AnnouncedMonsterId is not { } monsterId) + { + return []; } + + // RandomMap: the single chosen map was recorded in SelectedMaps during SelectSpawnMaps. + if (state.SelectedMaps.TryGetValue(monsterId, out var selectedMapId)) + { + return [selectedMapId]; + } + + // AllMaps: the monster spawns on every configured map, so name them all. + var mob = this.Configuration?.Mobs.FirstOrDefault(m => m.MonsterId == monsterId); + return mob is { IsSpawnOnAllMaps: true, MapIds.Count: > 0 } + ? mob.MapIds.ToArray() + : []; + } + + /// + /// Builds the comma-separated, localized list of map names announced in the broadcast. + /// + /// The current invasion state. + /// The player whose culture is used for translation. + private static string BuildAnnouncedMapNames(InvasionGameServerState state, Player player) + { + var names = state.AnnouncedMapIds + .Select(id => state.Context.Configuration.Maps + .FirstOrDefault(m => m.Number == id) + ?.Name.GetTranslation(player.Culture)) + .Where(name => !string.IsNullOrEmpty(name)); + + return string.Join(", ", names); } private bool IsPlayerOnRelevantMap(Player player, InvasionGameServerState state) diff --git a/src/GameLogic/PlugIns/InvasionEvents/GoldenInvasionPlugIn.cs b/src/GameLogic/PlugIns/InvasionEvents/GoldenInvasionPlugIn.cs index be99bb858..387a7ad6d 100644 --- a/src/GameLogic/PlugIns/InvasionEvents/GoldenInvasionPlugIn.cs +++ b/src/GameLogic/PlugIns/InvasionEvents/GoldenInvasionPlugIn.cs @@ -26,7 +26,7 @@ public sealed class GoldenInvasionPlugIn : SimpleInvasionPlugIn /// Initializes a new instance of the class. /// public GoldenInvasionPlugIn() - : base(MapEventType.GoldenDragonInvasion, DisplayMaps, () => InvasionConfigurationDefaults.Golden) + : base(MapEventType.GoldenDragonInvasion, DisplayMaps, () => InvasionConfigurationDefaults.Golden, InvasionMonsters.GoldenDragon) { } } \ No newline at end of file diff --git a/src/GameLogic/PlugIns/InvasionEvents/InvasionGameServerState.cs b/src/GameLogic/PlugIns/InvasionEvents/InvasionGameServerState.cs index e18800366..bbf8b8ea4 100644 --- a/src/GameLogic/PlugIns/InvasionEvents/InvasionGameServerState.cs +++ b/src/GameLogic/PlugIns/InvasionEvents/InvasionGameServerState.cs @@ -16,6 +16,7 @@ public class InvasionGameServerState : PeriodicTaskGameServerState { private readonly HashSet _mapIds = []; private readonly Dictionary _selectedMaps = []; + private readonly List _announcedMapIds = []; private readonly ConcurrentDictionary _monsters = new(); /// @@ -44,6 +45,25 @@ public InvasionGameServerState(IGameContext context) /// public IReadOnlyDictionary SelectedMaps => this._selectedMaps; + /// + /// Gets the list of map identifiers that are named in the invasion start/end broadcast. + /// These are the maps on which the announced (featured) monster actually spawns this run, + /// so the message always points players at a map that really contains it. + /// + public IReadOnlyList AnnouncedMapIds => this._announcedMapIds; + + /// + /// Sets the maps named in the broadcast message and the single used + /// for the map-event UI state. + /// + /// The maps on which the announced monster spawns this run. + internal void SetAnnouncedMaps(IReadOnlyCollection mapIds) + { + this._announcedMapIds.Clear(); + this._announcedMapIds.AddRange(mapIds); + this.MapId = this._announcedMapIds.Count > 0 ? this._announcedMapIds[0] : null; + } + /// /// Registers a map as active for this run and optionally records which map was /// randomly chosen for a particular monster type. @@ -70,6 +90,7 @@ internal void Reset() this.MapId = null; this._mapIds.Clear(); this._selectedMaps.Clear(); + this._announcedMapIds.Clear(); } /// diff --git a/src/GameLogic/PlugIns/InvasionEvents/RedDragonInvasionPlugIn.cs b/src/GameLogic/PlugIns/InvasionEvents/RedDragonInvasionPlugIn.cs index d703a5ec3..141d451a9 100644 --- a/src/GameLogic/PlugIns/InvasionEvents/RedDragonInvasionPlugIn.cs +++ b/src/GameLogic/PlugIns/InvasionEvents/RedDragonInvasionPlugIn.cs @@ -26,7 +26,7 @@ public sealed class RedDragonInvasionPlugIn : SimpleInvasionPlugIn /// Initializes a new instance of the class. /// public RedDragonInvasionPlugIn() - : base(MapEventType.RedDragonInvasion, DisplayMaps, () => InvasionConfigurationDefaults.RedDragon) + : base(MapEventType.RedDragonInvasion, DisplayMaps, () => InvasionConfigurationDefaults.RedDragon, InvasionMonsters.RedDragon) { } } \ No newline at end of file diff --git a/src/GameLogic/PlugIns/InvasionEvents/SimpleInvasionPlugIn.cs b/src/GameLogic/PlugIns/InvasionEvents/SimpleInvasionPlugIn.cs index c0306d1a2..04029a88c 100644 --- a/src/GameLogic/PlugIns/InvasionEvents/SimpleInvasionPlugIn.cs +++ b/src/GameLogic/PlugIns/InvasionEvents/SimpleInvasionPlugIn.cs @@ -15,6 +15,7 @@ public abstract class SimpleInvasionPlugIn { private readonly IReadOnlyList _displayMaps; private readonly Func _defaultConfigFactory; + private readonly ushort? _announcedMonsterId; /// /// Initializes a new instance of the class. @@ -22,16 +23,23 @@ public abstract class SimpleInvasionPlugIn /// The map-event type for UI broadcasting. /// Maps eligible to be shown in the event UI. /// Factory that returns the default configuration. - protected SimpleInvasionPlugIn(MapEventType mapEventType, IReadOnlyList displayMaps, Func defaultConfigFactory) + /// + /// The optional featured monster whose actual spawn map(s) are named in the broadcast. + /// + protected SimpleInvasionPlugIn(MapEventType mapEventType, IReadOnlyList displayMaps, Func defaultConfigFactory, ushort? announcedMonsterId = null) : base(mapEventType) { this._displayMaps = displayMaps; this._defaultConfigFactory = defaultConfigFactory; + this._announcedMonsterId = announcedMonsterId; } /// protected override IReadOnlyList EventDisplayMapIds => this._displayMaps; + /// + protected override ushort? AnnouncedMonsterId => this._announcedMonsterId; + /// public object CreateDefaultConfig() => this._defaultConfigFactory(); } \ No newline at end of file From 12f0c9dde6059d760c25149b158c74374959a8d0 Mon Sep 17 00:00:00 2001 From: nolt Date: Mon, 29 Jun 2026 12:58:51 +0200 Subject: [PATCH 2/2] Restrict Golden Dragon spawn maps to Lorencia, Noria, Devias The invasion-spawn-table refactor expanded the Golden Dragon's map pool from the original three (the implicit PossibleMaps used by the pre-refactor boss-on-event-map logic) to five, accidentally adding Atlans and Tarkan. Classic MU only spawns the Golden Dragon on Lorencia, Noria and Devias; the golden minions on Atlans/Tarkan are unaffected. Applies to freshly seeded configurations; existing databases keep their stored plugin configuration until reset in the admin panel. --- .../PlugIns/InvasionEvents/InvasionConfigurationDefaults.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GameLogic/PlugIns/InvasionEvents/InvasionConfigurationDefaults.cs b/src/GameLogic/PlugIns/InvasionEvents/InvasionConfigurationDefaults.cs index f071126df..97b7346b2 100644 --- a/src/GameLogic/PlugIns/InvasionEvents/InvasionConfigurationDefaults.cs +++ b/src/GameLogic/PlugIns/InvasionEvents/InvasionConfigurationDefaults.cs @@ -31,7 +31,7 @@ internal static class InvasionConfigurationDefaults new(InvasionMonsters.GoldenLizardKing, 10, [InvasionMaps.Atlans], SpawnMapStrategy.RandomMap), new(InvasionMonsters.GoldenWheel, 20, [InvasionMaps.Tarkan], SpawnMapStrategy.RandomMap), new(InvasionMonsters.GoldenTantallos, 10, [InvasionMaps.Tarkan], SpawnMapStrategy.RandomMap), - new(InvasionMonsters.GoldenDragon, 10, [InvasionMaps.Lorencia, InvasionMaps.Noria, InvasionMaps.Devias, InvasionMaps.Atlans, InvasionMaps.Tarkan], SpawnMapStrategy.RandomMap), + new(InvasionMonsters.GoldenDragon, 10, [InvasionMaps.Lorencia, InvasionMaps.Noria, InvasionMaps.Devias], SpawnMapStrategy.RandomMap), ], };