diff --git a/src/GameLogic/CastleSiege/CastleSiegeContext.cs b/src/GameLogic/CastleSiege/CastleSiegeContext.cs
index 4a5ef2e..1361fb9 100644
--- a/src/GameLogic/CastleSiege/CastleSiegeContext.cs
+++ b/src/GameLogic/CastleSiege/CastleSiegeContext.cs
@@ -4,9 +4,29 @@
namespace MUnique.OpenMU.GameLogic.CastleSiege;
+using MUnique.OpenMU.DataModel.Configuration;
+
///
-/// In-memory Castle Siege phase state machine and battle contention (P3).
+/// In-memory Castle Siege state machine and battle contention.
/// Time is injected via method parameters so it can be tested deterministically.
+///
+/// The cycle uses the original Season 6 values, which are exactly the values
+/// the game client expects (see CASTLESIEGE_STATE in the client's WSclient.h). AdaMu drives only
+/// a subset of them, because it registers guilds directly and has no Mark of Lord step:
+///
+///
+/// Idle1(0) -> RegisterGuild(1) -> Ready(6) -> Start(7) -> End(8) -> EndCycle(9) -> Idle1(0)
+///
+///
+/// The skipped states (, ,
+/// , ) keep their numbers so the
+/// client stays compatible; the server simply never enters them.
+///
+///
+/// Guilds are identified by their persistent , not by name. A guild rename (or a delete and
+/// re-create under the same name) therefore can no longer transfer castle ownership to the wrong guild. Names
+/// are carried alongside purely for display and for the packets that send a name to the client.
+///
/// Battle rule: attackers must destroy all castle defenses (gates + guardian statues) and then hold BOTH
/// Crown Switches at the same time — the switches are held by standing on them (evaluated per tick by the
/// plugin), and once both are held by one guild with the defenses down, that guild captures the throne.
@@ -17,96 +37,115 @@ public class CastleSiegeContext
/// The Crown Switch NPC numbers on Valley of Loren; both must be held to take the throne.
public static readonly short[] SwitchNumbers = { 217, 218 };
- private readonly List _registeredGuilds = new();
- private readonly Dictionary _switchHolders = new() { { 217, null }, { 218, null } };
- private DateTime _phaseStartedUtc;
- private string? _occupier;
+ private readonly Dictionary _registeredGuilds = new();
+ private readonly Dictionary _switchHolders = new() { { 217, null }, { 218, null } };
+ private DateTime _stateStartedUtc;
+ private Guid? _occupier;
+ private string? _occupierName;
private int _defensesRemaining;
private bool _dirty;
- private string? _crownHoldGuild;
+ private Guid? _crownHoldGuild;
private DateTime? _crownHoldStartUtc;
private bool _lastShieldDown;
/// Initializes a new instance of the class.
/// The cycle timing configuration.
- public CastleSiegeContext(CastleSiegeConfiguration configuration)
+ public CastleSiegeContext(CastleSiegeSettings configuration)
{
this.Configuration = configuration;
- this.Phase = CastleSiegePhase.Ownership;
+ this.State = CastleSiegeState.Idle1;
}
- /// Raised after the phase changes. Argument is the new phase.
- public event Action? PhaseChanged;
+ /// Raised after the state changes. Argument is the new state.
+ public event Action? StateChanged;
/// Gets the configuration (durations + schedule). Refreshed each tick from the live plugin config
/// so AdminPanel edits take effect without a restart.
- public CastleSiegeConfiguration Configuration { get; private set; }
+ public CastleSiegeSettings Configuration { get; private set; }
- /// Points the context at the current (possibly AdminPanel-edited) plugin configuration.
- /// The live configuration.
- public void UpdateConfiguration(CastleSiegeConfiguration configuration) => this.Configuration = configuration;
+ /// Gets the current state.
+ public CastleSiegeState State { get; private set; }
- /// Gets the current phase.
- public CastleSiegePhase Phase { get; private set; }
+ /// Gets the UTC time the current state started (used for persistence/restore).
+ public DateTime StateStartedUtc => this._stateStartedUtc;
- /// Gets the UTC time the current phase started (used for persistence/restore).
- public DateTime PhaseStartedUtc => this._phaseStartedUtc;
+ /// Gets the persistent identifier of the owner guild, or if unowned.
+ public Guid? OwnerGuildId { get; private set; }
- /// Gets the current owner guild name, or null if unowned.
+ /// Gets the owner guild's name for display and client packets, or null.
public string? OwnerGuildName { get; private set; }
- /// Gets the guild currently holding the throne during the siege (P3), or null.
- public string? OccupierGuildName => this._occupier;
+ /// Gets the guild currently holding the throne during the siege, or null.
+ public Guid? OccupierGuildId => this._occupier;
+
+ /// Gets the throne holder's name for display and client packets, or null.
+ public string? OccupierGuildName => this._occupierName;
/// Gets the number of castle defenses (gates + statues) still standing; the throne needs 0.
public int DefensesRemaining => this._defensesRemaining;
- /// Gets the guild names registered for the current cycle.
- public IReadOnlyList RegisteredGuilds => this._registeredGuilds;
+ /// Gets the persistent identifiers of the guilds registered for the current cycle.
+ public IReadOnlyCollection RegisteredGuildIds => this._registeredGuilds.Keys;
+
+ /// Gets the names of the guilds registered for the current cycle (display only).
+ public IReadOnlyCollection RegisteredGuildNames => this._registeredGuilds.Values;
+
+ /// Gets a value indicating whether the siege battle is currently running.
+ public bool IsSiegeRunning => this.State == CastleSiegeState.Start;
+
+ /// Points the context at the current (possibly AdminPanel-edited) plugin configuration.
+ /// The live configuration.
+ public void UpdateConfiguration(CastleSiegeSettings configuration) => this.Configuration = configuration;
+
+ /// Returns whether the given guild is registered for the current cycle.
+ /// The persistent guild identifier.
+ public bool IsRegistered(Guid guildId) => this._registeredGuilds.ContainsKey(guildId);
/// Advances the state machine based on the current time.
/// The current UTC time.
public ValueTask TickAsync(DateTime now)
{
- switch (this.Phase)
+ switch (this.State)
{
- case CastleSiegePhase.Ownership:
+ case CastleSiegeState.Idle1:
if (this.Configuration.IsRegistrationOpenTime(now))
{
return this.ForceStartRegistrationAsync(now);
}
break;
- case CastleSiegePhase.Registration:
- if (now >= this._phaseStartedUtc + this.Configuration.RegistrationDuration)
+ case CastleSiegeState.RegisterGuild:
+ if (now >= this._stateStartedUtc + this.Configuration.RegistrationDuration)
{
- return this.TransitionAsync(CastleSiegePhase.Preparation, now);
+ return this.TransitionAsync(CastleSiegeState.Ready, now);
}
break;
- case CastleSiegePhase.Preparation:
- if (now >= this._phaseStartedUtc + this.Configuration.PreparationDuration)
+ case CastleSiegeState.Ready:
+ if (now >= this._stateStartedUtc + this.Configuration.PreparationDuration)
{
- return this.TransitionAsync(CastleSiegePhase.Siege, now);
+ return this.TransitionAsync(CastleSiegeState.Start, now);
}
break;
- case CastleSiegePhase.Siege:
- if (now >= this._phaseStartedUtc + this.Configuration.SiegeDuration)
+ case CastleSiegeState.Start:
+ if (now >= this._stateStartedUtc + this.Configuration.SiegeDuration)
{
- return this.TransitionAsync(CastleSiegePhase.Settlement, now);
+ return this.TransitionAsync(CastleSiegeState.End, now);
}
break;
- case CastleSiegePhase.Settlement:
+ case CastleSiegeState.End:
// Winner = guild holding the throne at siege end. If none captured, owner unchanged.
if (this._occupier is { } occupier)
{
- this.SetOwner(occupier);
+ this.SetOwner(occupier, this._occupierName);
}
this.ClearBattleState();
- return this.TransitionAsync(CastleSiegePhase.Ownership, now);
+ return this.TransitionAsync(CastleSiegeState.EndCycle, now);
+ case CastleSiegeState.EndCycle:
+ return this.TransitionAsync(CastleSiegeState.Idle1, now);
default:
break;
}
@@ -114,62 +153,67 @@ public class CastleSiegeContext
return ValueTask.CompletedTask;
}
- /// Admin: forces the cycle into registration now (from any phase).
+ /// Admin: forces the cycle into guild registration now (from any state).
/// The current UTC time.
public ValueTask ForceStartRegistrationAsync(DateTime now)
{
this._registeredGuilds.Clear();
this.ClearBattleState();
- return this.TransitionAsync(CastleSiegePhase.Registration, now);
+ return this.TransitionAsync(CastleSiegeState.RegisterGuild, now);
}
- /// Admin: forces a specific phase now.
- /// The target phase.
+ /// Admin: forces a specific state now.
+ /// The target state.
/// The current UTC time.
- public ValueTask ForcePhaseAsync(CastleSiegePhase phase, DateTime now)
- => this.TransitionAsync(phase, now);
+ public ValueTask ForceStateAsync(CastleSiegeState state, DateTime now)
+ => this.TransitionAsync(state, now);
- /// Admin: resets to the ownership (resting) phase and clears registrations/battle state.
+ /// Admin: resets to the idle (resting) state and clears registrations/battle state.
/// The current UTC time.
public ValueTask ResetAsync(DateTime now)
{
this._registeredGuilds.Clear();
this.ClearBattleState();
- return this.TransitionAsync(CastleSiegePhase.Ownership, now);
+ return this.TransitionAsync(CastleSiegeState.Idle1, now);
}
- /// Registers a guild (by name) for the current cycle. No-op outside registration.
- /// The guild name.
- public void RegisterGuild(string guildName)
+ /// Registers a guild for the current cycle. No-op outside the registration state.
+ /// The persistent guild identifier.
+ /// The guild name, for display.
+ public void RegisterGuild(Guid guildId, string guildName)
{
- if (this.Phase == CastleSiegePhase.Registration
- && !this._registeredGuilds.Contains(guildName))
+ if (this.State == CastleSiegeState.RegisterGuild
+ && this._registeredGuilds.TryAdd(guildId, guildName))
{
- this._registeredGuilds.Add(guildName);
this._dirty = true;
}
}
- /// Admin: sets (or clears) the current owner guild name.
- /// The owner guild name, or null to clear.
- public void SetOwner(string? guildName)
+ /// Admin: sets (or clears) the current owner guild.
+ /// The owner guild identifier, or null to clear.
+ /// The owner guild name, or null.
+ public void SetOwner(Guid? guildId, string? guildName)
{
- this.OwnerGuildName = guildName;
+ this.OwnerGuildId = guildId;
+ this.OwnerGuildName = guildId is null ? null : guildName;
this._dirty = true;
}
///
- /// Mirrors the shared castle owner from the configuration. Used on game servers that do NOT host the
+ /// Mirrors the shared castle owner loaded from the database. Used on game servers that do NOT host the
/// siege, so their hunting-map gate and castle flag still reflect the current owner. Does not mark the
/// state dirty (these servers never persist).
///
- public void SyncOwnerFromConfig()
+ /// The owner guild identifier, or null.
+ /// The owner guild name, or null.
+ public void SyncOwner(Guid? guildId, string? guildName)
{
- this.OwnerGuildName = this.Configuration.PersistedOwnerGuildName;
+ this.OwnerGuildId = guildId;
+ this.OwnerGuildName = guildId is null ? null : guildName;
}
///
- /// Sets the weekly auto-schedule (days of week + UTC time) into the configuration and marks the state
+ /// Sets the auto-open schedule (days of week + UTC time) into the configuration and marks the state
/// dirty for persistence. Empty days disables auto-start (manual only). The configuration is the single
/// source of truth, so this is equivalent to editing the plugin config in the AdminPanel.
///
@@ -192,12 +236,36 @@ public class CastleSiegeContext
/// The current UTC time.
public TimeSpan GetRemainingSiegeTime(DateTime nowUtc)
{
- if (this.Phase != CastleSiegePhase.Siege)
+ if (!this.IsSiegeRunning)
{
return TimeSpan.Zero;
}
- var remaining = (this._phaseStartedUtc + this.Configuration.SiegeDuration) - nowUtc;
+ var remaining = (this._stateStartedUtc + this.Configuration.SiegeDuration) - nowUtc;
+ return remaining > TimeSpan.Zero ? remaining : TimeSpan.Zero;
+ }
+
+ ///
+ /// Returns how much time is left in the current state, or when the state has
+ /// no duration (idle states wait for an admin command or the auto-open time).
+ ///
+ /// The current UTC time.
+ public TimeSpan GetRemainingStateTime(DateTime nowUtc)
+ {
+ var duration = this.State switch
+ {
+ CastleSiegeState.RegisterGuild => this.Configuration.RegistrationDuration,
+ CastleSiegeState.Ready => this.Configuration.PreparationDuration,
+ CastleSiegeState.Start => this.Configuration.SiegeDuration,
+ _ => TimeSpan.Zero,
+ };
+
+ if (duration == TimeSpan.Zero)
+ {
+ return TimeSpan.Zero;
+ }
+
+ var remaining = (this._stateStartedUtc + duration) - nowUtc;
return remaining > TimeSpan.Zero ? remaining : TimeSpan.Zero;
}
@@ -205,9 +273,9 @@ public class CastleSiegeContext
/// Returns the guild that currently holds BOTH crown switches while all castle defenses are down (so the
/// crown's shield is dropped for them), or null. Only meaningful during the siege.
///
- public string? GetShieldEligibleGuild()
+ public Guid? GetShieldEligibleGuild()
{
- if (this.Phase != CastleSiegePhase.Siege || this._defensesRemaining > 0)
+ if (!this.IsSiegeRunning || this._defensesRemaining > 0)
{
return null;
}
@@ -223,20 +291,21 @@ public class CastleSiegeContext
/// Losing a switch or the master leaving the crown resets the hold (contestable until the siege ends).
///
/// The guild with both switches and no defenses, or null.
+ /// That guild's name, for display.
/// Whether that guild's master is on the crown.
/// The current UTC time.
/// How long the master must hold to capture.
- public CrownTickResult TickCrownHold(string? eligibleGuild, bool masterHolding, DateTime now, TimeSpan holdDuration)
+ public CrownTickResult TickCrownHold(Guid? eligibleGuild, string? eligibleGuildName, bool masterHolding, DateTime now, TimeSpan holdDuration)
{
- var shieldDown = this.Phase == CastleSiegePhase.Siege && eligibleGuild is not null;
+ var shieldDown = this.IsSiegeRunning && eligibleGuild is not null;
var shieldChanged = shieldDown != this._lastShieldDown;
this._lastShieldDown = shieldDown;
- if (this.Phase != CastleSiegePhase.Siege)
+ if (!this.IsSiegeRunning)
{
this._crownHoldGuild = null;
this._crownHoldStartUtc = null;
- return new CrownTickResult(false, shieldChanged, CrownEvent.None, null);
+ return new CrownTickResult(false, shieldChanged, CrownEvent.None, null, null);
}
var wasHolding = this._crownHoldGuild is not null;
@@ -249,26 +318,27 @@ public class CastleSiegeContext
{
this._crownHoldGuild = null;
this._crownHoldStartUtc = null;
- return new CrownTickResult(shieldDown, shieldChanged, wasHolding ? CrownEvent.HoldReset : CrownEvent.None, null);
+ return new CrownTickResult(shieldDown, shieldChanged, wasHolding ? CrownEvent.HoldReset : CrownEvent.None, null, null);
}
if (this._crownHoldGuild != eligibleGuild || this._crownHoldStartUtc is null)
{
this._crownHoldGuild = eligibleGuild;
this._crownHoldStartUtc = now;
- return new CrownTickResult(shieldDown, shieldChanged, CrownEvent.HoldStarted, eligibleGuild);
+ return new CrownTickResult(shieldDown, shieldChanged, CrownEvent.HoldStarted, eligibleGuild, eligibleGuildName);
}
if (now - this._crownHoldStartUtc.Value >= holdDuration)
{
this._occupier = eligibleGuild;
+ this._occupierName = eligibleGuildName;
this._crownHoldGuild = null;
this._crownHoldStartUtc = null;
this._dirty = true;
- return new CrownTickResult(shieldDown, shieldChanged, CrownEvent.Captured, eligibleGuild);
+ return new CrownTickResult(shieldDown, shieldChanged, CrownEvent.Captured, eligibleGuild, eligibleGuildName);
}
- return new CrownTickResult(shieldDown, shieldChanged, CrownEvent.None, eligibleGuild);
+ return new CrownTickResult(shieldDown, shieldChanged, CrownEvent.None, eligibleGuild, eligibleGuildName);
}
///
@@ -283,22 +353,27 @@ public class CastleSiegeContext
}
///
- /// Restores persisted state on startup (owner, phase, phase-start, registrations) directly, without
- /// firing or marking the state dirty. Battle state stays cleared.
+ /// Restores persisted state on startup (owner, state, state-start, registrations) directly, without
+ /// firing or marking the state dirty. Battle state stays cleared.
///
- /// The persisted owner guild name, or null.
- /// The persisted phase.
- /// When the persisted phase started (UTC), or null to keep the default.
- /// The persisted registered guild names, or null.
- public void RestoreState(string? owner, CastleSiegePhase phase, DateTime? phaseStartedUtc, IEnumerable? registeredGuilds)
+ /// The persisted owner guild identifier, or null.
+ /// The persisted owner guild name, or null.
+ /// The persisted state.
+ /// When the persisted state started (UTC), or null to keep the default.
+ /// The persisted registrations (id to name), or null.
+ public void RestoreState(Guid? ownerGuildId, string? ownerGuildName, CastleSiegeState state, DateTime? stateStartedUtc, IEnumerable>? registeredGuilds)
{
- this.OwnerGuildName = owner;
- this.Phase = phase;
- this._phaseStartedUtc = phaseStartedUtc ?? this._phaseStartedUtc;
+ this.OwnerGuildId = ownerGuildId;
+ this.OwnerGuildName = ownerGuildId is null ? null : ownerGuildName;
+ this.State = state;
+ this._stateStartedUtc = stateStartedUtc ?? this._stateStartedUtc;
this._registeredGuilds.Clear();
if (registeredGuilds is not null)
{
- this._registeredGuilds.AddRange(registeredGuilds);
+ foreach (var registration in registeredGuilds)
+ {
+ this._registeredGuilds[registration.Key] = registration.Value;
+ }
}
this._dirty = false;
@@ -322,12 +397,12 @@ public class CastleSiegeContext
/// based on player positions. Pass null when no registered member stands on it. No-op outside the siege.
///
/// The Crown Switch NPC number (217 or 218).
- /// The holding guild's name, or null.
- public void SetSwitchHolder(short switchNumber, string? guildName)
+ /// The holding guild's persistent identifier, or null.
+ public void SetSwitchHolder(short switchNumber, Guid? guildId)
{
- if (this.Phase == CastleSiegePhase.Siege && this._switchHolders.ContainsKey(switchNumber))
+ if (this.IsSiegeRunning && this._switchHolders.ContainsKey(switchNumber))
{
- this._switchHolders[switchNumber] = guildName;
+ this._switchHolders[switchNumber] = guildId;
}
}
@@ -336,20 +411,21 @@ public class CastleSiegeContext
/// Succeeds only during the siege when the throne is free, all castle defenses are destroyed, and the
/// guild is currently holding BOTH Crown Switches (a member standing on each).
///
- /// The capturing guild's name.
+ /// The capturing guild's persistent identifier.
+ /// The capturing guild's name, for display.
/// Whether it succeeded and a human-readable reason/result message.
- public (bool Success, string Reason) TryCaptureThrone(string guildName)
+ public (bool Success, string Reason) TryCaptureThrone(Guid guildId, string guildName)
{
- if (this.Phase != CastleSiegePhase.Siege)
+ if (!this.IsSiegeRunning)
{
return (false, "The siege is not running.");
}
- if (this._occupier is not null)
+ if (this._occupier is { } occupier)
{
- return (false, this._occupier == guildName
+ return (false, occupier == guildId
? "Your guild already holds the throne."
- : $"The throne is already held by '{this._occupier}'.");
+ : $"The throne is already held by '{this._occupierName ?? occupier.ToString()}'.");
}
if (this._defensesRemaining > 0)
@@ -357,21 +433,28 @@ public class CastleSiegeContext
return (false, $"Destroy the castle defenses first ({this._defensesRemaining} remaining).");
}
- if (this._switchHolders[217] != guildName || this._switchHolders[218] != guildName)
+ if (this._switchHolders[217] != guildId || this._switchHolders[218] != guildId)
{
return (false, "Your guild must be holding BOTH Crown Switches at once (stand a member on each).");
}
- this._occupier = guildName;
+ this._occupier = guildId;
+ this._occupierName = guildName;
+ this._dirty = true;
return (true, "throne captured");
}
/// Returns a human-readable status summary for admin display.
public string GetStatusText()
- => $"CS phase={this.Phase}, owner={this.OwnerGuildName ?? "(none)"}, "
- + $"registered={this._registeredGuilds.Count} [{string.Join(", ", this._registeredGuilds)}], "
- + $"defenses={this._defensesRemaining}, throne={this._occupier ?? "(none)"}, "
- + $"switch217={this._switchHolders[217] ?? "-"}, switch218={this._switchHolders[218] ?? "-"}";
+ => $"CS state={this.State}({(int)this.State}), owner={this.OwnerGuildName ?? "(none)"}, "
+ + $"registered={this._registeredGuilds.Count} [{string.Join(", ", this._registeredGuilds.Values)}], "
+ + $"defenses={this._defensesRemaining}, throne={this._occupierName ?? "(none)"}, "
+ + $"switch217={this.DescribeSwitch(217)}, switch218={this.DescribeSwitch(218)}";
+
+ private string DescribeSwitch(short switchNumber)
+ => this._switchHolders[switchNumber] is { } holder
+ ? (this._registeredGuilds.TryGetValue(holder, out var name) ? name : holder.ToString())
+ : "-";
private void ClearBattleState()
{
@@ -379,17 +462,18 @@ public class CastleSiegeContext
this._switchHolders[218] = null;
this._defensesRemaining = 0;
this._occupier = null;
+ this._occupierName = null;
this._crownHoldGuild = null;
this._crownHoldStartUtc = null;
this._lastShieldDown = false;
}
- private ValueTask TransitionAsync(CastleSiegePhase phase, DateTime now)
+ private ValueTask TransitionAsync(CastleSiegeState state, DateTime now)
{
- this.Phase = phase;
- this._phaseStartedUtc = now;
+ this.State = state;
+ this._stateStartedUtc = now;
this._dirty = true;
- this.PhaseChanged?.Invoke(phase);
+ this.StateChanged?.Invoke(state);
return ValueTask.CompletedTask;
}
}
@@ -414,5 +498,6 @@ public enum CrownEvent
/// Whether the crown shield is currently down (both switches held, defenses cleared).
/// Whether the shield state changed this tick (only then should the client be told).
/// The event that occurred this tick.
-/// The guild the event refers to, if any.
-public readonly record struct CrownTickResult(bool ShieldDown, bool ShieldChanged, CrownEvent Event, string? Guild);
+/// The guild the event refers to, if any.
+/// That guild's name, for display and client packets.
+public readonly record struct CrownTickResult(bool ShieldDown, bool ShieldChanged, CrownEvent Event, Guid? GuildId, string? GuildName);
diff --git a/src/GameLogic/CastleSiege/CastleSiegeGuardsmanTalkPlugIn.cs b/src/GameLogic/CastleSiege/CastleSiegeGuardsmanTalkPlugIn.cs
index a9e5a48..c212ac7 100644
--- a/src/GameLogic/CastleSiege/CastleSiegeGuardsmanTalkPlugIn.cs
+++ b/src/GameLogic/CastleSiege/CastleSiegeGuardsmanTalkPlugIn.cs
@@ -49,7 +49,7 @@ public class CastleSiegeGuardsmanTalkPlugIn : IPlayerTalkToNpcPlugIn
return;
}
- if (context.Phase != CastleSiegePhase.Registration)
+ if (context.State != CastleSiegeState.RegisterGuild)
{
await ShowAsync(player, "Castle Siege registration is not open right now.").ConfigureAwait(false);
return;
@@ -61,17 +61,15 @@ public class CastleSiegeGuardsmanTalkPlugIn : IPlayerTalkToNpcPlugIn
return;
}
- var guildName = guildStatus.GuildId.ToString();
- if (player.GameContext is IGameServerContext serverContext)
+ // Registrations are keyed on the guild's persistent id, so a later rename cannot detach them.
+ if (await CastleSiegeEventPlugIn.GetPersistentGuildAsync(player).ConfigureAwait(false) is not { } guild)
{
- var guild = await serverContext.GuildServer.GetGuildAsync(guildStatus.GuildId).ConfigureAwait(false);
- if (guild?.Name is { Length: > 0 } name)
- {
- guildName = name;
- }
+ await ShowAsync(player, "Your guild could not be resolved. Please try again in a moment.").ConfigureAwait(false);
+ return;
}
- if (context.RegisteredGuilds.Contains(guildName))
+ var guildName = guild.Name;
+ if (context.IsRegistered(guild.Id))
{
await ShowAsync(player, $"Your guild '{guildName}' is already registered for the Castle Siege.").ConfigureAwait(false);
return;
@@ -84,7 +82,7 @@ public class CastleSiegeGuardsmanTalkPlugIn : IPlayerTalkToNpcPlugIn
return;
}
- context.RegisterGuild(guildName);
+ context.RegisterGuild(guild.Id, guildName);
await ShowAsync(player, fee > 0
? $"Your guild '{guildName}' is registered for the Castle Siege. ({fee} zen paid)"
: $"Your guild '{guildName}' is registered for the Castle Siege.").ConfigureAwait(false);
diff --git a/src/GameLogic/CastleSiege/CastleSiegePhase.cs b/src/GameLogic/CastleSiege/CastleSiegePhase.cs
deleted file mode 100644
index 0317868..0000000
--- a/src/GameLogic/CastleSiege/CastleSiegePhase.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-//
-// Licensed under the MIT License. See LICENSE file in the project root for full license information.
-//
-
-namespace MUnique.OpenMU.GameLogic.CastleSiege;
-
-///
-/// The phases of a Castle Siege cycle.
-///
-public enum CastleSiegePhase
-{
- /// Resting phase: castle is (un)owned, waiting for the next registration window.
- Ownership,
-
- /// Guilds can register to attack.
- Registration,
-
- /// Registration closed; defenders prepare before the siege starts.
- Preparation,
-
- /// The siege battle is running.
- Siege,
-
- /// Siege ended; determining the new owner.
- Settlement,
-}
diff --git a/src/GameLogic/CastleSiege/CastleSiegeConfiguration.cs b/src/GameLogic/CastleSiege/CastleSiegeSettings.cs
similarity index 72%
rename from src/GameLogic/CastleSiege/CastleSiegeConfiguration.cs
rename to src/GameLogic/CastleSiege/CastleSiegeSettings.cs
index dfe54a0..a7bd6f4 100644
--- a/src/GameLogic/CastleSiege/CastleSiegeConfiguration.cs
+++ b/src/GameLogic/CastleSiege/CastleSiegeSettings.cs
@@ -1,4 +1,4 @@
-//
+//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
@@ -7,15 +7,31 @@ namespace MUnique.OpenMU.GameLogic.CastleSiege;
using System.ComponentModel;
using System.Linq;
using System.Text.Json.Serialization;
+using MUnique.OpenMU.DataModel.Configuration;
///
-/// Configuration for the Castle Siege cycle. Rides on the plugin custom-configuration system (no dedicated
-/// database table). A cycle runs: Ownership -> Registration -> Preparation -> Siege(war) -> Settlement, and
-/// auto-starts when the current day/time matches + .
-/// The AdminPanel-friendly properties (OpenDays checkboxes, minute/second durations) are proxies over the
-/// runtime fields, which are hidden from the editor to keep the form clean.
+/// AdaMu operational settings for the Castle Siege cycle. Rides on the plugin custom-configuration system, so
+/// it is editable in the AdminPanel and needs no dedicated database table.
+///
+/// This is deliberately separate from , which is
+/// the upstream, database-backed configuration holding the NPC/zone/upgrade definitions and the crown hold
+/// time. Keeping AdaMu's operational knobs out of that entity means upstream schema changes apply cleanly and
+/// no hand-editing of the generated persistence code is needed.
+///
+///
+/// What lives where:
+///
+/// - Castle owner and guild registrations: database (CastleSiegeData, CastleSiegeGuildRegistration).
+/// - NPC/zone/upgrade definitions and crown hold time: database (GameConfiguration.CastleSiegeConfiguration).
+/// - Cycle durations, registration fee, designated server and the current state: here.
+///
+///
+/// A cycle runs Idle1 -> RegisterGuild -> Ready -> Start -> End -> EndCycle -> Idle1, and auto-starts when the
+/// current day/time matches + .
+/// The AdminPanel-friendly properties (OpenDays checkboxes, minute durations) are proxies over the runtime
+/// fields, which are hidden from the editor to keep the form clean.
///
-public class CastleSiegeConfiguration
+public class CastleSiegeSettings
{
///
/// Gets or sets the days of week on which registration auto-opens (UTC). None = every day (still needs a
@@ -42,7 +58,7 @@ public class CastleSiegeConfiguration
///
/// Gets or sets the times of day (UTC) at which a new cycle opens registration.
- /// Empty = no auto-start (admins start cycles manually via the chat command).
+ /// Empty = no auto-start (admins start cycles manually via the chat command or the AdminPanel).
///
public IList RegistrationOpenTimes { get; set; } = new List();
@@ -70,17 +86,6 @@ public class CastleSiegeConfiguration
set => this.SiegeDuration = TimeSpan.FromMinutes(Math.Max(1, value));
}
- ///
- /// Gets or sets how long the guild master must hold the Crown to capture the throne, in seconds.
- /// The client shows a 60-second countdown, so 60 matches the on-screen timer.
- ///
- [JsonIgnore]
- public int CrownHoldSeconds
- {
- get => (int)this.CrownHoldDuration.TotalSeconds;
- set => this.CrownHoldDuration = TimeSpan.FromSeconds(Math.Max(1, value));
- }
-
///
/// Gets or sets the registration fee (in zen) a guild master must pay to register the guild
/// for the siege. 0 disables the fee.
@@ -112,29 +117,17 @@ public class CastleSiegeConfiguration
[Browsable(false)]
public TimeSpan SiegeDuration { get; set; } = TimeSpan.FromMinutes(10);
- /// Gets or sets how long the guild master must hold the Crown to capture the throne.
- [Browsable(false)]
- public TimeSpan CrownHoldDuration { get; set; } = TimeSpan.FromSeconds(60);
+ // --- Persisted cycle bookkeeping (hidden from the AdminPanel) ---
+ // Only the CURRENT state and when it started ride on the plugin's custom-configuration JSON. The castle
+ // owner and the guild registrations live in real database tables, so they are not duplicated here.
- // --- Persisted runtime state (hidden from the AdminPanel) ---
- // These ride on the plugin's custom-configuration JSON (stored in PostgreSQL), so the castle owner and the
- // current cycle survive server restarts. Written by CastleSiegeEventPlugIn; restored on startup.
-
- /// Gets or sets the persisted castle owner guild name (null = unowned).
+ /// Gets or sets the persisted current state, so the cycle resumes after a restart.
[Browsable(false)]
- public string? PersistedOwnerGuildName { get; set; }
+ public CastleSiegeState PersistedState { get; set; } = CastleSiegeState.Idle1;
- /// Gets or sets the persisted current phase, so the cycle resumes after a restart.
+ /// Gets or sets when the persisted state started (UTC), or null if never persisted.
[Browsable(false)]
- public CastleSiegePhase PersistedPhase { get; set; } = CastleSiegePhase.Ownership;
-
- /// Gets or sets when the persisted phase started (UTC), or null if never persisted.
- [Browsable(false)]
- public DateTime? PersistedPhaseStartedUtc { get; set; }
-
- /// Gets or sets the persisted registered guild names for the current cycle.
- [Browsable(false)]
- public IList PersistedRegisteredGuilds { get; set; } = new List();
+ public DateTime? PersistedStateStartedUtc { get; set; }
///
/// Returns true if (UTC) matches a scheduled registration-open day and falls
diff --git a/src/GameLogic/CastleSiege/CastleSiegeThroneCaptureTalkPlugIn.cs b/src/GameLogic/CastleSiege/CastleSiegeThroneCaptureTalkPlugIn.cs
index 891af70..b6fca0b 100644
--- a/src/GameLogic/CastleSiege/CastleSiegeThroneCaptureTalkPlugIn.cs
+++ b/src/GameLogic/CastleSiege/CastleSiegeThroneCaptureTalkPlugIn.cs
@@ -43,35 +43,25 @@ public class CastleSiegeThroneCaptureTalkPlugIn : IPlayerTalkToNpcPlugIn
return;
}
- if (player.GuildStatus is not { } guildStatus)
+ if (await CastleSiegeEventPlugIn.GetPersistentGuildAsync(player).ConfigureAwait(false) is not { } guild)
{
await ShowAsync(player, "Only members of a registered guild can take the throne.").ConfigureAwait(false);
return;
}
- var guildName = guildStatus.GuildId.ToString();
- if (player.GameContext is IGameServerContext serverContext)
- {
- var guild = await serverContext.GuildServer.GetGuildAsync(guildStatus.GuildId).ConfigureAwait(false);
- if (guild?.Name is { Length: > 0 } name)
- {
- guildName = name;
- }
- }
-
- if (!context.RegisteredGuilds.Contains(guildName))
+ if (!context.IsRegistered(guild.Id))
{
await ShowAsync(player, "Your guild is not registered for this Castle Siege.").ConfigureAwait(false);
return;
}
// The throne is taken by holding the Crown, not by talking here — give guidance based on the state.
- await ShowAsync(player, DescribeThroneStep(context, guildName)).ConfigureAwait(false);
+ await ShowAsync(player, DescribeThroneStep(context, guild.Id)).ConfigureAwait(false);
}
- private static string DescribeThroneStep(CastleSiegeContext context, string guildName)
+ private static string DescribeThroneStep(CastleSiegeContext context, Guid guildId)
{
- if (context.Phase != CastleSiegePhase.Siege)
+ if (!context.IsSiegeRunning)
{
return "The siege is not running yet.";
}
@@ -87,12 +77,12 @@ public class CastleSiegeThroneCaptureTalkPlugIn : IPlayerTalkToNpcPlugIn
return "All gates are down! Hold BOTH Crown Switches with your guild — the Crown's shield will drop.";
}
- if (eligible == guildName)
+ if (eligible == guildId)
{
return "Your guild holds both switches and the shield is down — send your GUILD MASTER to hold the Crown to take the throne!";
}
- return $"Guild '{eligible}' is holding both switches. Take a switch back to raise their shield.";
+ return "Another guild is holding both switches. Take a switch back to raise their shield.";
}
private static ValueTask ShowAsync(Player player, string text)
diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs
index 64a7da6..5b9928b 100644
--- a/src/GameLogic/Player.cs
+++ b/src/GameLogic/Player.cs
@@ -717,7 +717,7 @@ public class Player : AsyncDisposable, IBucketMapObserver, IAttackable, IAttacke
// ADAMU-CUSTOM: Castle Siege PvP gate — true while the siege phase runs on Valley of Loren (map 30).
private bool IsCastleSiegeBattleActive()
=> this.CurrentMap?.Definition.Number == 30
- && PlugIns.PeriodicTasks.CastleSiegeEventPlugIn.TryGetContext(this.GameContext)?.Phase == CastleSiege.CastleSiegePhase.Siege;
+ && PlugIns.PeriodicTasks.CastleSiegeEventPlugIn.TryGetContext(this.GameContext)?.IsSiegeRunning == true;
///
public async ValueTask AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0, bool? isFinalStreakHit = null)
diff --git a/src/GameLogic/PlugIns/ChatCommands/CastleSiegePhaseChatCommandPlugIn.cs b/src/GameLogic/PlugIns/ChatCommands/CastleSiegePhaseChatCommandPlugIn.cs
index 3ea4095..588fff7 100644
--- a/src/GameLogic/PlugIns/ChatCommands/CastleSiegePhaseChatCommandPlugIn.cs
+++ b/src/GameLogic/PlugIns/ChatCommands/CastleSiegePhaseChatCommandPlugIn.cs
@@ -30,9 +30,11 @@ public class CastleSiegePhaseChatCommandPlugIn : IChatCommandPlugIn
public async ValueTask HandleCommandAsync(Player player, string command)
{
var parts = command.Split(' ', StringSplitOptions.RemoveEmptyEntries);
- if (parts.Length < 2 || !Enum.TryParse(parts[1], true, out var phase))
+ // The cycle uses the original Season 6 state names the client knows. AdaMu drives only this subset;
+ // Idle2, RegisterMark, Idle3 and Notify exist for client compatibility but are never entered.
+ if (parts.Length < 2 || !Enum.TryParse(parts[1], true, out var phase))
{
- await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync("Usage: /csphase ", MessageType.BlueNormal)).ConfigureAwait(false);
+ await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync("Usage: /csphase ", MessageType.BlueNormal)).ConfigureAwait(false);
return;
}
@@ -43,7 +45,7 @@ public class CastleSiegePhaseChatCommandPlugIn : IChatCommandPlugIn
return;
}
- await context.ForcePhaseAsync(phase, DateTime.UtcNow).ConfigureAwait(false);
+ await context.ForceStateAsync(phase, DateTime.UtcNow).ConfigureAwait(false);
await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync($"Castle Siege: phase set to {phase}.", MessageType.BlueNormal)).ConfigureAwait(false);
}
}
diff --git a/src/GameLogic/PlugIns/ChatCommands/CastleSiegeSetOwnerChatCommandPlugIn.cs b/src/GameLogic/PlugIns/ChatCommands/CastleSiegeSetOwnerChatCommandPlugIn.cs
index 5f1e4e6..e389b93 100644
--- a/src/GameLogic/PlugIns/ChatCommands/CastleSiegeSetOwnerChatCommandPlugIn.cs
+++ b/src/GameLogic/PlugIns/ChatCommands/CastleSiegeSetOwnerChatCommandPlugIn.cs
@@ -38,7 +38,22 @@ public class CastleSiegeSetOwnerChatCommandPlugIn : IChatCommandPlugIn
return;
}
- context.SetOwner(string.IsNullOrWhiteSpace(owner) ? null : owner);
- await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync($"Castle Siege owner set to {owner ?? "(none)"}.", MessageType.BlueNormal)).ConfigureAwait(false);
+ if (string.IsNullOrWhiteSpace(owner))
+ {
+ context.SetOwner(null, null);
+ await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync("Castle Siege owner cleared.", MessageType.BlueNormal)).ConfigureAwait(false);
+ return;
+ }
+
+ // Ownership is stored by the guild's persistent id, so the name given here is resolved once.
+ var guildId = await CastleSiegeEventPlugIn.ResolveGuildIdByNameAsync(player.GameContext, owner).ConfigureAwait(false);
+ if (guildId is not { } id)
+ {
+ await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync($"No guild named '{owner}' was found.", MessageType.BlueNormal)).ConfigureAwait(false);
+ return;
+ }
+
+ context.SetOwner(id, owner);
+ await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync($"Castle Siege owner set to {owner}.", MessageType.BlueNormal)).ConfigureAwait(false);
}
}
diff --git a/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs b/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs
index 93c65d3..486c222 100644
--- a/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs
+++ b/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs
@@ -7,6 +7,7 @@ namespace MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
using System.Collections.Concurrent;
using System.Linq;
using System.Runtime.InteropServices;
+using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic.CastleSiege;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.Views;
@@ -15,65 +16,50 @@ using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.PlugIns;
+using CastleSiegeDefinition = MUnique.OpenMU.DataModel.Configuration.CastleSiegeConfiguration;
///
-/// Drives the Castle Siege phase state machine: ticks it every second and carries its configuration.
-/// State is per- and kept in memory (P1: no persistence).
-/// When the siege phase starts (P3), warps registered guild members to the Valley of Loren battle map.
+/// Drives the Castle Siege state machine: ticks it every second and carries its operational settings.
+///
+/// The cycle uses the original Season 6 state numbers the client expects. The castle owner and the guild
+/// registrations are persisted in real database tables ( and
+/// ) and keyed by the guild's persistent , so a
+/// guild rename can no longer move castle ownership to the wrong guild. Only the current state and when it
+/// started ride on the plugin's custom-configuration JSON.
+///
+///
+/// Castle NPCs (gates, statues, catapults, the crown and its switches) are read from
+/// , which the CastleSiegeInitializer seeds, instead
+/// of being hard-coded here.
+///
+/// When the siege starts, registered guild members are warped to the Valley of Loren battle map.
///
[PlugIn]
-[Display(Name = nameof(CastleSiegeEventPlugIn), Description = "Castle Siege event (phase state machine, scheduling, siege warp).")]
+[Display(Name = nameof(CastleSiegeEventPlugIn), Description = "Castle Siege event (state machine, scheduling, siege warp).")]
[Guid("6E2C8B41-9A4D-4C2E-9E7B-1F2A3B4C5D60")]
-public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustomConfiguration, ISupportDefaultCustomConfiguration
+public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustomConfiguration, ISupportDefaultCustomConfiguration
{
private const ushort ValleyOfLorenMapNumber = 30;
- private const short CastleGateNumber = 277; // client MONSTER_CASTLE_GATE1 -> renders a real gate + blocks terrain until broken
- private const short GateTemplateNumber = 131; // BloodCastle "Castle Gate" destructible (has HP) — HP template for 277
+ private const short GateTemplateNumber = 131; // BloodCastle "Castle Gate" destructible (has HP) — HP template for the CS gates.
+ private const short CrownNumber = 216;
+ private const short CatapultAttackNumber = 221; // client MONSTER_SLINGSHOT_ATTACK
+ private const short CatapultDefenseNumber = 222; // client MONSTER_SLINGSHOT_DEFENSE
private const int SwitchHoldRange = 3;
-
- // Castle defenses spawned at siege start — the 6 REAL castle gates (client g_byGateLocation).
- // The client renders them closed and blocks the terrain until each is broken. All must fall before the throne.
- private static readonly (short Number, byte X, byte Y)[] DefenseSpawns =
- {
- (CastleGateNumber, 67, 114),
- (CastleGateNumber, 93, 114),
- (CastleGateNumber, 119, 114),
- (CastleGateNumber, 81, 161),
- (CastleGateNumber, 107, 161),
- (CastleGateNumber, 93, 204),
- };
-
- // Siege weapons spawned at siege start purely for war atmosphere. The client renders monster 221/222 as
- // catapults (attacker/defender). They are NOT counted as defenses (destroying them doesn't open the throne).
- private const short CatapultAttackNumber = 221; // client MONSTER_SLINGSHOT_ATTACK
- private const short CatapultDefenseNumber = 222; // client MONSTER_SLINGSHOT_DEFENSE
- private static readonly (short Number, byte X, byte Y)[] CatapultSpawns =
- {
- (CatapultDefenseNumber, 80, 140),
- (CatapultDefenseNumber, 110, 140),
- (CatapultDefenseNumber, 93, 178),
- (CatapultAttackNumber, 74, 100),
- (CatapultAttackNumber, 112, 100),
- };
-
- // Crown Switch positions on Valley of Loren (from the map init) — held by standing on them: (number, x, y).
- private static readonly (short SwitchNumber, byte X, byte Y)[] SwitchPositions =
- {
- (217, 167, 194),
- (218, 184, 195),
- };
-
- // The Crown (NPC 216) position on Valley of Loren — the guild master holds it here to capture the throne.
- private static readonly Point CrownPosition = new(176, 212);
private const int CrownHoldRange = 4;
private static readonly ConcurrentDictionary Contexts = new();
- private string? _cachedFlagOwner;
+ ///
+ /// Maps the in-memory guild id (assigned by the guild server, not stable across restarts) to the guild's
+ /// persistent identifier. Populated lazily; a miss costs one database lookup per guild per process.
+ ///
+ private static readonly ConcurrentDictionary PersistentGuildIds = new();
+
+ private Guid? _cachedFlagOwner;
private byte[]? _cachedFlagLogo;
///
- public CastleSiegeConfiguration? Configuration { get; set; }
+ public CastleSiegeSettings? Configuration { get; set; }
///
/// Gets the Castle Siege context for a game context, if the periodic tick has initialized it.
@@ -99,41 +85,80 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
return serverContext.Id == context.Configuration.CastleSiegeServerId;
}
+ ///
+ /// Resolves the persistent identifier of the player's guild, or when the player is
+ /// not in a guild or the guild cannot be resolved.
+ ///
+ ///
+ /// deliberately carries no id: the guild server assigns short ids in memory
+ /// only. The persistent is therefore resolved through the guild name and cached, which
+ /// avoids adding a method to that upstream would keep changing.
+ ///
+ /// The player.
+ public static async ValueTask<(Guid Id, string Name)?> GetPersistentGuildAsync(Player player)
+ {
+ if (player.GuildStatus is not { } guildStatus
+ || player.GameContext is not IGameServerContext serverContext)
+ {
+ return null;
+ }
+
+ var guild = await serverContext.GuildServer.GetGuildAsync(guildStatus.GuildId).ConfigureAwait(false);
+ if (guild?.Name is not { Length: > 0 } name)
+ {
+ return null;
+ }
+
+ if (PersistentGuildIds.TryGetValue(guildStatus.GuildId, out var cached))
+ {
+ return (cached, name);
+ }
+
+ var resolved = await ResolveGuildIdByNameAsync(player.GameContext, name).ConfigureAwait(false);
+ if (resolved is not { } id)
+ {
+ return null;
+ }
+
+ PersistentGuildIds[guildStatus.GuildId] = id;
+ return (id, name);
+ }
+
///
- public object CreateDefaultConfig() => new CastleSiegeConfiguration();
+ public object CreateDefaultConfig() => new CastleSiegeSettings();
///
public async ValueTask ExecuteTaskAsync(GameContext gameContext)
{
var context = Contexts.GetOrAdd(gameContext, gc =>
{
- var config = this.Configuration ?? new CastleSiegeConfiguration();
- var created = new CastleSiegeContext(config);
+ var settings = this.Configuration ?? new CastleSiegeSettings();
+ var created = new CastleSiegeContext(settings);
- // Restore state persisted before the last restart (owner/phase/registrations) BEFORE subscribing,
- // so restoring doesn't announce phases or re-spawn defenses.
- created.RestoreState(config.PersistedOwnerGuildName, config.PersistedPhase, config.PersistedPhaseStartedUtc, config.PersistedRegisteredGuilds);
+ // Restore the cycle bookkeeping BEFORE subscribing, so restoring doesn't announce states or
+ // re-spawn defenses. The owner and registrations are loaded from the database right after.
+ created.RestoreState(null, null, settings.PersistedState, settings.PersistedStateStartedUtc, null);
+ _ = LoadPersistedStateAsync(gc, created);
- // Announce phase changes to the whole server and, when the siege begins, warp registered members.
- created.PhaseChanged += phase => _ = OnPhaseChangedAsync(gc, created, phase);
+ created.StateChanged += state => _ = OnStateChangedAsync(gc, created, state);
return created;
});
- // Point the context at the current (possibly AdminPanel-edited) config so schedule/durations are live.
- if (this.Configuration is { } liveConfig)
+ // Point the context at the current (possibly AdminPanel-edited) settings so schedule/durations are live.
+ if (this.Configuration is { } liveSettings)
{
- context.UpdateConfiguration(liveConfig);
+ context.UpdateConfiguration(liveSettings);
}
// With multiple game servers each has its own map instances, so the siege must run on ONE designated
// server (CastleSiegeServerId). Other servers skip the siege entirely — they only mirror the shared
- // castle owner from the config so the hunting-map gate + castle flag rewards still work everywhere.
+ // castle owner from the database so the hunting-map gate + castle flag rewards still work everywhere.
if (!IsCastleSiegeServer(gameContext))
{
- context.SyncOwnerFromConfig();
if (DateTime.UtcNow.Second % 15 == 0)
{
+ await LoadPersistedStateAsync(gameContext, context).ConfigureAwait(false);
await this.BroadcastCastleFlagAsync(gameContext, context).ConfigureAwait(false);
}
@@ -143,18 +168,18 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
await context.TickAsync(DateTime.UtcNow).ConfigureAwait(false);
// During the siege, evaluate the Crown Switches (held by standing on them) and the throne capture every tick.
- if (context.Phase == CastleSiegePhase.Siege)
+ if (context.IsSiegeRunning)
{
await ProcessSiegeTickAsync(gameContext, context).ConfigureAwait(false);
}
- // Persist owner/phase/registrations to the database whenever they changed, so they survive a restart.
+ // Persist owner/state/registrations whenever they changed, so they survive a restart.
if (context.ConsumeDirty())
{
await this.PersistStateAsync(gameContext, context).ConfigureAwait(false);
}
- // Keep the owner guild's logo painted on the castle flags for anyone on the castle map (any phase).
+ // Keep the owner guild's logo painted on the castle flags for anyone on the castle map (any state).
if (DateTime.UtcNow.Second % 15 == 0)
{
await this.BroadcastCastleFlagAsync(gameContext, context).ConfigureAwait(false);
@@ -171,25 +196,95 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
}
}
- private static async Task OnPhaseChangedAsync(IGameContext gameContext, CastleSiegeContext context, CastleSiegePhase phase)
+ /// Gets the seeded Castle Siege definition, or when it was not initialized.
+ /// The game context.
+ private static CastleSiegeDefinition? GetDefinition(IGameContext gameContext)
+ => gameContext.Configuration.CastleSiegeConfiguration;
+
+ /// Resolves a guild's persistent identifier from its name, or null when there is no such guild.
+ /// The game context.
+ /// The guild name.
+ internal static async ValueTask ResolveGuildIdByNameAsync(IGameContext gameContext, string guildName)
{
try
{
- switch (phase)
+ using var context = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(DataModel.Entities.Guild), false, gameContext.Configuration);
+ var guilds = await context.GetAsync().ConfigureAwait(false);
+ return guilds.FirstOrDefault(guild => guild.Name == guildName)?.Id;
+ }
+ catch (Exception ex)
+ {
+ gameContext.LoggerFactory.CreateLogger()
+ .LogError(ex, "Castle Siege: could not resolve the persistent id of guild '{guildName}'.", guildName);
+ return null;
+ }
+ }
+
+ private static async ValueTask ResolveGuildNameByIdAsync(IGameContext gameContext, Guid guildId)
+ {
+ try
+ {
+ using var context = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(DataModel.Entities.Guild), false, gameContext.Configuration);
+ var guild = await context.GetByIdAsync(guildId).ConfigureAwait(false);
+ return guild?.Name;
+ }
+ catch (Exception ex)
+ {
+ gameContext.LoggerFactory.CreateLogger()
+ .LogError(ex, "Castle Siege: could not resolve the name of guild {guildId}.", guildId);
+ return null;
+ }
+ }
+
+ /// Loads the persisted castle owner and guild registrations from the database into the context.
+ private static async ValueTask LoadPersistedStateAsync(IGameContext gameContext, CastleSiegeContext context)
+ {
+ try
+ {
+ using var dataContext = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CastleSiegeData), false, gameContext.Configuration);
+ var data = (await dataContext.GetAsync().ConfigureAwait(false)).FirstOrDefault();
+
+ Guid? ownerId = data?.IsOccupied == true ? data.OwnerGuildId : null;
+ var ownerName = ownerId is { } id ? await ResolveGuildNameByIdAsync(gameContext, id).ConfigureAwait(false) : null;
+
+ using var registrationContext = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CastleSiegeGuildRegistration), false, gameContext.Configuration);
+ var registrations = await registrationContext.GetAsync().ConfigureAwait(false);
+
+ var restored = new List>();
+ foreach (var registration in registrations)
{
- case CastleSiegePhase.Registration:
+ var name = await ResolveGuildNameByIdAsync(gameContext, registration.GuildId).ConfigureAwait(false);
+ restored.Add(new KeyValuePair(registration.GuildId, name ?? registration.GuildId.ToString()));
+ }
+
+ context.RestoreState(ownerId, ownerName, context.State, context.StateStartedUtc, restored);
+ }
+ catch (Exception ex)
+ {
+ gameContext.LoggerFactory.CreateLogger()
+ .LogError(ex, "Castle Siege: error while loading the persisted state.");
+ }
+ }
+
+ private static async Task OnStateChangedAsync(IGameContext gameContext, CastleSiegeContext context, CastleSiegeState state)
+ {
+ try
+ {
+ switch (state)
+ {
+ case CastleSiegeState.RegisterGuild:
await AnnounceAsync(gameContext, "Castle Siege registration is now open! Guild masters, register at the Guardsman in the Valley of Loren.").ConfigureAwait(false);
break;
- case CastleSiegePhase.Siege:
+ case CastleSiegeState.Start:
await AnnounceAsync(gameContext, "The Castle Siege has begun! Break the castle gates and guardian statues, then hold BOTH Crown Switches to take the throne!").ConfigureAwait(false);
await SpawnCastleDefensesAsync(gameContext, context).ConfigureAwait(false);
await WarpRegisteredMembersToSiegeAsync(gameContext, context).ConfigureAwait(false);
break;
- case CastleSiegePhase.Settlement:
+ case CastleSiegeState.End:
// Stop the on-map countdown for everyone still on the battle map.
await BroadcastSiegeStateAsync(gameContext, false, 0, 0).ConfigureAwait(false);
break;
- case CastleSiegePhase.Ownership when context.OwnerGuildName is { } owner:
+ case CastleSiegeState.Idle1 when context.OwnerGuildName is { } owner:
await AnnounceAsync(gameContext, $"The Castle Siege has ended. The castle now belongs to the guild '{owner}'!").ConfigureAwait(false);
break;
default:
@@ -199,52 +294,7 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
catch (Exception ex)
{
gameContext.LoggerFactory.CreateLogger()
- .LogError(ex, "Castle Siege: error handling phase change to {phase}.", phase);
- }
- }
-
- private async ValueTask PersistStateAsync(GameContext gameContext, CastleSiegeContext context)
- {
- try
- {
- if (this.Configuration is not { } config)
- {
- return;
- }
-
- // Snapshot the live context into the persisted config fields.
- config.PersistedOwnerGuildName = context.OwnerGuildName;
- config.PersistedPhase = context.Phase;
- config.PersistedPhaseStartedUtc = context.PhaseStartedUtc;
- config.PersistedRegisteredGuilds = context.RegisteredGuilds.ToList();
-
- // Find our plugin-configuration row via the in-memory config graph to get its id.
- var pluginTypeId = typeof(CastleSiegeEventPlugIn).GUID;
- var inMemory = gameContext.Configuration.PlugInConfigurations.FirstOrDefault(c => c.TypeId == pluginTypeId);
- if (inMemory is null)
- {
- return;
- }
-
- // Load a fresh, change-tracked copy of that row in its own (non-caching) context, rewrite its
- // custom-configuration JSON, and save — this is what actually persists to PostgreSQL.
- using var ctx = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(PlugInConfiguration), false, gameContext.Configuration);
- var row = await ctx.GetByIdAsync(inMemory.GetId()).ConfigureAwait(false);
- if (row is null)
- {
- return;
- }
-
- row.SetConfiguration(config, gameContext.PlugInManager.CustomConfigReferenceHandler);
- await ctx.SaveChangesAsync().ConfigureAwait(false);
-
- gameContext.LoggerFactory.CreateLogger()
- .LogInformation("Castle Siege: persisted state (owner={owner}, phase={phase}).", config.PersistedOwnerGuildName ?? "(none)", config.PersistedPhase);
- }
- catch (Exception ex)
- {
- gameContext.LoggerFactory.CreateLogger()
- .LogError(ex, "Castle Siege: error while persisting state to the database.");
+ .LogError(ex, "Castle Siege: error handling state change to {state}.", state);
}
}
@@ -269,52 +319,36 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
}
}
+ ///
+ /// Spawns the castle defenses from the seeded NPC definitions. Definitions flagged
+ /// are the breakable defenses (gates and
+ /// guardian statues) and are counted towards the throne; the catapults are pure war atmosphere.
+ ///
private static async Task SpawnCastleDefensesAsync(IGameContext gameContext, CastleSiegeContext context)
{
try
{
- if (gameContext is not GameContext concrete)
- {
- context.SetDefenseCount(0);
- return;
- }
-
- var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
- if (map is null)
+ if (gameContext is not GameContext concrete
+ || GetDefinition(gameContext) is not { } definition
+ || await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false) is not { } map)
{
context.SetDefenseCount(0);
return;
}
+ var template = gameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == GateTemplateNumber);
var spawned = 0;
- for (var i = 0; i < DefenseSpawns.Length; i++)
+ var index = 0;
+ foreach (var npc in definition.NpcDefinitions.Where(n => n.IsPersistedToDatabase && n.MonsterDefinition is not null))
{
- var spawn = DefenseSpawns[i];
- var definition = gameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == spawn.Number);
- if (definition is null)
- {
- continue;
- }
-
- // The stock CS gate (277) definition has no HP and isn't destructible; make it breakable
+ // The stock CS gate/statue definitions have no HP and aren't destructible; make them breakable
// by borrowing the HP/attributes of a working destructible (BloodCastle gate 131).
- var template = gameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == GateTemplateNumber);
- EnsureDestructible(definition, template);
+ EnsureDestructible(npc.MonsterDefinition!, template);
- var spawnArea = new MonsterSpawnArea
- {
- MonsterDefinition = definition,
- Quantity = 1,
- X1 = spawn.X,
- X2 = spawn.X,
- Y1 = spawn.Y,
- Y2 = spawn.Y,
- Direction = Direction.South,
- SpawnTrigger = SpawnTrigger.OnceAtEventStart,
- };
-
- var npc = await concrete.MapInitializer.InitializeSpawnAsync(1000 + i, map, spawnArea).ConfigureAwait(false);
- if (npc is AttackableNpcBase attackable)
+ var monster = await concrete.MapInitializer
+ .InitializeSpawnAsync(1000 + index++, map, CreateSpawnArea(npc))
+ .ConfigureAwait(false);
+ if (monster is AttackableNpcBase attackable)
{
attackable.Died += (_, _) => context.NotifyDefenseDestroyed();
spawned++;
@@ -323,29 +357,12 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
context.SetDefenseCount(spawned);
- // Spawn the catapults (siege weapons) for war atmosphere — not counted as defenses.
- for (var i = 0; i < CatapultSpawns.Length; i++)
+ index = 0;
+ foreach (var npc in definition.NpcDefinitions.Where(IsCatapult))
{
- var spawn = CatapultSpawns[i];
- var definition = gameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == spawn.Number);
- if (definition is null)
- {
- continue;
- }
-
- var spawnArea = new MonsterSpawnArea
- {
- MonsterDefinition = definition,
- Quantity = 1,
- X1 = spawn.X,
- X2 = spawn.X,
- Y1 = spawn.Y,
- Y2 = spawn.Y,
- Direction = Direction.South,
- SpawnTrigger = SpawnTrigger.OnceAtEventStart,
- };
-
- await concrete.MapInitializer.InitializeSpawnAsync(2000 + i, map, spawnArea).ConfigureAwait(false);
+ await concrete.MapInitializer
+ .InitializeSpawnAsync(2000 + index++, map, CreateSpawnArea(npc))
+ .ConfigureAwait(false);
}
}
catch (Exception ex)
@@ -356,27 +373,53 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
}
}
+ private static bool IsCatapult(CastleSiegeNpcDefinition npc)
+ => npc.MonsterDefinition is { } monster
+ && (monster.Number == CatapultAttackNumber || monster.Number == CatapultDefenseNumber);
+
+ private static MonsterSpawnArea CreateSpawnArea(CastleSiegeNpcDefinition npc) => new()
+ {
+ MonsterDefinition = npc.MonsterDefinition,
+ Quantity = 1,
+ X1 = npc.SpawnX,
+ X2 = npc.SpawnX,
+ Y1 = npc.SpawnY,
+ Y2 = npc.SpawnY,
+ Direction = npc.Direction,
+ SpawnTrigger = SpawnTrigger.OnceAtEventStart,
+ };
+
+ private static Point? GetNpcPosition(IGameContext gameContext, short monsterNumber)
+ {
+ var npc = GetDefinition(gameContext)?.NpcDefinitions
+ .FirstOrDefault(n => n.MonsterDefinition?.Number == monsterNumber);
+ return npc is null ? null : new Point(npc.SpawnX, npc.SpawnY);
+ }
+
private static async Task ProcessSiegeTickAsync(IGameContext gameContext, CastleSiegeContext context)
{
try
{
- var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
- if (map is null)
+ if (await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false) is not { } map)
{
return;
}
// Each Crown Switch is held by whichever registered guild currently has a member standing on it.
- foreach (var (switchNumber, x, y) in SwitchPositions)
+ foreach (var switchNumber in CastleSiegeContext.SwitchNumbers)
{
- string? holder = null;
- var nearby = map.GetAttackablesInRange(new Point(x, y), SwitchHoldRange).OfType();
- foreach (var player in nearby)
+ if (GetNpcPosition(gameContext, switchNumber) is not { } position)
{
- var guildName = await GetGuildNameAsync(player).ConfigureAwait(false);
- if (guildName is not null && context.RegisteredGuilds.Contains(guildName))
+ continue;
+ }
+
+ Guid? holder = null;
+ foreach (var player in map.GetAttackablesInRange(position, SwitchHoldRange).OfType())
+ {
+ if (await GetPersistentGuildAsync(player).ConfigureAwait(false) is { } guild
+ && context.IsRegistered(guild.Id))
{
- holder = guildName;
+ holder = guild.Id;
break;
}
}
@@ -386,24 +429,31 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
var now = DateTime.UtcNow;
- // Crown-hold capture: when a guild holds both switches with every gate down, the crown shield drops;
- // that guild's master then holds the crown for CrownHoldDuration to take the throne (contestable).
+ // Crown-hold capture: when a guild holds both switches with every defense down, the crown shield
+ // drops; that guild's master then holds the crown for the configured time to take the throne.
var eligible = context.GetShieldEligibleGuild();
Player? masterPlayer = null;
- if (eligible is not null)
+ string? eligibleName = null;
+ if (eligible is { } eligibleId && GetNpcPosition(gameContext, CrownNumber) is { } crownPosition)
{
- foreach (var player in map.GetAttackablesInRange(CrownPosition, CrownHoldRange).OfType())
+ foreach (var player in map.GetAttackablesInRange(crownPosition, CrownHoldRange).OfType())
{
- if (player.GuildStatus?.Position == GuildPosition.GuildMaster
- && await GetGuildNameAsync(player).ConfigureAwait(false) == eligible)
+ if (player.GuildStatus?.Position != GuildPosition.GuildMaster)
+ {
+ continue;
+ }
+
+ if (await GetPersistentGuildAsync(player).ConfigureAwait(false) is { } guild && guild.Id == eligibleId)
{
masterPlayer = player;
+ eligibleName = guild.Name;
break;
}
}
}
- var crown = context.TickCrownHold(eligible, masterPlayer is not null, now, context.Configuration.CrownHoldDuration);
+ var holdDuration = TimeSpan.FromSeconds(GetDefinition(gameContext)?.CrownHoldTimeSeconds ?? 60);
+ var crown = context.TickCrownHold(eligible, eligibleName, masterPlayer is not null, now, holdDuration);
// Shield drop/raise -> everyone on the battle map, but only when it flips (the packet pops a modal).
if (crown.ShieldChanged)
@@ -414,13 +464,13 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
switch (crown.Event)
{
case CrownEvent.HoldStarted when masterPlayer is not null:
- // The 60-second registration panel is shown ONLY to the master taking the crown.
+ // The registration panel is shown ONLY to the master taking the crown.
await masterPlayer.InvokeViewPlugInAsync(p => p.SetCrownRegistAsync(0, 0)).ConfigureAwait(false);
break;
case CrownEvent.HoldReset when masterPlayer is not null:
await masterPlayer.InvokeViewPlugInAsync(p => p.SetCrownRegistAsync(2, 0)).ConfigureAwait(false);
break;
- case CrownEvent.Captured when crown.Guild is { } captured:
+ case CrownEvent.Captured when crown.GuildName is { } captured:
await ForEachOnBattleMapAsync(gameContext, p => p.AnnounceSealCapturedAsync(captured)).ConfigureAwait(false);
await AnnounceAsync(gameContext, $"Guild '{captured}' has taken the Crown and now holds the throne!").ConfigureAwait(false);
break;
@@ -430,7 +480,7 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
// Keep the client's on-map countdown armed and in sync. Resend every 10s so players who just
// loaded the battle map pick it up, without visibly resetting the second-counter too often.
- if ((int)(now - context.PhaseStartedUtc).TotalSeconds % 10 == 0)
+ if ((int)(now - context.StateStartedUtc).TotalSeconds % 10 == 0)
{
var remaining = context.GetRemainingSiegeTime(now);
var totalMinutes = (int)Math.Ceiling(remaining.TotalMinutes);
@@ -470,16 +520,131 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
? player.InvokeViewPlugInAsync(action).AsTask()
: Task.CompletedTask);
- private async ValueTask BroadcastCastleFlagAsync(IGameContext gameContext, CastleSiegeContext context)
+ private static async Task WarpRegisteredMembersToSiegeAsync(IGameContext gameContext, CastleSiegeContext context)
{
try
{
- if (context.OwnerGuildName is not { Length: > 0 } owner)
+ var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
+ if (map?.SafeZoneSpawnGate is not { } gate)
{
return;
}
- var logo = await this.GetOwnerLogoAsync(gameContext, owner).ConfigureAwait(false);
+ await gameContext.ForEachPlayerAsync(async player =>
+ {
+ if (await GetPersistentGuildAsync(player).ConfigureAwait(false) is { } guild
+ && context.IsRegistered(guild.Id))
+ {
+ await player.WarpToAsync(gate).ConfigureAwait(false);
+ }
+ }).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ gameContext.LoggerFactory.CreateLogger()
+ .LogError(ex, "Castle Siege: error while warping registered members to the battle map.");
+ }
+ }
+
+ ///
+ /// Writes the castle owner and the guild registrations to their database tables, and the cycle
+ /// bookkeeping (current state and when it started) to the plugin's custom-configuration JSON.
+ ///
+ private async ValueTask PersistStateAsync(GameContext gameContext, CastleSiegeContext context)
+ {
+ try
+ {
+ await this.PersistOwnerAsync(gameContext, context).ConfigureAwait(false);
+ await PersistRegistrationsAsync(gameContext, context).ConfigureAwait(false);
+ await this.PersistCycleBookkeepingAsync(gameContext, context).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ gameContext.LoggerFactory.CreateLogger()
+ .LogError(ex, "Castle Siege: error while persisting state to the database.");
+ }
+ }
+
+ private async ValueTask PersistOwnerAsync(GameContext gameContext, CastleSiegeContext context)
+ {
+ using var dataContext = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CastleSiegeData), false, gameContext.Configuration);
+ var data = (await dataContext.GetAsync().ConfigureAwait(false)).FirstOrDefault()
+ ?? dataContext.CreateNew();
+
+ data.OwnerGuildId = context.OwnerGuildId;
+ data.IsOccupied = context.OwnerGuildId is not null;
+ await dataContext.SaveChangesAsync().ConfigureAwait(false);
+
+ gameContext.LoggerFactory.CreateLogger()
+ .LogInformation(
+ "Castle Siege: persisted owner={owner} state={state}.",
+ context.OwnerGuildName ?? "(none)",
+ context.State);
+ }
+
+ private static async ValueTask PersistRegistrationsAsync(GameContext gameContext, CastleSiegeContext context)
+ {
+ using var registrationContext = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CastleSiegeGuildRegistration), false, gameContext.Configuration);
+ var existing = (await registrationContext.GetAsync().ConfigureAwait(false)).ToList();
+ var wanted = context.RegisteredGuildIds.ToHashSet();
+
+ foreach (var registration in existing)
+ {
+ if (!wanted.Remove(registration.GuildId))
+ {
+ await registrationContext.DeleteAsync(registration).ConfigureAwait(false);
+ }
+ }
+
+ foreach (var guildId in wanted)
+ {
+ var registration = registrationContext.CreateNew();
+ registration.GuildId = guildId;
+ }
+
+ await registrationContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+
+ private async ValueTask PersistCycleBookkeepingAsync(GameContext gameContext, CastleSiegeContext context)
+ {
+ if (this.Configuration is not { } settings)
+ {
+ return;
+ }
+
+ settings.PersistedState = context.State;
+ settings.PersistedStateStartedUtc = context.StateStartedUtc;
+
+ // Find our plugin-configuration row via the in-memory config graph to get its id, then rewrite its
+ // custom-configuration JSON in its own (non-caching) context.
+ var pluginTypeId = typeof(CastleSiegeEventPlugIn).GUID;
+ var inMemory = gameContext.Configuration.PlugInConfigurations.FirstOrDefault(c => c.TypeId == pluginTypeId);
+ if (inMemory is null)
+ {
+ return;
+ }
+
+ using var ctx = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(PlugInConfiguration), false, gameContext.Configuration);
+ var row = await ctx.GetByIdAsync(inMemory.GetId()).ConfigureAwait(false);
+ if (row is null)
+ {
+ return;
+ }
+
+ row.SetConfiguration(settings, gameContext.PlugInManager.CustomConfigReferenceHandler);
+ await ctx.SaveChangesAsync().ConfigureAwait(false);
+ }
+
+ private async ValueTask BroadcastCastleFlagAsync(IGameContext gameContext, CastleSiegeContext context)
+ {
+ try
+ {
+ if (context.OwnerGuildId is not { } owner)
+ {
+ return;
+ }
+
+ var logo = await this.GetOwnerLogoAsync(gameContext, owner, context.OwnerGuildName).ConfigureAwait(false);
if (logo is null)
{
return;
@@ -500,77 +665,32 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
}
}
- private async ValueTask GetOwnerLogoAsync(IGameContext gameContext, string ownerName)
+ private async ValueTask GetOwnerLogoAsync(IGameContext gameContext, Guid ownerId, string? ownerName)
{
- if (this._cachedFlagOwner == ownerName && this._cachedFlagLogo is not null)
+ if (this._cachedFlagOwner == ownerId && this._cachedFlagLogo is not null)
{
return this._cachedFlagLogo;
}
- if (gameContext is not IGameServerContext serverContext)
+ if (gameContext is not IGameServerContext serverContext || ownerName is not { Length: > 0 })
{
return null;
}
- var guildId = await serverContext.GuildServer.GetGuildIdByNameAsync(ownerName).ConfigureAwait(false);
- if (guildId == 0)
+ var shortGuildId = await serverContext.GuildServer.GetGuildIdByNameAsync(ownerName).ConfigureAwait(false);
+ if (shortGuildId == 0)
{
return null;
}
- var guild = await serverContext.GuildServer.GetGuildAsync(guildId).ConfigureAwait(false);
+ var guild = await serverContext.GuildServer.GetGuildAsync(shortGuildId).ConfigureAwait(false);
if (guild?.Logo is not { Length: > 0 } logo)
{
return null;
}
- this._cachedFlagOwner = ownerName;
+ this._cachedFlagOwner = ownerId;
this._cachedFlagLogo = logo;
return logo;
}
-
- private static async Task WarpRegisteredMembersToSiegeAsync(IGameContext gameContext, CastleSiegeContext context)
- {
- try
- {
- var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
- if (map?.SafeZoneSpawnGate is not { } gate)
- {
- return;
- }
-
- await gameContext.ForEachPlayerAsync(async player =>
- {
- var guildName = await GetGuildNameAsync(player).ConfigureAwait(false);
- if (guildName is not null && context.RegisteredGuilds.Contains(guildName))
- {
- await player.WarpToAsync(gate).ConfigureAwait(false);
- }
- }).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- gameContext.LoggerFactory.CreateLogger()
- .LogError(ex, "Castle Siege: error while warping registered members to the battle map.");
- }
- }
-
- private static async ValueTask GetGuildNameAsync(Player player)
- {
- if (player.GuildStatus is not { } guildStatus)
- {
- return null;
- }
-
- if (player.GameContext is IGameServerContext serverContext)
- {
- var guild = await serverContext.GuildServer.GetGuildAsync(guildStatus.GuildId).ConfigureAwait(false);
- if (guild?.Name is { Length: > 0 } name)
- {
- return name;
- }
- }
-
- return guildStatus.GuildId.ToString();
- }
}