# CS P1 — Faz State Machine + Zamanlama + Admin — Uygulama Planı > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Castle Siege'in iskeletini kurmak: bellekte bir faz state machine (Ownership→Registration→Preparation→Siege→Settlement→Ownership), OpenMU'nun saniyelik periyodik-task runner'ıyla zaman-güdümlü ilerleyen, ve tamamen admin (GM chat komutları) kontrolünde. Savaş, harita, kalıcılık ve gerçek sahiplik YOK (sonraki fazlar). **Architecture:** Yeni `CastleSiegeContext` faz state machine'ini bellekte tutar; `TickAsync(DateTime now)` ile config sürelerine göre faz ilerletir (test için `now` enjekte edilir). İnce `CastleSiegeEventPlugIn : IPeriodicTaskPlugIn` her saniye context'i tick'ler ve custom config taşır (`ISupportCustomConfiguration` — yeni DB tablosu/migration YOK). GM chat komutları context'i zorlar. Durum per-GameContext static sözlükte (core dosyalara dokunmadan, tamamen additive). **Tech Stack:** .NET 10, C#, OpenMU PlugIn sistemi, NUnit. ## Global Constraints - Depo: `d:/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw` (bash'te tırnak içinde). - Tüm P1 kodu **additive** (yeni dosyalar) — core dosyalara dokunma; `// ADAMU-CUSTOM` gerekmez (yeni dosya = zaten bizim). - Lokal derleme daima `-p:ci=true`. - `DateTime.UtcNow` doğrudan state machine mantığında KULLANILMAZ; `TickAsync(DateTime now)` parametresi alır (test edilebilirlik). Plugin `DateTime.UtcNow` geçer. - Commit yazarı: `Acentech Dev `. - P1 sahipliği bellekte `OwnerGuildName` (string?) olarak tutar — guild EF referansı YOK (P3/P4). --- ### Task 1: Faz enum + config + state machine core + testler **Files:** - Create: `src/GameLogic/CastleSiege/CastleSiegePhase.cs` - Create: `src/GameLogic/CastleSiege/CastleSiegeConfiguration.cs` - Create: `src/GameLogic/CastleSiege/CastleSiegeContext.cs` - Test: `tests/MUnique.OpenMU.Tests/CastleSiege/CastleSiegeContextTest.cs` **Interfaces:** - Produces: `CastleSiegePhase` enum; `CastleSiegeConfiguration` (durations + timetable); `CastleSiegeContext` with `Phase`, `OwnerGuildName`, `RegisteredGuilds`, `TickAsync(DateTime now)`, `ForceStartRegistration(DateTime now)`, `ForcePhase(CastleSiegePhase, DateTime now)`, `RegisterGuild(string)`, `SetOwner(string?)`, `Reset(DateTime now)`, `GetStatusText()`. - [ ] **Step 1: Faz enum'unu yaz** Create `src/GameLogic/CastleSiege/CastleSiegePhase.cs`: ```csharp // // 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, } ``` - [ ] **Step 2: Config sınıfını yaz** Create `src/GameLogic/CastleSiege/CastleSiegeConfiguration.cs`: ```csharp // // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.CastleSiege; /// /// Configuration for the Castle Siege cycle timings. /// Rides on the plugin custom-configuration system (no dedicated database table in P1). /// public class CastleSiegeConfiguration { /// /// Gets or sets the times of day at which a new cycle opens registration. /// Empty by default; admins start cycles manually via chat command in P1. /// public IList RegistrationOpenTimes { get; set; } = new List(); /// Gets or sets how long the registration phase lasts. public TimeSpan RegistrationDuration { get; set; } = TimeSpan.FromMinutes(5); /// Gets or sets how long the preparation phase lasts. public TimeSpan PreparationDuration { get; set; } = TimeSpan.FromMinutes(2); /// Gets or sets how long the siege phase lasts. public TimeSpan SiegeDuration { get; set; } = TimeSpan.FromMinutes(10); /// /// Returns true if falls within a 5-second window of any configured /// registration-open time. /// public bool IsRegistrationOpenTime(DateTime now) { if (this.RegistrationOpenTimes.Count == 0) { return false; } var nowTime = TimeOnly.FromDateTime(now); var earlier = nowTime.Add(TimeSpan.FromSeconds(-5)); return this.RegistrationOpenTimes.Any(p => p.IsBetween(earlier, nowTime)); } } ``` - [ ] **Step 3: State machine testini yaz (implementasyondan ÖNCE)** Create `tests/MUnique.OpenMU.Tests/CastleSiege/CastleSiegeContextTest.cs`: ```csharp // // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Tests.CastleSiege; using MUnique.OpenMU.GameLogic.CastleSiege; /// /// Tests for the Castle Siege phase state machine (time-driven, injected clock). /// [TestFixture] public class CastleSiegeContextTest { private static CastleSiegeConfiguration Config() => new() { RegistrationDuration = TimeSpan.FromMinutes(5), PreparationDuration = TimeSpan.FromMinutes(2), SiegeDuration = TimeSpan.FromMinutes(10), }; private static readonly DateTime T0 = new(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc); [Test] public void StartsInOwnership() { var ctx = new CastleSiegeContext(Config()); Assert.That(ctx.Phase, Is.EqualTo(CastleSiegePhase.Ownership)); } [Test] public async Task ForceStartMovesToRegistrationAsync() { var ctx = new CastleSiegeContext(Config()); await ctx.ForceStartRegistrationAsync(T0); Assert.That(ctx.Phase, Is.EqualTo(CastleSiegePhase.Registration)); } [Test] public async Task RegistrationAdvancesToPreparationAfterDurationAsync() { var ctx = new CastleSiegeContext(Config()); await ctx.ForceStartRegistrationAsync(T0); await ctx.TickAsync(T0.AddMinutes(4)); // still within registration Assert.That(ctx.Phase, Is.EqualTo(CastleSiegePhase.Registration)); await ctx.TickAsync(T0.AddMinutes(5)); // registration duration elapsed Assert.That(ctx.Phase, Is.EqualTo(CastleSiegePhase.Preparation)); } [Test] public async Task FullCycleReturnsToOwnershipAsync() { var ctx = new CastleSiegeContext(Config()); await ctx.ForceStartRegistrationAsync(T0); await ctx.TickAsync(T0.AddMinutes(5)); // -> Preparation await ctx.TickAsync(T0.AddMinutes(7)); // +2 prep -> Siege Assert.That(ctx.Phase, Is.EqualTo(CastleSiegePhase.Siege)); await ctx.TickAsync(T0.AddMinutes(17)); // +10 siege -> Settlement -> Ownership Assert.That(ctx.Phase, Is.EqualTo(CastleSiegePhase.Ownership)); } [Test] public async Task RegisterGuildCollectsNamesDuringRegistrationAsync() { var ctx = new CastleSiegeContext(Config()); await ctx.ForceStartRegistrationAsync(T0); ctx.RegisterGuild("Attackers"); Assert.That(ctx.RegisteredGuilds, Does.Contain("Attackers")); } } ``` - [ ] **Step 4: Testin başarısız (derlenmez) olduğunu gör** Run: ```bash cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src" && \ dotnet test ../tests/MUnique.OpenMU.Tests/MUnique.OpenMU.Tests.csproj -c Release -p:ci=true --filter "CastleSiegeContextTest" --nologo 2>&1 | tail -8 ``` Expected: derleme hatası (`CastleSiegeContext` yok). - [ ] **Step 5: State machine'i yaz** Create `src/GameLogic/CastleSiege/CastleSiegeContext.cs`: ```csharp // // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.CastleSiege; /// /// In-memory Castle Siege phase state machine (P1 skeleton: no battle/persistence). /// Time is injected via method parameters so it can be tested deterministically. /// public class CastleSiegeContext { private readonly List _registeredGuilds = new(); private DateTime _phaseStartedUtc; /// Initializes a new instance of the class. /// The cycle timing configuration. public CastleSiegeContext(CastleSiegeConfiguration configuration) { this.Configuration = configuration; this.Phase = CastleSiegePhase.Ownership; } /// Raised after the phase changes. Argument is the new phase. public event Action? PhaseChanged; /// Gets the configuration. public CastleSiegeConfiguration Configuration { get; } /// Gets the current phase. public CastleSiegePhase Phase { get; private set; } /// Gets the current owner guild name, or null if unowned. public string? OwnerGuildName { get; private set; } /// Gets the guild names registered for the current cycle. public IReadOnlyList RegisteredGuilds => this._registeredGuilds; /// Advances the state machine based on the current time. /// The current UTC time. public ValueTask TickAsync(DateTime now) { switch (this.Phase) { case CastleSiegePhase.Ownership: if (this.Configuration.IsRegistrationOpenTime(now)) { return this.ForceStartRegistrationAsync(now); } break; case CastleSiegePhase.Registration: if (now >= this._phaseStartedUtc + this.Configuration.RegistrationDuration) { return this.TransitionAsync(CastleSiegePhase.Preparation, now); } break; case CastleSiegePhase.Preparation: if (now >= this._phaseStartedUtc + this.Configuration.PreparationDuration) { return this.TransitionAsync(CastleSiegePhase.Siege, now); } break; case CastleSiegePhase.Siege: if (now >= this._phaseStartedUtc + this.Configuration.SiegeDuration) { return this.TransitionAsync(CastleSiegePhase.Settlement, now); } break; case CastleSiegePhase.Settlement: // P1: no battle → no winner determination yet. Settle immediately back to ownership. return this.TransitionAsync(CastleSiegePhase.Ownership, now); default: break; } return ValueTask.CompletedTask; } /// Admin: forces the cycle into registration now (from any phase). /// The current UTC time. public ValueTask ForceStartRegistrationAsync(DateTime now) { this._registeredGuilds.Clear(); return this.TransitionAsync(CastleSiegePhase.Registration, now); } /// Admin: forces a specific phase now. /// The target phase. /// The current UTC time. public ValueTask ForcePhaseAsync(CastleSiegePhase phase, DateTime now) => this.TransitionAsync(phase, now); /// Admin: resets to the ownership (resting) phase and clears registrations. /// The current UTC time. public ValueTask ResetAsync(DateTime now) { this._registeredGuilds.Clear(); return this.TransitionAsync(CastleSiegePhase.Ownership, now); } /// Registers a guild (by name) for the current cycle. No-op outside registration. /// The guild name. public void RegisterGuild(string guildName) { if (this.Phase == CastleSiegePhase.Registration && !this._registeredGuilds.Contains(guildName)) { this._registeredGuilds.Add(guildName); } } /// Admin: sets (or clears) the current owner guild name. /// The owner guild name, or null to clear. public void SetOwner(string? guildName) => this.OwnerGuildName = guildName; /// 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)}]"; private ValueTask TransitionAsync(CastleSiegePhase phase, DateTime now) { this.Phase = phase; this._phaseStartedUtc = now; this.PhaseChanged?.Invoke(phase); return ValueTask.CompletedTask; } } ``` - [ ] **Step 6: Testin geçtiğini gör** Run: ```bash cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src" && \ dotnet test ../tests/MUnique.OpenMU.Tests/MUnique.OpenMU.Tests.csproj -c Release -p:ci=true --filter "CastleSiegeContextTest" --nologo 2>&1 | tail -8 ``` Expected: `Passed! - Failed: 0, Passed: 5`. - [ ] **Step 7: Commit** ```bash cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw" && \ git add src/GameLogic/CastleSiege/ tests/MUnique.OpenMU.Tests/CastleSiege/ && \ git -c user.name="Acentech Dev" -c user.email="acentech_dev@affinitybox.com" \ commit -m "feat(CS-P1): Castle Siege phase state machine (in-memory, time-injected) + tests" ``` --- ### Task 2: Periyodik-task plugin (saniyelik tick + custom config) **Files:** - Create: `src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs` **Interfaces:** - Consumes: `CastleSiegeContext`, `CastleSiegeConfiguration` (Task 1) - Produces: `CastleSiegeEventPlugIn` implementing `IPeriodicTaskPlugIn` + `ISupportCustomConfiguration` + `ISupportDefaultCustomConfiguration`; exposes `static CastleSiegeContext? TryGetContext(IGameContext)` so chat commands can reach the per-context state machine without a plugin instance. - [ ] **Step 1: Plugin'i yaz** Create `src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs`: ```csharp // // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks; using System.Collections.Concurrent; using System.Runtime.InteropServices; using MUnique.OpenMU.GameLogic.CastleSiege; using MUnique.OpenMU.PlugIns; /// /// 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). /// [PlugIn] [Display(Name = nameof(CastleSiegeEventPlugIn), Description = "Castle Siege event (P1 skeleton: phase state machine + scheduling).")] [Guid("6E2C8B41-9A4D-4C2E-9E7B-1F2A3B4C5D60")] public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustomConfiguration, ISupportDefaultCustomConfiguration { private static readonly ConcurrentDictionary Contexts = new(); /// public CastleSiegeConfiguration? Configuration { get; set; } /// public object CreateDefaultConfig() => new CastleSiegeConfiguration(); /// Gets the Castle Siege context for a game context, if the periodic tick has initialized it. /// The game context. /// Static so GM chat commands can reach the state machine without a plugin instance /// (GetKnownPlugInsOf returns Types, not instances). The tick runs every second, so the /// context exists within ~1s of server start. public static CastleSiegeContext? TryGetContext(IGameContext gameContext) => Contexts.TryGetValue(gameContext, out var context) ? context : null; /// public async ValueTask ExecuteTaskAsync(GameContext gameContext) { var context = Contexts.GetOrAdd(gameContext, _ => new CastleSiegeContext(this.Configuration ?? new CastleSiegeConfiguration())); await context.TickAsync(DateTime.UtcNow).ConfigureAwait(false); } /// public void ForceStart() { // Force-start applies to every active game context's siege. foreach (var context in Contexts.Values) { _ = context.ForceStartRegistrationAsync(DateTime.UtcNow); } } } ``` - [ ] **Step 2: Derlemeyi doğrula** ```bash cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src" && \ dotnet build GameLogic/MUnique.OpenMU.GameLogic.csproj -c Release -p:ci=true --nologo -clp:ErrorsOnly 2>&1 | tail -8 ``` Expected: `Build succeeded` (0 hata). `ISupportCustomConfiguration`/`ISupportDefaultCustomConfiguration` bulunamazsa `using MUnique.OpenMU.PlugIns;` yeterli — imzaları o namespace'te; değilse hatadaki tam namespace'i ekle. - [ ] **Step 3: Commit** ```bash cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw" && \ git add src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs && \ git -c user.name="Acentech Dev" -c user.email="acentech_dev@affinitybox.com" \ commit -m "feat(CS-P1): periodic Castle Siege plugin (per-context tick + custom config)" ``` --- ### Task 3: Admin GM chat komutları **Files:** - Create: `src/GameLogic/PlugIns/ChatCommands/CastleSiegeStatusChatCommandPlugIn.cs` - Create: `src/GameLogic/PlugIns/ChatCommands/CastleSiegeStartChatCommandPlugIn.cs` - Create: `src/GameLogic/PlugIns/ChatCommands/CastleSiegePhaseChatCommandPlugIn.cs` - Create: `src/GameLogic/PlugIns/ChatCommands/CastleSiegeSetOwnerChatCommandPlugIn.cs` - Create: `src/GameLogic/PlugIns/ChatCommands/CastleSiegeResetChatCommandPlugIn.cs` **Interfaces:** - Consumes: `CastleSiegeEventPlugIn.GetContext` (Task 2) - [ ] **Step 1: İki kanonik deseni not et (tüm komutlarda kullanılacak)** **Context erişimi** (static — `GetKnownPlugInsOf()` Type döndürür, instance değil; o yüzden static `TryGetContext` kullanıyoruz): ```csharp var context = CastleSiegeEventPlugIn.TryGetContext(player.GameContext); ``` **Arbitrary-text mesaj** (Player'da düz `ShowMessageAsync(string)` yok; view plugin ile sarılır): ```csharp await player.InvokeViewPlugInAsync( p => p.ShowMessageAsync(text, MessageType.BlueNormal)).ConfigureAwait(false); ``` **Her komut dosyasının using bloğu şunları içermeli:** `MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks` (TryGetContext), `MUnique.OpenMU.GameLogic.Views` (IShowMessagePlugIn), `MUnique.OpenMU.Interfaces` (MessageType, CharacterStatus), `MUnique.OpenMU.PlugIns` (attributes). `System.Linq` artık gerekmez. - [ ] **Step 2: `/csstatus` komutu** Create `src/GameLogic/PlugIns/ChatCommands/CastleSiegeStatusChatCommandPlugIn.cs`: ```csharp // // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; using System.Runtime.InteropServices; using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks; using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.Interfaces; using MUnique.OpenMU.PlugIns; /// Shows the current Castle Siege status. GM only. [Guid("A1B2C3D4-0001-4E5F-9A0B-CS0000000001")] [PlugIn] [Display(Name = "Castle Siege Status", Description = "GM command: /csstatus")] [ChatCommandHelp(Command, CharacterStatus.GameMaster)] public class CastleSiegeStatusChatCommandPlugIn : IChatCommandPlugIn { private const string Command = "/csstatus"; /// public string Key => Command; /// public CharacterStatus MinCharacterStatusRequirement => CharacterStatus.GameMaster; /// public async ValueTask HandleCommandAsync(Player player, string command) { var context = CastleSiegeEventPlugIn.TryGetContext(player.GameContext); var text = context?.GetStatusText() ?? "Castle Siege plugin not active."; await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync(text, MessageType.BlueNormal)).ConfigureAwait(false); } } ``` - [ ] **Step 3: `/csstart` komutu** Create `src/GameLogic/PlugIns/ChatCommands/CastleSiegeStartChatCommandPlugIn.cs`: ```csharp // // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; using System.Runtime.InteropServices; using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks; using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.Interfaces; using MUnique.OpenMU.PlugIns; /// Forces the Castle Siege into the registration phase. GM only. [Guid("A1B2C3D4-0002-4E5F-9A0B-CS0000000002")] [PlugIn] [Display(Name = "Castle Siege Start", Description = "GM command: /csstart")] [ChatCommandHelp(Command, CharacterStatus.GameMaster)] public class CastleSiegeStartChatCommandPlugIn : IChatCommandPlugIn { private const string Command = "/csstart"; /// public string Key => Command; /// public CharacterStatus MinCharacterStatusRequirement => CharacterStatus.GameMaster; /// public async ValueTask HandleCommandAsync(Player player, string command) { var context = CastleSiegeEventPlugIn.TryGetContext(player.GameContext); if (context is null) { await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync("Castle Siege plugin not active.", MessageType.BlueNormal)).ConfigureAwait(false); return; } await context.ForceStartRegistrationAsync(DateTime.UtcNow).ConfigureAwait(false); await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync("Castle Siege: registration started.", MessageType.BlueNormal)).ConfigureAwait(false); } } ``` - [ ] **Step 4: `/csphase `, `/cssetowner `, `/csreset` komutları** Create `src/GameLogic/PlugIns/ChatCommands/CastleSiegePhaseChatCommandPlugIn.cs`: ```csharp // // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; using System; using System.Runtime.InteropServices; using MUnique.OpenMU.GameLogic.CastleSiege; using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks; using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.Interfaces; using MUnique.OpenMU.PlugIns; /// Forces a specific Castle Siege phase. GM only. Usage: /csphase Siege. [Guid("A1B2C3D4-0003-4E5F-9A0B-CS0000000003")] [PlugIn] [Display(Name = "Castle Siege Phase", Description = "GM command: /csphase ")] [ChatCommandHelp(Command, CharacterStatus.GameMaster)] public class CastleSiegePhaseChatCommandPlugIn : IChatCommandPlugIn { private const string Command = "/csphase"; /// public string Key => Command; /// public CharacterStatus MinCharacterStatusRequirement => CharacterStatus.GameMaster; /// 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)) { await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync("Usage: /csphase ", MessageType.BlueNormal)).ConfigureAwait(false); return; } var context = CastleSiegeEventPlugIn.TryGetContext(player.GameContext); if (context is null) { await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync("Castle Siege plugin not active.", MessageType.BlueNormal)).ConfigureAwait(false); return; } await context.ForcePhaseAsync(phase, DateTime.UtcNow).ConfigureAwait(false); await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync($"Castle Siege: phase set to {phase}.", MessageType.BlueNormal)).ConfigureAwait(false); } } ``` Create `src/GameLogic/PlugIns/ChatCommands/CastleSiegeSetOwnerChatCommandPlugIn.cs`: ```csharp // // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; using System; using System.Runtime.InteropServices; using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks; using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.Interfaces; using MUnique.OpenMU.PlugIns; /// Sets the Castle Siege owner guild by name. GM only. Usage: /cssetowner GuildName (empty clears). [Guid("A1B2C3D4-0004-4E5F-9A0B-CS0000000004")] [PlugIn] [Display(Name = "Castle Siege Set Owner", Description = "GM command: /cssetowner ")] [ChatCommandHelp(Command, CharacterStatus.GameMaster)] public class CastleSiegeSetOwnerChatCommandPlugIn : IChatCommandPlugIn { private const string Command = "/cssetowner"; /// public string Key => Command; /// public CharacterStatus MinCharacterStatusRequirement => CharacterStatus.GameMaster; /// public async ValueTask HandleCommandAsync(Player player, string command) { var parts = command.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); var owner = parts.Length >= 2 ? parts[1].Trim() : null; var context = CastleSiegeEventPlugIn.TryGetContext(player.GameContext); if (context is null) { await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync("Castle Siege plugin not active.", MessageType.BlueNormal)).ConfigureAwait(false); 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); } } ``` Create `src/GameLogic/PlugIns/ChatCommands/CastleSiegeResetChatCommandPlugIn.cs`: ```csharp // // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; using System; using System.Runtime.InteropServices; using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks; using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.Interfaces; using MUnique.OpenMU.PlugIns; /// Resets Castle Siege to the ownership phase and clears registrations. GM only. [Guid("A1B2C3D4-0005-4E5F-9A0B-CS0000000005")] [PlugIn] [Display(Name = "Castle Siege Reset", Description = "GM command: /csreset")] [ChatCommandHelp(Command, CharacterStatus.GameMaster)] public class CastleSiegeResetChatCommandPlugIn : IChatCommandPlugIn { private const string Command = "/csreset"; /// public string Key => Command; /// public CharacterStatus MinCharacterStatusRequirement => CharacterStatus.GameMaster; /// public async ValueTask HandleCommandAsync(Player player, string command) { var context = CastleSiegeEventPlugIn.TryGetContext(player.GameContext); if (context is null) { await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync("Castle Siege plugin not active.", MessageType.BlueNormal)).ConfigureAwait(false); return; } await context.ResetAsync(DateTime.UtcNow).ConfigureAwait(false); await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync("Castle Siege reset to ownership phase.", MessageType.BlueNormal)).ConfigureAwait(false); } } ``` - [ ] **Step 5: `ShowMessageAsync` ve `GetKnownPlugInsOf` API'lerini doğrula** `player.ShowMessageAsync(string)` OpenMU'da mevcut mu kontrol et (yoksa `player.ShowMessageAsync(msg, MessageType.BlueNormal)` veya `ShowLocalizedBlueMessageAsync` kullan). `PlugInManager.GetKnownPlugInsOf()` mevcut mu kontrol et; değilse mevcut plugin-erişim API'sine göre düzelt (StartBloodCastle `GetStrategy` desenine benzer bir strateji kaydı veya `GetPlugInPoint`). ```bash cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src" && \ grep -rnE "public .*ShowMessageAsync\(string|GetKnownPlugInsOf" GameLogic/PlayerExtensions.cs GameLogic/Player.cs PlugIns/*.cs 2>/dev/null | head ``` - [ ] **Step 6: Derle + gerekiyorsa API'leri düzelt** ```bash cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src" && \ dotnet build GameLogic/MUnique.OpenMU.GameLogic.csproj -c Release -p:ci=true --nologo -clp:ErrorsOnly 2>&1 | tail -12 ``` Expected: `Build succeeded`. Hata varsa Step 5'teki API isimlerini mevcut koda göre düzelt, tekrar derle. - [ ] **Step 7: Commit** ```bash cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw" && \ git add src/GameLogic/PlugIns/ChatCommands/CastleSiege*.cs && \ git -c user.name="Acentech Dev" -c user.email="acentech_dev@affinitybox.com" \ commit -m "feat(CS-P1): GM chat commands (/csstatus /csstart /csphase /cssetowner /csreset)" ``` --- ### Task 4: Uçtan uca smoke test (çalışan server + admin komutu) **Files:** yok (doğrulama) - [ ] **Step 1: Tüm test suit'ini koş (regresyon)** ```bash cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src" && \ dotnet test ../tests/MUnique.OpenMU.Tests/MUnique.OpenMU.Tests.csproj -c Release -p:ci=true --filter "CastleSiege|GetNpcByNumber" --nologo 2>&1 | tail -8 ``` Expected: CS state-machine testleri + remote-NPC testi geçer. - [ ] **Step 2: Image'i yeniden derle ve container'ı güncelle** ```bash cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src" && \ docker build -f Startup/Dockerfile -t adamu-openmu:dev . && \ cd ../deploy-adamu && docker compose -f docker-compose.local.yml up -d ``` - [ ] **Step 3: Emülatörde GM ile `/csstatus` ve `/csstart` çalıştır (manuel)** GM yetkili bir karakterle chat'e `/csstatus` yaz → mavi mesajda `CS phase=Ownership, owner=(none), registered=0` görünmeli. `/csstart` → `registration started`; tekrar `/csstatus` → `phase=Registration`. Bu, state machine + plugin + admin zincirinin canlı server'da çalıştığını kanıtlar. - [ ] **Step 4: Gitea'ya push** ```bash cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw" && git push origin main ``` --- ## Sonraki adım P1 bitince CS iskeleti canlı: fazlar zamanlamayla/adminle geçiyor, durum sorgulanabiliyor. **P2** = kayıt akışı (Guardsman NPC, guild mark, ücret, min guild şartları) + protokol. Guild EF cross-context referansı ve kalıcılık, sahipliğin gerçek anlam kazandığı P3/P4'te eklenecek (bu plandaki `OwnerGuildName` string yer tutucusu o zaman gerçek guild referansına yükseltilecek).