using System; using System.Buffers; 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 => new {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() { } } """; private static readonly SearchValues _templatePlaceholders = SearchValues.Create(["{serverItemsNamespace}", "{namespace}", "{name}", "{components}"], StringComparison.Ordinal); private static string ExpandTemplate( ReadOnlySpan serverItemsNamespace, ReadOnlySpan ns, ReadOnlySpan name, ReadOnlySpan components ) { using var output = ValueStringBuilder.Create(); var remaining = _template.AsSpan(); int index; while ((index = remaining.IndexOfAny(_templatePlaceholders)) >= 0) { output.Append(remaining[..index]); var placeholder = remaining[index..]; if (placeholder.StartsWith("{serverItemsNamespace}")) { output.Append(serverItemsNamespace); remaining = placeholder["{serverItemsNamespace}".Length..]; } else if (placeholder.StartsWith("{namespace}")) { output.Append(ns); remaining = placeholder["{namespace}".Length..]; } else if (placeholder.StartsWith("{name}")) { output.Append(name); remaining = placeholder["{name}".Length..]; } else // {components} { output.Append(components); remaining = placeholder["{components}".Length..]; } } output.Append(remaining); return output.ToString(); } 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); } var startPoint2D = new Point2D(start.X, start.Y); var endPoint2D = new Point2D(end.X, end.Y); var bounds = new Rectangle2D(startPoint2D, endPoint2D); var name = pickerState.Name; var ns = pickerState.Namespace; var items = pickerState.Items; var statics = pickerState.Statics; var range = pickerState.Range; var min = pickerState.Min; var max = pickerState.Max; // Get center var center = new Point3D(); center.Z = 127; var x1 = bounds.End.X; var y1 = bounds.End.Y; var x2 = bounds.Start.X; var y2 = bounds.Start.Y; var tiles = new Dictionary>(); if (statics || items) { for (var x = start.X; x <= end.X; x++) { for (var y = start.Y; y <= end.Y; y++) { var tileMatrix = map.Tiles; List list = []; if (items) { foreach (var item in map.GetItemsAt(x, y)) { // Placed Static items are captured (with hue/light) by the // GetItemsInBounds pass below; skip them here to avoid duplicates. if (item is Static) { continue; } if (!range || item.Z >= min && item.Z <= max) { list.Add(new StaticTile((ushort)item.ItemID, (sbyte)item.Z)); } } } if (statics) { 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 (var 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 (var 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 (var p in tiles.Keys) { var list = tiles[p]; foreach (var 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 (var p in tiles.Keys) { var list = tiles[p]; var xOffset = p.X - center.X; var yOffset = p.Y - center.Y; foreach (var t in list) { var zOffset = t.Z - center.Z; var id = t.ID - 16384; sb.Append($" AddComponent(new AddonComponent({id}), {xOffset}, {yOffset}, {zOffset});\n"); } } foreach (var item in target) { var xOffset = item.X - center.X; var yOffset = item.Y - center.Y; var 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(new AddonComponent({item.ItemID})"); if (hue != 0 || light != LightType.Empty) { sb.Append(" {"); if (light != LightType.Empty) { sb.Append($" Light = LightType.{light}"); if (hue != 0) { sb.Append($", Hue = {hue}"); } } else { sb.Append($" Hue = {hue}"); } sb.Append(" }"); } sb.Append($", {xOffset}, {yOffset}, {zOffset});\n"); } // Only pull in Server.Items when the generated addon lives in a different namespace. var serverItemsNamespace = ns == "Server.Items" ? "" : "\nusing Server.Items;"; var output = ExpandTemplate(serverItemsNamespace, ns, name, sb.AsSpan()); var path = Path.Combine(_outputDirectory, $"{name}Addon.cs"); var 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 override bool Singleton => true; public InternalGump(Mobile m, PickerState state) : base(100, 50) { _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, "Min"); AddImageTiled(85, 175, 60, 1, 9304); AddTextEntry(85, 155, 60, 20, LabelHue, 2, _state.Min.ToString()); AddLabel(160, 160, LabelHue, "Max"); 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 var min)) { min = sbyte.MinValue; fail = true; } if (!sbyte.TryParse(info.GetTextEntry(3), out var 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) ); } } }