diff --git a/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs b/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs
new file mode 100644
index 0000000..d0304f5
--- /dev/null
+++ b/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs
@@ -0,0 +1,56 @@
+//
+// 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; }
+
+ ///
+ /// Gets the Castle Siege context for a game context, if the periodic tick has initialized it.
+ ///
+ /// The game context.
+ /// The context, or null if not yet initialized.
+ /// 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 object CreateDefaultConfig() => new CastleSiegeConfiguration();
+
+ ///
+ 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);
+ }
+ }
+}