using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Threading; using Server.Guilds; using Server.Network; namespace Server { public static class World { private static readonly bool m_Metrics = Config.Get("General.Metrics", false); private static readonly ManualResetEvent m_DiskWriteHandle = new ManualResetEvent(true); private static Queue _addQueue, _deleteQueue; public static bool Saving { get; private set; } public static bool Loaded { get; private set; } public static bool Loading { get; private set; } public static readonly string MobileIndexPath = Path.Combine("Saves/Mobiles/", "Mobiles.idx"); public static readonly string MobileTypesPath = Path.Combine("Saves/Mobiles/", "Mobiles.tdb"); public static readonly string MobileDataPath = Path.Combine("Saves/Mobiles/", "Mobiles.bin"); public static readonly string ItemIndexPath = Path.Combine("Saves/Items/", "Items.idx"); public static readonly string ItemTypesPath = Path.Combine("Saves/Items/", "Items.tdb"); public static readonly string ItemDataPath = Path.Combine("Saves/Items/", "Items.bin"); public static readonly string GuildIndexPath = Path.Combine("Saves/Guilds/", "Guilds.idx"); public static readonly string GuildDataPath = Path.Combine("Saves/Guilds/", "Guilds.bin"); public static void NotifyDiskWriteComplete() { if (m_DiskWriteHandle.Set()) { Console.WriteLine("Closing Save Files. "); } } public static void WaitForWriteCompletion() { m_DiskWriteHandle.WaitOne(); } public static Dictionary Mobiles { get; private set; } public static Dictionary Items { get; private set; } public static bool OnDelete(IEntity entity) { if (Saving || Loading) { if (Saving) { AppendSafetyLog("delete", entity); } _deleteQueue.Enqueue(entity); return false; } return true; } public static void Broadcast(int hue, bool ascii, string text) { Broadcast(hue, ascii, AccessLevel.Player, text); } public static void Broadcast(int hue, bool ascii, AccessLevel access, string text) { var e = new WorldBroadcastEventArgs(hue, ascii, access, text); EventSink.InvokeWorldBroadcast(e); hue = e.Hue; ascii = e.Ascii; text = e.Text; access = e.Access; if (String.IsNullOrWhiteSpace(text)) { return; } Packet p; if (ascii) { p = new AsciiMessage(Serial.MinusOne, -1, MessageType.Regular, hue, 3, "System", text); } else { p = new UnicodeMessage(Serial.MinusOne, -1, MessageType.Regular, hue, 3, "ENU", "System", text); } var list = NetState.Instances; p.Acquire(); foreach (var s in list) { if (s.Mobile != null && s.Mobile.AccessLevel >= access) { s.Send(p); } } p.Release(); NetState.FlushAll(); } public static void Broadcast(int hue, bool ascii, string format, params object[] args) { Broadcast(hue, ascii, AccessLevel.Player, format, args); } public static void Broadcast(int hue, bool ascii, AccessLevel access, string format, params object[] args) { Broadcast(hue, ascii, access, String.Format(format, args)); } private interface IEntityEntry { Serial Serial { get; } int TypeID { get; } long Position { get; } int Length { get; } } private sealed class GuildEntry : IEntityEntry { public BaseGuild Guild { get; } public Serial Serial => Guild == null ? 0 : Guild.Id; public int TypeID => 0; public long Position { get; } public int Length { get; } public GuildEntry(BaseGuild g, long pos, int length) { Guild = g; Position = pos; Length = length; } } private sealed class ItemEntry : IEntityEntry { public Item Item { get; } public Serial Serial => Item == null ? Serial.MinusOne : Item.Serial; public int TypeID { get; } public string TypeName { get; } public long Position { get; } public int Length { get; } public ItemEntry(Item item, int typeID, string typeName, long pos, int length) { Item = item; TypeID = typeID; TypeName = typeName; Position = pos; Length = length; } } private sealed class MobileEntry : IEntityEntry { public Mobile Mobile { get; } public Serial Serial => Mobile == null ? Serial.MinusOne : Mobile.Serial; public int TypeID { get; } public string TypeName { get; } public long Position { get; } public int Length { get; } public MobileEntry(Mobile mobile, int typeID, string typeName, long pos, int length) { Mobile = mobile; TypeID = typeID; TypeName = typeName; Position = pos; Length = length; } } public static string LoadingType { get; private set; } private static readonly Type[] m_SerialTypeArray = new Type[1] { typeof(Serial) }; private static List> ReadTypes(BinaryReader tdbReader) { var count = tdbReader.ReadInt32(); var types = new List>(count); for (var i = 0; i < count; ++i) { var typeName = tdbReader.ReadString(); var t = ScriptCompiler.FindTypeByFullName(typeName); if (t == null) { Console.WriteLine("failed"); if (!Core.Service) { Console.WriteLine("Error: Type '{0}' was not found. Delete all of those types? (y/n)", typeName); if (Console.ReadKey(true).Key == ConsoleKey.Y) { types.Add(null); Utility.PushColor(ConsoleColor.Yellow); Console.Write("World: Loading..."); Utility.PopColor(); continue; } Console.WriteLine("Types will not be deleted. An exception will be thrown."); } else { Console.WriteLine("Error: Type '{0}' was not found.", typeName); } throw new Exception(String.Format("Missing type '{0}'", typeName)); } if (t.IsAbstract) { foreach (var at in ScriptCompiler.FindTypesByFullName(t.FullName)) { if (at != t && !at.IsAbstract) { t = at; typeName = at.FullName; break; } } if (t.IsAbstract) { Console.WriteLine("failed"); if (!Core.Service) { Console.WriteLine("Error: Type '{0}' is abstract. Delete all of those types? (y/n)", typeName); if (Console.ReadKey(true).Key == ConsoleKey.Y) { types.Add(null); Utility.PushColor(ConsoleColor.Yellow); Console.Write("World: Loading..."); Utility.PopColor(); continue; } Console.WriteLine("Types will not be deleted. An exception will be thrown."); } else { Console.WriteLine("Error: Type '{0}' is abstract.", typeName); } throw new Exception(String.Format("Abstract type '{0}'", typeName)); } } var ctor = t.GetConstructor(m_SerialTypeArray); if (ctor != null) { types.Add(new Tuple(ctor, typeName)); } else { throw new Exception(String.Format("Type '{0}' does not have a serialization constructor", t)); } } return types; } public static void Load() { if (Loaded) { return; } Loaded = true; LoadingType = null; Utility.PushColor(ConsoleColor.Yellow); Console.WriteLine("World: Loading..."); Utility.PopColor(); var watch = Stopwatch.StartNew(); Loading = true; _addQueue = new Queue(); _deleteQueue = new Queue(); var ctorArgs = new object[1]; var items = new List(); var mobiles = new List(); var guilds = new List(); if (File.Exists(MobileIndexPath) && File.Exists(MobileTypesPath)) { using (var idx = new FileStream(MobileIndexPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { var idxReader = new BinaryReader(idx); using (var tdb = new FileStream(MobileTypesPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { var tdbReader = new BinaryReader(tdb); var types = ReadTypes(tdbReader); var mobileCount = idxReader.ReadInt32(); Mobiles = new Dictionary(mobileCount); for (var i = 0; i < mobileCount; ++i) { var typeID = idxReader.ReadInt32(); var serial = idxReader.ReadInt32(); var pos = idxReader.ReadInt64(); var length = idxReader.ReadInt32(); var objs = types[typeID]; if (objs == null) { continue; } Mobile m = null; var ctor = objs.Item1; var typeName = objs.Item2; try { ctorArgs[0] = (Serial)serial; m = (Mobile)ctor.Invoke(ctorArgs); } catch (Exception ex) { Diagnostics.ExceptionLogging.LogException(ex); } if (m != null) { mobiles.Add(new MobileEntry(m, typeID, typeName, pos, length)); AddMobile(m); } } tdbReader.Close(); } idxReader.Close(); } } else { Mobiles = new Dictionary(); } if (File.Exists(ItemIndexPath) && File.Exists(ItemTypesPath)) { using (var idx = new FileStream(ItemIndexPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { var idxReader = new BinaryReader(idx); using (var tdb = new FileStream(ItemTypesPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { var tdbReader = new BinaryReader(tdb); var types = ReadTypes(tdbReader); var itemCount = idxReader.ReadInt32(); Items = new Dictionary(itemCount); for (var i = 0; i < itemCount; ++i) { var typeID = idxReader.ReadInt32(); var serial = idxReader.ReadInt32(); var pos = idxReader.ReadInt64(); var length = idxReader.ReadInt32(); var objs = types[typeID]; if (objs == null) { continue; } Item item = null; var ctor = objs.Item1; var typeName = objs.Item2; try { ctorArgs[0] = (Serial)serial; item = (Item)ctor.Invoke(ctorArgs); } catch (Exception e) { Diagnostics.ExceptionLogging.LogException(e); } if (item != null) { items.Add(new ItemEntry(item, typeID, typeName, pos, length)); AddItem(item); } } tdbReader.Close(); } idxReader.Close(); } } else { Items = new Dictionary(); } if (File.Exists(GuildIndexPath)) { using (var idx = new FileStream(GuildIndexPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { var idxReader = new BinaryReader(idx); var guildCount = idxReader.ReadInt32(); var createEventArgs = new CreateGuildEventArgs(-1); for (var i = 0; i < guildCount; ++i) { idxReader.ReadInt32(); //no typeid for guilds var id = idxReader.ReadInt32(); var pos = idxReader.ReadInt64(); var length = idxReader.ReadInt32(); createEventArgs.Id = id; EventSink.InvokeCreateGuild(createEventArgs); var guild = createEventArgs.Guild; if (guild != null) { guilds.Add(new GuildEntry(guild, pos, length)); } } idxReader.Close(); } } bool failedMobiles = false, failedItems = false, failedGuilds = false; Type failedType = null; var failedSerial = Serial.Zero; Exception failed = null; var failedTypeID = 0; if (File.Exists(MobileDataPath)) { using (var bin = new FileStream(MobileDataPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { var reader = new BinaryFileReader(new BinaryReader(bin)); for (var i = 0; i < mobiles.Count; ++i) { var entry = mobiles[i]; var m = entry.Mobile; if (m != null) { reader.Seek(entry.Position, SeekOrigin.Begin); try { LoadingType = entry.TypeName; m.Deserialize(reader); if (reader.Position != (entry.Position + entry.Length)) { throw new Exception(String.Format("***** Bad serialize on {0} *****", m.GetType())); } } catch (Exception e) { mobiles.RemoveAt(i); failed = e; failedMobiles = true; failedType = m.GetType(); failedTypeID = entry.TypeID; failedSerial = m.Serial; break; } } } reader.Close(); } } if (!failedMobiles && File.Exists(ItemDataPath)) { using (var bin = new FileStream(ItemDataPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { var reader = new BinaryFileReader(new BinaryReader(bin)); for (var i = 0; i < items.Count; ++i) { var entry = items[i]; var item = entry.Item; if (item != null) { reader.Seek(entry.Position, SeekOrigin.Begin); try { LoadingType = entry.TypeName; item.Deserialize(reader); if (reader.Position != (entry.Position + entry.Length)) { throw new Exception(String.Format("***** Bad serialize on {0} *****", item.GetType())); } } catch (Exception e) { items.RemoveAt(i); failed = e; failedItems = true; failedType = item.GetType(); failedTypeID = entry.TypeID; failedSerial = item.Serial; break; } } } reader.Close(); } } LoadingType = null; if (!failedMobiles && !failedItems && File.Exists(GuildDataPath)) { using (var bin = new FileStream(GuildDataPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { var reader = new BinaryFileReader(new BinaryReader(bin)); for (var i = 0; i < guilds.Count; ++i) { var entry = guilds[i]; var g = entry.Guild; if (g != null) { reader.Seek(entry.Position, SeekOrigin.Begin); try { g.Deserialize(reader); if (reader.Position != (entry.Position + entry.Length)) { throw new Exception(String.Format("***** Bad serialize on Guild {0} *****", g.Id)); } } catch (Exception e) { guilds.RemoveAt(i); failed = e; failedGuilds = true; failedType = typeof(BaseGuild); failedTypeID = g.Id; failedSerial = g.Id; break; } } } reader.Close(); } } if (failedItems || failedMobiles || failedGuilds) { Utility.PushColor(ConsoleColor.Red); Console.WriteLine("An error was encountered while loading a saved object"); Utility.PopColor(); Console.WriteLine(" - Type: {0}", failedType); Console.WriteLine(" - Serial: {0}", failedSerial); if (!Core.Service) { Console.WriteLine("Delete the object? (y/n)"); if (Console.ReadKey(true).Key == ConsoleKey.Y) { if (failedType != typeof(BaseGuild)) { Console.WriteLine("Delete all objects of that type? (y/n)"); if (Console.ReadKey(true).Key == ConsoleKey.Y) { if (failedMobiles) { for (var i = 0; i < mobiles.Count;) { if (mobiles[i].TypeID == failedTypeID) { mobiles.RemoveAt(i); } else { ++i; } } } else if (failedItems) { for (var i = 0; i < items.Count;) { if (items[i].TypeID == failedTypeID) { items.RemoveAt(i); } else { ++i; } } } } } SaveIndex(mobiles, MobileIndexPath); SaveIndex(items, ItemIndexPath); SaveIndex(guilds, GuildIndexPath); } Console.WriteLine("After pressing return an exception will be thrown and the server will terminate."); Console.ReadLine(); } else { Utility.PushColor(ConsoleColor.Red); Console.WriteLine("An exception will be thrown and the server will terminate."); Utility.PopColor(); } throw new Exception( String.Format( "Load failed (items={0}, mobiles={1}, guilds={2}, type={3}, serial={4})", failedItems, failedMobiles, failedGuilds, failedType, failedSerial), failed); } EventSink.InvokeWorldLoad(); Loading = false; ProcessSafetyQueues(); foreach (var item in Items.Values) { if (item.Parent == null) item.UpdateTotals(); item.ClearProperties(); } foreach (var m in Mobiles.Values) { m.UpdateRegion(); // Is this really needed? m.UpdateTotals(); m.ClearProperties(); } watch.Stop(); Utility.PushColor(ConsoleColor.Green); Console.WriteLine( "...done ({1} items, {2} mobiles) ({0:F2} seconds)", watch.Elapsed.TotalSeconds, Items.Count, Mobiles.Count); Utility.PopColor(); } private static void ProcessSafetyQueues() { while (_addQueue.Count > 0) { var entity = _addQueue.Dequeue(); if (entity is Item item) { AddItem(item); } else if (entity is Mobile mob) { AddMobile(mob); } } while (_deleteQueue.Count > 0) { var entity = _deleteQueue.Dequeue(); if (entity is Item item) { item.Delete(); } else if (entity is Mobile mob) { mob.Delete(); } } } private static void AppendSafetyLog(string action, IEntity entity) { var message = String.Format( "Warning: Attempted to {1} {2} during world save." + "{0}This action could cause inconsistent state." + "{0}It is strongly advised that the offending scripts be corrected.", Environment.NewLine, action, entity); AppendSafetyLog(message); } private static void AppendSafetyLog(string message) { Console.WriteLine(message); try { using (var op = new StreamWriter("world-save-errors.log", true)) { op.WriteLine("{0}\t{1}", DateTime.UtcNow, message); op.WriteLine(new StackTrace(2).ToString()); op.WriteLine(); } } catch (Exception ex) { Diagnostics.ExceptionLogging.LogException(ex); } } private static void SaveIndex(List list, string path) where T : IEntityEntry { if (!Directory.Exists("Saves/Mobiles/")) { Directory.CreateDirectory("Saves/Mobiles/"); } if (!Directory.Exists("Saves/Items/")) { Directory.CreateDirectory("Saves/Items/"); } if (!Directory.Exists("Saves/Guilds/")) { Directory.CreateDirectory("Saves/Guilds/"); } using (var idx = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) { var idxWriter = new BinaryWriter(idx); idxWriter.Write(list.Count); for (var i = 0; i < list.Count; ++i) { var e = list[i]; idxWriter.Write(e.TypeID); idxWriter.Write(e.Serial); idxWriter.Write(e.Position); idxWriter.Write(e.Length); } idxWriter.Close(); } } static internal int m_Saves; public static void Save() { Save(true, false); } public static void Save(bool message, bool permitBackgroundWrite) { if (Saving) { return; } ++m_Saves; NetState.FlushAll(); NetState.Pause(); WaitForWriteCompletion(); //Blocks Save until current disk flush is done. Saving = true; m_DiskWriteHandle.Reset(); if (message) { Broadcast(0x35, false, AccessLevel.Player, "The world is saving, please wait."); } var strategy = SaveStrategy.Acquire(); Console.WriteLine("Core: Using {0} save strategy", strategy.Name.ToLowerInvariant()); Console.WriteLine("World: Saving..."); var watch = Stopwatch.StartNew(); if (!Directory.Exists("Saves/Mobiles/")) { Directory.CreateDirectory("Saves/Mobiles/"); } if (!Directory.Exists("Saves/Items/")) { Directory.CreateDirectory("Saves/Items/"); } if (!Directory.Exists("Saves/Guilds/")) { Directory.CreateDirectory("Saves/Guilds/"); } try { EventSink.InvokeBeforeWorldSave(new BeforeWorldSaveEventArgs()); } catch (Exception e) { throw new Exception("FATAL: Exception in EventSink.BeforeWorldSave", e); } if (m_Metrics) { using (var metrics = new SaveMetrics()) strategy.Save(metrics, permitBackgroundWrite); } else { strategy.Save(null, permitBackgroundWrite); } try { EventSink.InvokeWorldSave(new WorldSaveEventArgs(message)); } catch (Exception e) { throw new Exception("FATAL: Exception in EventSink.WorldSave", e); } watch.Stop(); Saving = false; if (!permitBackgroundWrite) { NotifyDiskWriteComplete(); //Sets the DiskWriteHandle. If we allow background writes, we leave this upto the individual save strategies. } ProcessSafetyQueues(); strategy.ProcessDecay(); Console.WriteLine("Save finished in {0:F2} seconds.", watch.Elapsed.TotalSeconds); if (message) { Broadcast(0x35, false, AccessLevel.Player, "World save done in {0:F1} seconds.", watch.Elapsed.TotalSeconds); } NetState.Resume(); try { EventSink.InvokeAfterWorldSave(new AfterWorldSaveEventArgs()); } catch (Exception e) { throw new Exception("FATAL: Exception in EventSink.AfterWorldSave", e); } } static internal List m_ItemTypes = new List(); static internal List m_MobileTypes = new List(); public static IEntity FindEntity(Serial serial) { if (serial.IsItem) { return FindItem(serial); } else if (serial.IsMobile) { return FindMobile(serial); } return null; } public static Mobile FindMobile(Serial serial) { Mobiles.TryGetValue(serial, out var mob); return mob; } public static void AddMobile(Mobile m) { if (Saving) { AppendSafetyLog("add", m); _addQueue.Enqueue(m); } else { Mobiles[m.Serial] = m; } } public static Item FindItem(Serial serial) { Items.TryGetValue(serial, out var item); return item; } public static void AddItem(Item item) { if (Saving) { AppendSafetyLog("add", item); _addQueue.Enqueue(item); } else { Items[item.Serial] = item; } } public static void RemoveMobile(Mobile m) { Mobiles.Remove(m.Serial); } public static void RemoveItem(Item item) { Items.Remove(item.Serial); } } }