diff --git a/src/GameLogic/CastleSiege/CastleSiegeMapAccess.cs b/src/GameLogic/CastleSiege/CastleSiegeMapAccess.cs
new file mode 100644
index 0000000..bd0359e
--- /dev/null
+++ b/src/GameLogic/CastleSiege/CastleSiegeMapAccess.cs
@@ -0,0 +1,75 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.CastleSiege;
+
+using MUnique.OpenMU.DataModel.Entities;
+using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
+using MUnique.OpenMU.Interfaces;
+
+///
+/// ADAMU-CUSTOM (Castle Siege P4 — ownership reward): gates entry to the castle-exclusive hunting map
+/// (Land of Trials, map 31). Only members of the current castle-owning guild may enter; while the castle
+/// has no owner the map stays sealed. Game masters always pass (admin/testing).
+///
+public static class CastleSiegeMapAccess
+{
+ /// The castle-exclusive hunting map number (Land of Trials).
+ public const short CastleHuntingMapNumber = 31;
+
+ ///
+ /// Checks whether the player may enter the given map. Only the castle hunting map (31) is restricted;
+ /// every other map is always allowed.
+ ///
+ /// The player attempting to enter.
+ /// The target map number.
+ /// Whether entry is allowed and, when not, a human-readable reason to show the player.
+ public static async ValueTask<(bool Allowed, string? Error)> CanEnterMapAsync(Player player, short mapNumber)
+ {
+ if (mapNumber != CastleHuntingMapNumber)
+ {
+ return (true, null);
+ }
+
+ // Game masters may always enter (admin/testing).
+ if (player.SelectedCharacter?.CharacterStatus >= CharacterStatus.GameMaster)
+ {
+ return (true, null);
+ }
+
+ var context = CastleSiegeEventPlugIn.TryGetContext(player.GameContext);
+ var owner = context?.OwnerGuildName;
+ if (string.IsNullOrEmpty(owner))
+ {
+ return (false, "The castle hunting ground is sealed until a guild owns the castle.");
+ }
+
+ var guildName = await GetGuildNameAsync(player).ConfigureAwait(false);
+ if (guildName is not null && string.Equals(guildName, owner, StringComparison.Ordinal))
+ {
+ return (true, null);
+ }
+
+ return (false, $"Only members of the castle-owning guild '{owner}' may enter this hunting ground.");
+ }
+
+ 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();
+ }
+}
diff --git a/src/GameLogic/PlayerActions/WarpAction.cs b/src/GameLogic/PlayerActions/WarpAction.cs
index 7973e35..5e0f988 100644
--- a/src/GameLogic/PlayerActions/WarpAction.cs
+++ b/src/GameLogic/PlayerActions/WarpAction.cs
@@ -21,6 +21,15 @@ public class WarpAction
/// The warp information.
public async ValueTask WarpToAsync(Player player, WarpInfo warpInfo)
{
+ // ADAMU-CUSTOM: Castle Siege hunting-map access (Land of Trials, owner guild only).
+ var (allowed, csError) = await CastleSiege.CastleSiegeMapAccess
+ .CanEnterMapAsync(player, warpInfo.Gate?.Map?.Number ?? 0).ConfigureAwait(false);
+ if (!allowed)
+ {
+ await player.ShowBlueMessageAsync(csError!).ConfigureAwait(false);
+ return;
+ }
+
if (this.CheckRequirements(player, warpInfo, out var errorMessage))
{
await player.WarpToAsync(warpInfo.Gate!).ConfigureAwait(false);
diff --git a/src/GameLogic/PlayerActions/WarpGateAction.cs b/src/GameLogic/PlayerActions/WarpGateAction.cs
index f073c4c..31ad9e0 100644
--- a/src/GameLogic/PlayerActions/WarpGateAction.cs
+++ b/src/GameLogic/PlayerActions/WarpGateAction.cs
@@ -55,6 +55,15 @@ public class WarpGateAction
return false;
}
+ // ADAMU-CUSTOM: Castle Siege hunting-map access (Land of Trials, owner guild only).
+ var (csAllowed, csError) = await CastleSiege.CastleSiegeMapAccess
+ .CanEnterMapAsync(player, enterGate.TargetGate.Map.Number).ConfigureAwait(false);
+ if (!csAllowed)
+ {
+ await player.ShowBlueMessageAsync(csError!).ConfigureAwait(false);
+ return false;
+ }
+
var currentPosition = player.IsWalking ? player.WalkTarget : player.Position;
var inaccuracy = player.GameContext.Configuration.InfoRange;
if (player.CurrentMap!.Definition.EnterGates.Contains(enterGate)