Stacked on #2594. Fixes jerky creature movement (lich / Fast-bucket melee chases) by choosing the client animation flag from the actual step pace instead of a caller-supplied `run` argument, and fixes three step-pacing defects in the move budget found while verifying it with paired server/client traces. ### Why The `Direction.Running` bit does nothing for creatures server-side (`Mobile.OnMove` reads it only for the player throttle and stealth reveal). Its whole effect is on the client, which animates each step over a fixed time selected by that bit: walk 400 ms / run 200 ms on foot, 200 / 100 ms mounted. ClassicUO queues up to 5 steps and *drops* the sixth, so a creature stepping every 300 ms while flagged as walking backs the queue up until it snaps forward — the observed jerk. The `run` argument never carried the one fact that matters (the step interval). RunUO passed `true` in combat / `false` for pets and gated it on `dist > 5`; #2271 flipped every combat site to `false`; pets passed `currentDistance > 2`. None of that is a coherent signal. ### What **Pace-derived run flag** - `BaseAI.ShouldRun()`: run iff the effective step delay (move clock + badly-hurt inflation) is shorter than `Movement.WalkFootDelay` / `WalkMountDelay` (mounted or flying) — with a continuity rule: an *isolated* step (taken after standing at least a walk interval) goes out as a walk, because the client renders each step alone and a lone run-flagged step is a 200 ms dart. Only a continuing cadence flags run; a true sprinter (pace under the run interpolation) always runs, since a walk-rendered first step would flood the client's 5-step queue. This reproduces RunUO's close-in feel (its `dist > 5` gate) from first principles. - `DoMoveImpl` stamps the bit; it is the single place the flag is set. - `run` removed from `MoveTo`, `WalkMobileRange`, `ApproachTarget`, `MoveToPoint`, `MoveToWithGroup`, `MoveToWithCollisionAvoidance`, the move intent, and `PathFollower.Follow`. All 35 call sites updated. **API change** for custom scripts — documented in the RunUO migration docs (`09-items-mobiles-creatures.md`, `11-api-reference.md`) and `content-patterns.md` § Creature Speeds. **Move-budget pacing fixes** (each confirmed by UTC-aligned server/client step traces) - A stall no longer banks catch-up steps: the budget's snap-to-now released up to three steps in ~300 ms when a creature resumed chasing after standing beside its target — rendered as a teleport. - Debt accrual removed entirely: a step landing sub-period late (think-grid vs budget misalignment during reactive mirroring) kept the remainder and fired a follow-up ~100 ms later — a dart pair. `ConsumeMoveBudget` now paces every step from when it was actually taken; in continuous pursuit the move-wake lands within wheel resolution of the deadline, so the cost is single-digit-ms drift. - Net effect: a creature can never step faster than its pace, verified across a full chase session (zero sub-pace steps; metronomic 350 ms cadence for a 0.3 s lich). - Test fixture now runs `Movement.Configure()` (the walk delays were 0 in tests). ### Accepted trade-off Animal (LOW group) bodies without a run animation slide on their stand frames when flagged as running. Most are slow enough to stay flagged as walking; the client-side fallback is in ClassicUO/ClassicUO#1930. ### Tests `RunFlagTests`: foot thresholds (0.3 / 0.125 run; 0.4 / 0.45 / 1.05 walk), flying uses the mount threshold, badly-hurt inflation flips a 0.35 s creature back to walk, a real `DoMove` stamps the bit, isolated steps drop to walk (sprinters keep running), a stall restarts the cadence with no banked steps, and a late step earns no quicker follow-up. Full suite: 837 Server + 747 UOContent green.
10 KiB
10 KiB
API Reference — RunUO to ModernUO Mapping
Quick Lookup
Alphabetical by RunUO API name. Use Ctrl+F / Cmd+F to search.
Attributes
| RunUO | ModernUO | Notes |
|---|---|---|
[Constructable] |
[Constructible] |
Spelling change |
[CommandProperty(AccessLevel)] |
[SerializedCommandProperty(AccessLevel)] |
On [SerializableField] fields |
[CommandProperty(AccessLevel)] |
[CommandProperty(AccessLevel)] |
On [SerializableProperty] properties (unchanged) |
[CorpseName("name")] |
public override string CorpseName => "name"; |
Attribute → property override |
[Constructable] |
[Constructible] |
Must use using ModernUO.Serialization; |
[Usage("...")] |
[Usage("...")] |
Unchanged |
[Description("...")] |
[Description("...")] |
Unchanged |
| N/A | [Aliases("a1", "a2")] |
New in ModernUO |
| N/A | [SerializationGenerator(ver, enc)] |
New — replaces manual Serialize/Deserialize |
| N/A | [SerializableField(idx)] |
New — auto-generates property + serialization |
| N/A | [SerializableProperty(idx)] |
New — custom property serialization |
| N/A | [InvalidateProperties] |
New — auto-calls InvalidateProperties on set |
| N/A | [AfterDeserialization] |
New — post-deserialize hook |
| N/A | [DeltaDateTime] |
New — relative DateTime storage |
| N/A | [EncodedInt] |
New — variable-length int encoding |
| N/A | [InternString] |
New — string interning on load |
| N/A | [Tidy] |
New — auto-clean null entries in collections |
| N/A | [CanBeNull] |
New — nullable reference field |
| N/A | [TypeAlias("old.name")] |
New — backward-compatible type mapping |
Classes
| RunUO | ModernUO | Notes |
|---|---|---|
BinaryFileWriter |
IGenericWriter (via GenericPersistence) |
No manual file writing |
BinaryFileReader |
IGenericReader (via GenericPersistence) |
No manual file reading |
GenericWriter |
IGenericWriter |
Interface now |
GenericReader |
IGenericReader |
Interface now |
Gump |
DynamicGump or StaticGump<T> |
See 04-gumps.md |
ObjectPropertyList (parameter) |
IPropertyList |
Interface for GetProperties |
Packet (base class) |
Removed — use static methods | See 05-packets.md |
PacketWriter |
SpanWriter |
Ref struct |
PacketReader |
SpanReader |
Ref struct |
Timer (subclass pattern) |
Timer.StartTimer() + TimerExecutionToken |
See 03-timers.md |
TextRelay |
Removed — use info.GetTextEntry(id) |
Returns string directly |
RelayInfo |
RelayInfo (passed with in) |
in RelayInfo in OnResponse |
Constructors
| RunUO | ModernUO | Notes |
|---|---|---|
MyItem(Serial serial) : base(serial) |
DELETE | Auto-generated |
BaseCreature(AI, Fight, 10, 1, 0.2, 0.4) |
BaseCreature(AI, Fight) |
Extra params have defaults |
Events (EventSink)
| RunUO | ModernUO | Signature |
|---|---|---|
EventSink.Login |
EventSink.Connected |
LoginEventArgs → Action<Mobile> |
EventSink.Logout |
EventSink.Disconnected |
LogoutEventArgs → Action<Mobile> |
EventSink.WorldSave |
EventSink.WorldSave |
WorldSaveEventArgs → Action |
EventSink.WorldLoad |
EventSink.WorldLoad |
Action |
EventSink.ServerStarted |
EventSink.ServerStarted |
Action |
EventSink.Crashed |
EventSink.ServerCrashed |
Renamed |
EventSink.Speech |
EventSink.Speech |
Action<SpeechEventArgs> |
EventSink.Movement |
EventSink.Movement |
Action<MovementEventArgs> |
EventSink.AggressiveAction |
EventSink.AggressiveAction |
Action<AggressiveActionEventArgs> |
EventSink.AccountLogin |
EventSink.AccountLogin |
Action<AccountLoginEventArgs> |
EventSink.SocketConnect |
EventSink.SocketConnect |
Action<SocketConnectEventArgs> |
EventSink.Shutdown |
EventSink.Shutdown |
Action |
EventSink.BeforeWorldSave |
Removed | Use WorldSave |
EventSink.PlayerDeath |
PlayerMobile.PlayerDeathEvent |
CodeGeneratedEvent |
EventSink.CreatureDeath |
BaseCreature.CreatureDeathEvent |
CodeGeneratedEvent |
Event Delegates
| RunUO | ModernUO |
|---|---|
new WorldSaveEventHandler(Save) |
Save (direct method reference) |
new WorldLoadEventHandler(Load) |
Load |
new LoginEventHandler(OnLogin) |
OnConnected |
new LogoutEventHandler(OnLogout) |
OnDisconnected |
new SpeechEventHandler(OnSpeech) |
OnSpeech |
new TimerCallback(Method) |
Method |
new TimerStateCallback(Method) |
Method (typed overloads) |
new OnPacketReceive(Handler) |
&Handler (function pointer) |
Gump Methods
| RunUO | ModernUO | Notes |
|---|---|---|
AddPage(0) |
builder.AddPage() |
0 is default |
AddBackground(...) |
builder.AddBackground(...) |
On builder |
AddAlphaRegion(...) |
builder.AddAlphaRegion(...) |
On builder |
AddLabel(x, y, hue, text) |
builder.AddLabel(x, y, hue, text) |
On builder |
AddHtml(x, y, w, h, text, bg, scroll) |
builder.AddHtml(x, y, w, h, text, background: bg, scrollbar: scroll) |
Named params |
AddHtmlLocalized(...) |
builder.AddHtmlLocalized(...) |
On builder |
AddButton(x, y, n, p, id, Reply, 0) |
builder.AddButton(x, y, n, p, id) |
Reply is default |
AddTextEntry(...) |
builder.AddTextEntry(...) |
On builder |
AddCheck(x, y, off, on, state, id) |
builder.AddCheckbox(x, y, off, on, state, id) |
Renamed |
Closable = false |
builder.SetNoClose() |
Property → method |
Dragable = false |
builder.SetNoMove() |
Renamed |
Resizable = false |
builder.SetNoResize() |
Property → method |
Disposable = false |
builder.SetNoDispose() |
Property → method |
from.SendGump(new G(...)) |
G.DisplayTo(from, ...) |
Static entry point |
from.CloseGump(typeof(G)) |
from.CloseGump<G>() |
Generic |
from.HasGump(typeof(G)) |
from.HasGump<G>() |
Generic |
Gump Response
| RunUO | ModernUO |
|---|---|
OnResponse(NetState, RelayInfo) |
OnResponse(NetState, in RelayInfo) |
info.TextEntries[i].Text |
info.GetTextEntry(id) |
info.IsSwitched(id) |
info.IsSwitched(id) (same) |
info.ButtonID |
info.ButtonID (same) |
Methods
| RunUO | ModernUO | Notes |
|---|---|---|
DateTime.UtcNow |
Core.Now |
Server time source |
Console.WriteLine(...) |
logger.Information(...) |
Structured logging |
World.FindMobile(serial) |
reader.ReadEntity<Mobile>() |
In deserialization |
World.FindItem(serial) |
reader.ReadEntity<Item>() |
In deserialization |
reader.ReadMobile() |
reader.ReadEntity<Mobile>() |
Generic method |
reader.ReadItem() |
reader.ReadEntity<Item>() |
Generic method |
reader.ReadInt() |
reader.ReadInt() |
Same |
writer.Write((int)value) |
writer.Write(value) |
No cast needed |
writer.WriteEncodedInt(value) |
writer.WriteEncodedInt(value) |
Same |
InvalidateProperties() |
InvalidateProperties() |
Same, or use [InvalidateProperties] |
this.MarkDirty() |
this.MarkDirty() |
NEW — required in custom setters |
MoveTo(m, run, range) |
MoveTo(m, range) |
run removed; the Running bit is derived from the step pace (BaseAI.ShouldRun) |
WalkMobileRange(m, steps, run, min, max) |
WalkMobileRange(m, steps, min, max) |
Same |
PathFollower.Follow(run, range) |
Follow(range) |
Same |
Networking
| RunUO | ModernUO | Notes |
|---|---|---|
ns.Send(new MyPacket(...)) |
ns.SendMyPacket(...) |
Extension method |
Packet.Compile() |
Not needed | Stack-allocated |
Packet.SetStatic() |
Not needed | Reuse via buffer[0] != 0 guard |
PacketHandlers.Register(...) |
IncomingPackets.Register(...) |
Function pointers |
m_Stream.Write(value) |
writer.Write(value) |
SpanWriter |
pvSrc.ReadInt32() |
reader.ReadInt32() |
SpanReader |
pvSrc.ReadString() |
reader.ReadAsciiSafe() |
Explicit encoding |
pvSrc.ReadUnicodeStringSafe() |
reader.ReadBigUniSafe() |
Explicit name |
Persistence
| RunUO | ModernUO | Notes |
|---|---|---|
EventSink.WorldSave += Save |
class X : GenericPersistence |
Subclass |
EventSink.WorldLoad += Load |
override Deserialize(IGenericReader) |
Method |
new BinaryFileWriter(path, true) |
Handled by framework | Automatic |
BinaryFileReader / FileStream |
Handled by framework | Automatic |
Directory.CreateDirectory(...) |
Handled by framework | Automatic |
Properties
| RunUO | ModernUO | Notes |
|---|---|---|
Name = "text" (in constructor) |
public override string DefaultName => "text"; |
Property override |
Name (custom text) |
DefaultName (property override) |
For fixed names |
int Prop { get { return m_X; } } |
int Prop => _x; |
Expression-bodied |
int Prop { get { return m_X; } set { m_X = value; } } |
Auto-generated by [SerializableField] |
Delete manual property |
Timer
| RunUO | ModernUO | Notes |
|---|---|---|
new InternalTimer().Start() |
Timer.StartTimer(..., out token) |
Fire-and-forget |
Timer.DelayCall(delay, callback) |
Timer.StartTimer(delay, callback) |
Similar |
Timer.DelayCall(delay, stateCallback, state) |
Timer.DelayCall(delay, callback, state) |
Typed state |
timer.Stop() |
token.Cancel() |
Struct, safe to call multiple times |
timer.Running |
token.Running |
Same concept |
TimerPriority.XXX |
Removed | Timer wheel auto-schedules |
Timer in Deserialize() |
[AfterDeserialization] |
Post-load hook |
timer != null check |
token.Running check |
Value type, always valid |
Threading / Performance
| RunUO | ModernUO | Notes |
|---|---|---|
lock (_obj) |
Remove | Single-threaded |
volatile |
Remove keyword | Single-threaded |
ConcurrentDictionary |
Dictionary |
Single-threaded |
Task.Run(...) |
Use Timer.StartTimer() |
Game loop only |
new Thread(...) |
Forbidden | Game loop only |
ArrayPool<T>.Shared |
STArrayPool<T>.Shared |
No-lock pool |
new List<T>() (hot path) |
PooledRefList<T>.Create() |
Zero-alloc |
World.Mobiles.Values iteration |
map.GetMobilesInRange<T>() |
Spatial query |
World.Items.Values iteration |
map.GetItemsInRange<T>() |
Spatial query |
Usings
| RunUO | ModernUO | Notes |
|---|---|---|
| (implicit) | using ModernUO.Serialization; |
For serialization attributes |
| (implicit) | using Server.Logging; |
For logging |
| (implicit) | using Server.Gumps; |
For gump extension methods |
| (implicit) | using Server.Collections; |
For PooledRefList |