refactor(factions): migrate Faction/Town from RunUO Parse(string) to ISpanParsable<T>
Faction and Town were never converted from RunUO's bare static Parse(string) to IParsable/ISpanParsable, so they had only a 1-arg Parse(string) that returned null on no-match. That made them unreachable through Server.Types' IParsable path (they fell to Convert.ChangeType and failed) and, worse, would have let [set assign null silently on a bad name. Both now implement ISpanParsable<T> (string + span Parse/TryParse, throw-on- failure contract), matching Race/Poison. No external callers depended on the old null-returning Parse. Also documents the convention in dev-docs/runuo-migration-docs/01-foundation-changes.md (new section 15 + checklist item): ModernUO expects any RunUO static Parse(string) to be converted to IParsable<T>/ISpanParsable<T>. Server.Types still binds a legacy Parse(string) by reflection as a safety net. Full suite 535/535.
This commit is contained in:
parent
8997aaf498
commit
861c7e5f4d
3 changed files with 115 additions and 10 deletions
|
|
@ -16,7 +16,7 @@ using Server.Targeting;
|
|||
namespace Server.Factions;
|
||||
|
||||
[CustomEnum(["Minax", "Council of Mages", "True Britannians", "Shadowlords"])]
|
||||
public abstract class Faction : IComparable<Faction>
|
||||
public abstract class Faction : IComparable<Faction>, ISpanParsable<Faction>
|
||||
{
|
||||
public const int StabilityFactor = 300; // 300% greater (3 times) than smallest faction
|
||||
public const int StabilityActivation = 200; // Stability code goes into effect when largest faction has > 200 people
|
||||
|
|
@ -1313,7 +1313,27 @@ public abstract class Faction : IComparable<Faction>
|
|||
return null;
|
||||
}
|
||||
|
||||
public static Faction Parse(string name)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Faction Parse(string s) => Parse(s, null);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Faction Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool TryParse(string s, IFormatProvider provider, out Faction result) =>
|
||||
TryParse(s.AsSpan(), provider, out result);
|
||||
|
||||
public static Faction Parse(ReadOnlySpan<char> s, IFormatProvider provider)
|
||||
{
|
||||
if (TryParse(s, provider, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new FormatException($"The input string '{s}' was not in a correct format.");
|
||||
}
|
||||
|
||||
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider provider, out Faction result)
|
||||
{
|
||||
var factions = Factions;
|
||||
|
||||
|
|
@ -1321,13 +1341,15 @@ public abstract class Faction : IComparable<Faction>
|
|||
{
|
||||
var faction = factions[i];
|
||||
|
||||
if (faction.Definition.FriendlyName.InsensitiveEquals(name))
|
||||
if (s.InsensitiveEquals(faction.Definition.FriendlyName))
|
||||
{
|
||||
return faction;
|
||||
result = faction;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool InSkillLoss(Mobile mob) => m_SkillLoss.ContainsKey(mob);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Factions;
|
||||
|
||||
[CustomEnum(["Britain", "Magincia", "Minoc", "Moonglow", "Skara Brae", "Trinsic", "Vesper", "Yew"])]
|
||||
public abstract class Town : IComparable<Town>
|
||||
public abstract class Town : IComparable<Town>, ISpanParsable<Town>
|
||||
{
|
||||
public const int SilverCaptureBonus = 10000;
|
||||
|
||||
|
|
@ -482,7 +483,27 @@ public abstract class Town : IComparable<Town>
|
|||
return null;
|
||||
}
|
||||
|
||||
public static Town Parse(string name)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Town Parse(string s) => Parse(s, null);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Town Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool TryParse(string s, IFormatProvider provider, out Town result) =>
|
||||
TryParse(s.AsSpan(), provider, out result);
|
||||
|
||||
public static Town Parse(ReadOnlySpan<char> s, IFormatProvider provider)
|
||||
{
|
||||
if (TryParse(s, provider, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new FormatException($"The input string '{s}' was not in a correct format.");
|
||||
}
|
||||
|
||||
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider provider, out Town result)
|
||||
{
|
||||
var towns = Towns;
|
||||
|
||||
|
|
@ -490,13 +511,15 @@ public abstract class Town : IComparable<Town>
|
|||
{
|
||||
var town = towns[i];
|
||||
|
||||
if (town.Definition.FriendlyName.InsensitiveEquals(name))
|
||||
if (s.InsensitiveEquals(town.Definition.FriendlyName))
|
||||
{
|
||||
return town;
|
||||
result = town;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
[Usage("GrantTownSilver <amount>")]
|
||||
|
|
|
|||
|
|
@ -282,6 +282,65 @@ public MyItem(Serial serial) : base(serial) { }
|
|||
// ModernUO — DELETE THIS CONSTRUCTOR. The source generator creates it.
|
||||
```
|
||||
|
||||
## 15. Static `Parse(string)` → `IParsable<T>` / `ISpanParsable<T>`
|
||||
|
||||
RunUO predates `IParsable<T>`/`ISpanParsable<T>` (C# 11 / .NET 7 static-abstract interface
|
||||
members), so RunUO types that convert from a string expose a bare `public static T Parse(string value)`.
|
||||
**ModernUO expects any such type to implement `IParsable<T>` (string) and, where practical,
|
||||
`ISpanParsable<T>` (span; it extends `IParsable<T>`, so implement span and you get both).**
|
||||
|
||||
This matters because the engine's string→value converter, `Server.Types.TryParse` — used by `[set`,
|
||||
`[props`, spawner property assignment, the conditional-command compiler (`[where`), and Advanced
|
||||
Search — binds to the `Parse(string, IFormatProvider)` signature. A type with **only** a legacy
|
||||
`Parse(string)` is discovered by `Types` through a reflection fallback, but that fallback is a safety
|
||||
net, not the intended path: a bare `Parse(string)` is easy to miss, doesn't participate in the
|
||||
span-based fast paths, and (if it returns `null` instead of throwing) makes `[set` silently assign
|
||||
`null` on bad input. Convert it.
|
||||
|
||||
The `Parse` overloads throw `FormatException` on failure; `TryParse` returns `false`. Delegate the
|
||||
string overloads to a span core (see `Race`, `Poison`, `Point3D` for the established pattern):
|
||||
|
||||
```csharp
|
||||
// RunUO
|
||||
public abstract class Faction : IComparable<Faction>
|
||||
{
|
||||
public static Faction Parse(string name) // returns null on no-match — wrong contract, not IParsable
|
||||
{
|
||||
// ... linear search by name ...
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ModernUO
|
||||
public abstract class Faction : IComparable<Faction>, ISpanParsable<Faction>
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Faction Parse(string s) => Parse(s, null);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Faction Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool TryParse(string s, IFormatProvider provider, out Faction result) =>
|
||||
TryParse(s.AsSpan(), provider, out result);
|
||||
|
||||
public static Faction Parse(ReadOnlySpan<char> s, IFormatProvider provider) =>
|
||||
TryParse(s, provider, out var result)
|
||||
? result
|
||||
: throw new FormatException($"The input string '{s}' was not in a correct format.");
|
||||
|
||||
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider provider, out Faction result)
|
||||
{
|
||||
// ... linear search by name using s.InsensitiveEquals(...) ...
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To find un-migrated types: search for `public static [A-Za-z0-9_<>]+ Parse\(string ` and check whether
|
||||
the declaring type lists `IParsable<T>`/`ISpanParsable<T>`.
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
When migrating any RunUO script, apply these changes in order:
|
||||
|
|
@ -300,6 +359,7 @@ When migrating any RunUO script, apply these changes in order:
|
|||
12. [ ] Modernize property syntax
|
||||
13. [ ] Remove `Serial` constructor (handled by serialization generator)
|
||||
14. [ ] Update usings
|
||||
15. [ ] Convert bare static `Parse(string)` to `IParsable<T>`/`ISpanParsable<T>`
|
||||
|
||||
## See Also
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue