fix: Fixes spawners, adds convertpremiumspawners, exportspawners, and replaces with neruns distro (#751)

### Additions
* Adds `[exportspawners <relative json file path to distro>`
  * If no path is provided, it will be saved to the `Data\Spawns` folder with the current timestamp as the file name.
  * Supports global, facet, region, multi, and area
* Fixes client disconnect for bad spawner generation command.
* Moves spawner commands to their own folder.
* Adds GUIDs for spawners. This allows for easy replacement during import to reduce duplication.
* Allows exporting the name of the spawner.
* Fixes globbing when using [generatespawners
* Adds [convertpremiumspawners to convert Premium Spawners (from Neruns distro)

Possibly in .NET 6 serializing JSON will be more performant using JSON nodes.


### Screenshots
![Screen_Shot_2021-08-31_at_10 20 06_PM](https://user-images.githubusercontent.com/3953314/131618495-cf7e3ae5-58e5-4f86-9d3c-3cc49906d9fc.png)
![Screen_Shot_2021-08-31_at_10 21 08_PM](https://user-images.githubusercontent.com/3953314/131618502-c39cc368-2266-47a5-9b87-e5ffa108b875.png)
![Screen_Shot_2021-08-31_at_10 13 29_PM](https://user-images.githubusercontent.com/3953314/131618512-f9807f1b-76e4-4503-989d-42324798fbf5.png)
This commit is contained in:
Kamron Batman 2021-09-03 21:52:44 -07:00 • committed by GitHub
parent dbd4d6f13f
commit 0d1fffd9fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
211 changed files with 200435 additions and 91312 deletions

View file

@ -0,0 +1,261 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2021 - 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.Network;
namespace Server.Engines.Spawners
{
public static class ConvertPremiumSpawners
{
public static void Initialize()
{
CommandSystem.Register("ConvertPremiumSpawners", AccessLevel.Developer, ConvertPremiumSpawners_OnCommand);
}
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> files = new List<FileInfo>();
foreach (var match in patternMatches)
{
files.Add(new FileInfo(match.Path));
}
if (files.Count == 0)
{
from.SendMessage("ConvertPremiumSpawners: No files found.");
return;
}
var inputDir = Path.Combine(Core.BaseDirectory, args.Arguments[0]);
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 = files[i];
from.SendMessage("ConvertPremiumSpawners: Converting spawners for {0}...", file.Name);
NetState.FlushAll();
try
{
var relativePath = Path.GetRelativePath(inputDir, file.DirectoryName!);
var fullOutputDir = Path.Combine(outputDir, relativePath);
AssemblyHandler.EnsureDirectory(fullOutputDir);
ParsePremiumSpawnerFile(file, fullOutputDir, options);
}
catch (Exception e)
{
Console.WriteLine(e);
from.SendMessage(
"ConvertPremiumSpawners: Exception parsing {0}, file may not be in the correct format.",
file.FullName
);
}
}
}
private static void ParsePremiumSpawnerFile(FileInfo file, string outputDirectory, JsonSerializerOptions options)
{
var lines = File.ReadAllLines(file.FullName);
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;
}
}
}
var outputFile = Path.Combine(outputDirectory, $"{file.Name[..^file.Extension.Length]}.json");
Console.WriteLine("Writing to: {0}", outputFile);
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 ? new[] { Map.Felucca, Map.Trammel } : new[] { 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()
};
for (var i = 0; i < 6; i++)
{
spawner.Entries.AddRange(CreateSpawnerEntries(parts[i + 1], int.Parse(parts[i + 15])));
}
if (spawner.Entries.Count == 0)
{
spawner.Delete();
continue;
}
var totalCount = 0;
foreach (var entry in spawner.Entries)
{
totalCount += entry.SpawnedMaxCount;
}
spawner.Count = totalCount;
spawnersList.Add(spawner);
}
return spawnersList;
}
private static List<SpawnerEntry> CreateSpawnerEntries(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(actualType, 100, maxCount));
}
return list;
}
private static TimeSpan GetTimeSpan(string time)
{
if (string.IsNullOrEmpty(time))
{
return TimeSpan.MinValue;
}
time = time.ToLowerInvariant();
return time[^1] switch
{
'h' => TimeSpan.FromHours(double.Parse(time[..^1])),
'm' => TimeSpan.FromMinutes(double.Parse(time[..^1])),
's' => TimeSpan.FromSeconds(double.Parse(time[..^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
};
}
}
}

View file

@ -0,0 +1,134 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2021 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: EditSpawnerCommand.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 Server.Commands.Generic;
using static Server.Types;
namespace Server.Engines.Spawners
{
public class EditSpawnCommand : BaseCommand
{
public static void Initialize()
{
TargetCommands.Register(new EditSpawnCommand());
}
public EditSpawnCommand()
{
AccessLevel = AccessLevel.GameMaster;
Supports = CommandSupport.Complex | CommandSupport.Simple;
Commands = new[] { "EditSpawner" };
ObjectTypes = ObjectTypes.Items;
Usage = "EditSpawner <type> <arguments> set <properties> where <properties>";
Description = "Modifies spawners arguments and properties for the given type";
ListOptimized = true;
}
public override void ExecuteList(CommandEventArgs e, List<object> list)
{
var args = e.Arguments;
if (args.Length <= 1)
{
LogFailure(Usage);
return;
}
if (list.Count == 0)
{
LogFailure("No matching objects found.");
return;
}
var name = args[0];
var type = AssemblyHandler.FindTypeByName(name);
if (!IsEntity(type))
{
LogFailure("No type with that name was found.");
return;
}
var argSpan = e.ArgString.AsSpan(name.Length + 1);
var setIndex = argSpan.InsensitiveIndexOf("set ");
var where = argSpan.InsensitiveIndexOf("where ");
ReadOnlySpan<char> props = null, findmatch = null;
if (setIndex > -1 || where > -1)
{
var start = setIndex + 4;
var len = where > -1 ? where : argSpan.Length;
findmatch = argSpan.Slice(len < argSpan.Length ? len + 6 : argSpan.Length);
if (setIndex > -1)
{
props = argSpan[start..^len];
argSpan = argSpan[..setIndex];
}
else
{
argSpan = argSpan[..^len];
}
}
var argStr = argSpan.ToString().DefaultIfNullOrEmpty(null);
var propsStr = props.ToString().DefaultIfNullOrEmpty(null);
var whereStr = findmatch.ToString().DefaultIfNullOrEmpty(null);
e.Mobile.SendMessage("Updating spawners...");
foreach (var obj in list)
{
if (obj is BaseSpawner spawner)
{
UpdateSpawner(spawner, name, argStr, propsStr, whereStr);
}
}
e.Mobile.SendMessage("Update completed.");
}
public static void UpdateSpawner(BaseSpawner spawner, string name, string arguments, string properties, string find = null)
{
foreach (var entry in spawner.Entries)
{
// TODO: Should cache spawn type on the entry
if (!entry.SpawnedName.InsensitiveEquals(name))
{
continue;
}
var found = find != null ? entry.Properties?.LastIndexOf(find, StringComparison.OrdinalIgnoreCase) : null;
var shouldUpdate = find == null || found > -1 &&
(found + find.Length == entry.Properties.Length ||
char.IsWhiteSpace(entry.Properties[(int)found + find.Length]));
if (shouldUpdate)
{
if (arguments != null)
{
entry.Parameters = arguments;
}
entry.Properties = properties;
}
}
}
}
}

View file

@ -0,0 +1,93 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2021 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: ExportSpawnersCommand.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.Collections.Generic;
using System.IO;
using Server.Commands.Generic;
using Server.Json;
using Server.Network;
namespace Server.Engines.Spawners
{
public class ExportSpawnersCommand : BaseCommand
{
public static void Initialize()
{
TargetCommands.Register(new ExportSpawnersCommand());
}
public ExportSpawnersCommand()
{
AccessLevel = AccessLevel.GameMaster;
Supports = CommandSupport.AllItems & ~CommandSupport.Contained;
Commands = new[] { "ExportSpawners" };
ObjectTypes = ObjectTypes.Items;
Usage = "ExportSpawners";
Description = "Exports the given the spawners to the a file";
ListOptimized = true;
}
public override void ExecuteList(CommandEventArgs e, List<object> list)
{
var path = e.Arguments.Length == 0 ? null : e.Arguments[0].Trim();
if (string.IsNullOrEmpty(path))
{
path = Path.Combine(Core.BaseDirectory, $"Data/Spawns/{Utility.GetTimeStamp()}.json");
}
else
{
var directory = Path.GetDirectoryName(Path.GetFullPath(path!))!;
if (!Path.IsPathRooted(path))
{
path = Path.Combine(Core.BaseDirectory, path);
AssemblyHandler.EnsureDirectory(directory);
}
else if (!Directory.Exists(directory))
{
LogFailure("Directory doesn't exist.");
return;
}
}
NetState.FlushAll();
var options = JsonConfig.GetOptions(new TextDefinitionConverterFactory());
var spawnRecords = new List<DynamicJson>(list.Count);
for (var i = 0; i < list.Count; i++)
{
if (list[i] is BaseSpawner spawner)
{
var dynamicJson = DynamicJson.Create(spawner.GetType());
spawner.ToJson(dynamicJson, options);
spawnRecords.Add(dynamicJson);
}
}
if (spawnRecords.Count == 0)
{
LogFailure("No matching spawners found.");
return;
}
e.Mobile.SendMessage("Exporting spawners...");
JsonConfig.Serialize(path, spawnRecords, options);
e.Mobile.SendMessage($"Spawners exported to {path}");
}
}
}

View file

@ -0,0 +1,217 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2021 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GenerateSpawnersCommand.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.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 Initialize()
{
CommandSystem.Register("GenerateSpawners", AccessLevel.Developer, GenerateSpawners_OnCommand);
}
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 = new List<FileInfo>();
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 {0}...", file.Name);
logger.Information($"{from} is generating spawners 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 {0}, file may not be in the correct format.",
file.FullName
);
}
}
watch.Stop();
logger.Information("Generated {0} spawners ({1:F2} seconds, {2} failures)");
from.SendMessage(
"GenerateSpawners: Generated {0} spawners ({1:F2} seconds, {2} failures)",
totalGenerated,
watch.Elapsed.TotalSeconds,
totalFailures
);
}
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?
var eable = map.GetItemsInRange<BaseSpawner>(location, 0);
foreach (var spawner in eable)
{
if (spawner.GetType() == type)
{
queue.Enqueue(spawner);
allSpawners.Remove(spawner.Guid);
}
}
while (queue.Count > 0)
{
queue.Dequeue().Delete();
}
eable.Free();
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($"{message}\n{ex}");
#endif
}
}
}

