ModernUO/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs

942 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Type, (int[], int)> _mlQuestTypes = new()
{
{ typeof(EscortToYew), (Array.Empty<int>(), 0) },
{ typeof(EscortToVesper), (Array.Empty<int>(), 0) },
{ typeof(EscortToTrinsic), (Array.Empty<int>(), 0) },
{ typeof(EscortToSkaraBrae), (Array.Empty<int>(), 0) },
{ typeof(EscortToSerpentsHold), (Array.Empty<int>(), 0) },
{ typeof(EscortToNujelm), (Array.Empty<int>(), 0) },
{ typeof(EscortToMoonglow), (Array.Empty<int>(), 0) },
{ typeof(EscortToMinoc), (Array.Empty<int>(), 0) },
{ typeof(EscortToMagincia), (Array.Empty<int>(), 0) },
{ typeof(EscortToJhelom), (Array.Empty<int>(), 0) },
{ typeof(EscortToCove), (Array.Empty<int>(), 0) },
{ typeof(EscortToBritain), (Array.Empty<int>(), 0) }
// Ocllo was removed in pub 56
// { typeof(EscortToOcllo), (Array.Empty<int>(), 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<Type, (int[], int)> _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?
// Im 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?
// Im 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.
// Im 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 - Im 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?
// Im 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?
// Im 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.
// Im 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.
// Im 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?
// Im 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?
// Im 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?
// Im 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.
// Im 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?
// Im 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> _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<Mobile, BaseEscortable> EscortTable { get; } = new();
protected override List<MLQuest> ConstructQuestList()
{
if (_mlQuestType == null)
{
var reg = Region;
var types = reg.IsPartOf("Haven Island") ? _mlQuestTypesNH : _mlQuestTypes;
// Get a rented buffer
var list = STArrayPool<Type>.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<Type>.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<ContextMenuEntry> 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.Veridia.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.Veridia.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<string, EDI> _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.Veridia.Regions.Values;
if (list.Count == 0)
{
return;
}
_table = new Dictionary<string, EDI>();
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
}
}