diff --git a/Projects/Server/Attributes.cs b/Projects/Server/Attributes.cs index e0458364d..e0ea130d2 100644 --- a/Projects/Server/Attributes.cs +++ b/Projects/Server/Attributes.cs @@ -125,6 +125,30 @@ public class ConstructibleAttribute : Attribute public AccessLevel AccessLevel { get; set; } } +[AttributeUsage(AttributeTargets.Method)] +public class UsageAttribute : Attribute +{ + public UsageAttribute(string usage) => Usage = usage; + + public string Usage { get; } +} + +[AttributeUsage(AttributeTargets.Method)] +public class DescriptionAttribute : Attribute +{ + public DescriptionAttribute(string description) => Description = description; + + public string Description { get; } +} + +[AttributeUsage(AttributeTargets.Method)] +public class AliasesAttribute : Attribute +{ + public AliasesAttribute(params string[] aliases) => Aliases = aliases; + + public string[] Aliases { get; } +} + [AttributeUsage(AttributeTargets.Property)] public class CommandPropertyAttribute : Attribute { diff --git a/Projects/Server/Commands.cs b/Projects/Server/Commands.cs index bf8c4bb9f..6819dfe91 100644 --- a/Projects/Server/Commands.cs +++ b/Projects/Server/Commands.cs @@ -15,6 +15,9 @@ using System; using System.Collections.Generic; +using System.Reflection; +using System.Runtime.InteropServices; +using Server.Logging; namespace Server; @@ -125,6 +128,8 @@ public class CommandInfoSorter : IComparer public static class CommandSystem { + private static readonly ILogger logger = LogFactory.GetLogger(typeof(CommandSystem)); + public static string Prefix { get; set; } = "["; public static Dictionary Entries { get; } = new(StringComparer.OrdinalIgnoreCase); @@ -194,7 +199,38 @@ public static class CommandSystem public static void Register(string command, AccessLevel access, CommandEventHandler handler) { - Entries[command] = new CommandEntry(command, handler, access); + DoRegister(command, access, handler); + + var mi = handler.Method; + var aliasesAttr = mi.GetCustomAttribute(typeof(AliasesAttribute), false) as AliasesAttribute; + var aliases = aliasesAttr?.Aliases; + + if (aliases == null) + { + return; + } + + foreach (var alias in aliases) + { + DoRegister(alias, access, handler); + } + } + + private static void DoRegister(string command, AccessLevel accessLevel, CommandEventHandler handler) + { + ref var commandEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(Entries, command, out var exists); + if (exists) + { + if (commandEntry.AccessLevel == accessLevel && commandEntry.Handler == handler) + { + return; + } + + logger.Warning("Command {Command} already registered to {Handler}.", command, commandEntry.Handler.Method.Name); + return; + } + + commandEntry = new CommandEntry(command, handler, accessLevel); } public static bool Handle(Mobile from, string text, MessageType type = MessageType.Regular) diff --git a/Projects/Server/Json/JsonConfig.cs b/Projects/Server/Json/JsonConfig.cs index e38ae9637..f3b5fdda9 100644 --- a/Projects/Server/Json/JsonConfig.cs +++ b/Projects/Server/Json/JsonConfig.cs @@ -49,6 +49,7 @@ public static class JsonConfig options.Converters.Add(new IPEndPointConverterFactory()); options.Converters.Add(new TypeConverterFactory()); options.Converters.Add(new WorldLocationConverterFactory()); + options.Converters.Add(new TextDefinitionConverterFactory()); for (var i = 0; i < converters.Length; i++) { diff --git a/Projects/UOContent/Commands/Attributes.cs b/Projects/UOContent/Commands/Attributes.cs deleted file mode 100644 index 2abf56ce7..000000000 --- a/Projects/UOContent/Commands/Attributes.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -namespace Server -{ - [AttributeUsage(AttributeTargets.Method)] - public class UsageAttribute : Attribute - { - public UsageAttribute(string usage) => Usage = usage; - - public string Usage { get; } - } - - [AttributeUsage(AttributeTargets.Method)] - public class DescriptionAttribute : Attribute - { - public DescriptionAttribute(string description) => Description = description; - - public string Description { get; } - } - - [AttributeUsage(AttributeTargets.Method)] - public class AliasesAttribute : Attribute - { - public AliasesAttribute(params string[] aliases) => Aliases = aliases; - - public string[] Aliases { get; } - } -} diff --git a/Projects/UOContent/Commands/Handlers.cs b/Projects/UOContent/Commands/Handlers.cs index 0e20c1830..0201423b5 100644 --- a/Projects/UOContent/Commands/Handlers.cs +++ b/Projects/UOContent/Commands/Handlers.cs @@ -33,7 +33,6 @@ namespace Server.Commands Register("Where", AccessLevel.Counselor, Where_OnCommand); Register("AutoPageNotify", AccessLevel.Counselor, APN_OnCommand); - Register("APN", AccessLevel.Counselor, APN_OnCommand); Register("Animate", AccessLevel.GameMaster, Animate_OnCommand); @@ -47,12 +46,8 @@ namespace Server.Commands Register("Client", AccessLevel.Counselor, Client_OnCommand); Register("SMsg", AccessLevel.Counselor, StaffMessage_OnCommand); - Register("SM", AccessLevel.Counselor, StaffMessage_OnCommand); - Register("S", AccessLevel.Counselor, StaffMessage_OnCommand); Register("BCast", AccessLevel.GameMaster, BroadcastMessage_OnCommand); - Register("BC", AccessLevel.GameMaster, BroadcastMessage_OnCommand); - Register("B", AccessLevel.GameMaster, BroadcastMessage_OnCommand); Register("Bank", AccessLevel.GameMaster, Bank_OnCommand); @@ -736,14 +731,16 @@ namespace Server.Commands } } - [Usage("SMsg "), Aliases("S", "SM")] + [Usage("SMsg ")] + [Aliases("S", "SM")] [Description("Broadcasts a message to all online staff.")] public static void StaffMessage_OnCommand(CommandEventArgs e) { BroadcastMessage(AccessLevel.Counselor, e.Mobile.SpeechHue, $"[{e.Mobile.Name}] {e.ArgString}"); } - [Usage("BCast "), Aliases("B", "BC")] + [Usage("BCast ")] + [Aliases("B", "BC")] [Description("Broadcasts a message to everyone online.")] public static void BroadcastMessage_OnCommand(CommandEventArgs e) { @@ -764,7 +761,8 @@ namespace Server.Commands } } - [Usage("AutoPageNotify"), Aliases("APN")] + [Usage("AutoPageNotify")] + [Aliases("APN")] [Description("Toggles your auto-page-notify status.")] public static void APN_OnCommand(CommandEventArgs e) { @@ -782,8 +780,8 @@ namespace Server.Commands } } - [Usage("Animate "), - Description("Makes your character do a specified animation.")] + [Usage("Animate ")] + [Description("Makes your character do a specified animation.")] public static void Animate_OnCommand(CommandEventArgs e) { if (e.Length == 6) diff --git a/Projects/UOContent/Engines/Spawners/BaseSpawner.cs b/Projects/UOContent/Engines/Spawners/BaseSpawner.cs index 1371567b9..446d08156 100644 --- a/Projects/UOContent/Engines/Spawners/BaseSpawner.cs +++ b/Projects/UOContent/Engines/Spawners/BaseSpawner.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Reflection; using System.Text.Json; using ModernUO.Serialization; @@ -216,11 +215,8 @@ public abstract partial class BaseSpawner : Item, ISpawner if (spawn != null) { - Spawned.TryGetValue(spawn, out var entry); - - entry?.Spawned.Remove(spawn); - - Spawned.Remove(spawn); + Spawned.Remove(spawn, out var entry); + entry?.RemoveFromSpawned(spawn); } if (_running && !IsFull && _timer?.Running == false) @@ -419,12 +415,7 @@ public abstract partial class BaseSpawner : Item, ISpawner }; } - if (remove) - { - Spawned.Remove(spawned); - } - - return remove; + return remove && Spawned.Remove(spawned); } public void OnTick() @@ -683,7 +674,7 @@ public abstract partial class BaseSpawner : Item, ISpawner if (entity is Mobile m) { Spawned.Add(m, entry); - entry.Spawned.Add(m); + entry.AddToSpawned(m); var loc = m is BaseVendor ? Location : GetSpawnPosition(m, map); @@ -712,7 +703,7 @@ public abstract partial class BaseSpawner : Item, ISpawner else if (entity is Item item) { Spawned.Add(item, entry); - entry.Spawned.Add(item); + entry.AddToSpawned(item); var loc = GetSpawnPosition(item, map); @@ -884,8 +875,6 @@ public abstract partial class BaseSpawner : Item, ISpawner private void Deserialize(IGenericReader reader, int version) { - Spawned = new Dictionary(); - _guid = reader.ReadGuid(); _returnOnDeactivate = reader.ReadBool(); _entries = new List(reader.ReadInt()); @@ -910,9 +899,19 @@ public abstract partial class BaseSpawner : Item, ISpawner _end = _running ? reader.ReadDeltaTime() : Core.Now; } - [AfterDeserialization(false)] + [AfterDeserialization] private void AfterDeserialization() { + Spawned = new Dictionary(); + + foreach (var entry in Entries) + { + foreach (var spawned in entry.Spawned) + { + Spawned.Add(spawned, entry); + } + } + if (_running && _end > Core.Now) { DoTimer(_end - Core.Now); diff --git a/Projects/UOContent/Engines/Spawners/Commands/ConvertPremiumSpawners.cs b/Projects/UOContent/Engines/Spawners/Commands/ConvertPremiumSpawners.cs deleted file mode 100644 index 0fbc549fb..000000000 --- a/Projects/UOContent/Engines/Spawners/Commands/ConvertPremiumSpawners.cs +++ /dev/null @@ -1,257 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * - * Email: hi@modernuo.com * - * File: ConvertPremiumSpawners.cs * - * * - * This program is free software: you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation, either version 3 of the License, or * - * (at your option) any later version. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program. If not, see . * - *************************************************************************/ - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.Json; -using Microsoft.Extensions.FileSystemGlobbing; -using Microsoft.Extensions.FileSystemGlobbing.Abstractions; -using Server.Json; -using Server.Logging; -using Server.Network; - -namespace Server.Engines.Spawners; - -public static class ConvertPremiumSpawners -{ - private static readonly ILogger logger = LogFactory.GetLogger(typeof(ConvertPremiumSpawners)); - - public static void Configure() - { - CommandSystem.Register("ConvertPremiumSpawners", AccessLevel.Developer, ConvertPremiumSpawners_OnCommand); - } - - [Usage("ConvertPremiumSpawners ")] - [Description("Converts Nerun's Premium Spawners to JSON.")] - private static void ConvertPremiumSpawners_OnCommand(CommandEventArgs args) - { - var from = args.Mobile; - - if (args.Arguments.Length != 2) - { - from.SendMessage("Usage: [ConvertPremiumSpawners "); - return; - } - - var inputDi = new DirectoryInfo(Core.BaseDirectory); - - var patternMatches = new Matcher() - .AddInclude(args.Arguments[0]) - .Execute(new DirectoryInfoWrapper(inputDi)) - .Files; - - List<(FileInfo, string)> files = []; - foreach (var match in patternMatches) - { - files.Add((new FileInfo(match.Path), match.Stem)); - } - - if (files.Count == 0) - { - from.SendMessage("ConvertPremiumSpawners: No files found."); - return; - } - - var outputDir = Path.Combine(Core.BaseDirectory, args.Arguments[1]); - var options = JsonConfig.GetOptions(new TextDefinitionConverterFactory()); - - for (var i = 0; i < files.Count; i++) - { - var (file, stem) = files[i]; - from.SendMessage($"ConvertPremiumSpawners: Converting spawners for {stem}..."); - - NetState.FlushAll(); - - try - { - var stemDir = stem[..^file.Name.Length]; - var fullOutputDir = Path.Combine(outputDir, stemDir); - PathUtility.EnsureDirectory(fullOutputDir); - - var outputFileName = $"{file.Name.AsSpan(0, file.Name.Length - file.Extension.Length)}.json"; - ParsePremiumSpawnerFile(file.FullName, Path.Combine(fullOutputDir, outputFileName), options); - } - catch (Exception e) - { - logger.Error(e.ToString()); - from.SendMessage($"ConvertPremiumSpawners: Exception parsing {stem}, file may not be in the correct format."); - } - } - } - - private static void ParsePremiumSpawnerFile(string inputFile, string outputFile, JsonSerializerOptions options) - { - var lines = File.ReadAllLines(inputFile); - - TimeSpan minTime = TimeSpan.MinValue; - TimeSpan maxTime = TimeSpan.MinValue; - int mapId = -1; - string spawnerId = null; - - var json = new List(); - - foreach (var line in lines) - { - if (line.StartsWithOrdinal("#")) - { - continue; - } - - if (line.StartsWith('*')) - { - var spawners = ParsePremiumSpawner(line.Split('|'), spawnerId, mapId, minTime, maxTime); - foreach (var spawner in spawners) - { - var dynamicJson = new DynamicJson - { - Type = "Spawner", - Data = new Dictionary() - }; - - spawner.ToJson(dynamicJson, options); - json.Add(dynamicJson); - spawner.Delete(); - } - - spawners.Clear(); - continue; - } - - var over = line.Split(' '); - switch (over[0].ToLowerInvariant()) - { - case "overrideid": - { - spawnerId = over[1]; - break; - } - case "overridemap": - { - mapId = int.Parse(over[1]); - break; - } - case "overridemintime": - { - minTime = GetTimeSpan(over[1]); - break; - } - case "overridemaxtime": - { - maxTime = GetTimeSpan(over[1]); - break; - } - } - } - - JsonConfig.Serialize(outputFile, json, options); - } - - private static List ParsePremiumSpawner( - string[] parts, - string spawnerIdOverride, - int mapIdOverride, - TimeSpan minTimeOverride, - TimeSpan maxTimeOverride - ) - { - var spawnersList = new List(); - var mapId = mapIdOverride != -1 ? mapIdOverride : int.Parse(parts[10]); - Map[] maps = mapId == 0 ? [Map.Felucca, Map.Trammel] : [Map.Maps[mapId - 1]]; - - foreach (var map in maps) - { - var spawner = new Spawner - { - X = int.Parse(parts[7]), - Y = int.Parse(parts[8]), - Z = int.Parse(parts[9]), - Map = map, - MinDelay = minTimeOverride != TimeSpan.MinValue ? minTimeOverride : GetTimeSpan(parts[11]), - MaxDelay = maxTimeOverride != TimeSpan.MinValue ? maxTimeOverride : GetTimeSpan(parts[12]), - WalkingRange = int.Parse(parts[13]), - HomeRange = int.Parse(parts[14]), - Name = $"Spawner ({spawnerIdOverride ?? parts[15]})", - Guid = Guid.NewGuid() - }; - - var totalCount = 0; - - for (var i = 0; i < 6; i++) - { - var count = int.Parse(parts[i + 16]); - totalCount += count; - spawner.Entries.AddRange(CreateSpawnerEntries(spawner, parts[i + 1], count)); - } - - if (spawner.Entries.Count == 0) - { - spawner.Delete(); - continue; - } - - spawner.Count = totalCount; - spawnersList.Add(spawner); - } - - return spawnersList; - } - - private static List CreateSpawnerEntries(BaseSpawner spawner, string typeList, int maxCount) - { - var list = new List(); - if (string.IsNullOrWhiteSpace(typeList)) - { - return list; - } - - foreach (var spawnType in typeList.Split(':')) - { - var actualType = AssemblyHandler.FindTypeByName(ConvertType(spawnType))?.Name ?? spawnType; - list.Add(new SpawnerEntry(spawner, actualType, 100, maxCount)); - } - - return list; - } - - private static TimeSpan GetTimeSpan(string time) - { - if (string.IsNullOrEmpty(time)) - { - return TimeSpan.MinValue; - } - - return char.ToLowerInvariant(time[^1]) switch - { - 'h' => TimeSpan.FromHours(double.Parse(time.AsSpan(0, time.Length - 1))), - 'm' => TimeSpan.FromMinutes(double.Parse(time.AsSpan(0, time.Length - 1))), - 's' => TimeSpan.FromSeconds(double.Parse(time.AsSpan(0, time.Length - 1))), - _ => TimeSpan.FromMinutes(double.Parse(time)) - }; - } - - private static string ConvertType(string type) - { - return type.ToLowerInvariant() switch - { - "treasurelevel1" => "treasurechestlevel1", - "treasurelevel2" => "treasurechestlevel2", - "treasurelevel3" => "treasurechestlevel3", - "treasurelevel4" => "treasurechestlevel4", - "treasurelevel5" => "treasurechestlevel5", - _ => type - }; - } -} \ No newline at end of file diff --git a/Projects/UOContent/Engines/Spawners/Commands/GenerateSpawnersCommand.cs b/Projects/UOContent/Engines/Spawners/Commands/GenerateSpawnersCommand.cs deleted file mode 100644 index 757dc8b6d..000000000 --- a/Projects/UOContent/Engines/Spawners/Commands/GenerateSpawnersCommand.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Text.Json; -using Microsoft.Extensions.FileSystemGlobbing; -using Microsoft.Extensions.FileSystemGlobbing.Abstractions; -using Server.Collections; -using Server.Json; -using Server.Logging; -using Server.Network; -using Server.Utilities; - -namespace Server.Engines.Spawners; - -public static class GenerateSpawnersCommand -{ - private static readonly ILogger logger = LogFactory.GetLogger(typeof(GenerateSpawnersCommand)); - - public static void Configure() - { - CommandSystem.Register("GenerateSpawners", AccessLevel.Developer, GenerateSpawners_OnCommand); - } - - [Usage("GenerateSpawners ")] - [Aliases("ImportSpawners", "GenSpawners", "GenSpawns")] - [Description("Generates spawners from JSON files. Supports basic globbing.")] - private static void GenerateSpawners_OnCommand(CommandEventArgs e) - { - var from = e.Mobile; - - if (e.Arguments.Length == 0) - { - from.SendMessage("Usage: [GenerateSpawners "); - return; - } - - var di = new DirectoryInfo(Core.BaseDirectory); - - var patternMatches = new Matcher() - .AddInclude(e.Arguments[0]) - .Execute(new DirectoryInfoWrapper(di)) - .Files; - - List files = []; - foreach (var match in patternMatches) - { - files.Add(new FileInfo(match.Path)); - } - - if (files.Count == 0) - { - from.SendMessage("GenerateSpawners: No files found matching the pattern"); - return; - } - - var watch = Stopwatch.StartNew(); - - var allSpawners = new Dictionary(); - foreach (var item in World.Items.Values) - { - if (item is ISpawner spawner) - { - allSpawners[spawner.Guid] = spawner; - } - } - - var options = JsonConfig.GetOptions(new TextDefinitionConverterFactory()); - var totalGenerated = 0; - var totalFailures = 0; - - for (var i = 0; i < files.Count; i++) - { - var file = files[i]; - from.SendMessage($"GenerateSpawners: Generating spawners from {file.Name}..."); - logger.Information("{User} is generating spawners from {File}", from, file.FullName); - - NetState.FlushAll(); - - try - { - var spawners = JsonConfig.Deserialize>(file.FullName); - ParseSpawnerList(spawners, options, allSpawners, out var generated, out var failed); - totalGenerated += generated; - totalFailures += failed; - } - catch (JsonException) - { - from.SendMessage( - $"GenerateSpawners: Exception parsing {file.FullName}, file may not be in the correct format." - ); - } - } - - watch.Stop(); - - logger.Information( - "Generated {Count} spawners ({Duration:F2} seconds, {Failures} failures)", - totalGenerated, - watch.Elapsed.TotalSeconds, - totalFailures - ); - - from.SendMessage( - $"GenerateSpawners: Generated {totalGenerated} spawners ({watch.Elapsed.TotalSeconds:F2} seconds, {totalFailures} failures)" - ); - } - - private static void ParseSpawnerList( - List spawners, - JsonSerializerOptions options, - Dictionary allSpawners, - out int totalGenerated, - out int failureCount - ) - { - failureCount = 0; - totalGenerated = 0; - - using var queue = PooledRefQueue.Create(); - for (var i = 0; i < spawners.Count; i++) - { - var json = spawners[i]; - var type = AssemblyHandler.FindTypeByName(json.Type); - - if (type == null || !typeof(BaseSpawner).IsAssignableFrom(type)) - { - logger.Error($"Invalid spawner type {json.Type ?? "(-null-)"} ({i})."); - failureCount++; - continue; - } - - json.GetProperty("location", options, out Point3D location); - json.GetProperty("map", options, out Map map); - - // Delete all spawners at this location. - // Probably shouldn't do this outside of migrations? Is there a better way to find/fix spawners? - foreach (var spawner in map.GetItemsAt(location)) - { - if (spawner.GetType() == type) - { - queue.Enqueue(spawner); - allSpawners.Remove(spawner.Guid); - } - } - - while (queue.Count > 0) - { - queue.Dequeue().Delete(); - } - - try - { - var spawner = type.CreateInstance(json, options); - - spawner!.MoveToWorld(location, map); - spawner!.Respawn(); - - if (allSpawners.Remove(spawner.Guid, out var oldSpawner)) - { - oldSpawner.Delete(); - } - - allSpawners.Add(spawner.Guid, spawner); - totalGenerated++; - } - catch (Exception ex) - { - json.GetProperty("guid", options, out Guid guid); - TraceException(ex, $"Failed to generate spawner {guid}."); - - failureCount++; - } - } - } - - private static void TraceException(Exception ex, string message = "") - { - try - { - using var op = new StreamWriter("spawner-errors.log", true); - op.WriteLine("# {0}", Core.Now); - - if (!string.IsNullOrEmpty(message)) - { - op.WriteLine(message); - } - - op.WriteLine(ex); - - op.WriteLine(); - op.WriteLine(); - } - catch - { - // ignored - } - -#if DEBUG - logger.Error(ex, message); -#endif - } -} diff --git a/Projects/UOContent/Engines/Spawners/Commands/ImportSpawnersCommand.cs b/Projects/UOContent/Engines/Spawners/Commands/ImportSpawnersCommand.cs new file mode 100644 index 000000000..6094de015 --- /dev/null +++ b/Projects/UOContent/Engines/Spawners/Commands/ImportSpawnersCommand.cs @@ -0,0 +1,469 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Xml; +using Microsoft.Extensions.FileSystemGlobbing; +using Microsoft.Extensions.FileSystemGlobbing.Abstractions; +using Server.Collections; +using Server.Json; +using Server.Logging; +using Server.Network; +using Server.Utilities; + +namespace Server.Engines.Spawners; + +public static class ImportSpawnersCommand +{ + private static readonly ILogger logger = LogFactory.GetLogger(typeof(ImportSpawnersCommand)); + + public static void Configure() + { + CommandSystem.Register("ImportSpawners", AccessLevel.Developer, GenerateSpawners_OnCommand); + } + + [Usage("ImportSpawners ")] + [Aliases("ImportSpawner", "GenerateSpawners", "GenSpawners")] + [Description("Imports JSON, Nerun Premium, and RunUO spawner files. Supports basic globbing.")] + private static void GenerateSpawners_OnCommand(CommandEventArgs e) + { + var from = e.Mobile; + + if (e.Arguments.Length == 0) + { + from.SendMessage("Usage: [GenerateSpawners "); + return; + } + + var di = new DirectoryInfo(Core.BaseDirectory); + + var patternMatches = new Matcher() + .AddInclude(e.Arguments[0]) + .Execute(new DirectoryInfoWrapper(di)) + .Files; + + List files = []; + foreach (var match in patternMatches) + { + files.Add(new FileInfo(match.Path)); + } + + if (files.Count == 0) + { + from.SendMessage("GenerateSpawners: No files found matching the pattern."); + return; + } + + var watch = Stopwatch.StartNew(); + + var allSpawners = new Dictionary(); + foreach (var item in World.Items.Values) + { + if (item is ISpawner spawner) + { + allSpawners[spawner.Guid] = spawner; + } + } + + var totalGenerated = 0; + var totalFailures = 0; + + for (var i = 0; i < files.Count; i++) + { + var file = files[i]; + from.SendMessage($"GenerateSpawners: Generating spawners from {file.Name}..."); + logger.Information("{User} is generating spawners from {File}", from, file.FullName); + + NetState.FlushAll(); + + if (file.Extension == ".json") + { + ImportJsonSpawners(from, file, allSpawners, ref totalGenerated, ref totalFailures); + } + else if (file.Extension == ".xml") + { + ImportXmlSpawners(from, file, allSpawners, ref totalGenerated, ref totalFailures); + } + else if (file.Extension == ".map") + { + ImportPremiumSpawners(from, file, allSpawners, ref totalGenerated, ref totalFailures); + } + else + { + from.SendMessage($"GenerateSpawners: Unsupported file type {file.Extension}."); + } + } + + watch.Stop(); + + logger.Information( + "Generated {Count} spawners ({Duration:F2} seconds, {Failures} failures)", + totalGenerated, + watch.Elapsed.TotalSeconds, + totalFailures + ); + + from.SendMessage( + $"GenerateSpawners: Generated {totalGenerated} spawners ({watch.Elapsed.TotalSeconds:F2} seconds, {totalFailures} failures)" + ); + } + + private static void ImportXmlSpawners( + Mobile from, + FileInfo file, + Dictionary allSpawners, + ref int totalGenerated, + ref int totalFailures + ) + { + XmlDocument doc = new XmlDocument(); + doc.Load(file.FullName); + + XmlElement root = doc["spawners"]; + + if (root == null) + { + return; + } + + var index = 0; + foreach (XmlElement node in root.GetElementsByTagName("spawner")) + { + try + { + int count = int.Parse(Utility.GetText(node["count"], "1")); + int homeRange = int.Parse(Utility.GetText(node["homerange"], "4")); + + int walkingRange = int.Parse(Utility.GetText(node["walkingrange"], "-1")); + + int team = int.Parse(Utility.GetText(node["team"], "0")); + + TimeSpan maxDelay = TimeSpan.Parse(Utility.GetText(node["maxdelay"], "10:00")); + TimeSpan minDelay = TimeSpan.Parse(Utility.GetText(node["mindelay"], "05:00")); + var creaturesNameNode = node["creaturesname"]; + List creatureNames = []; + if (creaturesNameNode != null) + { + foreach (XmlElement ele in creaturesNameNode.GetElementsByTagName("creaturename")) + { + if (ele != null) + { + creatureNames.Add(ele.InnerText); + } + } + } + + string name = Utility.GetText(node["name"], "Spawner"); + Point3D location = Point3D.Parse(Utility.GetText(node["location"], "Error")); + Map map = Map.Parse(Utility.GetText(node["map"], "Error")); + + Spawner spawner = new Spawner(count, minDelay, maxDelay, team, homeRange, creatureNames.ToArray()); + if (walkingRange >= 0) + { + spawner.WalkingRange = walkingRange; + } + + spawner.Name = name; + spawner.MoveToWorld(location, map); + if (spawner.Map == Map.Internal) + { + spawner.Delete(); + totalFailures++; + throw new Exception("Spawner created on Internal map."); + } + + spawner.Respawn(); + allSpawners.Add(spawner.Guid, spawner); + totalGenerated++; + } + catch (Exception ex) + { + TraceException(ex, $"Failed to generate spawner {index} in {file.FullName}."); + from.SendMessage( + $"GenerateSpawners: Exception parsing {file.FullName}, file may not be in the correct format." + ); + } + + index++; + } + } + + private static void ImportJsonSpawners( + Mobile from, + FileInfo file, + Dictionary allSpawners, + ref int totalGenerated, + ref int totalFailures + ) + { + var options = JsonConfig.GetOptions(); + try + { + var spawners = JsonConfig.Deserialize>(file.FullName); + using var queue = PooledRefQueue.Create(); + for (var i = 0; i < spawners.Count; i++) + { + var json = spawners[i]; + var type = AssemblyHandler.FindTypeByName(json.Type); + + if (type == null || !typeof(BaseSpawner).IsAssignableFrom(type)) + { + logger.Error($"Invalid spawner type {json.Type ?? "(-null-)"} ({i})."); + totalFailures++; + continue; + } + + json.GetProperty("location", options, out Point3D location); + json.GetProperty("map", options, out Map map); + + // Delete all spawners at this location. + // Probably shouldn't do this outside of migrations? Is there a better way to find/fix spawners? + foreach (var spawner in map.GetItemsAt(location)) + { + if (spawner.GetType() == type) + { + queue.Enqueue(spawner); + allSpawners.Remove(spawner.Guid); + } + } + + while (queue.Count > 0) + { + queue.Dequeue().Delete(); + } + + try + { + var spawner = type.CreateInstance(json, options); + + spawner!.MoveToWorld(location, map); + spawner!.Respawn(); + + if (allSpawners.Remove(spawner.Guid, out var oldSpawner)) + { + oldSpawner.Delete(); + } + + allSpawners.Add(spawner.Guid, spawner); + totalGenerated++; + } + catch (Exception ex) + { + json.GetProperty("guid", options, out Guid guid); + TraceException(ex, $"Failed to generate spawner {guid}."); + + totalFailures++; + } + } + } + catch (JsonException) + { + from.SendMessage( + $"GenerateSpawners: Exception parsing {file.FullName}, file may not be in the correct format." + ); + } + } + + private static void TraceException(Exception ex, string message = "") + { + try + { + using var op = new StreamWriter("spawner-errors.log", true); + op.WriteLine("# {0}", Core.Now); + + if (!string.IsNullOrEmpty(message)) + { + op.WriteLine(message); + } + + op.WriteLine(ex); + + op.WriteLine(); + op.WriteLine(); + } + catch + { + // ignored + } + +#if DEBUG + logger.Error(ex, message); +#endif + } + + private static void ImportPremiumSpawners( + Mobile from, + FileInfo file, + Dictionary allSpawners, + ref int totalGenerated, + ref int totalFailures + ) + { + var lines = File.ReadAllLines(file.FullName); + + TimeSpan minTimeOverride = TimeSpan.MinValue; + TimeSpan maxTimeOverride = TimeSpan.MinValue; + int mapIdOverride = -1; + string spawnerIdOverride = null; + + for (var i = 0; i < lines.Length; i++) + { + var line = lines[i]; + if (line.StartsWithOrdinal("#")) + { + continue; + } + + try + { + + if (line.StartsWith('*')) + { + var parts = line.Split('|'); + var mapId = mapIdOverride != -1 ? mapIdOverride : int.Parse(parts[10]); + Map[] maps = mapId == 0 ? [Map.Felucca, Map.Trammel] : [Map.Maps[mapId - 1]]; + + foreach (var map in maps) + { + try + { + List<(string, int)> spawnerEntries = []; + + var totalCount = 0; + for (var j = 0; j < 6; j++) + { + var count = int.Parse(parts[j + 16]); + totalCount += count; + spawnerEntries.AddRange(CreateSpawnerEntries(parts[j + 1], count)); + } + + if (spawnerEntries.Count == 0) + { + continue; + } + + var minDelay = GetTimeSpan(minTimeOverride, parts[11]); + var maxDelay = GetTimeSpan(maxTimeOverride, parts[12]); + var homeRange = int.Parse(parts[14]); + + var spawner = new Spawner(totalCount, minDelay, maxDelay, 0, homeRange) + { + WalkingRange = int.Parse(parts[13]), + Name = $"Spawner ({spawnerIdOverride ?? parts[15]})" + }; + + foreach (var (type, amount) in spawnerEntries) + { + spawner.AddEntry(type, amount: amount); + } + + spawner.MoveToWorld( + new Point3D(int.Parse(parts[7]), int.Parse(parts[8]), int.Parse(parts[9])), + map + ); + + spawner.Respawn(); + allSpawners.Add(spawner.Guid, spawner); + totalGenerated++; + } + catch (Exception ex) + { + TraceException(ex, $"Failed to generate spawner on line {i} in {file.FullName}."); + from.SendMessage( + $"GenerateSpawners: Exception parsing {file.FullName}, file may not be in the correct format." + ); + totalFailures++; + } + } + + continue; + } + + var over = line.Split(' '); + switch (over[0].ToLowerInvariant()) + { + case "overrideid": + { + spawnerIdOverride = over[1]; + break; + } + case "overridemap": + { + mapIdOverride = int.Parse(over[1]); + break; + } + case "overridemintime": + { + minTimeOverride = GetTimeSpan(over[1]); + break; + } + case "overridemaxtime": + { + maxTimeOverride = GetTimeSpan(over[1]); + break; + } + } + } + catch (Exception ex) + { + TraceException(ex, $"Failed to generate spawner on line {i} in {file.FullName}."); + from.SendMessage( + $"GenerateSpawners: Exception parsing {file.FullName}, file may not be in the correct format." + ); + return; + } + } + } + + private static List<(string, int)> CreateSpawnerEntries(string typeList, int maxCount) + { + var list = new List<(string, int)>(); + if (string.IsNullOrWhiteSpace(typeList)) + { + return list; + } + + foreach (var spawnType in typeList.Split(':')) + { + var actualType = AssemblyHandler.FindTypeByName(ConvertType(spawnType))?.Name ?? spawnType; + list.Add((actualType, maxCount)); + } + + return list; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TimeSpan GetTimeSpan(TimeSpan timeOverride, string time) => + timeOverride != TimeSpan.MinValue ? timeOverride : GetTimeSpan(time); + + private static TimeSpan GetTimeSpan(string time) + { + if (string.IsNullOrEmpty(time)) + { + return TimeSpan.MinValue; + } + + return char.ToLowerInvariant(time[^1]) switch + { + 'h' => TimeSpan.FromHours(double.Parse(time.AsSpan(0, time.Length - 1))), + 'm' => TimeSpan.FromMinutes(double.Parse(time.AsSpan(0, time.Length - 1))), + 's' => TimeSpan.FromSeconds(double.Parse(time.AsSpan(0, time.Length - 1))), + _ => TimeSpan.FromMinutes(double.Parse(time)) + }; + } + + private static string ConvertType(string type) + { + return type.ToLowerInvariant() switch + { + "treasurelevel1" => "treasurechestlevel1", + "treasurelevel2" => "treasurechestlevel2", + "treasurelevel3" => "treasurechestlevel3", + "treasurelevel4" => "treasurechestlevel4", + "treasurelevel5" => "treasurechestlevel5", + _ => type + }; + } +} diff --git a/Projects/UOContent/Engines/Spawners/SpawnerEntry.cs b/Projects/UOContent/Engines/Spawners/SpawnerEntry.cs index bb09a677f..d75a0d907 100644 --- a/Projects/UOContent/Engines/Spawners/SpawnerEntry.cs +++ b/Projects/UOContent/Engines/Spawners/SpawnerEntry.cs @@ -76,14 +76,20 @@ public partial class SpawnerEntry if (e != null) { - e.Spawner = _parent; - Spawned.Add(e); - _parent.Spawned.TryAdd(e, this); } } } + [AfterDeserialization] + private void AfterDeserialization() + { + foreach (var e in Spawned) + { + e.Spawner = _parent; + } + } + [JsonIgnore] public EntryFlags Valid { get; set; } @@ -99,6 +105,7 @@ public partial class SpawnerEntry if (parent.OnDefragSpawn(spawned, false)) { Spawned.RemoveAt(i--); + _parent.MarkDirty(); } } } diff --git a/Projects/UOContent/Gumps/WhoGump.cs b/Projects/UOContent/Gumps/WhoGump.cs index c03672ec2..dd581e6e2 100644 --- a/Projects/UOContent/Gumps/WhoGump.cs +++ b/Projects/UOContent/Gumps/WhoGump.cs @@ -43,12 +43,12 @@ namespace Server.Gumps public static void Configure() { - CommandSystem.Register("Who", AccessLevel.Counselor, WhoList_OnCommand); CommandSystem.Register("WhoList", AccessLevel.Counselor, WhoList_OnCommand); } - [Usage("WhoList [filter]"), Aliases("Who"), - Description("Lists all connected clients. Optionally filters results by name.")] + [Usage("WhoList [filter]")] + [Aliases("Who")] + [Description("Lists all connected clients. Optionally filters results by name.")] private static void WhoList_OnCommand(CommandEventArgs e) { e.Mobile.SendGump(new WhoGump(e.Mobile, e.ArgString)); diff --git a/docs/commands/commands.7z b/docs/commands/commands.7z index b34b344c9..a7067618d 100644 Binary files a/docs/commands/commands.7z and b/docs/commands/commands.7z differ