View file

@ -0,0 +1,63 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2021 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: RespawnCommand.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.Collections.Generic;
using Server.Commands.Generic;
using Server.Network;
namespace Server.Engines.Spawners
{
public class RespawnCommand : BaseCommand
{
public static void Initialize()
{
TargetCommands.Register(new RespawnCommand());
}
public RespawnCommand()
{
AccessLevel = AccessLevel.GameMaster;
Supports = CommandSupport.Complex | CommandSupport.Simple;
Commands = new[] { "Respawn" };
ObjectTypes = ObjectTypes.Items;
Usage = "Respawn";
Description = "Respawns the given the spawners.";
ListOptimized = true;
}
public override void ExecuteList(CommandEventArgs e, List<object> list)
{
if (list.Count == 0)
{
LogFailure("No matching objects found.");
return;
}
e.Mobile.SendMessage("Respawning...");
NetState.FlushAll();
foreach (var obj in list)
{
if (obj is ISpawner spawner)
{
spawner.Respawn();
}
}
e.Mobile.SendMessage("Respawn completed.");
}
}
}

View file

@ -0,0 +1,73 @@
/*************************************************************************
* ModernUO *
* Copyright (C) 2019-2021 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: SpawnPropsCommand.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.Collections.Generic;
using Server.Commands.Generic;
using Server.Gumps;
using Server.Targeting;
using static Server.Types;
namespace Server.Engines.Spawners
{
public class SpawnPropsCommand : BaseCommand
{
public static void Initialize()
{
TargetCommands.Register(new SpawnPropsCommand());
}
public SpawnPropsCommand()
{
AccessLevel = AccessLevel.GameMaster;
Supports = CommandSupport.Complex | CommandSupport.Simple;
Commands = new[] { "SpawnProps" };
ObjectTypes = ObjectTypes.Items;
Usage = "SpawnProps";
Description = "Shows a props gump that will modify the properties of spawn entries related to the chosen entity";
ListOptimized = true;
}
public override void ExecuteList(CommandEventArgs e, List<object> list)
{
if (list.Count == 0)
{
LogFailure("No matching objects found.");
return;
}
e.Mobile.SendMessage("Target the object you want to use as a template for modifying the spawner properties.");
e.Mobile.Target = new InternalTarget(list);
}
private class InternalTarget : Target
{
private readonly List<object> _list;
public InternalTarget(List<object> list) : base(-1, false, TargetFlags.None) => _list = list;
protected override void OnTarget(Mobile from, object targeted)
{
var type = targeted.GetType();
if (!IsEntity(type))
{
from.SendMessage("No type with that name was found.");
}
from.SendGump(new SpawnPropsGump(from, targeted, _list));
}
}
}
}