using System; using System.Collections.Generic; using Server; using Server.Commands; using Server.Engines.MLQuests.Definitions; using Server.Engines.MLQuests.Gumps; using Server.Engines.MLQuests.Objectives; using Server.Gumps; using Server.Mobiles; using Server.Network; using System.Collections; using Server.Commands.Generic; using Server.Items; using Server.Targeting; using System.IO; namespace Server.Engines.MLQuests { public static class MLQuestSystem { public static bool Enabled { get { return Core.ML; } } public const int MaxConcurrentQuests = 10; public const int SpeechColor = 0x3B2; public static readonly bool AutoGenerateNew = true; public static readonly bool Debug = false; private static Dictionary m_Quests; private static Dictionary> m_QuestGivers; private static Dictionary m_Contexts; public static readonly List EmptyList = new List(); public static Dictionary Quests { get { return m_Quests; } } public static Dictionary> QuestGivers { get { return m_QuestGivers; } } public static Dictionary Contexts { get { return m_Contexts; } } static MLQuestSystem() { m_Quests = new Dictionary(); m_QuestGivers = new Dictionary>(); m_Contexts = new Dictionary(); string cfgPath = Path.Combine( Core.BaseDirectory, Path.Combine( "Data", "MLQuests.cfg" ) ); Type baseQuestType = typeof( MLQuest ); Type baseQuesterType = typeof( IQuestGiver ); if ( File.Exists( cfgPath ) ) { using ( StreamReader sr = new StreamReader( cfgPath ) ) { string line; while ( ( line = sr.ReadLine() ) != null ) { if ( line.Length == 0 || line.StartsWith( "#" ) ) continue; string[] split = line.Split( '\t' ); Type type = ScriptCompiler.FindTypeByName( split[0] ); if ( type == null || !baseQuestType.IsAssignableFrom( type ) ) { if ( Debug ) Console.WriteLine( "Warning: {1} quest type '{0}'", split[0], ( type == null ) ? "Unknown" : "Invalid" ); continue; } MLQuest quest = null; try { quest = Activator.CreateInstance( type ) as MLQuest; } catch { } if ( quest == null ) continue; Register( type, quest ); for ( int i = 1; i < split.Length; ++i ) { Type questerType = ScriptCompiler.FindTypeByName( split[i] ); if ( questerType == null || !baseQuesterType.IsAssignableFrom( questerType ) ) { if ( Debug ) Console.WriteLine( "Warning: {1} quester type '{0}'", split[i], ( questerType == null ) ? "Unknown" : "Invalid" ); continue; } RegisterQuestGiver( quest, questerType ); } } } } } private static void Register( Type type, MLQuest quest ) { m_Quests[type] = quest; } private static void RegisterQuestGiver( MLQuest quest, Type questerType ) { List questList; if ( !m_QuestGivers.TryGetValue( questerType, out questList ) ) m_QuestGivers[questerType] = questList = new List(); questList.Add( quest ); } public static void Register( MLQuest quest, params Type[] questerTypes ) { Register( quest.GetType(), quest ); foreach ( Type questerType in questerTypes ) RegisterQuestGiver( quest, questerType ); } public static void Initialize() { if ( !Enabled ) return; if ( AutoGenerateNew ) { foreach ( MLQuest quest in m_Quests.Values ) { if ( quest != null && !quest.Deserialized ) quest.Generate(); } } MLQuestPersistence.EnsureExistence(); CommandSystem.Register( "MLQuestsInfo", AccessLevel.Administrator, new CommandEventHandler( MLQuestsInfo_OnCommand ) ); CommandSystem.Register( "SaveQuest", AccessLevel.Administrator, new CommandEventHandler( SaveQuest_OnCommand ) ); CommandSystem.Register( "SaveAllQuests", AccessLevel.Administrator, new CommandEventHandler( SaveAllQuests_OnCommand ) ); CommandSystem.Register( "InvalidQuestItems", AccessLevel.Administrator, new CommandEventHandler( InvalidQuestItems_OnCommand ) ); TargetCommands.Register( new ViewQuestsCommand() ); TargetCommands.Register( new ViewContextCommand() ); EventSink.QuestGumpRequest += new QuestGumpRequestHandler( EventSink_QuestGumpRequest ); } [Usage( "MLQuestsInfo" )] [Description( "Displays general information about the ML quest system, or a quest by type name." )] public static void MLQuestsInfo_OnCommand( CommandEventArgs e ) { Mobile m = e.Mobile; if ( e.Length == 0 ) { m.SendMessage( "Quest table length: {0}", m_Quests.Count ); return; } Type index = ScriptCompiler.FindTypeByName( e.GetString( 0 ) ); MLQuest quest; if ( index == null || !m_Quests.TryGetValue( index, out quest ) ) { m.SendMessage( "Invalid quest type name." ); return; } m.SendMessage( "Activated: {0}", quest.Activated ); m.SendMessage( "Number of objectives: {0}", quest.Objectives.Count ); m.SendMessage( "Objective type: {0}", quest.ObjectiveType ); m.SendMessage( "Number of active instances: {0}", quest.Instances.Count ); } public class ViewQuestsCommand : BaseCommand { public ViewQuestsCommand() { AccessLevel = AccessLevel.GameMaster; Supports = CommandSupport.Simple; Commands = new string[] { "ViewQuests" }; ObjectTypes = ObjectTypes.Mobiles; Usage = "ViewQuests"; Description = "Displays a targeted mobile's quest overview."; } public override void Execute( CommandEventArgs e, object obj ) { Mobile from = e.Mobile; PlayerMobile pm = obj as PlayerMobile; if ( pm == null ) { LogFailure( "That is not a player." ); return; } CommandLogging.WriteLine( from, "{0} {1} viewing quest overview of {2}", from.AccessLevel, CommandLogging.Format( from ), CommandLogging.Format( pm ) ); from.SendGump( new QuestLogGump( pm, false ) ); } } private class ViewContextCommand : BaseCommand { public ViewContextCommand() { AccessLevel = AccessLevel.GameMaster; Supports = CommandSupport.Simple; Commands = new string[] { "ViewMLContext" }; ObjectTypes = ObjectTypes.Mobiles; Usage = "ViewMLContext"; Description = "Opens the ML quest context for a targeted mobile."; } public override void Execute( CommandEventArgs e, object obj ) { PlayerMobile pm = obj as PlayerMobile; if ( pm == null ) LogFailure( "They have no ML quest context." ); else e.Mobile.SendGump( new PropertiesGump( e.Mobile, GetOrCreateContext( pm ) ) ); } } [Usage( "SaveQuest [saveEnabled=true]" )] [Description( "Allows serialization for a specific quest to be turned on or off." )] public static void SaveQuest_OnCommand( CommandEventArgs e ) { Mobile m = e.Mobile; if ( e.Length == 0 || e.Length > 2 ) { m.SendMessage( "Syntax: SaveQuest [saveEnabled=true]" ); return; } Type index = ScriptCompiler.FindTypeByName( e.GetString( 0 ) ); MLQuest quest; if ( index == null || !m_Quests.TryGetValue( index, out quest ) ) { m.SendMessage( "Invalid quest type name." ); return; } bool enable = ( e.Length == 2 ) ? e.GetBoolean( 1 ) : true; quest.SaveEnabled = enable; m.SendMessage( "Serialization for quest {0} is now {1}.", quest.GetType().Name, enable ? "enabled" : "disabled" ); if ( AutoGenerateNew && !enable ) m.SendMessage( "Please note that automatic generation of new quests is ON. This quest will be regenerated on the next server start." ); } [Usage( "SaveAllQuests [saveEnabled=true]" )] [Description( "Allows serialization for all quests to be turned on or off." )] public static void SaveAllQuests_OnCommand( CommandEventArgs e ) { Mobile m = e.Mobile; if ( e.Length > 1 ) { m.SendMessage( "Syntax: SaveAllQuests [saveEnabled=true]" ); return; } bool enable = ( e.Length == 1 ) ? e.GetBoolean( 0 ) : true; foreach ( MLQuest quest in m_Quests.Values ) quest.SaveEnabled = enable; m.SendMessage( "Serialization for all quests is now {0}.", enable ? "enabled" : "disabled" ); if ( AutoGenerateNew && !enable ) m.SendMessage( "Please note that automatic generation of new quests is ON. All quests will be regenerated on the next server start." ); } [Usage( "InvalidQuestItems" )] [Description( "Provides an overview of all quest items not located in the top-level of a player's backpack." )] public static void InvalidQuestItems_OnCommand( CommandEventArgs e ) { Mobile m = e.Mobile; ArrayList found = new ArrayList(); foreach ( Item item in World.Items.Values ) { if ( item.QuestItem ) { Backpack pack = item.Parent as Backpack; if ( pack != null ) { PlayerMobile player = pack.Parent as PlayerMobile; if ( player != null && player.Backpack == pack ) continue; } found.Add( item ); } } if ( found.Count == 0 ) m.SendMessage( "No matching objects found." ); else m.SendGump( new InterfaceGump( m, new string[] { "Object" }, found, 0, null ) ); } private static bool FindQuest( IQuestGiver quester, PlayerMobile pm, MLQuestContext context, out MLQuest quest, out MLQuestInstance entry ) { quest = null; entry = null; List quests = quester.MLQuests; Type questerType = quester.GetType(); // 1. Check quests in progress with this NPC (overriding deliveries is intended) if ( context != null ) { foreach ( MLQuest questEntry in quests ) { MLQuestInstance instance = context.FindInstance( questEntry ); if ( instance != null && ( instance.Quester == quester || ( !questEntry.IsEscort && instance.QuesterType == questerType ) ) ) { entry = instance; quest = questEntry; return true; } } } // 2. Check deliveries (overriding chain offers is intended) if ( ( entry = HandleDelivery( pm, quester, questerType ) ) != null ) { quest = entry.Quest; return true; } // 3. Check chain quest offers if ( context != null ) { foreach ( MLQuest questEntry in quests ) { if ( questEntry.IsChainTriggered && context.ChainOffers.Contains( questEntry ) ) { quest = questEntry; return true; } } } // 4. Random quest quest = RandomStarterQuest( quester, pm, context ); return ( quest != null ); } public static void OnDoubleClick( IQuestGiver quester, PlayerMobile pm ) { if ( quester.Deleted || !pm.Alive ) return; MLQuestContext context = GetContext( pm ); MLQuest quest; MLQuestInstance entry; if ( !FindQuest( quester, pm, context, out quest, out entry ) ) { Tell( quester, pm, 1080107 ); // I'm sorry, I have nothing for you at this time. return; } if ( entry != null ) { TurnToFace( quester, pm ); if ( entry.Failed ) return; // Note: OSI sends no gump at all for failed quests, they have to be cancelled in the quest overview else if ( entry.ClaimReward ) entry.SendRewardOffer(); else if ( entry.IsCompleted() ) entry.SendReportBackGump(); else entry.SendProgressGump(); } else if ( quest.CanOffer( quester, pm, context, true ) ) { TurnToFace( quester, pm ); quest.SendOffer( quester, pm ); } } public static bool CanMarkQuestItem( PlayerMobile pm, Item item, Type type ) { MLQuestContext context = GetContext( pm ); if ( context != null ) { foreach ( MLQuestInstance quest in context.QuestInstances ) { if ( !quest.ClaimReward && quest.AllowsQuestItem( item, type ) ) return true; } } return false; } private static void OnMarkQuestItem( PlayerMobile pm, Item item, Type type ) { MLQuestContext context = GetContext( pm ); if ( context == null ) return; List instances = context.QuestInstances; // We don't foreach because CheckComplete() can potentially modify the MLQuests list for ( int i = instances.Count - 1; i >= 0; --i ) { MLQuestInstance instance = instances[i]; if ( instance.ClaimReward ) continue; foreach ( BaseObjectiveInstance objective in instance.Objectives ) { if ( !objective.Expired && objective.AllowsQuestItem( item, type ) ) { objective.CheckComplete(); // yes, this can happen multiple times (for multiple quests) break; } } } } public static bool MarkQuestItem( PlayerMobile pm, Item item ) { Type type = item.GetType(); if ( CanMarkQuestItem( pm, item, type ) ) { item.QuestItem = true; OnMarkQuestItem( pm, item, type ); return true; } return false; } public static void HandleSkillGain( PlayerMobile pm, SkillName skill ) { MLQuestContext context = GetContext( pm ); if ( context == null ) return; List instances = context.QuestInstances; for ( int i = instances.Count - 1; i >= 0; --i ) { MLQuestInstance instance = instances[i]; if ( instance.ClaimReward ) continue; foreach ( BaseObjectiveInstance objective in instance.Objectives ) { if ( !objective.Expired && objective is GainSkillObjectiveInstance && ( (GainSkillObjectiveInstance)objective ).Handles( skill ) ) { objective.CheckComplete(); break; } } } } public static void HandleKill( PlayerMobile pm, Mobile mob ) { MLQuestContext context = GetContext( pm ); if ( context == null ) return; List instances = context.QuestInstances; Type type = null; for ( int i = instances.Count - 1; i >= 0; --i ) { MLQuestInstance instance = instances[i]; if ( instance.ClaimReward ) continue; /* A kill only counts for a single objective within a quest, * but it can count for multiple quests. This is something not * currently observable on OSI, so it is assumed behavior. */ foreach ( BaseObjectiveInstance objective in instance.Objectives ) { if ( !objective.Expired && objective is KillObjectiveInstance ) { KillObjectiveInstance kill = (KillObjectiveInstance)objective; if ( type == null ) type = mob.GetType(); if ( kill.AddKill( mob, type ) ) { kill.CheckComplete(); break; } } } } } public static MLQuestInstance HandleDelivery( PlayerMobile pm, IQuestGiver quester, Type questerType ) { MLQuestContext context = GetContext( pm ); if ( context == null ) return null; List instances = context.QuestInstances; MLQuestInstance deliverInstance = null; for ( int i = instances.Count - 1; i >= 0; --i ) { MLQuestInstance instance = instances[i]; // Do NOT skip quests on ClaimReward, because the quester still needs the quest ref! //if ( instance.ClaimReward ) // continue; foreach ( BaseObjectiveInstance objective in instance.Objectives ) { // Note: On OSI, expired deliveries can still be completed. Bug? if ( !objective.Expired && objective is DeliverObjectiveInstance ) { DeliverObjectiveInstance deliver = (DeliverObjectiveInstance)objective; if ( deliver.IsDestination( quester, questerType ) ) { if ( !deliver.HasCompleted ) // objective completes only once { deliver.HasCompleted = true; deliver.CheckComplete(); // The quest is continued with this NPC (important for chains) instance.Quester = quester; } if ( deliverInstance == null ) deliverInstance = instance; break; // don't return, we may have to complete more deliveries } } } } return deliverInstance; } public static MLQuestContext GetContext( PlayerMobile pm ) { MLQuestContext context; m_Contexts.TryGetValue( pm, out context ); return context; } public static MLQuestContext GetOrCreateContext( PlayerMobile pm ) { MLQuestContext context; if ( !m_Contexts.TryGetValue( pm, out context ) ) m_Contexts[pm] = context = new MLQuestContext( pm ); return context; } public static void HandleDeath( PlayerMobile pm ) { MLQuestContext context = GetContext( pm ); if ( context != null ) context.HandleDeath(); } public static void HandleDeletion( PlayerMobile pm ) { MLQuestContext context = GetContext( pm ); if ( context != null ) { context.HandleDeletion(); m_Contexts.Remove( pm ); } } public static void HandleDeletion( IQuestGiver quester ) { foreach ( MLQuest quest in quester.MLQuests ) { List instances = quest.Instances; for ( int i = instances.Count - 1; i >= 0; --i ) { MLQuestInstance instance = instances[i]; if ( instance.Quester == quester ) instance.OnQuesterDeleted(); } } } public static void EventSink_QuestGumpRequest( QuestGumpRequestArgs args ) { PlayerMobile pm = args.Mobile as PlayerMobile; if ( !Enabled || pm == null ) return; pm.SendGump( new QuestLogGump( pm ) ); } private static List m_EligiblePool = new List(); public static MLQuest RandomStarterQuest( IQuestGiver quester, PlayerMobile pm, MLQuestContext context ) { List quests = quester.MLQuests; if ( quests.Count == 0 ) return null; m_EligiblePool.Clear(); MLQuest fallback = null; foreach ( MLQuest quest in quests ) { if ( quest.IsChainTriggered || ( context != null && context.IsDoingQuest( quest ) ) ) continue; /* * Save first quest that reaches the CanOffer call. * If no quests are valid at all, return this quest for displaying the CanOffer error message. */ if ( fallback == null ) fallback = quest; if ( quest.CanOffer( quester, pm, context, false ) ) m_EligiblePool.Add( quest ); } if ( m_EligiblePool.Count == 0 ) return fallback; return m_EligiblePool[Utility.Random( m_EligiblePool.Count )]; } public static void TurnToFace( IQuestGiver quester, Mobile mob ) { if ( quester is Mobile ) { Mobile m = (Mobile)quester; m.Direction = m.GetDirectionTo( mob ); } } public static void Tell( IQuestGiver quester, PlayerMobile pm, int cliloc ) { TurnToFace( quester, pm ); if ( quester is Mobile ) ((Mobile)quester).PrivateOverheadMessage( MessageType.Regular, SpeechColor, cliloc, pm.NetState ); else if ( quester is Item ) MessageHelper.SendLocalizedMessageTo( (Item)quester, pm, cliloc, SpeechColor ); else pm.SendLocalizedMessage( cliloc, "", SpeechColor ); } public static void Tell( IQuestGiver quester, PlayerMobile pm, int cliloc, string args ) { TurnToFace( quester, pm ); if ( quester is Mobile ) ((Mobile)quester).PrivateOverheadMessage( MessageType.Regular, SpeechColor, cliloc, args, pm.NetState ); else if ( quester is Item ) MessageHelper.SendLocalizedMessageTo( (Item)quester, pm, cliloc, args, SpeechColor ); else pm.SendLocalizedMessage( cliloc, args, SpeechColor ); } public static void Tell( IQuestGiver quester, PlayerMobile pm, string message ) { TurnToFace( quester, pm ); if ( quester is Mobile ) ((Mobile)quester).PrivateOverheadMessage( MessageType.Regular, SpeechColor, false, message, pm.NetState ); else if ( quester is Item ) MessageHelper.SendMessageTo( (Item)quester, pm, message, SpeechColor ); else pm.SendMessage( SpeechColor, message ); } public static void TellDef( IQuestGiver quester, PlayerMobile pm, TextDefinition def ) { if ( def == null ) return; if ( def.Number > 0 ) Tell( quester, pm, def.Number ); else if ( def.String != null ) Tell( quester, pm, def.String ); } public static void WriteQuestRef( GenericWriter writer, MLQuest quest ) { writer.Write( ( quest != null && quest.SaveEnabled ) ? quest.GetType().FullName : null ); } public static MLQuest ReadQuestRef( GenericReader reader ) { string typeName = reader.ReadString(); if ( typeName == null ) return null; // not serialized Type questType = ScriptCompiler.FindTypeByFullName( typeName ); if ( questType == null ) return null; // no longer a type return FindQuest( questType ); } public static MLQuest FindQuest( Type questType ) { MLQuest result; m_Quests.TryGetValue( questType, out result ); return result; } public static List FindQuestList( Type questerType ) { List result; if ( m_QuestGivers.TryGetValue( questerType, out result ) ) return result; return EmptyList; } } }