using System; using System.Collections.Generic; using System.Net; using System.Runtime.CompilerServices; using ModernUO.Serialization; using Server.Accounting; using Server.Collections; using Server.ContextMenus; using Server.Gumps; using Server.Mobiles; using Server.Regions; using Server.Text; namespace Server.Items; [SerializationGenerator(0)] public partial class RaffleEntry { [SerializableField(0, setter: "private")] private Mobile _from; [SerializableField(1, setter: "private")] private IPAddress _address; [SerializableField(2, setter: "private")] private DateTime _date; public RaffleEntry(Mobile from) { _from = from; _address = from?.NetState?.Address ?? IPAddress.None; _date = Core.Now; } public RaffleEntry() { _from = null; _address = null; _date = Core.Now; } } public enum HouseRaffleState { Inactive, Active, Completed } public enum HouseRaffleExpireAction { None, HideStone, DeleteStone } [SerializationGenerator(4, false)] [Flippable(0xEDD, 0xEDE)] public partial class HouseRaffleStone : Item { private const int EntryLimitPerIP = 4; private const int DefaultTicketPrice = 5000; private const int MessageHue = 1153; public static readonly TimeSpan DefaultDuration = TimeSpan.FromDays(7.0); // Duration of the raffle itself public static readonly TimeSpan ExpirationTime = TimeSpan.FromDays(30.0); // Time a person has to use the stone to place the house private static Timer _allStonesTimer; private static HashSet _allStones; private HouseRaffleRegion _region; [SerializableField(1)] [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] private HouseRaffleExpireAction _expireAction; [SerializableField(2)] [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] private HouseRaffleDeed _deed; [InvalidateProperties] [SerializableField(5)] [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] private Mobile _winner; [InvalidateProperties] [SerializableField(7)] [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] private DateTime _started; [InvalidateProperties] [SerializableField(8)] [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] private TimeSpan _duration; [SerializableField(9, setter: "private")] private List _entries; [Constructible] public HouseRaffleStone() : base(0xEDD) { _region = null; _plotBounds = new Rectangle2D(); _plotFacet = null; _winner = null; _deed = null; _currentState = HouseRaffleState.Inactive; _started = DateTime.MinValue; _duration = DefaultDuration; _expireAction = HouseRaffleExpireAction.None; _ticketPrice = DefaultTicketPrice; Entries = new List(); Movable = false; } [SerializableProperty(0)] [CommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] public HouseRaffleState CurrentState { get => _currentState; set { if (_currentState != value) { if (value == HouseRaffleState.Active) { ClearEntries(); Winner = null; Deed = null; Started = Core.Now; AddRaffleStone(this); } else { RemoveRaffleStone(this); } _currentState = value; InvalidateProperties(); this.MarkDirty(); } } } [SerializableProperty(3)] [CommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] public Rectangle2D PlotBounds { get => _plotBounds; set { _plotBounds = value; InvalidateRegion(); InvalidateProperties(); this.MarkDirty(); } } [SerializableProperty(4)] [CommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] public Map PlotFacet { get => _plotFacet; set { _plotFacet = value; InvalidateRegion(); InvalidateProperties(); this.MarkDirty(); } } [CommandProperty(AccessLevel.GameMaster)] public bool IsExpired { get { if (_currentState != HouseRaffleState.Completed) { return false; } return Core.Now > _started + _duration + ExpirationTime; } } [SerializableProperty(6)] [CommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] public int TicketPrice { get => _ticketPrice; set { _ticketPrice = Math.Max(0, value); InvalidateProperties(); this.MarkDirty(); } } public override string DefaultName => "a house raffle stone"; public override bool DisplayWeight => false; public static void CheckEnd_OnTick() { foreach (var stone in _allStones) { stone.CheckEnd(); } } private static void AddRaffleStone(HouseRaffleStone stone) { _allStones ??= new HashSet(); _allStones.Add(stone); if (_allStones.Count == 1) { Timer.DelayCall(TimeSpan.FromMinutes(1.0), TimeSpan.FromMinutes(1.0), CheckEnd_OnTick); } } private static void RemoveRaffleStone(HouseRaffleStone stone) { if (_allStones?.Remove(stone) == true && _allStones.Count == 0) { _allStonesTimer.Stop(); _allStonesTimer = null; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ValidLocation(Rectangle2D bounds, Map map) => bounds.Start != Point2D.Zero && bounds.End != Point2D.Zero && map != null && map != Map.Internal; private void InvalidateRegion() { if (_region != null) { _region.Unregister(); _region = null; } if (ValidLocation(_plotBounds, _plotFacet)) { _region = new HouseRaffleRegion(this); _region.Register(); } } private bool HasEntered(Mobile from) { if (from.Account is not Account acc) { return false; } foreach (var entry in Entries) { if (entry.From != null) { var entryAcc = entry.From.Account as Account; if (entryAcc == acc) { return true; } } } return false; } private bool IsAtIPLimit(Mobile from) { if (from.NetState == null) { return false; } var address = from.NetState.Address; var tickets = 0; foreach (var entry in Entries) { if (entry.Address.MatchClassC(address)) { if (++tickets >= EntryLimitPerIP) { return true; } } } return false; } public static string FormatLocation(Point3D loc, Map map, bool displayMap) { using var result = new ValueStringBuilder(stackalloc char[48]); var xLong = 0; var yLat = 0; var xMins = 0; var yMins = 0; bool xEast = false, ySouth = false; if (Sextant.Format(loc, map, ref xLong, ref yLat, ref xMins, ref yMins, ref xEast, ref ySouth)) { result.Append($"{yLat}°{yMins}'{(ySouth ? "S" : "N")},{xLong}°{xMins}'{(xEast ? "E" : "W")}"); } else { result.Append($"{loc.X},{loc.Y}"); } if (displayMap) { result.Append($" ({map})"); } return result.ToString(); } public Point3D GetPlotCenter() { var x = _plotBounds.X + _plotBounds.Width / 2; var y = _plotBounds.Y + _plotBounds.Height / 2; var z = _plotFacet?.GetAverageZ(x, y) ?? 0; return new Point3D(x, y, z); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string FormatLocation(Rectangle2D bounds, Point3D center, Map map) => !ValidLocation(bounds, map) ? "no location set" : FormatLocation(center, map, true); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string FormatPrice(int price) => price == 0 ? "FREE" : $"{price} gold"; public override void GetProperties(IPropertyList list) { base.GetProperties(list); if (ValidLocation(_plotBounds, _plotFacet)) { list.Add(FormatLocation(_plotBounds, GetPlotCenter(), _plotFacet)); } switch (_currentState) { case HouseRaffleState.Active: { list.Add(1060658, $"{"ticket price"}\t{FormatPrice(_ticketPrice)}"); // ~1_val~: ~2_val~ list.Add(1060659, $"{"ends"}\t{_started + _duration}"); // ~1_val~: ~2_val~ break; } case HouseRaffleState.Completed: { list.Add(1060658, $"{"winner"}\t{_winner?.Name ?? "Unknown"}"); // ~1_val~: ~2_val~ break; } } } public override void OnSingleClick(Mobile from) { base.OnSingleClick(from); switch (_currentState) { case HouseRaffleState.Active: { LabelTo(from, 1060658, $"{"Ends"}\t{_started + _duration}"); // ~1_val~: ~2_val~ break; } case HouseRaffleState.Completed: { LabelTo( from, 1060658, // ~1_val~: ~2_val~ $"{"Winner"}\t{_winner?.Name ?? "Unknown"}" ); break; } } } public override void GetContextMenuEntries(Mobile from, ref PooledRefList list) { base.GetContextMenuEntries(from, ref list); if (from.AccessLevel >= AccessLevel.Seer) { list.Add(new EditEntry()); if (_currentState == HouseRaffleState.Inactive) { list.Add(new ActivateEntry(ValidLocation(_plotBounds, _plotFacet))); } else { list.Add(new ManagementEntry()); } } } public override void OnDoubleClick(Mobile from) { if (_currentState != HouseRaffleState.Active || !from.CheckAlive()) { return; } if (!from.InRange(GetWorldLocation(), 2)) { from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019045); // I can't reach that. return; } if (HasEntered(from)) { from.SendMessage(MessageHue, "You have already entered this plot's raffle."); } else if (IsAtIPLimit(from)) { from.SendMessage(MessageHue, "You may not enter this plot's raffle."); } else { from.SendGump( new ConfirmTicketPurchaseGump( _plotBounds, GetPlotCenter(), _plotFacet, _ticketPrice, okay => Purchase_Callback(from, okay) ) ); } } public void Purchase_Callback(Mobile from, bool okay) { if (Deleted || _currentState != HouseRaffleState.Active || !from.CheckAlive() || HasEntered(from) || IsAtIPLimit(from)) { return; } if (from.Account is not Account) { return; } if (okay) { Container bank = from.FindBankNoCreate(); if (_ticketPrice == 0 || from.Backpack?.ConsumeTotal(typeof(Gold), _ticketPrice) == true || Banker.Withdraw(from, _ticketPrice)) { AddToEntries(new RaffleEntry(from)); from.SendMessage(MessageHue, "You have successfully entered the plot's raffle."); } else { from.SendMessage(MessageHue, $"You do not have the {FormatPrice(_ticketPrice)} required to enter the raffle."); } } else { from.SendMessage(MessageHue, "You have chosen not to enter the raffle."); } } public void CheckEnd() { if (_currentState != HouseRaffleState.Active || Core.Now < _started + _duration) { return; } CurrentState = HouseRaffleState.Completed; if (_region != null && _entries.Count != 0) { Winner = _entries.RandomElement().From; if (_winner != null) { Deed = new HouseRaffleDeed(this, _winner); _winner.SendMessage( MessageHue, $"Congratulations, {_winner.Name}! You have won the raffle for the plot located at {FormatLocation(_plotBounds, GetPlotCenter(), _plotFacet)}." ); if (_winner.AddToBackpack(Deed)) { _winner.SendMessage(MessageHue, "The writ of lease has been placed in your backpack."); } else { _winner.BankBox.DropItem(Deed); _winner.SendMessage( MessageHue, "As your backpack is full, the writ of lease has been placed in your bank box." ); } } } InvalidateProperties(); } public override void OnDelete() { if (_region != null) { _region.Unregister(); _region = null; } RemoveRaffleStone(this); base.OnDelete(); } private void Deserialize(IGenericReader reader, int version) { _currentState = (HouseRaffleState)reader.ReadEncodedInt(); _expireAction = (HouseRaffleExpireAction)reader.ReadEncodedInt(); _deed = reader.ReadEntity(); _plotBounds = reader.ReadRect2D(); _plotFacet = reader.ReadMap(); _winner = reader.ReadEntity(); _ticketPrice = reader.ReadInt(); _started = reader.ReadDateTime(); _duration = reader.ReadTimeSpan(); var entryCount = reader.ReadInt(); _entries = new List(entryCount); for (var i = 0; i < entryCount; i++) { var entry = new RaffleEntry(); entry.Deserialize(reader); if (entry.From == null) { continue; // Character was deleted } _entries.Add(entry); } } [AfterDeserialization] private void AfterDeserialization() { InvalidateRegion(); // Only check for expirations on world load since the wait time is so long (30 days) if (IsExpired) { switch (ExpireAction) { case HouseRaffleExpireAction.HideStone: { if (Visible) { Visible = false; ItemID = 0x1B7B; // Non-blocking ItemID } break; } case HouseRaffleExpireAction.DeleteStone: { Timer.DelayCall(Delete); break; } } } else if (_currentState == HouseRaffleState.Active) { AddRaffleStone(this); } } private class EditEntry : ContextMenuEntry { public EditEntry() : base(5101) { } public override void OnClick(Mobile from, IEntity target) { if (from.AccessLevel < AccessLevel.Seer || target is not HouseRaffleStone stone || stone.Deleted) { return; } from.SendGump(new PropertiesGump(from, stone)); } } private class ActivateEntry : ContextMenuEntry { public ActivateEntry(bool enabled) : base(5113) => Enabled = enabled; public override void OnClick(Mobile from, IEntity target) { if (from.AccessLevel >= AccessLevel.Seer && target is HouseRaffleStone { Deleted: false } stone && ValidLocation(stone._plotBounds, stone._plotFacet)) { stone.CurrentState = HouseRaffleState.Active; } } } private class ManagementEntry : ContextMenuEntry { public ManagementEntry() : base(5032) { } public override void OnClick(Mobile from, IEntity target) { if (from.AccessLevel >= AccessLevel.Seer && target is HouseRaffleStone { Deleted: false } stone) { from.SendGump(new HouseRaffleManagementGump(stone)); } } } private class ConfirmTicketPurchaseGump : StaticWarningGump { public override int Header => 1150470; // CONFIRM TICKET PURCHASE public override int HeaderColor => 0x7F00; public override string ContentColor => "#FFFFFF"; public override int Width => 420; public override int Height => 280; public override string Content { get; } public ConfirmTicketPurchaseGump(Rectangle2D bounds, Point3D center, Map map, int price, Action callback) : base(callback) { Content = $"You are about to purchase a raffle ticket for the house plot located at {FormatLocation(bounds, center, map)}. The ticket price is {FormatPrice(price)}. Tickets are non-refundable and you can only purchase one ticket per account. Do you wish to continue?"; } } }