From 7104f56ebfedd6575305ce28c554e67ba2a4b97a Mon Sep 17 00:00:00 2001 From: WarrentyExpired Date: Thu, 20 Aug 2026 14:08:13 -0400 Subject: [PATCH] #W# Rentable Inn rooms! --- Data/Decoration/Towns/Bastion_Doors.cfg | 3 - Data/Decoration/inndoors.cfg | 2 + Scripts/Commands/BuildWorld.cs | 2 +- Scripts/Commands/GenInnDoors.cs | 140 ++++++++++ Scripts/Engines/InnDoor.cs | 352 ++++++++++++++++++++++++ Scripts/Engines/InnRoomHouse.cs | 79 ++++++ Scripts/Settings.cs | 2 +- Scripts/TaskManager.cs | 6 +- 8 files changed, 579 insertions(+), 7 deletions(-) create mode 100644 Data/Decoration/inndoors.cfg create mode 100644 Scripts/Commands/GenInnDoors.cs create mode 100644 Scripts/Engines/InnDoor.cs create mode 100644 Scripts/Engines/InnRoomHouse.cs diff --git a/Data/Decoration/Towns/Bastion_Doors.cfg b/Data/Decoration/Towns/Bastion_Doors.cfg index ac846ec..fcafcb2 100644 --- a/Data/Decoration/Towns/Bastion_Doors.cfg +++ b/Data/Decoration/Towns/Bastion_Doors.cfg @@ -39,9 +39,6 @@ StrongWoodDoor 1775 (Facing=NorthCW) DarkWoodDoor 1711 (Facing=NorthCW) 829 671 0 -DarkWoodDoor 1711 (Facing=NorthCW) -835 666 0 - DarkWoodDoor 1711 (Facing=NorthCW) 835 661 0 diff --git a/Data/Decoration/inndoors.cfg b/Data/Decoration/inndoors.cfg new file mode 100644 index 0000000..22a24ed --- /dev/null +++ b/Data/Decoration/inndoors.cfg @@ -0,0 +1,2 @@ +#Bastion Inn +Vaelen 835 666 0 Vaelen 7138 4066 0 1779 diff --git a/Scripts/Commands/BuildWorld.cs b/Scripts/Commands/BuildWorld.cs index 96222fd..c236055 100644 --- a/Scripts/Commands/BuildWorld.cs +++ b/Scripts/Commands/BuildWorld.cs @@ -111,7 +111,7 @@ namespace Server.Scripts.Commands Console.WriteLine( "Spawning Dungeon Chests..." ); Server.SpawnGenerator.Parse( m, "chests.map" ); - if ( Server.Settings.S_DungeonTraps ) + if ( Server.Settings.S_DungeonFloorTraps ) { Console.WriteLine( "Spawining Dungeon Floor Traps..." ); Server.SpawnGenerator.Parse( m, "traps.map" ); diff --git a/Scripts/Commands/GenInnDoors.cs b/Scripts/Commands/GenInnDoors.cs new file mode 100644 index 0000000..8a47ebe --- /dev/null +++ b/Scripts/Commands/GenInnDoors.cs @@ -0,0 +1,140 @@ +using System; +using System.IO; +using System.Collections; +using Server; +using Server.Items; +using Server.Custom.InnRooms; // Ensure it can find our InnDoor! + +namespace Server.Commands +{ + public class GenInnDoors + { + public static void Initialize() + { + CommandSystem.Register("InnGen", AccessLevel.Administrator, new CommandEventHandler(GenInnDoors_OnCommand)); + } + + [Usage("InnGen")] + [Description("Generates Inn Doors from a configuration file.")] + public static void GenInnDoors_OnCommand(CommandEventArgs e) + { + e.Mobile.SendMessage("Generating Inn Doors from config, please wait."); + + int count = new InnDoorCreator().CreateDoors(); + + e.Mobile.SendMessage("Inn Door generation complete. {0} doors were generated.", count); + } + + public class InnDoorCreator + { + private int m_Count; + private static Queue m_Queue = new Queue(); + + // Finds and deletes existing InnDoors at the target location to prevent stacking + public static bool FindExistingDoor(Map map, Point3D p) + { + IPooledEnumerable eable = map.GetItemsInRange(p, 0); + + foreach (Item item in eable) + { + if (item is InnDoor) + { + int delta = item.Z - p.Z; + + if (delta >= -12 && delta <= 12) // Uses your existing Z-delta logic + m_Queue.Enqueue(item); + } + } + + eable.Free(); + + while (m_Queue.Count > 0) + ((Item)m_Queue.Dequeue()).Delete(); + + return false; + } + + public void CreateDoor(Point3D loc, Map mapLoc, Point3D roomLoc, Map roomMap, int itemID) + { + if (!FindExistingDoor(mapLoc, loc)) + { + m_Count++; + + // Uses our new parameterized constructor for custom facings! + InnDoor door = new InnDoor(itemID); + + // Wire up the void room coordinates automatically + door.RoomLocation = roomLoc; + door.RoomMap = roomMap; + + door.MoveToWorld(loc, mapLoc); + } + } + + public int CreateDoors() + { + // Reads from the same Decoration directory[cite: 3] + string filePath = Path.Combine(Core.BaseDirectory, "Data", "Decoration", "inndoors.cfg"); + + if (!File.Exists(filePath)) + { + Console.WriteLine("Warning: {0} not found. No Inn Doors generated.", filePath); + return 0; + } + + using (StreamReader ip = new StreamReader(filePath)) + { + string line; + while ((line = ip.ReadLine()) != null) + { + line = line.Trim(); + + if (line.Length == 0 || line.StartsWith("#")) + continue; + + string[] split = line.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + + // We need at least 8 arguments (Door Map/X/Y/Z and Room Map/X/Y/Z) + if (split.Length >= 8) + { + try + { + Map mapLoc = Map.Parse(split[0]); + int xLoc = int.Parse(split[1]); + int yLoc = int.Parse(split[2]); + int zLoc = int.Parse(split[3]); + + Map mapRoom = Map.Parse(split[4]); + int xRoom = int.Parse(split[5]); + int yRoom = int.Parse(split[6]); + int zRoom = int.Parse(split[7]); + + // Optional 9th argument: ItemID for custom facings! Defaults to 0x6E5 (1765) if missing. + int itemID = 1765; + if (split.Length >= 9) + { + // Handles both hex (0x6F3) and integer (1779) formats + if (split[8].StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + itemID = Convert.ToInt32(split[8], 16); + else + itemID = int.Parse(split[8]); + } + + if (mapLoc != null && mapRoom != null) + { + CreateDoor(new Point3D(xLoc, yLoc, zLoc), mapLoc, new Point3D(xRoom, yRoom, zRoom), mapRoom, itemID); + } + } + catch (Exception ex) + { + Console.WriteLine("Error parsing Inn Door config line: {0}\n{1}", line, ex.Message); + } + } + } + } + + return m_Count; + } + } + } +} diff --git a/Scripts/Engines/InnDoor.cs b/Scripts/Engines/InnDoor.cs new file mode 100644 index 0000000..5599e33 --- /dev/null +++ b/Scripts/Engines/InnDoor.cs @@ -0,0 +1,352 @@ +using System; +using Server; +using Server.Items; +using Server.Mobiles; +using Server.Custom.InnRooms; +using Server.Multis; +using System.Collections.Generic; +using Server.ContextMenus; + +namespace Server.Custom.InnRooms +{ + public class InnDoor : Item + { + private Mobile m_Renter; + private InnRoomHouse m_House; + private DateTime m_RentExpires; + + // Where is the void room located? + private Point3D m_RoomLocation; + private Map m_RoomMap; + + [CommandProperty(AccessLevel.GameMaster)] + public bool ForceEviction + { get { return false; } set { if (value == true) { EvictTenant(); } } } + + // Configuration + private int m_RentCost = 5000; // 5,000 gold per week + private TimeSpan m_RentDuration = TimeSpan.FromDays(7); // 1 week + + [CommandProperty(AccessLevel.GameMaster)] + public Mobile Renter { get { return m_Renter; } set { m_Renter = value; InvalidateProperties(); } } + + [CommandProperty(AccessLevel.GameMaster)] + public Point3D RoomLocation { get { return m_RoomLocation; } set { m_RoomLocation = value; } } + + [CommandProperty(AccessLevel.GameMaster)] + public Map RoomMap { get { return m_RoomMap; } set { m_RoomMap = value; } } + + [Constructable] + public InnDoor() : this(0x6E5) + { + } + + [Constructable] + public InnDoor(int itemID) : base(itemID) // Standard wooden door graphic + { + Movable = false; + Name = "A Vacant Inn Room"; + } + + public InnDoor(Serial serial) : base(serial) + { + } + + public override void OnDoubleClick(Mobile from) + { + if (!from.InRange(GetWorldLocation(), 2)) + { + from.LocalOverheadMessage(Server.Network.MessageType.Regular, 0x3B2, 1019045); // I can't reach that. + return; + } + + // SCENARIO A: The room is VACANT + if (m_Renter == null) + { + if (RoomLocation == Point3D.Zero || RoomMap == null) + { + from.SendMessage(33, "This door is out of order (No Room Linked)."); + return; + } + + if (from.Backpack != null && from.Backpack.ConsumeTotal(typeof(Gold), m_RentCost)) + { + // Rent the room! + m_Renter = from; + m_RentExpires = DateTime.UtcNow + m_RentDuration; + Name = from.Name + "'s Inn Room"; + + // Spawn the invisible house controller in the void room + m_House = new InnRoomHouse(from); + //m_House.MoveToWorld(RoomLocation, RoomMap); + // BURY THE STAIRCASE! We spawn the house 20 Z-levels beneath the actual floor. + Point3D buriedLocation = new Point3D(RoomLocation.X, RoomLocation.Y, RoomLocation.Z - 20); + m_House.MoveToWorld(buriedLocation, RoomMap); + + from.PlaySound(0x249); + from.SendMessage(68, $"You have rented this room for {m_RentDuration.Days} days."); + TeleportToRoom(from); + } + else + { + from.SendMessage(33, $"You need {m_RentCost} gold in your backpack to rent this room."); + } + } + // SCENARIO B: The room is RENTED + else + { + // Check if the rent has expired + if (DateTime.UtcNow > m_RentExpires) + { + EvictTenant(); + from.SendMessage(33, "The rent expired and the room has been cleared. It is now vacant."); + return; + } + + // Check Access using your BaseHouse logic! + if (m_House != null && (m_House.IsOwner(from) || m_House.IsFriend(from) || m_House.HasAccess(from))) + { + TeleportToRoom(from); + } + else + { + from.SendMessage(33, "The door is locked and you do not have permission to enter."); + } + } + } + + private void TeleportToRoom(Mobile m) + { + m.PlaySound(0x1EA); + m.MoveToWorld(RoomLocation, RoomMap); + m.SendMessage(0x3B2, "You enter the inn room."); + } + + public void EvictTenant() + { + if (m_Renter != null && m_House != null && !m_House.Deleted) + { + // 1. Locate the player's bank box + BankBox bank = m_Renter.BankBox; + + if (bank != null) + { + // 2. Spawn our heavy-duty moving crate + InnEvictionCrate crate = new InnEvictionCrate(); + + // 3. Pack up all Secured Containers + // We must iterate backward (Count - 1 to 0) when removing things from lists! + for (int i = m_House.Secures.Count - 1; i >= 0; --i) + { + SecureInfo info = m_House.Secures[i] as SecureInfo; + if (info != null && info.Item != null) + { + Container secureContainer = info.Item; + secureContainer.IsSecure = false; + secureContainer.IsLockedDown = false; + secureContainer.Movable = true; + + // Drop the entire secured bag/chest into the crate + crate.DropItem(secureContainer); + } + } + m_House.Secures.Clear(); + + // 4. Pack up all individual Locked Down Items + for (int i = m_House.LockDowns.Count - 1; i >= 0; --i) + { + Item item = m_House.LockDowns[i] as Item; + if (item != null) + { + item.IsLockedDown = false; + item.Movable = true; + + // Drop the loose item into the crate + crate.DropItem(item); + } + } + m_House.LockDowns.Clear(); + + // 5. Deliver the crate to the bank! + if (crate.Items.Count > 0) + { + // Using DropItem forces it into the bank, bypassing standard bank limits + bank.DropItem(crate); + m_Renter.SendMessage(33, "Your inn room rent has expired. Your belongings have been packed into a crate and placed in your bank box."); + } + else + { + // If they didn't have any items in the room, just delete the empty crate + crate.Delete(); + } + } + + // 6. Demolish the invisible house controller + m_House.Delete(); + } + + // 7. Reset the door so a new player can rent it + m_Renter = null; + m_House = null; + Name = "A Vacant Inn Room"; + } + + public override void GetProperties(ObjectPropertyList list) + { + base.GetProperties(list); + + if (m_Renter != null) + { + list.Add(1060658, "Rent Cost\tPaid"); // ~1_val~: ~2_val~ + list.Add(1060659, "Expires\t{0}", m_RentExpires.ToString("g")); // ~1_val~: ~2_val~ + } + else + { + list.Add(1060658, "Rent Cost\t{0} Gold", m_RentCost); // ~1_val~: ~2_val~ + } + } + + public override void Serialize(GenericWriter writer) + { + base.Serialize(writer); + writer.Write((int)0); // version + + writer.Write(m_Renter); + writer.WriteItem(m_House); + writer.Write(m_RentExpires); + writer.Write(m_RoomLocation); + writer.Write(m_RoomMap); + } + + public override void Deserialize(GenericReader reader) + { + base.Deserialize(reader); + int version = reader.ReadInt(); + + m_Renter = reader.ReadMobile(); + m_House = reader.ReadItem() as InnRoomHouse; + m_RentExpires = reader.ReadDateTime(); + m_RoomLocation = reader.ReadPoint3D(); + m_RoomMap = reader.ReadMap(); + } + // --- 1. THE CONTEXT MENU MENU OVERRIDE --- + public override void GetContextMenuEntries(Mobile from, List list) + { + base.GetContextMenuEntries(from, list); + + // Only the active renter gets the option to renew + if (from.Alive && m_Renter == from) + { + // 1062400 is the UO Client Cliloc for "Renew Contract" + list.Add(new RenewRentEntry(from, this)); + } + } + + // --- 2. THE CONTEXT MENU ACTION LOGIC --- + private class RenewRentEntry : ContextMenuEntry + { + private Mobile m_From; + private InnDoor m_Door; + + // The '2' is the interaction range + public RenewRentEntry(Mobile from, InnDoor door) : base(1034309, 2) + { + m_From = from; + m_Door = door; + } + + public override void OnClick() + { + if (m_Door.Deleted || m_Door.Renter != m_From) + return; + + // Check their backpack for the gold + if (m_From.Backpack != null && m_From.Backpack.ConsumeTotal(typeof(Gold), m_Door.m_RentCost)) + { + // Add 7 days to their current expiration time + m_Door.m_RentExpires += m_Door.m_RentDuration; + + m_From.PlaySound(0x249); // Coin jingle sound + m_From.SendMessage(68, $"You have paid {m_Door.m_RentCost} gold to extend your rent."); + m_From.SendMessage(68, $"Your room is now paid until: {m_Door.m_RentExpires.ToString("g")}"); + m_Door.InvalidateProperties(); // Updates the door's mouse-over text + } + else + { + m_From.SendMessage(33, $"You need {m_Door.m_RentCost} gold in your backpack to renew this room."); + } + } + } + + // --- 3. BONUS: DRAG AND DROP RENEWAL --- + public override bool OnDragDrop(Mobile from, Item dropped) + { + // If the renter drops gold directly onto the door... + if (from == m_Renter && dropped is Gold) + { + if (dropped.Amount == m_RentCost) + { + dropped.Delete(); // Consume the gold + m_RentExpires += m_RentDuration; + + from.PlaySound(0x249); + from.SendMessage(68, $"You hand the gold to the landlord and extend your rent."); + from.SendMessage(68, $"Your room is now paid until: {m_RentExpires.ToString("g")}"); + InvalidateProperties(); + + return true; + } + else + { + from.SendMessage(33, $"You must drop exactly {m_RentCost} gold on the door to renew it."); + return false; // Rejects the drop and bounces the gold back to their cursor + } + } + + return base.OnDragDrop(from, dropped); + } + } + + public class InnEvictionCrate : WoodenBox + { + public override int DefaultMaxItems { get { return 500; } } + public override int DefaultMaxWeight { get { return 5000; } } + + [Constructable] + public InnEvictionCrate() + { + Name = "Evicted Inn Room Belongings"; + Hue = 33; // Bright red + } + + public InnEvictionCrate(Serial serial) : base(serial) { } + + // 1. This prevents players from putting anything INTO the crate + public override bool CheckHold(Mobile m, Item item, bool message, bool checkItems, int plusItems, int plusWeight) + { + if (m.AccessLevel < AccessLevel.GameMaster) + { + if (message) + m.SendLocalizedMessage(1061145); // "You cannot place items into a house moving crate." + + return false; + } + + return base.CheckHold(m, item, message, checkItems, plusItems, plusWeight); + } + + // 2. This makes the crate automatically vanish once the player empties it + public override void OnItemRemoved(Item item) + { + base.OnItemRemoved(item); + + if (this.TotalItems == 0) + { + Delete(); + } + } + + public override void Serialize(GenericWriter writer) { base.Serialize(writer); writer.Write((int)0); } + public override void Deserialize(GenericReader reader) { base.Deserialize(reader); int version = reader.ReadInt(); } + } +} diff --git a/Scripts/Engines/InnRoomHouse.cs b/Scripts/Engines/InnRoomHouse.cs new file mode 100644 index 0000000..cd91c9a --- /dev/null +++ b/Scripts/Engines/InnRoomHouse.cs @@ -0,0 +1,79 @@ +using System; +using Server; +using Server.Multis; + +namespace Server.Custom.InnRooms +{ + public class InnRoomHouse : BaseHouse + { + public override Rectangle2D[] Area + { + get { return new Rectangle2D[] { new Rectangle2D(-5, -5, 10, 10) }; } + } + + public override Point3D BaseBanLocation + { + get { return new Point3D(this.X, this.Y - 5, this.Z + 20); } + } + + public InnRoomHouse(Mobile owner) : base(0x1DF3, owner, 50, 2) + { + RestrictDecay = true; + } + + public InnRoomHouse(Serial serial) : base(serial) + { + } + + // --- THE LOCKDOWN FIX --- + public override int GetAosMaxLockdowns() + { + return 50; // 50 floor items + } + + public override int GetAosMaxSecures() + { + return 250; // 250 total items in the room + } + // ------------------------ + + public override bool IsInside(Point3D p, int height) + { + if (Deleted) + return false; + + int rx = p.X - this.X; + int ry = p.Y - this.Y; + + bool inArea = false; + foreach (Rectangle2D rect in Area) + { + if (rect.Contains(new Point2D(rx, ry))) + { + inArea = true; + break; + } + } + + if (!inArea) + return false; + + if (p.Z >= this.Z && (p.Z + height) <= this.Z + 40) + return true; + + return false; + } + + public override void Serialize(GenericWriter writer) + { + base.Serialize(writer); + writer.Write((int)0); + } + + public override void Deserialize(GenericReader reader) + { + base.Deserialize(reader); + int version = reader.ReadInt(); + } + } +} diff --git a/Scripts/Settings.cs b/Scripts/Settings.cs index 3bc6659..680a520 100644 --- a/Scripts/Settings.cs +++ b/Scripts/Settings.cs @@ -19,7 +19,7 @@ namespace Server public static int S_MaxGold = 1000; // maximum is 10,000 public static bool S_CanStealWhileHoldingThings = true; public static bool S_MonstersSurprise = true; - public static bool S_DungeonTraps = false; + public static bool S_DungeonFloorTraps = false; public static double S_FarmSpawnTimer = 15.0; public static int S_HarvestRange = 1; } diff --git a/Scripts/TaskManager.cs b/Scripts/TaskManager.cs index 660a634..d4ef335 100644 --- a/Scripts/TaskManager.cs +++ b/Scripts/TaskManager.cs @@ -21,8 +21,10 @@ namespace Server.Scripts.Custom { Console.WriteLine("TasksManager: Running scheduled hourly tasks..."); - // Task 1: Wipe all floor traps so spawners can place new ones - ClearSpawnedTraps(); + if ( Settings.S_DungeonFloorTraps ) + { + ClearSpawnedTraps(); + } // FUTURE TASKS: // Just add new methods here as your server grows!