Moves AdaMu's working Castle Siege onto the upstream data model that the previous commit introduced, without changing how the siege plays. State model - CastleSiegePhase is replaced by DataModel's CastleSiegeState, whose values are exactly what the game client's CASTLESIEGE_STATE enum expects. The cycle now runs Idle1(0) -> RegisterGuild(1) -> Ready(6) -> Start(7) -> End(8) -> EndCycle(9) -> Idle1(0). - Idle2(2), RegisterMark(3), Idle3(4) and Notify(5) keep their numbers for client compatibility but are never entered: AdaMu registers guilds directly and has no Mark of Lord step. Guild identity - Guilds are now identified by their persistent Guid instead of by name, so a rename (or a delete and re-create under the same name) can no longer hand castle ownership to the wrong guild. Names are carried alongside only for display and for the packets that send a name to the client. - Interfaces.Guild deliberately has no id and the guild server's short ids are in-memory only, so the persistent id is resolved through the guild name once and cached per process. This avoids adding a method to IGuildServer, which upstream keeps changing. Persistence - The castle owner is stored in the CastleSiegeData row and the registrations in CastleSiegeGuildRegistration rows, replacing the previous plugin-configuration JSON blob. Only the current state and when it started still ride on the plugin configuration, because they have no column in the upstream schema. Castle NPCs - The hard-coded gate, catapult, crown and switch coordinates are gone. They are read from GameConfiguration.CastleSiegeConfiguration, seeded by CastleSiegeInitializer. Definitions flagged IsPersistedToDatabase are the breakable defenses and count towards the throne, which additionally brings in the 4 guardian statues the previous implementation did not spawn. - The crown hold time now comes from the seeded configuration instead of the plugin settings. The AdaMu operational settings (cycle durations, registration fee, designated server id, auto-open schedule) moved to a renamed CastleSiegeSettings class, so they no longer collide with upstream's CastleSiegeConfiguration entity. Verified: full server build succeeds with 0 errors. Not yet done: the 0xB2 0x00 CastleSiegeState request handler, and the docker / local run.
504 lines
23 KiB
C#
504 lines
23 KiB
C#
// <copyright file="CastleSiegeContext.cs" company="MUnique">
|
|
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
|
// </copyright>
|
|
|
|
namespace MUnique.OpenMU.GameLogic.CastleSiege;
|
|
|
|
using MUnique.OpenMU.DataModel.Configuration;
|
|
|
|
/// <summary>
|
|
/// In-memory Castle Siege state machine and battle contention.
|
|
/// Time is injected via method parameters so it can be tested deterministically.
|
|
/// <para>
|
|
/// The cycle uses the original Season 6 <see cref="CastleSiegeState"/> values, which are exactly the values
|
|
/// the game client expects (see <c>CASTLESIEGE_STATE</c> in the client's <c>WSclient.h</c>). AdaMu drives only
|
|
/// a subset of them, because it registers guilds directly and has no Mark of Lord step:
|
|
/// </para>
|
|
/// <code>
|
|
/// Idle1(0) -> RegisterGuild(1) -> Ready(6) -> Start(7) -> End(8) -> EndCycle(9) -> Idle1(0)
|
|
/// </code>
|
|
/// <para>
|
|
/// The skipped states (<see cref="CastleSiegeState.Idle2"/>, <see cref="CastleSiegeState.RegisterMark"/>,
|
|
/// <see cref="CastleSiegeState.Idle3"/>, <see cref="CastleSiegeState.Notify"/>) keep their numbers so the
|
|
/// client stays compatible; the server simply never enters them.
|
|
/// </para>
|
|
/// <para>
|
|
/// Guilds are identified by their persistent <see cref="Guid"/>, 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.
|
|
/// </para>
|
|
/// 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.
|
|
/// The throne holder when the siege ends becomes the castle owner.
|
|
/// </summary>
|
|
public class CastleSiegeContext
|
|
{
|
|
/// <summary>The Crown Switch NPC numbers on Valley of Loren; both must be held to take the throne.</summary>
|
|
public static readonly short[] SwitchNumbers = { 217, 218 };
|
|
|
|
private readonly Dictionary<Guid, string> _registeredGuilds = new();
|
|
private readonly Dictionary<short, Guid?> _switchHolders = new() { { 217, null }, { 218, null } };
|
|
private DateTime _stateStartedUtc;
|
|
private Guid? _occupier;
|
|
private string? _occupierName;
|
|
private int _defensesRemaining;
|
|
private bool _dirty;
|
|
private Guid? _crownHoldGuild;
|
|
private DateTime? _crownHoldStartUtc;
|
|
private bool _lastShieldDown;
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="CastleSiegeContext"/> class.</summary>
|
|
/// <param name="configuration">The cycle timing configuration.</param>
|
|
public CastleSiegeContext(CastleSiegeSettings configuration)
|
|
{
|
|
this.Configuration = configuration;
|
|
this.State = CastleSiegeState.Idle1;
|
|
}
|
|
|
|
/// <summary>Raised after the state changes. Argument is the new state.</summary>
|
|
public event Action<CastleSiegeState>? StateChanged;
|
|
|
|
/// <summary>Gets the configuration (durations + schedule). Refreshed each tick from the live plugin config
|
|
/// so AdminPanel edits take effect without a restart.</summary>
|
|
public CastleSiegeSettings Configuration { get; private set; }
|
|
|
|
/// <summary>Gets the current state.</summary>
|
|
public CastleSiegeState State { get; private set; }
|
|
|
|
/// <summary>Gets the UTC time the current state started (used for persistence/restore).</summary>
|
|
public DateTime StateStartedUtc => this._stateStartedUtc;
|
|
|
|
/// <summary>Gets the persistent identifier of the owner guild, or <see langword="null"/> if unowned.</summary>
|
|
public Guid? OwnerGuildId { get; private set; }
|
|
|
|
/// <summary>Gets the owner guild's name for display and client packets, or null.</summary>
|
|
public string? OwnerGuildName { get; private set; }
|
|
|
|
/// <summary>Gets the guild currently holding the throne during the siege, or null.</summary>
|
|
public Guid? OccupierGuildId => this._occupier;
|
|
|
|
/// <summary>Gets the throne holder's name for display and client packets, or null.</summary>
|
|
public string? OccupierGuildName => this._occupierName;
|
|
|
|
/// <summary>Gets the number of castle defenses (gates + statues) still standing; the throne needs 0.</summary>
|
|
public int DefensesRemaining => this._defensesRemaining;
|
|
|
|
/// <summary>Gets the persistent identifiers of the guilds registered for the current cycle.</summary>
|
|
public IReadOnlyCollection<Guid> RegisteredGuildIds => this._registeredGuilds.Keys;
|
|
|
|
/// <summary>Gets the names of the guilds registered for the current cycle (display only).</summary>
|
|
public IReadOnlyCollection<string> RegisteredGuildNames => this._registeredGuilds.Values;
|
|
|
|
/// <summary>Gets a value indicating whether the siege battle is currently running.</summary>
|
|
public bool IsSiegeRunning => this.State == CastleSiegeState.Start;
|
|
|
|
/// <summary>Points the context at the current (possibly AdminPanel-edited) plugin configuration.</summary>
|
|
/// <param name="configuration">The live configuration.</param>
|
|
public void UpdateConfiguration(CastleSiegeSettings configuration) => this.Configuration = configuration;
|
|
|
|
/// <summary>Returns whether the given guild is registered for the current cycle.</summary>
|
|
/// <param name="guildId">The persistent guild identifier.</param>
|
|
public bool IsRegistered(Guid guildId) => this._registeredGuilds.ContainsKey(guildId);
|
|
|
|
/// <summary>Advances the state machine based on the current time.</summary>
|
|
/// <param name="now">The current UTC time.</param>
|
|
public ValueTask TickAsync(DateTime now)
|
|
{
|
|
switch (this.State)
|
|
{
|
|
case CastleSiegeState.Idle1:
|
|
if (this.Configuration.IsRegistrationOpenTime(now))
|
|
{
|
|
return this.ForceStartRegistrationAsync(now);
|
|
}
|
|
|
|
break;
|
|
case CastleSiegeState.RegisterGuild:
|
|
if (now >= this._stateStartedUtc + this.Configuration.RegistrationDuration)
|
|
{
|
|
return this.TransitionAsync(CastleSiegeState.Ready, now);
|
|
}
|
|
|
|
break;
|
|
case CastleSiegeState.Ready:
|
|
if (now >= this._stateStartedUtc + this.Configuration.PreparationDuration)
|
|
{
|
|
return this.TransitionAsync(CastleSiegeState.Start, now);
|
|
}
|
|
|
|
break;
|
|
case CastleSiegeState.Start:
|
|
if (now >= this._stateStartedUtc + this.Configuration.SiegeDuration)
|
|
{
|
|
return this.TransitionAsync(CastleSiegeState.End, now);
|
|
}
|
|
|
|
break;
|
|
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._occupierName);
|
|
}
|
|
|
|
this.ClearBattleState();
|
|
return this.TransitionAsync(CastleSiegeState.EndCycle, now);
|
|
case CastleSiegeState.EndCycle:
|
|
return this.TransitionAsync(CastleSiegeState.Idle1, now);
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
/// <summary>Admin: forces the cycle into guild registration now (from any state).</summary>
|
|
/// <param name="now">The current UTC time.</param>
|
|
public ValueTask ForceStartRegistrationAsync(DateTime now)
|
|
{
|
|
this._registeredGuilds.Clear();
|
|
this.ClearBattleState();
|
|
return this.TransitionAsync(CastleSiegeState.RegisterGuild, now);
|
|
}
|
|
|
|
/// <summary>Admin: forces a specific state now.</summary>
|
|
/// <param name="state">The target state.</param>
|
|
/// <param name="now">The current UTC time.</param>
|
|
public ValueTask ForceStateAsync(CastleSiegeState state, DateTime now)
|
|
=> this.TransitionAsync(state, now);
|
|
|
|
/// <summary>Admin: resets to the idle (resting) state and clears registrations/battle state.</summary>
|
|
/// <param name="now">The current UTC time.</param>
|
|
public ValueTask ResetAsync(DateTime now)
|
|
{
|
|
this._registeredGuilds.Clear();
|
|
this.ClearBattleState();
|
|
return this.TransitionAsync(CastleSiegeState.Idle1, now);
|
|
}
|
|
|
|
/// <summary>Registers a guild for the current cycle. No-op outside the registration state.</summary>
|
|
/// <param name="guildId">The persistent guild identifier.</param>
|
|
/// <param name="guildName">The guild name, for display.</param>
|
|
public void RegisterGuild(Guid guildId, string guildName)
|
|
{
|
|
if (this.State == CastleSiegeState.RegisterGuild
|
|
&& this._registeredGuilds.TryAdd(guildId, guildName))
|
|
{
|
|
this._dirty = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>Admin: sets (or clears) the current owner guild.</summary>
|
|
/// <param name="guildId">The owner guild identifier, or null to clear.</param>
|
|
/// <param name="guildName">The owner guild name, or null.</param>
|
|
public void SetOwner(Guid? guildId, string? guildName)
|
|
{
|
|
this.OwnerGuildId = guildId;
|
|
this.OwnerGuildName = guildId is null ? null : guildName;
|
|
this._dirty = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
/// <param name="guildId">The owner guild identifier, or null.</param>
|
|
/// <param name="guildName">The owner guild name, or null.</param>
|
|
public void SyncOwner(Guid? guildId, string? guildName)
|
|
{
|
|
this.OwnerGuildId = guildId;
|
|
this.OwnerGuildName = guildId is null ? null : guildName;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="days">The days of week registration should auto-open.</param>
|
|
/// <param name="time">The UTC time of day registration should auto-open, or null to disable.</param>
|
|
public void SetSchedule(IEnumerable<DayOfWeek> days, TimeOnly? time)
|
|
{
|
|
var orderedDays = days.Distinct().OrderBy(d => d).ToList();
|
|
this.Configuration.RegistrationOpenDays = orderedDays;
|
|
this.Configuration.RegistrationOpenTimes = orderedDays.Count > 0 && time is { } t
|
|
? new List<TimeOnly> { t }
|
|
: new List<TimeOnly>();
|
|
this._dirty = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns how much time is left in the running siege battle (the on-map countdown value), or
|
|
/// <see cref="TimeSpan.Zero"/> when the siege is not currently running.
|
|
/// </summary>
|
|
/// <param name="nowUtc">The current UTC time.</param>
|
|
public TimeSpan GetRemainingSiegeTime(DateTime nowUtc)
|
|
{
|
|
if (!this.IsSiegeRunning)
|
|
{
|
|
return TimeSpan.Zero;
|
|
}
|
|
|
|
var remaining = (this._stateStartedUtc + this.Configuration.SiegeDuration) - nowUtc;
|
|
return remaining > TimeSpan.Zero ? remaining : TimeSpan.Zero;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns how much time is left in the current state, or <see cref="TimeSpan.Zero"/> when the state has
|
|
/// no duration (idle states wait for an admin command or the auto-open time).
|
|
/// </summary>
|
|
/// <param name="nowUtc">The current UTC time.</param>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public Guid? GetShieldEligibleGuild()
|
|
{
|
|
if (!this.IsSiegeRunning || this._defensesRemaining > 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var holder = this._switchHolders[217];
|
|
return holder is not null && holder == this._switchHolders[218] ? holder : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advances the crown-hold capture. <paramref name="eligibleGuild"/> is the guild with both switches held
|
|
/// and no defenses left (shield down); <paramref name="masterHolding"/> is whether that guild's master is
|
|
/// standing on the crown. Captures the throne for the guild once it has held for <paramref name="holdDuration"/>.
|
|
/// Losing a switch or the master leaving the crown resets the hold (contestable until the siege ends).
|
|
/// </summary>
|
|
/// <param name="eligibleGuild">The guild with both switches and no defenses, or null.</param>
|
|
/// <param name="eligibleGuildName">That guild's name, for display.</param>
|
|
/// <param name="masterHolding">Whether that guild's master is on the crown.</param>
|
|
/// <param name="now">The current UTC time.</param>
|
|
/// <param name="holdDuration">How long the master must hold to capture.</param>
|
|
public CrownTickResult TickCrownHold(Guid? eligibleGuild, string? eligibleGuildName, bool masterHolding, DateTime now, TimeSpan holdDuration)
|
|
{
|
|
var shieldDown = this.IsSiegeRunning && eligibleGuild is not null;
|
|
var shieldChanged = shieldDown != this._lastShieldDown;
|
|
this._lastShieldDown = shieldDown;
|
|
|
|
if (!this.IsSiegeRunning)
|
|
{
|
|
this._crownHoldGuild = null;
|
|
this._crownHoldStartUtc = null;
|
|
return new CrownTickResult(false, shieldChanged, CrownEvent.None, null, null);
|
|
}
|
|
|
|
var wasHolding = this._crownHoldGuild is not null;
|
|
|
|
// The guild that already occupies the throne just holds it — no re-registration (avoids a capture loop).
|
|
// Only a DIFFERENT guild taking both switches can register/capture (contest).
|
|
var canCapture = eligibleGuild is not null && eligibleGuild != this._occupier;
|
|
|
|
if (!canCapture || !masterHolding)
|
|
{
|
|
this._crownHoldGuild = null;
|
|
this._crownHoldStartUtc = 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, 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, eligibleGuildName);
|
|
}
|
|
|
|
return new CrownTickResult(shieldDown, shieldChanged, CrownEvent.None, eligibleGuild, eligibleGuildName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether the persistable state changed since the last call, resetting the flag.
|
|
/// Called each tick by the plugin to decide whether to write state to the database.
|
|
/// </summary>
|
|
public bool ConsumeDirty()
|
|
{
|
|
var dirty = this._dirty;
|
|
this._dirty = false;
|
|
return dirty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restores persisted state on startup (owner, state, state-start, registrations) directly, without
|
|
/// firing <see cref="StateChanged"/> or marking the state dirty. Battle state stays cleared.
|
|
/// </summary>
|
|
/// <param name="ownerGuildId">The persisted owner guild identifier, or null.</param>
|
|
/// <param name="ownerGuildName">The persisted owner guild name, or null.</param>
|
|
/// <param name="state">The persisted state.</param>
|
|
/// <param name="stateStartedUtc">When the persisted state started (UTC), or null to keep the default.</param>
|
|
/// <param name="registeredGuilds">The persisted registrations (id to name), or null.</param>
|
|
public void RestoreState(Guid? ownerGuildId, string? ownerGuildName, CastleSiegeState state, DateTime? stateStartedUtc, IEnumerable<KeyValuePair<Guid, string>>? registeredGuilds)
|
|
{
|
|
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)
|
|
{
|
|
foreach (var registration in registeredGuilds)
|
|
{
|
|
this._registeredGuilds[registration.Key] = registration.Value;
|
|
}
|
|
}
|
|
|
|
this._dirty = false;
|
|
}
|
|
|
|
/// <summary>Sets how many castle defenses (gates + guardian statues) exist this siege (when spawned).</summary>
|
|
/// <param name="count">The defense count.</param>
|
|
public void SetDefenseCount(int count) => this._defensesRemaining = count > 0 ? count : 0;
|
|
|
|
/// <summary>Notifies that a castle defense (gate/statue) was destroyed. Lowers the remaining count (min 0).</summary>
|
|
public void NotifyDefenseDestroyed()
|
|
{
|
|
if (this._defensesRemaining > 0)
|
|
{
|
|
this._defensesRemaining--;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets which guild is currently standing on (holding) a Crown Switch. Called every tick by the plugin
|
|
/// based on player positions. Pass <c>null</c> when no registered member stands on it. No-op outside the siege.
|
|
/// </summary>
|
|
/// <param name="switchNumber">The Crown Switch NPC number (217 or 218).</param>
|
|
/// <param name="guildId">The holding guild's persistent identifier, or null.</param>
|
|
public void SetSwitchHolder(short switchNumber, Guid? guildId)
|
|
{
|
|
if (this.IsSiegeRunning && this._switchHolders.ContainsKey(switchNumber))
|
|
{
|
|
this._switchHolders[switchNumber] = guildId;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to capture the throne for a guild (called when a member registers at the Sinior/Crown NPC).
|
|
/// 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).
|
|
/// </summary>
|
|
/// <param name="guildId">The capturing guild's persistent identifier.</param>
|
|
/// <param name="guildName">The capturing guild's name, for display.</param>
|
|
/// <returns>Whether it succeeded and a human-readable reason/result message.</returns>
|
|
public (bool Success, string Reason) TryCaptureThrone(Guid guildId, string guildName)
|
|
{
|
|
if (!this.IsSiegeRunning)
|
|
{
|
|
return (false, "The siege is not running.");
|
|
}
|
|
|
|
if (this._occupier is { } occupier)
|
|
{
|
|
return (false, occupier == guildId
|
|
? "Your guild already holds the throne."
|
|
: $"The throne is already held by '{this._occupierName ?? occupier.ToString()}'.");
|
|
}
|
|
|
|
if (this._defensesRemaining > 0)
|
|
{
|
|
return (false, $"Destroy the castle defenses first ({this._defensesRemaining} remaining).");
|
|
}
|
|
|
|
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 = guildId;
|
|
this._occupierName = guildName;
|
|
this._dirty = true;
|
|
return (true, "throne captured");
|
|
}
|
|
|
|
/// <summary>Returns a human-readable status summary for admin display.</summary>
|
|
public string GetStatusText()
|
|
=> $"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()
|
|
{
|
|
this._switchHolders[217] = null;
|
|
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(CastleSiegeState state, DateTime now)
|
|
{
|
|
this.State = state;
|
|
this._stateStartedUtc = now;
|
|
this._dirty = true;
|
|
this.StateChanged?.Invoke(state);
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
}
|
|
|
|
/// <summary>The event produced by a single <see cref="CastleSiegeContext.TickCrownHold"/> call.</summary>
|
|
public enum CrownEvent
|
|
{
|
|
/// <summary>Nothing changed this tick.</summary>
|
|
None,
|
|
|
|
/// <summary>A guild master just started holding the crown (start the client countdown).</summary>
|
|
HoldStarted,
|
|
|
|
/// <summary>The crown was held long enough — the guild captured the throne.</summary>
|
|
Captured,
|
|
|
|
/// <summary>An in-progress hold was interrupted (switch lost or master left the crown).</summary>
|
|
HoldReset,
|
|
}
|
|
|
|
/// <summary>The result of a crown-hold tick: the shield state and any event that occurred.</summary>
|
|
/// <param name="ShieldDown">Whether the crown shield is currently down (both switches held, defenses cleared).</param>
|
|
/// <param name="ShieldChanged">Whether the shield state changed this tick (only then should the client be told).</param>
|
|
/// <param name="Event">The event that occurred this tick.</param>
|
|
/// <param name="GuildId">The guild the event refers to, if any.</param>
|
|
/// <param name="GuildName">That guild's name, for display and client packets.</param>
|
|
public readonly record struct CrownTickResult(bool ShieldDown, bool ShieldChanged, CrownEvent Event, Guid? GuildId, string? GuildName);
|