Fixes walk-up aggro latency (up to a full 10 s of obliviousness) and hardens the reacquire gate so no state can silence acquisition, while turning `AcquireOnApproach` into the reaction-time knob for future per-creature intelligence tuning. ### Why `AcquireFocusMob` re-armed the 10 s `ReacquireDelay` **before** scanning, success or failure. A creature that scanned an empty room was blind for 10 s to a player walking up — walk-up aggro latency was uniform in 0..10 s. Waking from sector sleep stacked the AI timer's 0–3 s construction stagger on top. And `NextReacquireTime` is not serialized: on hosts whose tick counter starts negative (GCP pass-through), the 0 default blocked **all** acquisition shard-wide after a restart until the counter crossed zero. ### What **Event-driven reaction — `AcquireOnApproachDelay` (the intelligence gradient)** - The paragon `AcquireOnApproach` bool becomes a `TimeSpan` on every creature: an enemy moving inside `AcquireOnApproachRange` (10 for all creatures — on-screen reactive aggro; the periodic scan keeps the wide `RangePerception` sweep) *clamps* the next scan to at most the delay. Repeated steps cannot shorten it further — one scan per delay period, not per step or think. - `Zero` (paragons) also prods the AI timer: the ranked scan engages within a wheel turn — the old snap, minus the special-cased engage path. The target now comes from the normal FightMode ranking instead of whichever mobile happened to move, and the `Combatant == null` guard stops re-engage spam. - The 2 s default reads as "took a beat to notice you"; larger values are dumber; `ReacquireDelay` alone is the oblivious floor. Mover checks are the approach logic's `IsEnemy` + `CanBeHarmful` (so pets count and hidden movers are excluded via `CanSee`), with `IsEnemy` first to cheaply reject same-team wild creatures wandering past. The check rides the `OnMovement` callback every step already pays for — no polling added. **Gate correctness** - Every scan re-arms the full `ReacquireDelay`, success or failure (classic semantics; reaction time is the approach path, not the poll). - Self-healing by construction: a deadline further out than `ReacquireDelay` is an illegal state and reads as open — no wedged or wrapped value can silence acquisition beyond one delay period. - `NextReacquireTime` is seeded from a live tick on deserialize (the GCP negative-tick blackout). **AI timer wake** - Activation (sector wake, spawn, resurrection) starts within a 0–256 ms spread instead of the 0–3 s construction stagger, which read as lag. - The stagger's real job — keeping same-speed cohorts out of lock-step (the RunUO town artifact) — is now a zero-mean ±period/8 jitter on each **idle** think, so phases random-walk apart within seconds and can never re-lock. Instrumentation showed why a one-shot spread can't do this job: the timer wheel fires within ±1 ms, so with 10 creatures on a 500 ms period some pair collides on nearly the same phase ~75% of the time (birthday paradox) and then steps in the same loop iteration *forever*. Jitter is scoped to passive speed: engaged cadence stays exact, since pursuit timing anchors to real step times. **Debug** - The `AcquireFocusMob` scan message no longer re-arms the shared 5 s debug cooldown, which swallowed every AI's "I have detected X" transition line. **API change** for custom scripts: `AcquireOnApproach` (bool) → `AcquireOnApproachDelay` (TimeSpan). Documented in `content-patterns.md` § Target Acquisition, `runuo-migration-docs/09` + `11`, and the migration skill checklist. ### Tests `AcquisitionTests`: both scan outcomes honor `ReacquireDelay`; a 60 s-wedged gate still acquires; enemy movement clamps the deadline (same-team wild movers and out-of-range movers ignored); repeated movement cannot shorten below the delay; `Zero` opens the gate and prods without a direct engage. Full suite: 755 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 |
AcquireOnApproach (bool) |
AcquireOnApproachDelay (TimeSpan) |
Reaction-time gradient; Zero = old instant behavior |
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 |