ModernUO/Projects/UOContent.Tests/Tests/Utilities/TryParseTests.cs
Kamron Batman 459674ce3b
fix(commands): parse and edit IPoint2D/IPoint3D properties (#2646)
## 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.
2026-09-14 21:20:22 -07:00

82 lines
2.8 KiB
C#

using System;
using Xunit;
namespace Server.Tests.Utility;
public class TryParseTests
{
[Theory]
[InlineData("True", null, true)]
[InlineData("False", null, false)]
[InlineData("Alakazam", "Not a valid boolean string.", true)]
public void TestTryParseBool(string value, string returned, bool parsedAs)
{
var actualReturned = Server.Types.TryParse(typeof(bool), value, out var constructed);
Assert.Equal(returned, actualReturned);
if (returned == null)
{
Assert.Equal(parsedAs, constructed);
}
}
[Theory]
// Parsed directly into the target type (INumber<T>.TryParse), not via ulong + Convert.ChangeType.
[InlineData(typeof(int), "42", true, 42)]
[InlineData(typeof(int), "-5", true, -5)] // signed values parse directly now
[InlineData(typeof(int), "0xFF", true, 255)] // hex
[InlineData(typeof(int), "notanumber", false, null)]
[InlineData(typeof(byte), "255", true, (byte)255)]
[InlineData(typeof(byte), "256", false, null)] // out of the byte range
[InlineData(typeof(uint), "4294967295", true, 4294967295u)]
[InlineData(typeof(long), "-9000000000", true, -9000000000L)]
public void TestTryParseNumeric(Type type, string value, bool success, object expected)
{
var error = Server.Types.TryParse(type, value, out var constructed);
if (success)
{
Assert.Null(error);
Assert.Equal(expected, constructed);
}
else
{
Assert.NotNull(error);
}
}
}
// Interface-typed properties (BaseCreature.TargetLocation : IPoint2D) have no static Parse, so
// without these branches the parser fell through to Convert.ChangeType and reported the value as
// "not properly formatted".
public class InterfacePointParseTests
{
[Theory]
[InlineData(typeof(IPoint3D), "(1, 2, 3)", 1, 2, 3)]
[InlineData(typeof(IPoint2D), "(1, 2, 3)", 1, 2, 3)] // a 3-tuple is a valid IPoint2D
public void TuplesParseIntoPoint3D(Type type, string value, int x, int y, int z)
{
Assert.Null(Server.Types.TryParse(type, value, out var constructed));
Assert.Equal(new Point3D(x, y, z), constructed);
}
[Fact]
public void PairParsesIntoPoint2D()
{
Assert.Null(Server.Types.TryParse(typeof(IPoint2D), "(4, 5)", out var constructed));
Assert.Equal(new Point2D(4, 5), constructed);
}
[Fact]
public void PairIsNotAPoint3D()
{
Assert.NotNull(Server.Types.TryParse(typeof(IPoint3D), "(4, 5)", out _));
}
[Fact]
public void NullSentinelClearsAnInterfaceProperty()
{
Assert.Null(Server.Types.TryParse(typeof(IPoint2D), "(-null-)", out var constructed));
Assert.Null(constructed);
}
}