From 1c5813d8f4bc02b8c6cba34fb52d50f081bf4e5f Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 28 Mar 2024 16:50:27 -0700 Subject: [PATCH] feat: Adds [AddonGen (#1712) ### Summary * Adds Arya's addon generator. * Updates it for MUO ### ToDos * Update to use search values when .NET 9 is introduced * Add ability to codegen basic items from data files (JSON?) --- Projects/UOContent/Commands/AddonGenerator.cs | 409 ++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 Projects/UOContent/Commands/AddonGenerator.cs diff --git a/Projects/UOContent/Commands/AddonGenerator.cs b/Projects/UOContent/Commands/AddonGenerator.cs new file mode 100644 index 000000000..49bd1d6d5 --- /dev/null +++ b/Projects/UOContent/Commands/AddonGenerator.cs @@ -0,0 +1,409 @@ +using System; +using System.IO; +using System.Collections.Generic; +using Server.Collections; +using Server.Items; +using Server.Gumps; +using Server.Text; + +namespace Server.Commands; + +public class AddonGenerator +{ + private static readonly string _outputDirectory = Path.Combine(Core.BaseDirectory, "Data", "Generated Addons"); + + public delegate void PickerCallbackDelegate(Mobile from, Map map, Point3D start, Point3D end, object state); + + public static void PickerCallbackWrapper(Mobile from, Map map, Point3D start, Point3D end, PickerCallbackDelegate callback, object state) + { + callback(from, map, start, end, state); + } + + private const string _template = + """ + ///////////////////////////////////////////////// + // + // Automatically generated by the + // AddonGenerator script for ModernUO + // + ///////////////////////////////////////////////// + using System; + using Server;{serverItemsNamespace} + + namespace {namespace}; + + [SerializationGenerator(0)] + public class {name}Addon : BaseAddon + { + public override BaseAddonDeed Deed => {name}AddonDeed(); + + [Constructible] + public {name}Addon() + { + {components} + } + } + + [SerializationGenerator(0)] + public class {name}AddonDeed : BaseAddonDeed + { + public override BaseAddon Addon => new {name}Addon(); + public override string DefaultName => "{name}"; + + [Constructible] + public {name}AddonDeed() + { + } + } + """; + + // TODO: .NET 9 - Use search values + // private static SearchValues _templatePlaceholders = SearchValues.Create("{serverItemsNamespace}", "{namespace}", "{name}", "{components}"); + + public static void Configure() + { + CommandSystem.Register("AddonGen", AccessLevel.Administrator, OnAddonGen); + } + + [Usage("AddonGen [ [namespace]]")] + [Description("Brings up the addon script generator gump. Specify a name to skip the gump and go directly to picking an area.")] + private static void OnAddonGen(CommandEventArgs e) + { + var pickerState = new PickerState + { + Name = "", + Namespace = "Server.Items", + Items = true, + Statics = true, + Range = false, + Min = sbyte.MinValue, + Max = sbyte.MaxValue + }; + + if (e.Arguments.Length <= 0) + { + // Send gump + e.Mobile.SendGump(new InternalGump(e.Mobile, pickerState)); + return; + } + + pickerState.Name = e.Arguments[0]; + pickerState.Namespace = e.Arguments.Length > 1 ? e.Arguments[1] : "Server.Items"; + + BoundingBoxPicker.Begin( + e.Mobile, + (map, start, end) => PickerCallbackWrapper(e.Mobile, map, start, end, PickerCallback, pickerState) + ); + } + + private static void PickerCallback(Mobile from, Map map, Point3D start, Point3D end, object state) + { + if (state is not PickerState pickerState) + { + return; + } + + if (start.X > end.X) + { + (start.X, end.X) = (end.X, start.X); + } + + if (start.Y > end.Y) + { + (start.Y, end.Y) = (end.Y, start.Y); + } + + Point2D startPoint2D = new Point2D(start.X, start.Y); + Point2D endPoint2D = new Point2D(end.X, end.Y); + Rectangle2D bounds = new Rectangle2D(startPoint2D, endPoint2D); + + string name = pickerState.Name; + string ns = pickerState.Namespace; + bool items = pickerState.Items; + bool statics = pickerState.Statics; + bool range = pickerState.Range; + sbyte min = pickerState.Min; + sbyte max = pickerState.Max; + + // Get center + Point3D center = new Point3D(); + center.Z = 127; + + int x1 = bounds.End.X; + int y1 = bounds.End.Y; + int x2 = bounds.Start.X; + int y2 = bounds.Start.Y; + + Dictionary> tiles = new Dictionary>(); + + if (statics) + { + for (int x = start.X; x <= end.X; x++) + { + for (int y = start.Y; y <= end.Y; y++) + { + TileMatrix tileMatrix = map.Tiles; + List list = []; + + if (items) + { + foreach (Item item in map.GetItemsAt(x, y)) + { + if (range && item.Z >= min && item.Z <= max) + { + list.Add(new StaticTile((ushort)item.ItemID, (sbyte)item.Z)); + } + } + } + + foreach (var staticTile in tileMatrix.GetStaticTiles(x, y)) + { + if (range && staticTile.Z >= min && staticTile.Z <= max) + { + list.Add(staticTile); + } + } + + if (list is { Count: > 0 }) + { + tiles[new Point2D(x, y)] = list; + } + } + } + } + + using var target = PooledRefList.Create(); + + foreach(Static s in map.GetItemsInBounds(bounds)) + { + if (!range || s.Z >= min && s.Z <= max) + { + target.Add(s); + } + } + + if (target.Count == 0 && tiles.Keys.Count == 0) + { + from.SendMessage(0x40, "No items have been selected"); + return; + } + + // Get correct bounds + foreach(Item item in target) + { + if (item.Z < center.Z) + { + center.Z = item.Z; + } + + x1 = Math.Min(x1, item.X); + y1 = Math.Min(y1, item.Y); + x2 = Math.Max(x2, item.X); + y2 = Math.Max(y2, item.Y); + } + + foreach (Point2D p in tiles.Keys) + { + List list = tiles[p]; + + foreach (StaticTile t in list) + { + if (t.Z < center.Z) + { + center.Z = t.Z; + } + } + + x1 = Math.Min(x1, p.X); + y1 = Math.Min(y1, p.Y); + x2 = Math.Max(x2, p.X); + y2 = Math.Max(y2, p.Y); + } + + center.X = x1 + (x2 - x1) / 2; + center.Y = y1 + (y2 - y1) / 2; + + using var sb = ValueStringBuilder.Create(); + + foreach (Point2D p in tiles.Keys) + { + List list = tiles[p]; + + int xOffset = p.X - center.X; + int yOffset = p.Y - center.Y; + + foreach (StaticTile t in list) + { + int zOffset = t.Z - center.Z; + int id = t.ID - 16384; + + sb.Append($" AddComponent(new AddonComponent({id}), {xOffset}, {yOffset}, {zOffset});\n"); + } + } + + foreach(Item item in target) + { + int xOffset = item.X - center.X; + int yOffset = item.Y - center.Y; + int zOffset = item.Z - center.Z; + + var light = (item.ItemData.Flags & TileFlag.LightSource) == TileFlag.LightSource ? item.Light : LightType.Empty; + var hue = item.Hue; + + sb.Append(" AddComponent(\n"); + if (hue != 0 || light != LightType.Empty) + { + sb.Append($" new AddonComponent({item.ItemID})\n"); + sb.Append(" {\n"); + if (light != LightType.Empty) + { + sb.Append($" Light = LightType.{light},\n"); + } + if (hue != 0) + { + sb.Append($" Hue = {hue},\n"); + } + sb.Append(" },\n"); + } + else + { + sb.Append($" new AddonComponent({item.ItemID}),\n"); + } + sb.Append($" {xOffset},\n"); + sb.Append($" {yOffset},\n"); + sb.Append($" {zOffset},\n"); + sb.Append(" )\n"); + } + + string output = _template.Replace("{name}", name); + output = output.Replace("{namespace}", ns); + output = output.Replace("{components}", sb.ToString()); + + var path = Path.Combine(_outputDirectory, $"{name}Addon.cs"); + + string folder = Path.GetDirectoryName(path); + PathUtility.EnsureDirectory(folder); + + using var writer = new StreamWriter(path, false); + writer.Write(output); + + from.SendMessage(0x40, $"Script saved to {path}"); + } + + private class PickerState + { + public string Name { get; set; } + public string Namespace { get; set; } + public bool Items { get; set; } + public bool Statics { get; set; } + public bool Range { get; set; } + public sbyte Min { get; set; } + public sbyte Max { get; set; } + } + + private class InternalGump : Gump + { + private const int LabelHue = 0x480; + private const int GreenHue = 0x40; + private readonly PickerState _state; + + public InternalGump(Mobile m, PickerState state) : base(100, 50) + { + m.CloseGump(); + _state = state; + MakeGump(); + } + + private void MakeGump() + { + Closable = true; + Disposable = true; + Draggable = true; + Resizable = false; + AddPage(0); + AddBackground(0, 0, 280, 225, 9270); + AddAlphaRegion(10, 10, 260, 205); + AddLabel(64, 15, GreenHue, "Addon Script Generator"); + + AddLabel(20, 40, LabelHue, "Name"); + AddImageTiled(95, 55, 165, 1, 9304); + AddTextEntry(95, 35, 165, 20, LabelHue, 0, _state.Name); + + AddLabel(20, 60, LabelHue, "Namespace"); + AddImageTiled(95, 75, 165, 1, 9304); + AddTextEntry(95, 55, 165, 20, LabelHue, 1, _state.Namespace); + + AddCheck(20, 85, 2510, 2511, _state.Items, 0); + AddLabel(40, 85, LabelHue, "Export Items"); + + AddCheck(20, 110, 2510, 2511, _state.Statics, 1); + AddLabel(40, 110, LabelHue, "Export Statics"); + + AddCheck(20, 135, 2510, 2511, _state.Range, 2); + AddLabel(40, 135, LabelHue, "Specify Z Range"); + + AddLabel(50, 160, LabelHue, "Max"); + AddImageTiled(85, 175, 60, 1, 9304); + AddTextEntry(85, 155, 60, 20, LabelHue, 2, _state.Min.ToString()); + + AddLabel(160, 160, LabelHue, "Min"); + AddImageTiled(200, 175, 60, 1, 9304); + AddTextEntry(200, 155, 60, 20, LabelHue, 3, _state.Max.ToString()); + + AddButton(20, 185, 4020, 4021, 0); + AddLabel(55, 185, LabelHue, "Cancel"); + + AddButton(155, 185, 4005, 4006, 1); + AddLabel(195, 185, LabelHue, "Generate"); + } + + public override void OnResponse(Network.NetState sender, in RelayInfo info) + { + if (info.ButtonID == 0) + { + return; + } + + _state.Name = info.GetTextEntry(0); + _state.Namespace = info.GetTextEntry(1); + + _state.Items = info.IsSwitched(0); + _state.Statics = info.IsSwitched(1); + _state.Range = info.IsSwitched(2); + + var fail = !_state.Items && !_state.Statics; + + if (!sbyte.TryParse(info.GetTextEntry(2), out sbyte min)) + { + min = sbyte.MinValue; + fail = true; + } + + if (!sbyte.TryParse(info.GetTextEntry(3), out sbyte max)) + { + max = sbyte.MaxValue; + fail = true; + } + + if (max < min) + { + (max, min) = (min, max); + } + + _state.Min = min; + _state.Max = max; + + if (fail) + { + sender.Mobile.SendMessage(0x40, "Please review the generation parameters, some are invalid."); + sender.Mobile.SendGump(new InternalGump(sender.Mobile, _state)); + return; + } + + BoundingBoxPicker.Begin( + sender.Mobile, + (map, start, end) => PickerCallbackWrapper(sender.Mobile, map, start, end, PickerCallback, _state) + ); + } + } +}