## Summary
`[AddonGen` currently produces addon scripts that **do not compile**, plus a few
gather-logic and UI bugs. This fixes all of them.
## Compile-breaking (verified)
Every generated addon failed to build because of the item-component emission path:
- **Trailing comma + missing semicolon.** Items were emitted as a multi-line
`AddComponent(\n … ,\n)` — a trailing comma in the argument list and no terminating
`;`, i.e. `CS1525: Invalid expression term ')'` and `CS1002: ; expected`.
- **`Deed` missing `new`.** The template emitted
`public override BaseAddonDeed Deed => {name}AddonDeed();` — invoking the type as a
method (`CS1955: Non-invocable member … cannot be used like a method`).
Both are now fixed; components are emitted on a single line matching the existing
static-tile path:
```csharp
AddComponent(new AddonComponent(3215) { Light = LightType.Circle300, Hue = 5 }, 2, 3, 5);
```
**Verification:** compiled the generator's *output* (a representative two-component addon —
one plain, one hued + light-source) before and after the change against minimal
`BaseAddon`/`AddonComponent` stubs:
- Before: `CS1525` + `CS1002` (item path), and `CS1955` in isolation for the `Deed` line.
- After: **Build succeeded.**
`Projects/UOContent` also builds clean with the source change.
## Gather-logic + UI (reasoned from the code, not runtime-tested)
- **Inverted Z-range guards.** The tile/item scan used `if (range && …)`, so with the range
filter off (the default) map tiles and items were never captured — inconsistent with the
Static pass's `if (!range || …)`. Corrected to match.
- **"Export Items" was dead unless "Export Statics" was also checked** — the items scan was
nested inside `if (statics)`. Items now scan independently. Placed `Static` items are
skipped in this path because they're already captured (with hue/light) by the
`GetItemsInBounds<Static>` pass, which also removes a pre-existing double-count.
- **Swapped gump Min/Max labels** — the "Max" label sat over the Min entry and vice versa.
## Notes
The three gather/UI fixes are reasoned from the code rather than exercised through the
in-game gump, so they're worth a close look in review. The compile fixes are the headline
and are output-verified.
460 lines
14 KiB
C#
460 lines
14 KiB
C#
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<string> _templatePlaceholders = SearchValues.Create(["{serverItemsNamespace}", "{namespace}", "{name}", "{components}"], StringComparison.Ordinal);
|
|
|
|
private static string ExpandTemplate(
|
|
ReadOnlySpan<char> serverItemsNamespace,
|
|
ReadOnlySpan<char> ns,
|
|
ReadOnlySpan<char> name,
|
|
ReadOnlySpan<char> 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 [<name> [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<Point2D, List<StaticTile>>();
|
|
|
|
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<StaticTile> list = [];
|
|
|
|
if (items)
|
|
{
|
|
foreach (var item in map.GetItemsAt(x, y))
|
|
{
|
|
// Placed Static items are captured (with hue/light) by the
|
|
// GetItemsInBounds<Static> 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<Item>.Create();
|
|
|
|
foreach (var s in map.GetItemsInBounds<Static>(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)
|
|
);
|
|
}
|
|
}
|
|
}
|