using System; using System.Collections.Generic; using ModernUO.Serialization; using Server.Buffers; using Server.Collections; using Server.ContextMenus; using Server.Engines.MLQuests; using Server.Engines.MLQuests.Definitions; using Server.Engines.MLQuests.Objectives; using Server.Engines.Virtues; using Server.Items; using Server.Logging; using Server.Misc; using Server.Network; using Server.Regions; using EDI = Server.Mobiles.EscortDestinationInfo; namespace Server.Mobiles; [SerializationGenerator(3, false)] public partial class BaseEscortable : BaseCreature { private static readonly ILogger logger = LogFactory.GetLogger(typeof(BaseEscortable)); public static readonly TimeSpan EscortDelay = TimeSpan.FromMinutes(5.0); public static readonly TimeSpan AbandonDelay = MLQuestSystem.Enabled ? TimeSpan.FromMinutes(1.0) : TimeSpan.FromMinutes(2.0); public static readonly TimeSpan DeleteTime = MLQuestSystem.Enabled ? TimeSpan.FromSeconds(100) : TimeSpan.FromSeconds(30); public static bool Initialized { get; set; } // Classic list // Used when: !MLQuestSystem.Enabled && !Core.ML public static readonly string[] TownNames = [ "Cove", "Britain", "Jhelom", "Minoc", "Ocllo", "Trinsic", "Vesper", "Yew", "Skara Brae", "Nujel'm", "Moonglow", "Magincia" ]; // ML list, pre-ML quest system // Used when: !MLQuestSystem.Enabled && Core.ML public static readonly string[] MlTownNames = [ "Cove", "Serpent's Hold", "Jhelom", "Nujel'm" ]; // ML quest system general list // Used when: MLQuestSystem.Enabled && !Region.IsPartOf( "Haven Island" ) private static readonly Dictionary _mlQuestTypes = new() { { typeof(EscortToYew), (Array.Empty(), 0) }, { typeof(EscortToVesper), (Array.Empty(), 0) }, { typeof(EscortToTrinsic), (Array.Empty(), 0) }, { typeof(EscortToSkaraBrae), (Array.Empty(), 0) }, { typeof(EscortToSerpentsHold), (Array.Empty(), 0) }, { typeof(EscortToNujelm), (Array.Empty(), 0) }, { typeof(EscortToMoonglow), (Array.Empty(), 0) }, { typeof(EscortToMinoc), (Array.Empty(), 0) }, { typeof(EscortToMagincia), (Array.Empty(), 0) }, { typeof(EscortToJhelom), (Array.Empty(), 0) }, { typeof(EscortToCove), (Array.Empty(), 0) }, { typeof(EscortToBritain), (Array.Empty(), 0) } // Ocllo was removed in pub 56 // { typeof(EscortToOcllo), (Array.Empty(), 0) } }; // ML quest system New Haven list // Used when: MLQuestSystem.Enabled && Region.IsPartOf("Haven Island") // TODO: Find out if these specific quest messages are for special one-off quest characters // that teach you where things are in a city instead of the randomly spawned ones. private static readonly Dictionary _mlQuestTypesNH = new() { // I am missing several components for my new potions, and I need to find the local alchemist. // My daughter is sick , and I need medicine. Do you know the way to the local alchemist? // I need some potions before I set out for a long journey. Can you take me to the alchemist in The Bottled Imp? // I’m looking to go to the Alchemist's shop. Will you take me? { typeof(EscortToNHAlchemist), ([1042767, 1042768, 1042769, 1042824], 1042811) }, // I need new string for my lute, yet I do not know the way to the local music shop, could you take me? // I was hoping to hire a bard for my birthday party. Can you take me to one? // I fear my talent for music is less than my desire to learn, yet still I would like to try. Can you take me to the local music shop? // I’m looking to go to the music center. Will you take me? { typeof(EscortToNHBard), ([1042770, 1042771, 1042772, 1042825], 1042812) }, // A family heirloom, our armoire, is falling apart. I need to see the local carpenter. Would you guide me to her? // My goat has broken through our fence, and I need new boards. Can you direct me to the local wood worker? // I need a hammer and nails. Never mind for what. Take me to the local carpenter or leave me be. // I’m looking to go to the local woodworker. Will you take me? { typeof(EscortToNHCarpenter), ([1042773, 1042774, 1042775, 1042829], 1042816) }, //TODO: Add woodsman (camping, tracker, etc) // 1042776 - I have a job for the local woodsman. I have lost my dog. Can you take me to see him? // 1042777 - The tracker here is a good man. I know he will help me find my pet scorpion. Do you know the way to his house? // 1042778 - I am hoping to learn something about camping. Can you take me to the local woodsman? // 1042836 - I’m looking to go to the local woodsman's. Will you take me? // 1042814 - Lead on! Payment will be made when we arrive at the weapon trainer's. // Me spouse's cookin' is too good. I fear me belly is outgrowin' me clothes. I need to find the local seamstress. Can you take me to her? // I want to learn how to sew. Can you take me to see the tailor? // I need new clothes for a party, and I was wondering if you could take me to the tailor? // I’m looking to go to the local tailor. Will you take me? { typeof(EscortToNHTailor), ([1042779, 1042780, 1042781, 1042828], 1042815) }, // I need to deposit some gold. You look like a trustworthy soul, so could you direct me to the local bank? // A rich relative of mine said they deposited some gold in to my account. Would you be able to lead me to the bank? // I have a debt I need to pay off at the bank. Do you know the way there? // I’m looking to go to the city bank. Will you take me? { typeof(EscortToNHBank), ([1042782, 1042783, 1042784, 1042832], 1042819) }, // I wish to travel and see the world, but I fear I need martial skills. Would you direct me to the local weapons trainer? // I need a sword to accompany me on a journey. Would escort me to the local fighter's union? // I need someone to help me rid my home of mongbats. Please take me to the local swordfighter. // I’m looking to go to the weapon trainer's. Will you take me? { typeof(EscortToNHWarrior), ([1042785, 1042786, 1042787, 1042827], 1042814) }, // My new house requires blessing, but I am not a mage and I have not the scroll. Would you take me to the local mages guild? // I need a wizard. I can't say why. You'll take me to one, or won't you? // You there. Take me to see a sorcerer so I can turn a friend back in to a human. He is currently a cat and keeps demanding milk. // I’m looking to go to the magic shop. Will you take me? { typeof(EscortToNHMage), ([1042788, 1042789, 1042790, 1042833], 1042820) }, // Psst - I hate to admit it, but I am lost. Can you take me to a place where they sell maps? // I am trying to confirm the location of dungeons around here. Would you take me to a map maker so I might buy supplies? // Where am I? Who am I? Do you know me? Hmmm - on second thought, I think I best stick with where I am first. Do you know where I can get a map? // I’m looking to go to the local Map maker's. Will you take me? { typeof(EscortToNHMapmaker), ([1042791, 1042792, 1042793, 1042835], 1042822) }, // I am in search of a loaf of fresh bread. Do you know where I might find some? // I need to find some spices for my stew. The local chef might have some for me, can you take me to see him? // I need something to eat. I am starving. Can you take me to the inn? // I’m looking to go to the New Haven Inn. Will you take me? { typeof(EscortToNHInn), ([1042794, 1042795, 1042796, 1042834], 1042821) }, // Hey you! I need to find a farmer because all my plants keep dying. Please take me to one. // Do you know where I might find a person who sells seeds for crops? // I am hoping to swap soil stories with a local farmer, but I cannot find one. Can you take me to one? // I’m looking to go to a local farm. Will you take me? // { typeof(EscortToNHFarm), (new[] { 1042797, 1042798, 1042799, 1042830 }, 1042817) }, // Farm destination was remove // Hey! I need to visit the local fisherman to ask about what kind of bait he is using. I keep pulling up sea serpents. Where is he? // You know, I would really like some fish, but I do not know where a fisherman is. Do you? // I have heard of a magical fish that grants wishes. I bet THAT fisherman knows where the fish is. Please take me to him. // I’m looking to go to the fishing wharf. Will you take me? { typeof(EscortToNHDocks), ([1042800, 1042801, 1042802, 1042826], 1042813) }, // I need arrows to hunt rabbits for me rabbit stew. Can you take me to the local archer? // I have a huge amount of feathers for the local Fletcher. Where might I find him? // You there. Do you know the way to the local archer? // I’m looking to go to the local archery range. Will you take me? { typeof(EscortToNHBowyer), ([1042803, 1042804, 1042805, 1042831], 1042818) }, }; [SerializableField(0, setter: "private")] private string _destinationString; [SerializableField(1)] [DeserializeTimer(nameof(DeserializeDeleteTimer))] private Timer _deleteTimer; private void DeserializeDeleteTimer(TimeSpan delay) => Timer.DelayCall(delay, Delete); private void MigrateFrom(V2Content content) { _destinationString = content.DestinationString; _mlQuestType = content.MlQuestType; _mlQuestDestinationMessage = content.MlQuestDestinationMessage; _mlQuestPaymentMessage = content.MlQuestPaymentMessage; if (content.DeleteTimerDelay != TimeSpan.MinValue) { DeserializeDeleteTimer(content.DeleteTimerDelay); } } [SerializableField(2)] private Type _mlQuestType; [SerializableField(3)] private TextDefinition _mlQuestDestinationMessage; [SerializableField(4)] private TextDefinition _mlQuestPaymentMessage; private bool _deleteCorpse; private EDI _destination; private DateTime _lastSeenEscorter; // Not serialized private List _mlQuest; [Constructible] public BaseEscortable() : base(AIType.AI_Melee, FightMode.Aggressor, 22) { InitBody(); InitOutfit(); Fame = 200; Karma = 4000; SetSpeed(0.2, 1.0); } public override bool StaticMLQuester => false; // Suppress automatic quest registration on creation/deserialization public override bool CanShout => !Controlled && !IsBeingDeleted; public bool IsBeingDeleted => _deleteTimer != null; public override bool Commandable => false; // Our master cannot boss us around! public override bool DeleteCorpseOnDeath => _deleteCorpse; [CommandProperty(AccessLevel.GameMaster)] public string Destination { get => _destination?.Name; set { DestinationString = value; _destination = EDI.Find(value); } } public static Dictionary EscortTable { get; } = new(); protected override List ConstructQuestList() { if (_mlQuestType == null) { var reg = Region; var types = reg.IsPartOf("Haven Island") ? _mlQuestTypesNH : _mlQuestTypes; // Get a rented buffer var list = STArrayPool.Shared.Rent(types.Keys.Count); // Copy to the new list types.Keys.CopyTo(list, 0); // Create a span that is the right size var listSpan = list.AsSpan(0, types.Keys.Count); // We need a span to manipulate // Shuffle listSpan.Shuffle(); // Sequentially choose a quest until we have a valid one. for (var i = 0; i < listSpan.Length; ++i) { var questType = list[i]; var quest = MLQuestSystem.FindQuest(questType); if (quest != null) { var okay = true; foreach (var obj in quest.Objectives) { if (obj is EscortObjective objective && objective.Destination.Contains(reg)) { okay = false; // We're already there! break; } } if (okay) { var (destinationMessage, paymentMessage) = types[questType]; _mlQuestType = questType; _mlQuestDestinationMessage = destinationMessage?.Length > 0 ? destinationMessage.RandomElement() : 0; _mlQuestPaymentMessage = paymentMessage; // Cached by BaseCreature in m_MLQuests _mlQuest = [quest]; break; } } else if (MLQuestSystem.Debug) { logger.Warning("Escortable cannot be assigned quest type '{Type}' because the quest was not registered.", questType); } } if (_mlQuest != null && MLQuestSystem.Debug) { logger.Warning("No suitable quest found for '{Serial}'", Serial); } // Return the rented array STArrayPool.Shared.Return(list); } return _mlQuest; } public override void Shout(PlayerMobile pm) { /* * 1072301 - You there! Care to hear how to earn some easy gold? * 1072302 - Adventurer! I have an offer for you. * 1072303 - Wait! I have an opportunity for you to make some gold! */ MLQuestSystem.Tell(this, pm, Utility.Random(1072301, 3)); } public virtual void InitBody() { SetStr(90, 100); SetDex(90, 100); SetInt(15, 25); Hue = Race.Human.RandomSkinHue(); if (Female = Utility.RandomBool()) { Body = 401; Name = NameList.RandomName("female"); } else { Body = 400; Name = NameList.RandomName("male"); } } public virtual void InitOutfit() { AddItem(new FancyShirt(Utility.RandomNeutralHue())); AddItem(new ShortPants(Utility.RandomNeutralHue())); AddItem(new Boots(Utility.RandomNeutralHue())); Utility.AssignRandomHair(this); PackGold(200, 250); } public virtual bool SayDestinationTo(Mobile m) { var dest = GetDestination(); if (dest == null || !m.Alive) { return false; } var escorter = GetEscorter(); if (escorter == null) { if (_mlQuestDestinationMessage != null) { if (_mlQuestDestinationMessage.Number > 0) { Say(_mlQuestDestinationMessage.Number); return true; } if (!string.IsNullOrEmpty(_mlQuestDestinationMessage.String)) { Say(_mlQuestDestinationMessage.String); return true; } } // I'm looking to go somewhere: Say(1042807, AffixType.Append, dest.Name == "Ocllo" && m.Map == Map.Trammel ? "Haven" : dest.Name, ""); return true; } if (escorter == m) { if (_mlQuestPaymentMessage != null) { if (_mlQuestPaymentMessage.Number > 0) { Say(_mlQuestPaymentMessage.Number); return true; } if (!string.IsNullOrEmpty(_mlQuestPaymentMessage.String)) { Say(_mlQuestPaymentMessage.String); return true; } } // Lead on! Payment will be made when we arrive at ~1_DESTINATION~! Say(1042806, dest.Name == "Ocllo" && m.Map == Map.Trammel ? "Haven" : dest.Name); return true; } return false; } public virtual bool AcceptEscorter(Mobile m) { var dest = GetDestination(); if (dest == null) { return false; } if (GetEscorter() != null || !m.Alive) { return false; } if (EscortTable.TryGetValue(m, out var escortable) && escortable?.Deleted == false && escortable.GetEscorter() == m) { Say("I see you already have an escort."); return false; } if (m is PlayerMobile mobile && mobile.LastEscortTime + EscortDelay >= Core.Now) { var minutes = (int)Math.Ceiling((mobile.LastEscortTime + EscortDelay - Core.Now).TotalMinutes); if (minutes == 1) { Say($"You must rest {minutes} minute before we set out on this journey."); } else { Say($"You must rest {minutes} minutes before we set out on this journey."); } return false; } if (SetControlMaster(m)) { _lastSeenEscorter = Core.Now; if (m is PlayerMobile playerMobile) { playerMobile.LastEscortTime = Core.Now; } Say( $"Lead on! Payment will be made when we arrive in {(dest.Name == "Ocllo" && m.Map == Map.Trammel ? "Haven" : dest.Name)}." ); EscortTable[m] = this; StartFollow(); return true; } return false; } public override bool HandlesOnSpeech(Mobile from) => !MLQuestSystem.Enabled && (from.InRange(Location, 3) || base.HandlesOnSpeech(from)); public override void OnSpeech(SpeechEventArgs e) { base.OnSpeech(e); if (GetDestination() == null || e.Handled || !e.Mobile.InRange(Location, 3)) { return; } if (e.HasKeyword(0x1D)) // *destination* { e.Handled = SayDestinationTo(e.Mobile); } else if (e.HasKeyword(0x1E)) // *i will take thee* { e.Handled = AcceptEscorter(e.Mobile); } } public override void OnAfterDelete() { _deleteTimer?.Stop(); _deleteTimer = null; base.OnAfterDelete(); } public override void OnThink() { base.OnThink(); CheckAtDestination(); } protected override bool OnMove(Direction d) { if (!base.OnMove(d)) { return false; } CheckAtDestination(); return true; } // TODO: Pre-ML methods below, might be mergeable with the ML methods in EscortObjective public virtual void StartFollow() { StartFollow(GetEscorter()); } public virtual void StartFollow(Mobile escorter) { if (escorter == null) { return; } ControlOrder = OrderType.Follow; ControlTarget = escorter; if (IsPrisoner && CantWalk) { CantWalk = false; } SetSpeed(0.1, 0.2, false); } public virtual void StopFollow() { ControlOrder = OrderType.None; ControlTarget = null; SetSpeed(0.2, 1.0); } public virtual Mobile GetEscorter() { if (!Controlled) { return null; } var master = ControlMaster; if (MLQuestSystem.Enabled || master == null) { return master; } if (master.Deleted || master.Map != Map || !master.InRange(Location, 30) || !master.Alive) { StopFollow(); var lastSeenDelay = Core.Now - _lastSeenEscorter; if (lastSeenDelay >= AbandonDelay) { master.SendLocalizedMessage(1042473); // You have lost the person you were escorting. Say(1005653); // Hmmm. I seem to have lost my master. SetControlMaster(null); EscortTable.Remove(master); Timer.StartTimer(TimeSpan.FromSeconds(5.0), Delete); return null; } ControlOrder = OrderType.Stay; return master; } if (ControlOrder != OrderType.Follow) { StartFollow(master); } _lastSeenEscorter = Core.Now; return master; } public virtual void BeginDelete() { if (_deleteTimer != null) { _deleteTimer.Stop(); _deleteTimer.Delay = DeleteTime; _deleteTimer.Start(); } else { _deleteTimer = Timer.DelayCall(DeleteTime, Delete); } } public virtual bool CheckAtDestination() { if (MLQuestSystem.Enabled) { return false; } var dest = GetDestination(); if (dest == null) { return false; } var escorter = GetEscorter(); if (escorter == null) { return false; } if (dest.Contains(Location)) { // We have arrived! I thank thee, ~1_PLAYER_NAME~! I have no further need of thy services. Here is thy pay. Say(1042809, escorter.Name); // not going anywhere _destination = null; DestinationString = null; var cont = escorter.Backpack ?? escorter.BankBox; var gold = new Gold(500, 1000); if (!cont.TryDropItem(escorter, gold, false)) { gold.MoveToWorld(escorter.Location, escorter.Map); } StopFollow(); SetControlMaster(null); EscortTable.Remove(escorter); BeginDelete(); Titles.AwardFame(escorter, 10, true); var gainedPath = false; if (escorter is PlayerMobile pm) { var virtues = VirtueSystem.GetOrCreateVirtues(pm); if (virtues.CompassionGains > 0 && Core.Now > virtues.NextCompassionDay) { virtues.NextCompassionDay = DateTime.MinValue; virtues.CompassionGains = 0; } if (virtues.CompassionGains >= 5) // have already gained 5 times in one day, can gain no more { // You must wait about a day before you can gain in compassion again. pm.SendLocalizedMessage(1053004); } else if (VirtueSystem.Award(pm, VirtueName.Compassion, IsPrisoner ? 400 : 200, ref gainedPath)) { if (gainedPath) { pm.SendLocalizedMessage(1053005); // You have achieved a path in compassion! } else { pm.SendLocalizedMessage(1053002); // You have gained in compassion. } // in one day CompassionGains gets reset to 0 virtues.NextCompassionDay = Core.Now + TimeSpan.FromDays(1.0); if (++virtues.CompassionGains >= 5) { // You must wait about a day before you can gain in compassion again. pm.SendLocalizedMessage(1053004); } } else { // You have achieved the highest path of compassion and can no longer gain any further. pm.SendLocalizedMessage(1053003); } } return true; } return false; } public override bool OnBeforeDeath() { _deleteCorpse = Controlled || IsBeingDeleted; return base.OnBeforeDeath(); } private void Deserialize(IGenericReader reader, int version) { if (reader.ReadBool()) { // NOTE: We cannot EDI.Find here, regions have not yet been loaded :-( _destinationString = reader.ReadString(); } if (reader.ReadBool()) { var deleteTime = reader.ReadDeltaTime(); _deleteTimer = Timer.DelayCall(deleteTime - Core.Now, Delete); _deleteTimer.Start(); } var quest = MLQuestSystem.ReadQuestRef(reader); if (MLQuestSystem.Enabled && quest != null && !StaticMLQuester) { _mlQuestType = quest.GetType(); _mlQuest = [quest]; // This isn't serialized before codegen if (_mlQuestTypesNH.TryGetValue(_mlQuestType, out var tuple)) { var (destinationMessages, paymentMessage) = tuple; _mlQuestDestinationMessage = destinationMessages?.Length > 0 ? destinationMessages.RandomElement() : 0; _mlQuestPaymentMessage = paymentMessage; } } } [AfterDeserialization] private void AfterDeserialization() { if (MLQuestSystem.Enabled && !StaticMLQuester && _mlQuest != null && _mlQuestType != null) { var quest = MLQuestSystem.FindQuest(_mlQuestType); if (quest != null) { _mlQuest = [quest]; } } } public override bool CanBeRenamedBy(Mobile from) => from.AccessLevel >= AccessLevel.GameMaster; public override void AddCustomContextEntries(Mobile from, ref PooledRefList list) { if (from.Alive) { var escorter = GetEscorter(); if (!MLQuestSystem.Enabled && GetDestination() != null) { if (escorter == null || escorter == from) { list.Add(new AskDestinationEntry()); } if (escorter == null) { list.Add(new AcceptEscortEntry()); } } if (escorter == from) { list.Add(new AbandonEscortEntry()); } } base.AddCustomContextEntries(from, ref list); } public virtual string[] GetPossibleDestinations() => Core.ML ? MlTownNames : TownNames; public virtual string PickRandomDestination() { if (Map.Felucca.Regions.Count == 0 || Map == null || Map == Map.Internal || Location == Point3D.Zero) { return null; // Not yet fully initialized } var possible = GetPossibleDestinations(); string picked = null; while (picked == null) { picked = possible.RandomElement(); var test = EDI.Find(picked); if (test?.Contains(Location) != false) { picked = null; } } return picked; } public EDI GetDestination() { if (MLQuestSystem.Enabled || !Initialized) { return null; } if (_destinationString == null && _deleteTimer == null) { _destinationString = PickRandomDestination(); } if (_destination != null && _destination.Name == _destinationString) { return _destination; } if (Map.Felucca.Regions.Count > 0) { _destination = EDI.Find(_destinationString); // The destination string used to exist, but doesn't anymore. if (_destination == null) { _destinationString = null; return null; } return _destination; } // Destination is invalid, so set it to null and return. return _destination = null; } } public class EscortDestinationInfo { private static readonly ILogger logger = LogFactory.GetLogger(typeof(EscortDestinationInfo)); private static Dictionary _table; public EscortDestinationInfo(string name, Region region) { Name = name; Region = region; } public string Name { get; } public Region Region { get; } public bool Contains(Point3D p) => Region.Contains(p); public static void Initialize() { var list = Map.Felucca.Regions.Values; if (list.Count == 0) { return; } _table = new Dictionary(); foreach (var r in list) { if (r is DungeonRegion or TownRegion && r.Name != null) { _table[r.Name] = new EDI(r.Name, r); } } // Validate that we have at least one valid destination var validTown = false; var townNamesVariable = Core.ML ? nameof(BaseEscortable.MlTownNames) : nameof(BaseEscortable.TownNames); var towns = Core.ML ? BaseEscortable.MlTownNames : BaseEscortable.TownNames; for (var i = 0; i < towns.Length; ++i) { var town = towns[i]; if (_table.ContainsKey(town)) { validTown = true; break; } } BaseEscortable.Initialized = validTown; if (!validTown) { logger.Error("No valid escort destinations found. Please check {TownNames}.", townNamesVariable); } } public static EDI Find(string name) { if (name == null || _table == null) { return null; } _table.TryGetValue(name, out var info); return info; } } public class AskDestinationEntry : ContextMenuEntry { public AskDestinationEntry() : base(6100, 3) { } public override void OnClick(Mobile from, IEntity target) { (target as BaseEscortable)?.SayDestinationTo(from); } } public class AcceptEscortEntry : ContextMenuEntry { public AcceptEscortEntry() : base(6101, 3) { } public override void OnClick(Mobile from, IEntity target) { (target as BaseEscortable)?.AcceptEscorter(from); } } public class AbandonEscortEntry : ContextMenuEntry { public AbandonEscortEntry() : base(6102, 3) { } public override void OnClick(Mobile from, IEntity target) { (target as BaseEscortable)?.Delete(); // OSI just seems to delete instantly } }