## Symptom
`[set TargetLocation (x, y)` (quoted or not) answers **"That is not properly formatted."**, and in `[props` the `>` next to `TargetLocation` does nothing when the value is null — which is its normal idle state (`BaseAI.cs:650` clears it).
This looked like a `Point3D` parsing regression from #2624/#2625, but `Point3D`/`Point2D`-typed properties (`Location`, etc.) were never affected. The only `[CommandProperty]` in the tree declared as an **interface** is `BaseCreature.TargetLocation : IPoint2D` (`BaseCreature.cs:1111`), and both code paths only knew the structs. `git log -S"IPoint"` over the parser and gump files hits nothing but the initial import — the gap is inherited from RunUO, not recent.
## Root cause
- **`[set`** — `Types.TryParse` has no branch for `IPoint2D`/`IPoint3D`. An interface has no static `Parse`, so `GetParseMethod` returns null and the value falls into `Convert.ChangeType("(x, y)", typeof(IPoint2D))`, which throws → "not properly formatted".
- **Props gump** — `PropsGump` routes on `obj?.GetType() ?? prop.PropertyType` (since #2180). With a null value the type is `IPoint2D`; `Point2D.IsAssignableFrom(IPoint2D)` is false, no branch matches, and the click is inert. It only worked when the slot already held a `Point2D`, because the runtime type is then the struct.
## Fix
- `Types.TryParse`: `IPoint3D`/`IPoint2D` targets resolve to the concrete struct — `Point3D` first, then `Point2D` for an `IPoint2D` target (a 3-tuple is a valid `IPoint2D`). `(-null-)` still clears; the existing null branch runs first.
- `PropsGump`: the interface types route to `SetPoint3DGump`/`SetPoint2DGump`. The entity branch stays ahead of them — `TargetLocation` legitimately holds a Mobile too (`ShepherdsCrook.cs:148`, herding toward the shepherd), and that case still opens `SetObjectGump`.
- `SetPoint2DGump`/`SetPoint3DGump`: seed the text entries from `value is IPoint2D/IPoint3D` rather than a hard cast, so a `Point3D` sitting in an `IPoint2D` slot cannot `InvalidCast`.
## Not covered
`[set TargetLocation 0x40001234` (assigning a mobile by serial) still reports "not properly formatted" — the entity branch keys on the *target* type being `IEntity`, which `IPoint2D` isn't. Real state, but niche; left out to keep this to the reported symptom.
## Testing
Five cases in `InterfacePointParseTests`, watched fail before the change (three returned the error string; two pin existing behaviour that must survive): tuple → `Point3D` for both interfaces, pair → `Point2D`, pair rejected for `IPoint3D`, `(-null-)` clears.
`dotnet build` 0 warnings. **1059 UOContent** and **891 Server** tests pass, 0 failures. The gump routing is a one-line branch with no automated test — needs an in-game check: `[props` a creature with a null `TargetLocation`, press `>`, expect the Point2D editor.
168 lines
6.2 KiB
C#
168 lines
6.2 KiB
C#
using System.Reflection;
|
|
using Server.Commands;
|
|
using Server.Network;
|
|
using Server.Targeting;
|
|
|
|
using static Server.Gumps.PropsConfig;
|
|
|
|
namespace Server.Gumps
|
|
{
|
|
public class SetPoint3DGump : Gump
|
|
{
|
|
private const int CoordWidth = 70;
|
|
private const int EntryWidth = CoordWidth + OffsetSize + CoordWidth + OffsetSize + CoordWidth;
|
|
|
|
private const int TotalWidth = OffsetSize + EntryWidth + OffsetSize + SetWidth + OffsetSize;
|
|
|
|
private readonly Mobile m_Mobile;
|
|
private readonly object m_Object;
|
|
private readonly PropertyInfo m_Property;
|
|
private readonly PropertiesGump m_PropertiesGump;
|
|
|
|
public SetPoint3DGump(PropertyInfo prop, Mobile mobile, object o, PropertiesGump propertiesGump)
|
|
: base(GumpOffsetX, GumpOffsetY)
|
|
{
|
|
m_PropertiesGump = propertiesGump;
|
|
m_Property = prop;
|
|
m_Mobile = mobile;
|
|
m_Object = o;
|
|
|
|
var p = prop?.GetValue(o, null) is IPoint3D current ? new Point3D(current) : new Point3D();
|
|
|
|
AddPage(0);
|
|
|
|
this.AddPropsFrame(TotalWidth, 4, out var x, out var y);
|
|
this.AddPropsEntryLabel(ref x, ref y, EntryWidth, prop?.Name);
|
|
PropsLayout.NextRow(ref x, ref y);
|
|
this.AddPropsEntryButton(ref x, ref y, EntryWidth, "Use your location", true, 1);
|
|
PropsLayout.NextRow(ref x, ref y);
|
|
this.AddPropsEntryButton(ref x, ref y, EntryWidth, "Target a location", true, 2);
|
|
PropsLayout.NextRow(ref x, ref y);
|
|
|
|
AddImageTiled(x, y, CoordWidth, EntryHeight, EntryGumpID);
|
|
AddLabelCropped(x + TextOffsetX, y, CoordWidth - TextOffsetX, EntryHeight, TextHue, "X:");
|
|
AddTextEntry(x + 16, y, CoordWidth - 16, EntryHeight, TextHue, 0, p.X.ToString());
|
|
x += CoordWidth + OffsetSize;
|
|
|
|
AddImageTiled(x, y, CoordWidth, EntryHeight, EntryGumpID);
|
|
AddLabelCropped(x + TextOffsetX, y, CoordWidth - TextOffsetX, EntryHeight, TextHue, "Y:");
|
|
AddTextEntry(x + 16, y, CoordWidth - 16, EntryHeight, TextHue, 1, p.Y.ToString());
|
|
x += CoordWidth + OffsetSize;
|
|
|
|
AddImageTiled(x, y, CoordWidth, EntryHeight, EntryGumpID);
|
|
AddLabelCropped(x + TextOffsetX, y, CoordWidth - TextOffsetX, EntryHeight, TextHue, "Z:");
|
|
AddTextEntry(x + 16, y, CoordWidth - 16, EntryHeight, TextHue, 2, p.Z.ToString());
|
|
x += CoordWidth + OffsetSize;
|
|
|
|
if (SetGumpID != 0)
|
|
{
|
|
AddImageTiled(x, y, SetWidth, EntryHeight, SetGumpID);
|
|
}
|
|
|
|
AddButton(x + SetOffsetX, y + SetOffsetY, SetButtonID1, SetButtonID2, 3);
|
|
}
|
|
|
|
public override void OnResponse(NetState sender, in RelayInfo info)
|
|
{
|
|
Point3D toSet;
|
|
bool shouldSet, shouldSend;
|
|
|
|
switch (info.ButtonID)
|
|
{
|
|
case 1: // Current location
|
|
{
|
|
toSet = m_Mobile.Location;
|
|
shouldSet = true;
|
|
shouldSend = true;
|
|
|
|
break;
|
|
}
|
|
case 2: // Pick location
|
|
{
|
|
m_Mobile.Target = new InternalTarget(m_Property, m_Mobile, m_Object, m_PropertiesGump);
|
|
|
|
toSet = Point3D.Zero;
|
|
shouldSet = false;
|
|
shouldSend = false;
|
|
|
|
break;
|
|
}
|
|
case 3: // Use values
|
|
{
|
|
toSet = new Point3D(
|
|
Utility.ToInt32(info.GetTextEntry(0)),
|
|
Utility.ToInt32(info.GetTextEntry(1)),
|
|
Utility.ToInt32(info.GetTextEntry(2))
|
|
);
|
|
shouldSet = true;
|
|
shouldSend = true;
|
|
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
toSet = Point3D.Zero;
|
|
shouldSet = false;
|
|
shouldSend = true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (shouldSet)
|
|
{
|
|
try
|
|
{
|
|
CommandLogging.LogChangeProperty(m_Mobile, m_Object, m_Property.Name, toSet.ToString());
|
|
m_Property.SetValue(m_Object, toSet, null);
|
|
m_PropertiesGump.OnValueChanged(m_Object, m_Property);
|
|
}
|
|
catch
|
|
{
|
|
m_Mobile.SendMessage("An exception was caught. The property may not have changed.");
|
|
}
|
|
}
|
|
|
|
if (shouldSend)
|
|
{
|
|
m_PropertiesGump.SendPropertiesGump();
|
|
}
|
|
}
|
|
|
|
private class InternalTarget : Target
|
|
{
|
|
private readonly Mobile m_Mobile;
|
|
private readonly object m_Object;
|
|
private readonly PropertyInfo m_Property;
|
|
private readonly PropertiesGump m_PropertiesGump;
|
|
|
|
public InternalTarget(PropertyInfo prop, Mobile mobile, object o, PropertiesGump propertiesGump)
|
|
: base(-1, true, TargetFlags.None)
|
|
{
|
|
m_PropertiesGump = propertiesGump;
|
|
m_Property = prop;
|
|
m_Mobile = mobile;
|
|
m_Object = o;
|
|
}
|
|
|
|
protected override void OnTarget(Mobile from, object targeted)
|
|
{
|
|
if (targeted is IPoint3D p)
|
|
{
|
|
try
|
|
{
|
|
CommandLogging.LogChangeProperty(m_Mobile, m_Object, m_Property.Name, new Point3D(p).ToString());
|
|
m_Property.SetValue(m_Object, new Point3D(p), null);
|
|
m_PropertiesGump.OnValueChanged(m_Object, m_Property);
|
|
}
|
|
catch
|
|
{
|
|
m_Mobile.SendMessage("An exception was caught. The property may not have changed.");
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void OnTargetFinish(Mobile from) => m_PropertiesGump.SendPropertiesGump();
|
|
}
|
|
}
|
|
}
|