refactor(castle-siege): drive the cycle on the client's state numbers and persist guilds by id
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.
This commit is contained in:
186
src/GameLogic/CastleSiege/CastleSiegeSettings.cs
Normal file
186
src/GameLogic/CastleSiege/CastleSiegeSettings.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
// <copyright file="CastleSiegeSettings.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 System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// <para>
|
||||
/// This is deliberately separate from <see cref="DataModel.Configuration.CastleSiegeConfiguration"/>, 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// What lives where:
|
||||
/// <list type="bullet">
|
||||
/// <item>Castle owner and guild registrations: database (<c>CastleSiegeData</c>, <c>CastleSiegeGuildRegistration</c>).</item>
|
||||
/// <item>NPC/zone/upgrade definitions and crown hold time: database (<c>GameConfiguration.CastleSiegeConfiguration</c>).</item>
|
||||
/// <item>Cycle durations, registration fee, designated server and the current state: here.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// A cycle runs Idle1 -> RegisterGuild -> Ready -> Start -> End -> EndCycle -> Idle1, and auto-starts when the
|
||||
/// current day/time matches <see cref="OpenDays"/> + <see cref="RegistrationOpenTimes"/>.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class CastleSiegeSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the days of week on which registration auto-opens (UTC). None = every day (still needs a
|
||||
/// time). Editable in the AdminPanel as check boxes; proxies the runtime <see cref="RegistrationOpenDays"/>.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public CastleSiegeScheduleDays OpenDays
|
||||
{
|
||||
get
|
||||
{
|
||||
var days = CastleSiegeScheduleDays.None;
|
||||
foreach (var day in this.RegistrationOpenDays)
|
||||
{
|
||||
days |= (CastleSiegeScheduleDays)(1 << (int)day);
|
||||
}
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
set => this.RegistrationOpenDays = Enum.GetValues<DayOfWeek>()
|
||||
.Where(day => value.HasFlag((CastleSiegeScheduleDays)(1 << (int)day)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 or the AdminPanel).
|
||||
/// </summary>
|
||||
public IList<TimeOnly> RegistrationOpenTimes { get; set; } = new List<TimeOnly>();
|
||||
|
||||
/// <summary>Gets or sets how long the registration period lasts, in minutes (guilds may register).</summary>
|
||||
[JsonIgnore]
|
||||
public int RegistrationMinutes
|
||||
{
|
||||
get => (int)this.RegistrationDuration.TotalMinutes;
|
||||
set => this.RegistrationDuration = TimeSpan.FromMinutes(Math.Max(1, value));
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets how long the preparation period lasts, in minutes (between registration and war).</summary>
|
||||
[JsonIgnore]
|
||||
public int PreparationMinutes
|
||||
{
|
||||
get => (int)this.PreparationDuration.TotalMinutes;
|
||||
set => this.PreparationDuration = TimeSpan.FromMinutes(Math.Max(0, value));
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets how long the war (siege) period lasts, in minutes.</summary>
|
||||
[JsonIgnore]
|
||||
public int SiegeMinutes
|
||||
{
|
||||
get => (int)this.SiegeDuration.TotalMinutes;
|
||||
set => this.SiegeDuration = TimeSpan.FromMinutes(Math.Max(1, value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the registration fee (in zen) a guild master must pay to register the guild
|
||||
/// for the siege. 0 disables the fee.
|
||||
/// </summary>
|
||||
public int RegistrationFee { get; set; } = 100000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the game server (SW) id on which the Castle Siege runs. With multiple game servers each
|
||||
/// has its own map instances, so the siege must happen on ONE designated server; the others skip it (they
|
||||
/// only honor the shared castle owner for the rewards). Players must be on this server to take part.
|
||||
/// </summary>
|
||||
public byte CastleSiegeServerId { get; set; }
|
||||
|
||||
// --- Runtime fields (hidden from the AdminPanel; proxied by the friendly properties above) ---
|
||||
|
||||
/// <summary>Gets or sets the days of week registration auto-opens (UTC). Empty = every day.</summary>
|
||||
[Browsable(false)]
|
||||
public IList<DayOfWeek> RegistrationOpenDays { get; set; } = new List<DayOfWeek>();
|
||||
|
||||
/// <summary>Gets or sets how long the registration period lasts.</summary>
|
||||
[Browsable(false)]
|
||||
public TimeSpan RegistrationDuration { get; set; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>Gets or sets how long the preparation period lasts.</summary>
|
||||
[Browsable(false)]
|
||||
public TimeSpan PreparationDuration { get; set; } = TimeSpan.FromMinutes(2);
|
||||
|
||||
/// <summary>Gets or sets how long the war (siege) period lasts.</summary>
|
||||
[Browsable(false)]
|
||||
public TimeSpan SiegeDuration { get; set; } = TimeSpan.FromMinutes(10);
|
||||
|
||||
// --- 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.
|
||||
|
||||
/// <summary>Gets or sets the persisted current state, so the cycle resumes after a restart.</summary>
|
||||
[Browsable(false)]
|
||||
public CastleSiegeState PersistedState { get; set; } = CastleSiegeState.Idle1;
|
||||
|
||||
/// <summary>Gets or sets when the persisted state started (UTC), or null if never persisted.</summary>
|
||||
[Browsable(false)]
|
||||
public DateTime? PersistedStateStartedUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if <paramref name="now"/> (UTC) matches a scheduled registration-open day and falls
|
||||
/// within a 5-second window of a configured open time. When <see cref="RegistrationOpenDays"/> is empty,
|
||||
/// the day is not restricted (every day). When no times are configured, auto-start is disabled.
|
||||
/// </summary>
|
||||
/// <param name="now">The current UTC time.</param>
|
||||
public bool IsRegistrationOpenTime(DateTime now)
|
||||
{
|
||||
if (this.RegistrationOpenTimes.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.RegistrationOpenDays.Count > 0 && !this.RegistrationOpenDays.Contains(now.DayOfWeek))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var nowTime = TimeOnly.FromDateTime(now);
|
||||
var earlier = nowTime.Add(TimeSpan.FromSeconds(-5));
|
||||
return this.RegistrationOpenTimes.Any(p => p.IsBetween(earlier, nowTime));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The days of week on which the Castle Siege registration auto-opens. A [Flags] enum so the AdminPanel
|
||||
/// renders it as a set of check boxes.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum CastleSiegeScheduleDays
|
||||
{
|
||||
/// <summary>No day (auto-start day-unrestricted; still needs a time).</summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>Sunday.</summary>
|
||||
Sunday = 1 << 0,
|
||||
|
||||
/// <summary>Monday.</summary>
|
||||
Monday = 1 << 1,
|
||||
|
||||
/// <summary>Tuesday.</summary>
|
||||
Tuesday = 1 << 2,
|
||||
|
||||
/// <summary>Wednesday.</summary>
|
||||
Wednesday = 1 << 3,
|
||||
|
||||
/// <summary>Thursday.</summary>
|
||||
Thursday = 1 << 4,
|
||||
|
||||
/// <summary>Friday.</summary>
|
||||
Friday = 1 << 5,
|
||||
|
||||
/// <summary>Saturday.</summary>
|
||||
Saturday = 1 << 6,
|
||||
}
|
||||
Reference in New Issue
Block a user