fix: Fixes codegenned spawners and adds RunUO spawner import support (#1682)
> [!IMPORTANT] > This code change includes important fixes for all spawners after they were converted to codegen! > [!NOTE] > **Developer Note** > Usage/Description/Aliases attributes were moved to the core so they can be used by the command registration system. ### Summary - **Fixes spawners not registering their spawns on world load** - Changes `[GenerateSpawners` to `[ImportSpawners` - Removes `ConvertPremiumSpawners` - Adds Premium spawner import support to `[ImportSpawners` - Adds RunUO XML import support: https://github.com/ruaduck/xmlspawner/blob/ServUOMaster/XmlSpawner/XmlSpawner%20Core/SpawnerExporter.cs - Fixed an issue where not manually registering an alias meant it wasn't available as a command
This commit is contained in:
parent
cc064a27a5
commit
d0d7be8de0
12 changed files with 568 additions and 522 deletions
|
|
@ -125,6 +125,30 @@ public class ConstructibleAttribute : Attribute
|
||||||
public AccessLevel AccessLevel { get; set; }
|
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)]
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
public class CommandPropertyAttribute : Attribute
|
public class CommandPropertyAttribute : Attribute
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Server.Logging;
|
||||||
|
|
||||||
namespace Server;
|
namespace Server;
|
||||||
|
|
||||||
|
|
@ -125,6 +128,8 @@ public class CommandInfoSorter : IComparer<CommandInfo>
|
||||||
|
|
||||||
public static class CommandSystem
|
public static class CommandSystem
|
||||||
{
|
{
|
||||||
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(CommandSystem));
|
||||||
|
|
||||||
public static string Prefix { get; set; } = "[";
|
public static string Prefix { get; set; } = "[";
|
||||||
|
|
||||||
public static Dictionary<string, CommandEntry> Entries { get; } = new(StringComparer.OrdinalIgnoreCase);
|
public static Dictionary<string, CommandEntry> Entries { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
@ -194,7 +199,38 @@ public static class CommandSystem
|
||||||
|
|
||||||
public static void Register(string command, AccessLevel access, CommandEventHandler handler)
|
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)
|
public static bool Handle(Mobile from, string text, MessageType type = MessageType.Regular)
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ public static class JsonConfig
|
||||||
options.Converters.Add(new IPEndPointConverterFactory());
|
options.Converters.Add(new IPEndPointConverterFactory());
|
||||||
options.Converters.Add(new TypeConverterFactory());
|
options.Converters.Add(new TypeConverterFactory());
|
||||||
options.Converters.Add(new WorldLocationConverterFactory());
|
options.Converters.Add(new WorldLocationConverterFactory());
|
||||||
|
options.Converters.Add(new TextDefinitionConverterFactory());
|
||||||
|
|
||||||
for (var i = 0; i < converters.Length; i++)
|
for (var i = 0; i < converters.Length; i++)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -33,7 +33,6 @@ namespace Server.Commands
|
||||||
Register("Where", AccessLevel.Counselor, Where_OnCommand);
|
Register("Where", AccessLevel.Counselor, Where_OnCommand);
|
||||||
|
|
||||||
Register("AutoPageNotify", AccessLevel.Counselor, APN_OnCommand);
|
Register("AutoPageNotify", AccessLevel.Counselor, APN_OnCommand);
|
||||||
Register("APN", AccessLevel.Counselor, APN_OnCommand);
|
|
||||||
|
|
||||||
Register("Animate", AccessLevel.GameMaster, Animate_OnCommand);
|
Register("Animate", AccessLevel.GameMaster, Animate_OnCommand);
|
||||||
|
|
||||||
|
|
@ -47,12 +46,8 @@ namespace Server.Commands
|
||||||
Register("Client", AccessLevel.Counselor, Client_OnCommand);
|
Register("Client", AccessLevel.Counselor, Client_OnCommand);
|
||||||
|
|
||||||
Register("SMsg", AccessLevel.Counselor, StaffMessage_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("BCast", AccessLevel.GameMaster, BroadcastMessage_OnCommand);
|
||||||
Register("BC", AccessLevel.GameMaster, BroadcastMessage_OnCommand);
|
|
||||||
Register("B", AccessLevel.GameMaster, BroadcastMessage_OnCommand);
|
|
||||||
|
|
||||||
Register("Bank", AccessLevel.GameMaster, Bank_OnCommand);
|
Register("Bank", AccessLevel.GameMaster, Bank_OnCommand);
|
||||||
|
|
||||||
|
|
@ -736,14 +731,16 @@ namespace Server.Commands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Usage("SMsg <text>"), Aliases("S", "SM")]
|
[Usage("SMsg <text>")]
|
||||||
|
[Aliases("S", "SM")]
|
||||||
[Description("Broadcasts a message to all online staff.")]
|
[Description("Broadcasts a message to all online staff.")]
|
||||||
public static void StaffMessage_OnCommand(CommandEventArgs e)
|
public static void StaffMessage_OnCommand(CommandEventArgs e)
|
||||||
{
|
{
|
||||||
BroadcastMessage(AccessLevel.Counselor, e.Mobile.SpeechHue, $"[{e.Mobile.Name}] {e.ArgString}");
|
BroadcastMessage(AccessLevel.Counselor, e.Mobile.SpeechHue, $"[{e.Mobile.Name}] {e.ArgString}");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Usage("BCast <text>"), Aliases("B", "BC")]
|
[Usage("BCast <text>")]
|
||||||
|
[Aliases("B", "BC")]
|
||||||
[Description("Broadcasts a message to everyone online.")]
|
[Description("Broadcasts a message to everyone online.")]
|
||||||
public static void BroadcastMessage_OnCommand(CommandEventArgs e)
|
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.")]
|
[Description("Toggles your auto-page-notify status.")]
|
||||||
public static void APN_OnCommand(CommandEventArgs e)
|
public static void APN_OnCommand(CommandEventArgs e)
|
||||||
{
|
{
|
||||||
|
|
@ -782,8 +780,8 @@ namespace Server.Commands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Usage("Animate <action> <frameCount> <repeatCount> <forward> <repeat> <delay>"),
|
[Usage("Animate <action> <frameCount> <repeatCount> <forward> <repeat> <delay>")]
|
||||||
Description("Makes your character do a specified animation.")]
|
[Description("Makes your character do a specified animation.")]
|
||||||
public static void Animate_OnCommand(CommandEventArgs e)
|
public static void Animate_OnCommand(CommandEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Length == 6)
|
if (e.Length == 6)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using ModernUO.Serialization;
|
using ModernUO.Serialization;
|
||||||
|
|
@ -216,11 +215,8 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
||||||
|
|
||||||
if (spawn != null)
|
if (spawn != null)
|
||||||
{
|
{
|
||||||
Spawned.TryGetValue(spawn, out var entry);
|
Spawned.Remove(spawn, out var entry);
|
||||||
|
entry?.RemoveFromSpawned(spawn);
|
||||||
entry?.Spawned.Remove(spawn);
|
|
||||||
|
|
||||||
Spawned.Remove(spawn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_running && !IsFull && _timer?.Running == false)
|
if (_running && !IsFull && _timer?.Running == false)
|
||||||
|
|
@ -419,12 +415,7 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remove)
|
return remove && Spawned.Remove(spawned);
|
||||||
{
|
|
||||||
Spawned.Remove(spawned);
|
|
||||||
}
|
|
||||||
|
|
||||||
return remove;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnTick()
|
public void OnTick()
|
||||||
|
|
@ -683,7 +674,7 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
||||||
if (entity is Mobile m)
|
if (entity is Mobile m)
|
||||||
{
|
{
|
||||||
Spawned.Add(m, entry);
|
Spawned.Add(m, entry);
|
||||||
entry.Spawned.Add(m);
|
entry.AddToSpawned(m);
|
||||||
|
|
||||||
var loc = m is BaseVendor ? Location : GetSpawnPosition(m, map);
|
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)
|
else if (entity is Item item)
|
||||||
{
|
{
|
||||||
Spawned.Add(item, entry);
|
Spawned.Add(item, entry);
|
||||||
entry.Spawned.Add(item);
|
entry.AddToSpawned(item);
|
||||||
|
|
||||||
var loc = GetSpawnPosition(item, map);
|
var loc = GetSpawnPosition(item, map);
|
||||||
|
|
||||||
|
|
@ -884,8 +875,6 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
||||||
|
|
||||||
private void Deserialize(IGenericReader reader, int version)
|
private void Deserialize(IGenericReader reader, int version)
|
||||||
{
|
{
|
||||||
Spawned = new Dictionary<ISpawnable, SpawnerEntry>();
|
|
||||||
|
|
||||||
_guid = reader.ReadGuid();
|
_guid = reader.ReadGuid();
|
||||||
_returnOnDeactivate = reader.ReadBool();
|
_returnOnDeactivate = reader.ReadBool();
|
||||||
_entries = new List<SpawnerEntry>(reader.ReadInt());
|
_entries = new List<SpawnerEntry>(reader.ReadInt());
|
||||||
|
|
@ -910,9 +899,19 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
||||||
_end = _running ? reader.ReadDeltaTime() : Core.Now;
|
_end = _running ? reader.ReadDeltaTime() : Core.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
[AfterDeserialization(false)]
|
[AfterDeserialization]
|
||||||
private void AfterDeserialization()
|
private void AfterDeserialization()
|
||||||
{
|
{
|
||||||
|
Spawned = new Dictionary<ISpawnable, SpawnerEntry>();
|
||||||
|
|
||||||
|
foreach (var entry in Entries)
|
||||||
|
{
|
||||||
|
foreach (var spawned in entry.Spawned)
|
||||||
|
{
|
||||||
|
Spawned.Add(spawned, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_running && _end > Core.Now)
|
if (_running && _end > Core.Now)
|
||||||
{
|
{
|
||||||
DoTimer(_end - Core.Now);
|
DoTimer(_end - Core.Now);
|
||||||
|
|
|
||||||
|
|
@ -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 <http://www.gnu.org/licenses/>. *
|
|
||||||
*************************************************************************/
|
|
||||||
|
|
||||||
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 <relative search pattern to distribution> <output directory relative to distribution>")]
|
|
||||||
[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 <relative search pattern to distribution> <output directory relative to distribution>");
|
|
||||||
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<DynamicJson>();
|
|
||||||
|
|
||||||
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<string, JsonElement>()
|
|
||||||
};
|
|
||||||
|
|
||||||
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<Spawner> ParsePremiumSpawner(
|
|
||||||
string[] parts,
|
|
||||||
string spawnerIdOverride,
|
|
||||||
int mapIdOverride,
|
|
||||||
TimeSpan minTimeOverride,
|
|
||||||
TimeSpan maxTimeOverride
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var spawnersList = new List<Spawner>();
|
|
||||||
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<SpawnerEntry> CreateSpawnerEntries(BaseSpawner spawner, string typeList, int maxCount)
|
|
||||||
{
|
|
||||||
var list = new List<SpawnerEntry>();
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 <relative search pattern to distribution>")]
|
|
||||||
[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 <relative search pattern to distribution>");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var di = new DirectoryInfo(Core.BaseDirectory);
|
|
||||||
|
|
||||||
var patternMatches = new Matcher()
|
|
||||||
.AddInclude(e.Arguments[0])
|
|
||||||
.Execute(new DirectoryInfoWrapper(di))
|
|
||||||
.Files;
|
|
||||||
|
|
||||||
List<FileInfo> 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<Guid, ISpawner>();
|
|
||||||
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<List<DynamicJson>>(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<DynamicJson> spawners,
|
|
||||||
JsonSerializerOptions options,
|
|
||||||
Dictionary<Guid, ISpawner> allSpawners,
|
|
||||||
out int totalGenerated,
|
|
||||||
out int failureCount
|
|
||||||
)
|
|
||||||
{
|
|
||||||
failureCount = 0;
|
|
||||||
totalGenerated = 0;
|
|
||||||
|
|
||||||
using var queue = PooledRefQueue<Item>.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<BaseSpawner>(location))
|
|
||||||
{
|
|
||||||
if (spawner.GetType() == type)
|
|
||||||
{
|
|
||||||
queue.Enqueue(spawner);
|
|
||||||
allSpawners.Remove(spawner.Guid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (queue.Count > 0)
|
|
||||||
{
|
|
||||||
queue.Dequeue().Delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var spawner = type.CreateInstance<ISpawner>(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 <relative search pattern to distribution>")]
|
||||||
|
[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 <relative search pattern to distribution>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var di = new DirectoryInfo(Core.BaseDirectory);
|
||||||
|
|
||||||
|
var patternMatches = new Matcher()
|
||||||
|
.AddInclude(e.Arguments[0])
|
||||||
|
.Execute(new DirectoryInfoWrapper(di))
|
||||||
|
.Files;
|
||||||
|
|
||||||
|
List<FileInfo> 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<Guid, ISpawner>();
|
||||||
|
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<Guid, ISpawner> 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<string> 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<Guid, ISpawner> allSpawners,
|
||||||
|
ref int totalGenerated,
|
||||||
|
ref int totalFailures
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var options = JsonConfig.GetOptions();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var spawners = JsonConfig.Deserialize<List<DynamicJson>>(file.FullName);
|
||||||
|
using var queue = PooledRefQueue<Item>.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<BaseSpawner>(location))
|
||||||
|
{
|
||||||
|
if (spawner.GetType() == type)
|
||||||
|
{
|
||||||
|
queue.Enqueue(spawner);
|
||||||
|
allSpawners.Remove(spawner.Guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
queue.Dequeue().Delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var spawner = type.CreateInstance<ISpawner>(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<Guid, ISpawner> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -76,14 +76,20 @@ public partial class SpawnerEntry
|
||||||
|
|
||||||
if (e != null)
|
if (e != null)
|
||||||
{
|
{
|
||||||
e.Spawner = _parent;
|
|
||||||
|
|
||||||
Spawned.Add(e);
|
Spawned.Add(e);
|
||||||
_parent.Spawned.TryAdd(e, this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AfterDeserialization]
|
||||||
|
private void AfterDeserialization()
|
||||||
|
{
|
||||||
|
foreach (var e in Spawned)
|
||||||
|
{
|
||||||
|
e.Spawner = _parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public EntryFlags Valid { get; set; }
|
public EntryFlags Valid { get; set; }
|
||||||
|
|
||||||
|
|
@ -99,6 +105,7 @@ public partial class SpawnerEntry
|
||||||
if (parent.OnDefragSpawn(spawned, false))
|
if (parent.OnDefragSpawn(spawned, false))
|
||||||
{
|
{
|
||||||
Spawned.RemoveAt(i--);
|
Spawned.RemoveAt(i--);
|
||||||
|
_parent.MarkDirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,12 @@ namespace Server.Gumps
|
||||||
|
|
||||||
public static void Configure()
|
public static void Configure()
|
||||||
{
|
{
|
||||||
CommandSystem.Register("Who", AccessLevel.Counselor, WhoList_OnCommand);
|
|
||||||
CommandSystem.Register("WhoList", AccessLevel.Counselor, WhoList_OnCommand);
|
CommandSystem.Register("WhoList", AccessLevel.Counselor, WhoList_OnCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Usage("WhoList [filter]"), Aliases("Who"),
|
[Usage("WhoList [filter]")]
|
||||||
Description("Lists all connected clients. Optionally filters results by name.")]
|
[Aliases("Who")]
|
||||||
|
[Description("Lists all connected clients. Optionally filters results by name.")]
|
||||||
private static void WhoList_OnCommand(CommandEventArgs e)
|
private static void WhoList_OnCommand(CommandEventArgs e)
|
||||||
{
|
{
|
||||||
e.Mobile.SendGump(new WhoGump(e.Mobile, e.ArgString));
|
e.Mobile.SendGump(new WhoGump(e.Mobile, e.ArgString));
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue