/************************************************************************* * ModernUO * * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: JailSystem.cs * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * *************************************************************************/ using System; using System.Collections.Generic; using Server.Mobiles; using Server.Network; using Server.Commands; using Server.Gumps; namespace Server.Systems.JailSystem; public class JailSystem : GenericPersistence { // Jail locations (modify if using custom maps) // felucca void private static readonly Point3D[] JailLocations = [ new(5276, 1164, 0), new(5286, 1164, 0), new(5296, 1164, 0), new(5306, 1164, 0), new(5276, 1174, 0), new(5286, 1174, 0), new(5296, 1174, 0), new(5306, 1174, 0), new(5283, 1184, 0), new(5304, 1184, 0) ]; // Jail map, change this for custom maps public static readonly Map JailMap = Map.Felucca; private static readonly JailRecord EmptyRecord = new(); private static readonly HashSet CurrentlyBeingJailed = []; private static readonly Dictionary PlayerJailRecords = []; private static readonly Dictionary JailTimers = []; // Jail time scales from 5 minutes to 12 hours based on the number of offenses private static readonly TimeSpan MinJailTime = TimeSpan.FromMinutes(5); private static readonly TimeSpan MaxJailTime = TimeSpan.FromHours(12); // Release location, change this for custom maps private static readonly Point3D ReleaseLocation = new(1444, 1697, 10); // Britain Bank public static readonly Map ReleaseMap = Map.Felucca; private static JailSystem Instance; // [jail [reason] - Jail with time escalation per offense (GM only) // [unjail - Manual release from jail regardless of time (GM only) // [jailinfo - Check jail status and history (GM only) // [jailrecord - Checks their own jail record stats (player access) public static void Configure() { Instance = new JailSystem(); CommandSystem.Register("Jail", AccessLevel.GameMaster, Jail_OnCommand); CommandSystem.Register("Unjail", AccessLevel.GameMaster, Unjail_OnCommand); CommandSystem.Register("JailInfo", AccessLevel.Counselor, JailInfo_OnCommand); CommandSystem.Register("JailRecord", AccessLevel.Player, MyJailInfo_OnCommand); } public JailSystem() : base("Jail", 3) { } // Can be used by other systems to check player jail status public static bool IsPlayerJailed(PlayerMobile player) => PlayerJailRecords.GetValueOrDefault(player)?.IsCurrentlyJailed == true; private static TimeSpan CalculateJailTime(int jailCount) { var totalMinutes = MinJailTime + (jailCount - 1) * (MaxJailTime - MinJailTime) / 9.0; return totalMinutes.Clamp(MinJailTime, MaxJailTime); } public static void JailPlayer(Mobile from, PlayerMobile player, string reason = "") { if (!CurrentlyBeingJailed.Add(player)) { return; } if (!PlayerJailRecords.TryGetValue(player, out var record)) { PlayerJailRecords[player] = record = new JailRecord(); } record.JailCount++; record.LastJailed = Core.Now; record.LastJailReason = reason; record.JailedBy = from; var jailTime = CalculateJailTime(record.JailCount); record.JailEndTime = Core.Now + jailTime; player.Frozen = true; player.SendMessage(0x35, "You are being sent to jail!"); player.PlaySound(0x204); CommandLogging.WriteLine(from, $"Player {player.Name} jailed for: {reason} (Offense #{record.JailCount}, {jailTime.TotalMinutes} minutes)"); foreach (var ns in NetState.Instances) { if (ns.Mobile is PlayerMobile staff && staff.AccessLevel >= AccessLevel.Counselor) { staff.SendMessage(0x35, $"Player {player.Name} has been jailed for {jailTime.TotalMinutes} minutes. Reason: {reason} (Offense #{record.JailCount})"); } } Timer.DelayCall(TimeSpan.FromSeconds(2.0), DismountPlayer, from, player, jailTime); } private static void DismountPlayer(Mobile from, PlayerMobile player, TimeSpan jailTime) { if (player.Mount != null) { var mount = player.Mount; mount.Rider = null; player.SendMessage(0x35, "You have been dismounted."); if (mount is BaseCreature bc) { if (bc.Summoned) { bc.Dispel(bc); } else // Stable the pet { bc.ControlTarget = null; bc.ControlOrder = OrderType.Stay; bc.Internalize(); bc.SetControlMaster(null); bc.IsStabled = true; bc.StabledBy = from; player.AddStabled(bc); } } } CommandLogging.WriteLine(from, $"Player {player.Name} dismounted before jail teleport"); Timer.DelayCall(TimeSpan.FromSeconds(3.0), TeleportToJail, from, player, jailTime); } private static void TeleportToJail(Mobile from, PlayerMobile player, TimeSpan jailTime) { var jailLocation = JailLocations.RandomElement(); player.MoveToWorld(jailLocation, JailMap); player.SendMessage(0x35, "Use [jailrecord to pull up your record."); player.SendMessage(0x35, "Please contact staff if you believe this was a mistake."); CommandLogging.WriteLine(from, $"Player {player.Name} teleported to jail at {jailLocation}"); Timer.DelayCall(TimeSpan.FromSeconds(5.0), UnfreezePlayer, from, player, jailTime); } private static void UnfreezePlayer(Mobile from, PlayerMobile player, TimeSpan jailTime) { player.Frozen = false; CommandLogging.WriteLine(from, $"Player {player.Name} unfrozen in jail"); CurrentlyBeingJailed.Remove(player); var releaseTimer = Timer.DelayCall(jailTime, ReleasePlayer, from, player); JailTimers[player] = releaseTimer; } private static void ReleasePlayer(Mobile from, PlayerMobile player) { if (PlayerJailRecords.TryGetValue(player, out var record)) { record.JailEndTime = Core.Now; } JailTimers.Remove(player); // Freeze player for release sequence player.Frozen = true; player.SendMessage(0x35, "You have been released from jail!"); player.PlaySound(0x1FF); if (from != null) { CommandLogging.WriteLine(from, $"Player {player.Name} released from jail, starting teleport sequence"); } foreach (var ns in NetState.Instances) { if (ns.Mobile is PlayerMobile staff && staff.AccessLevel >= AccessLevel.Counselor) { staff.SendMessage(0x35, $"Player {player.Name} has been released from jail."); } } Timer.DelayCall(TimeSpan.FromSeconds(5.0), TeleportFromJail, from, player); } private static void TeleportFromJail(Mobile from, PlayerMobile player) { player.MoveToWorld(ReleaseLocation, ReleaseMap); if (from != null) { CommandLogging.WriteLine(player, $"Player {player.Name} teleported from jail to {ReleaseLocation}"); } Timer.DelayCall(TimeSpan.FromSeconds(5.0), UnfreezeFromRelease, from, player); } private static void UnfreezeFromRelease(Mobile from, PlayerMobile player) { player.Frozen = false; player.SendMessage(0x35, "Welcome back!"); player.SendMessage(0x35, "Please follow the shard rules."); player.SendMessage(0x35, "Have a nice day!"); if (from != null) { CommandLogging.WriteLine(from, $"Player {player.Name} unfrozen after jail release"); } } [Usage("Jail [reason]")] [Description("Jails a player with freeze/teleport/unfreeze sequence.")] private static void Jail_OnCommand(CommandEventArgs e) { if (e.Length < 1) { e.Mobile.SendMessage(0x35, "Usage: [jail [reason]"); return; } var reason = e.GetString(1); var playerName = e.GetString(0); var playerSerial = Serial.TryParse(playerName, null, out var serial) ? serial : Serial.MinusOne; PlayerMobile player = null; foreach (var m in World.Mobiles.Values) { if (m is PlayerMobile pm && (pm.Serial == playerSerial || m.RawName.InsensitiveEquals(playerName))) { player = pm; break; } } if (player == null) { e.Mobile.SendMessage(0x35, $"Player '{playerName}' not found."); return; } if (player.AccessLevel > AccessLevel.Player) { e.Mobile.SendMessage(0x35, "You cannot jail staff members."); return; } if (IsPlayerJailed(player)) { e.Mobile.SendMessage(0x35, $"Player {player.Name} is already jailed."); return; } JailPlayer(e.Mobile, player, reason); e.Mobile.SendMessage(0x35, $"Player {player.Name} is being jailed. Reason: {reason}"); } [Usage("Unjail ")] [Description("Manually releases a player from jail.")] private static void Unjail_OnCommand(CommandEventArgs e) { var from = e.Mobile; if (e.Length < 1) { from.SendMessage(0x35, "Usage: [unjail "); return; } var playerName = e.GetString(0); var playerSerial = Serial.TryParse(playerName, null, out var serial) ? serial : Serial.MinusOne; PlayerMobile player = null; foreach (var m in World.Mobiles.Values) { if (m is PlayerMobile pm && (pm.Serial == playerSerial || m.RawName.InsensitiveEquals(playerName))) { player = pm; break; } } if (player == null) { from.SendMessage(0x35, $"Player '{playerName}' not found."); return; } if (!IsPlayerJailed(player)) { from.SendMessage(0x35, $"Player {player.Name} is not currently jailed."); return; } if (JailTimers.Remove(player, out var value)) { value.Stop(); } ReleasePlayer(from, player); from.SendMessage(0x35, $"Player {player.Name} has been manually released from jail."); } [Usage("JailInfo ")] [Description("Shows jail information for a player.")] private static void JailInfo_OnCommand(CommandEventArgs e) { if (e.Length < 1) { e.Mobile.SendMessage(0x35, "Usage: [jailinfo "); return; } var playerName = e.GetString(0); var playerSerial = Serial.TryParse(playerName, null, out var serial) ? serial : Serial.MinusOne; PlayerMobile player = null; foreach (var m in World.Mobiles.Values) { if (m is PlayerMobile pm && (pm.Serial == playerSerial || m.RawName.InsensitiveEquals(playerName))) { player = pm; break; } } if (player == null) { e.Mobile.SendMessage(0x35, $"Player '{playerName}' not found."); return; } e.Mobile.SendGump(new JailRecordGump(player, PlayerJailRecords.GetValueOrDefault(player, EmptyRecord))); } private static readonly Dictionary JailRecordCooldowns = new(); private static readonly TimeSpan JailRecordCooldown = TimeSpan.FromSeconds(30); [Usage("JailRecord")] [Description("Shows your own jail information.")] private static void MyJailInfo_OnCommand(CommandEventArgs e) { var player = (PlayerMobile)e.Mobile; if (JailRecordCooldowns.TryGetValue(player, out var cooldown)) { var timeSinceLastUse = Core.Now - cooldown; if (timeSinceLastUse < JailRecordCooldown) { var remaining = JailRecordCooldown - timeSinceLastUse; e.Mobile.SendMessage(0x35, $"You must wait {remaining.TotalSeconds:F0} seconds."); return; } } JailRecordCooldowns[player] = Core.Now; player.SendGump(new JailRecordGump(player, PlayerJailRecords.GetValueOrDefault(player, EmptyRecord))); } public override void Serialize(IGenericWriter writer) { writer.WriteEncodedInt(0); // version writer.WriteEncodedInt(PlayerJailRecords.Count); foreach (var (m, record) in PlayerJailRecords) { writer.Write(m); record.Serialize(writer); } } public override void Deserialize(IGenericReader reader) { var version = reader.ReadEncodedInt(); var count = reader.ReadEncodedInt(); for (var i = 0; i < count; i++) { var player = reader.ReadEntity(); var record = new JailRecord(); record.Deserialize(reader); if (player != null) { PlayerJailRecords[player] = record; if (record.IsCurrentlyJailed) { CurrentlyBeingJailed.Add(player); var jailTime = record.JailEndTime - Core.Now; JailTimers[player] = Timer.DelayCall(jailTime, ReleasePlayer, record.JailedBy, player); } } } } }