diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index f9f5fbe15..8b18d54e8 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "modernuoschemagenerator": { - "version": "4.0.0", + "version": "3.0.0", "commands": [ "ModernUOSchemaGenerator" ] diff --git a/.gitignore b/.gitignore index d29ee313a..d5b26e268 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ # Distribution Files -/Distribution/Data/Files /Distribution/Logger /Distribution/Logger.* /Distribution/ModernUO diff --git a/CLAUDE.md b/CLAUDE.md index c77d39e19..849ff3175 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,7 @@ Apply these when writing or reviewing `.cs` files under `Projects/`. 6. **Cancel timers in `OnDelete()`/`OnAfterDelete()`** — call `_token.Cancel()` or `_timer?.Stop()` 7. **`STArrayPool.Shared`** not `ArrayPool.Shared` — single-threaded optimized, no locks 8. **`PooledRefList`** not `new List()` on hot paths — zero GC pressure, stack-allocated ref struct -9. **Serialization** — class must be `partial`, constructor needs `[Constructible]`, `TimerExecutionToken` must NOT have `[SerializableField]`. New classes: use `[SerializationGenerator(version)]` (omit `encoded`). Setters that coerce/veto/run side effects: use `[SerializableField]` args `allowFieldChange: nameof(BoolRefMethod)` / `fieldChanged: nameof(OldNewMethod)` — reserve `[SerializableProperty]` for custom getters. Serializable `Timer` members declare `[DeserializeTimer(nameof(Method))]` on the field (anchored by default — downtime preserves remaining delay; `wallClock: true` = absolute; method runs only when a timer was running at save). Conditional writes: `[SaveFlag(nameof(Should), nameof(Default))]` on the field. When bumping versions, add `MigrateFrom(VXContent)` (X = previous version). Never modify `Deserialize(reader, version)` for version bumps — that method is only for pre-codegen legacy saves. When migrating from pre-codegen Serialize/Deserialize: pass `false` if old code used `reader.ReadInt()`, bump version +1, and keep old logic as `private void Deserialize(IGenericReader reader, int version)` → `dev-docs/serialization.md`, `dev-docs/runuo-migration-docs/02-serialization.md` +9. **Serialization** — class must be `partial`, constructor needs `[Constructible]`, `TimerExecutionToken` must NOT have `[SerializableField]`. New classes: use `[SerializationGenerator(version)]` (omit `encoded`). When bumping versions, add `MigrateFrom(VXContent)` (X = previous version). Never modify `Deserialize(reader, version)` for version bumps — that method is only for pre-codegen legacy saves. When migrating from pre-codegen Serialize/Deserialize: pass `false` if old code used `reader.ReadInt()`, bump version +1, and keep old logic as `private void Deserialize(IGenericReader reader, int version)` → `dev-docs/runuo-migration-docs/02-serialization.md` 10. **No `Task.Run`/`new Thread()` for game logic** (tandem with rule #3) — game logic is the single-threaded event loop. Backgrounding is allowed only for work that does not itself touch game state (external service calls, large-file parse). **Prove the need before adding a thread**: measure **on-loop** time, not wall-clock (frozen world is the cost, player latency is not), and gate on `Environment.ProcessorCount` — off-loading creates no CPU and buys nothing on 1–2 cores. New workers go in the vetted table in `dev-docs/threading-model.md` with their measurement. When such work must *feed* game logic: run the heavy/I/O part off-loop and `ConfigureAwait(false)` its awaits so a continuation never resumes on the loop and silently foregrounds heavy work; then hand the result back **explicitly** — publish an immutable snapshot swapped via a `volatile` reference (the loop reads it lock-free), or marshal the apply step with `Core.LoopContext.Post(() => …)`, re-validating in the continuation whatever may have changed while it ran. Never touch game state off-thread; never let the scheduler decide where the heavy work runs → `dev-docs/threading-model.md` 11. **Never assume era** — if code uses `Core.AOS`/`Core.SE`/etc., ask which expansion to target 12. **Naming** — `_camelCase` private fields, `PascalCase` properties/methods/classes; don't flag legacy `m_` but use `_` for new code diff --git a/Distribution/Data/npc-speeds.json b/Distribution/Data/npc-speeds.json index 707f61e4a..6e07859d5 100644 --- a/Distribution/Data/npc-speeds.json +++ b/Distribution/Data/npc-speeds.json @@ -3,16 +3,12 @@ "level": "VerySlow", "active": 0.4, "passive": 0.8, - "activeMove": 0.9, - "passiveMove": 1.5, "types": [] }, { "level": "Slow", "active": 0.3, "passive": 0.6, - "activeMove": 0.6, - "passiveMove": 1.2, "types": [ "AntLion", "ArcticOgreLord", "BogThing", "Bogle", "BoneKnight", "EarthElemental", @@ -32,8 +28,6 @@ "level": "Medium", "active": 0.25, "passive": 0.5, - "activeMove": 0.45, - "passiveMove": 1.05, "types": [ "AcidElemental", "AgapiteElemental", "Alligator", "AncientLich", "Betrayer", "Bird", @@ -114,8 +108,6 @@ "level": "Fast", "active": 0.2, "passive": 0.4, - "activeMove": 0.3, - "passiveMove": 0.9, "types": [ "LordOaks", "Silvani", "AirElemental", "AncientWyrm", "Balron", "BladeSpirits", @@ -147,8 +139,6 @@ "level": "VeryFast", "active": 0.125, "passive": 0.30, - "activeMove": 0.125, - "passiveMove": 0.6, "types": [ "Barracoon", "Mephitis", "Neira", "Rikktor", "Semidar", "EnergyVortex", diff --git a/Projects/Server.Tests/Tests/Mobiles/DamageEntryTests.cs b/Projects/Server.Tests/Tests/Mobiles/DamageEntryTests.cs deleted file mode 100644 index 9c3cdfd14..000000000 --- a/Projects/Server.Tests/Tests/Mobiles/DamageEntryTests.cs +++ /dev/null @@ -1,311 +0,0 @@ -using System; -using System.Collections.Generic; -using Server.Collections; -using Xunit; - -namespace Server.Tests; - -[Collection("Sequential Server Tests")] -public class DamageEntryTests -{ - private class TestMobile : Mobile - { - } - - private class PetMobile : Mobile - { - public Mobile Master { get; set; } - - public override Mobile GetDamageMaster(Mobile damagee) => Master; - } - - private static List Damagers(Mobile victim) - { - var result = new List(); - foreach (var de in victim.DamageEntries) - { - result.Add(de.Damager); - } - - return result; - } - - [Fact] - public void FreshMobile_HasNoEntries() - { - var m = new TestMobile(); - - try - { - Assert.Equal(0, m.DamageEntries.Count); - Assert.Null(m.FindMostRecentDamageEntry(true)); - Assert.Null(m.FindLeastRecentDamageEntry(true)); - Assert.Null(m.FindMostTotalDamageEntry(true)); - Assert.Null(m.FindLeastTotalDamageEntry(true)); - Assert.Null(m.FindDamageEntryFor(m)); - } - finally - { - m.Delete(); - } - } - - [Fact] - public void RegisterDamage_OrdersLeastRecentToMostRecent() - { - var victim = new TestMobile(); - var a = new TestMobile(); - var b = new TestMobile(); - - try - { - victim.RegisterDamage(10, a); - victim.RegisterDamage(20, b); - victim.RegisterDamage(5, a); // a becomes most recent again - - Assert.Equal(2, victim.DamageEntries.Count); - Assert.Equal(new[] { b, a }, Damagers(victim)); - Assert.Equal(15, victim.FindDamageEntryFor(a).DamageGiven); - Assert.Same(a, victim.FindMostRecentDamager(true)); - Assert.Same(b, victim.FindLeastRecentDamager(true)); - } - finally - { - victim.Delete(); - a.Delete(); - b.Delete(); - } - } - - [Fact] - public void FindRecent_HonorsAllowSelf() - { - var victim = new TestMobile(); - var a = new TestMobile(); - - try - { - victim.RegisterDamage(10, a); - victim.RegisterDamage(10, victim); // self is most recent - - Assert.Same(victim, victim.FindMostRecentDamager(true)); - Assert.Same(a, victim.FindMostRecentDamager(false)); - Assert.Same(a, victim.FindLeastRecentDamager(false)); - } - finally - { - victim.Delete(); - a.Delete(); - } - } - - [Fact] - public void FindLeastRecent_HonorsAllowSelf() - { - var victim = new TestMobile(); - var a = new TestMobile(); - - try - { - victim.RegisterDamage(10, victim); // self is least recent, so the head is the one to skip - victim.RegisterDamage(10, a); - - Assert.Same(victim, victim.FindLeastRecentDamager(true)); - Assert.Same(a, victim.FindLeastRecentDamager(false)); - } - finally - { - victim.Delete(); - a.Delete(); - } - } - - [Fact] - public void FindTotal_PicksByDamage_MostRecentWinsTies() - { - var victim = new TestMobile(); - var a = new TestMobile(); - var b = new TestMobile(); - var c = new TestMobile(); - - try - { - victim.RegisterDamage(30, a); - victim.RegisterDamage(30, b); // ties a; b is more recent - victim.RegisterDamage(1, c); - - Assert.Same(b, victim.FindMostTotalDamager(true)); - Assert.Same(c, victim.FindLeastTotalDamager(true)); - } - finally - { - victim.Delete(); - a.Delete(); - b.Delete(); - c.Delete(); - } - } - - [Fact] - public void FindLeastTotal_MostRecentWinsTies() - { - var victim = new TestMobile(); - var a = new TestMobile(); - var b = new TestMobile(); - var c = new TestMobile(); - - try - { - victim.RegisterDamage(30, a); - victim.RegisterDamage(5, b); - victim.RegisterDamage(5, c); // ties b for the minimum; c is more recent - - Assert.Same(a, victim.FindMostTotalDamager(true)); - Assert.Same(c, victim.FindLeastTotalDamager(true)); - } - finally - { - victim.Delete(); - a.Delete(); - b.Delete(); - c.Delete(); - } - } - - [Fact] - public void Prune_RemovesExpiredPrefix_KeepsOrder() - { - var start = Core._now; - var victim = new TestMobile(); - var a = new TestMobile(); - var b = new TestMobile(); - - try - { - victim.RegisterDamage(10, a); - - Core._now = start + DamageEntry.ExpireDelay + TimeSpan.FromSeconds(1); - victim.RegisterDamage(10, b); // a is now expired, b is live - - Assert.Equal(new[] { b }, Damagers(victim)); - Assert.Null(victim.FindDamageEntryFor(a)); - } - finally - { - Core._now = start; - victim.Delete(); - a.Delete(); - b.Delete(); - } - } - - [Fact] - public void Prune_AllExpired_EmptiesList() - { - var start = Core._now; - var victim = new TestMobile(); - var a = new TestMobile(); - var b = new TestMobile(); - - try - { - victim.RegisterDamage(10, a); - victim.RegisterDamage(10, b); - - Core._now = start + DamageEntry.ExpireDelay + TimeSpan.FromSeconds(1); - - Assert.Equal(0, victim.DamageEntries.Count); - Assert.Null(victim.FindMostRecentDamageEntry(true)); - } - finally - { - Core._now = start; - victim.Delete(); - a.Delete(); - b.Delete(); - } - } - - [Fact] - public void ClearDamageEntries_UnlinksEveryNode() - { - var victim = new TestMobile(); - var a = new TestMobile(); - var b = new TestMobile(); - - try - { - var ea = victim.RegisterDamage(10, a); - var eb = victim.RegisterDamage(10, b); - - victim.ClearDamageEntries(); - - Assert.Equal(0, victim.DamageEntries.Count); - Assert.False(ea.OnLinkList); - Assert.False(eb.OnLinkList); - Assert.Null(ea.Next); - Assert.Null(ea.Previous); - Assert.Null(eb.Next); - Assert.Null(eb.Previous); - } - finally - { - victim.Delete(); - a.Delete(); - b.Delete(); - } - } - - [Fact] - public void FullHitPoints_ClearsEntries() - { - var victim = new TestMobile(); - var a = new TestMobile(); - - try - { - victim.RawStr = 50; // HitsMax follows Str for a base Mobile - victim.Hits = 10; - victim.RegisterDamage(10, a); - Assert.Equal(1, victim.DamageEntries.Count); - - // Also stops the HitsTimer the Hits = 10 write started, so the test leaves no timer behind. - victim.Hits = victim.HitsMax; - - Assert.Equal(0, victim.DamageEntries.Count); - } - finally - { - victim.Delete(); - a.Delete(); - } - } - - [Fact] - public void RegisterDamage_AccumulatesResponsibleMaster() - { - var victim = new TestMobile(); - var master = new TestMobile(); - var pet = new PetMobile { Master = master }; - - try - { - victim.RegisterDamage(10, pet); - var entry = victim.RegisterDamage(5, pet); - - Assert.Same(pet, entry.Damager); - Assert.Equal(15, entry.DamageGiven); - Assert.NotNull(entry.Responsible); - Assert.Single(entry.Responsible); - Assert.Same(master, entry.Responsible[0].Damager); - Assert.Equal(15, entry.Responsible[0].DamageGiven); - Assert.False(entry.Responsible[0].OnLinkList); // sub-entries never join the main list - } - finally - { - victim.Delete(); - master.Delete(); - pet.Delete(); - } - } -} diff --git a/Projects/Server.Tests/Tests/Serialization/AnchoredItemSerializationTests.cs b/Projects/Server.Tests/Tests/Serialization/AnchoredItemSerializationTests.cs deleted file mode 100644 index 7889c57c8..000000000 --- a/Projects/Server.Tests/Tests/Serialization/AnchoredItemSerializationTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using Xunit; - -namespace Server.Tests; - -[Collection("Sequential Server Tests")] -public class AnchoredItemSerializationTests -{ - private static byte[] SerializeItem(Item item) - { - var writer = new BufferWriter(new byte[256], true); - item.Serialize(writer); - return writer.Buffer[..(int)writer.Position]; - } - - /// - /// Item v11 stores LastMoved and DecayResetTime as anchored time: the serialized bytes - /// are a function of item state only, not of when the save runs. Pre-v11 stored - /// minutes-since-moved and delta time, which rewrote the bytes on every save. - /// - [Fact] - public void ItemBytes_AreStable_AcrossSavesAtDifferentTimes() - { - var start = Core._now; - - try - { - var item = new Item(0x1F13); - item.MoveToWorld(new Point3D(120, 100, 0), Map.Felucca); - item.RestartDecay(); - - var first = SerializeItem(item); - - // A save hours later, with no state change, must produce identical bytes. - Core._now = start + TimeSpan.FromHours(5); - var second = SerializeItem(item); - - Assert.Equal(first, second); - - item.Delete(); - } - finally - { - Core._now = start; - } - } - - /// - /// Pre-v11 LastMoved was stored at whole-minute precision relative to the save time and - /// could never round-trip exactly. Anchored storage is absolute and exact. - /// - [Fact] - public void LastMovedAndDecayReset_RoundTripExactly() - { - var item = new Item(0x1F13); - item.MoveToWorld(new Point3D(121, 100, 0), Map.Felucca); - - // Sub-minute precision that the old minutes encoding would have destroyed. - var moved = Core.Now - TimeSpan.FromSeconds(90.5) - TimeSpan.FromMilliseconds(123); - item.LastMoved = moved; - - item.RestartDecay(); - var decayReset = item.DecayResetTime; - Assert.NotEqual(default(DateTime), decayReset); - - var bytes = SerializeItem(item); - - var restored = new Item((Serial)0x7ffff123u); - restored.Deserialize(new BufferReader(bytes)); - - Assert.Equal(moved, restored.LastMoved); - Assert.Equal(decayReset, restored.DecayResetTime); - - item.Delete(); - } -} diff --git a/Projects/Server.Tests/Tests/Serialization/AnchoredTimeTests.cs b/Projects/Server.Tests/Tests/Serialization/AnchoredTimeTests.cs deleted file mode 100644 index a99b9850c..000000000 --- a/Projects/Server.Tests/Tests/Serialization/AnchoredTimeTests.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Xunit; - -namespace Server.Tests; - -public class AnchoredTimeTests -{ - private static (BufferWriter Writer, Func Read) CreateRoundTrip() - { - var writer = new BufferWriter(new byte[64], true); - return (writer, shift => new BufferReader(writer.Buffer) { AnchoredTimeShift = shift }); - } - - [Fact] - public void AnchoredTime_RoundTripsExactly_WithZeroShift() - { - var (writer, read) = CreateRoundTrip(); - var value = new DateTime(2026, 8, 22, 12, 30, 0, DateTimeKind.Utc); - - writer.WriteAnchoredTime(value); - - Assert.Equal(value, read(TimeSpan.Zero).ReadAnchoredTime()); - } - - [Fact] - public void AnchoredTime_AppliesShiftOnRead() - { - var (writer, read) = CreateRoundTrip(); - var value = new DateTime(2026, 8, 22, 12, 30, 0, DateTimeKind.Utc); - var shift = TimeSpan.FromHours(3); - - writer.WriteAnchoredTime(value); - - Assert.Equal(value + shift, read(shift).ReadAnchoredTime()); - } - - [Fact] - public void AnchoredTime_SentinelsPassThroughUnshifted() - { - var (writer, read) = CreateRoundTrip(); - - writer.WriteAnchoredTime(DateTime.MinValue); - writer.WriteAnchoredTime(DateTime.MaxValue); - - var reader = read(TimeSpan.FromDays(2)); - Assert.Equal(DateTime.MinValue, reader.ReadAnchoredTime()); - Assert.Equal(DateTime.MaxValue, reader.ReadAnchoredTime()); - } - - [Fact] - public void AnchoredTime_SaturatesInsteadOfOverflowing() - { - var (writer, read) = CreateRoundTrip(); - - writer.WriteAnchoredTime(DateTime.MaxValue - TimeSpan.FromMinutes(1)); - - Assert.Equal(DateTime.MaxValue, read(TimeSpan.FromDays(1)).ReadAnchoredTime()); - } - - [Fact] - public void AnchoredTime_NormalizesLocalKindOnWrite() - { - var (writer, read) = CreateRoundTrip(); - var local = new DateTime(2026, 8, 22, 12, 30, 0, DateTimeKind.Local); - - writer.WriteAnchoredTime(local); - - Assert.Equal(local.ToUniversalTime(), read(TimeSpan.Zero).ReadAnchoredTime()); - } -} - -internal class AnchoredEntity : ISerializable -{ - public AnchoredEntity(Serial serial) => Serial = serial; - - public Serial Serial { get; } - public DateTime Created { get; set; } = DateTime.UtcNow; - public bool Deleted => false; - - public DateTime LastRested { get; set; } - - public void Delete() - { - } - - public void Serialize(IGenericWriter writer) => writer.WriteAnchoredTime(LastRested); - - public void Deserialize(IGenericReader reader) => LastRested = reader.ReadAnchoredTime(); -} - -[Collection("Sequential Server Tests")] -public class AnchoredTimePersistenceTests -{ - private class AnchoredPersistence : GenericEntityPersistence - { - public AnchoredPersistence(int priority) : base("AnchoredTrip", priority, 1, 0x7FFFFFFF) - { - } - } - - /// - /// The idx v5 header carries the save-start anchor; loading re-bases anchored timestamps - /// by the elapsed time since the save started, so downtime does not age them. - /// - [Fact] - public void SaveStartAnchor_RebasesAnchoredTimestampsAtLoad() - { - var previousAssemblies = AssemblyHandler.Assemblies; - AssemblyHandler.Assemblies = [.. previousAssemblies ?? [], typeof(AnchoredEntity).Assembly]; - - var source = new SerializationChunkSource(); - var workers = new SerializationThreadWorker[2]; - for (var i = 0; i < workers.Length; i++) - { - workers[i] = new SerializationThreadWorker(i, source); - workers[i].AllocateHeap(); - } - - var previousWorkers = World._threadWorkers; - World._threadWorkers = workers; - - var previousSaveStart = World.SaveStartTime; - - var persistence = new AnchoredPersistence(2100); - AnchoredPersistence loaded = null; - - var dir = Path.Combine(Path.GetTempPath(), $"muo-anchored-{Guid.NewGuid():N}"); - Directory.CreateDirectory(dir); - - try - { - var lastRested = Core.Now - TimeSpan.FromMinutes(10); - var serial = (Serial)1u; - persistence.EntitiesBySerial[serial] = new AnchoredEntity(serial) { LastRested = lastRested }; - persistence.RegisterType(typeof(AnchoredEntity)); - - // Pretend the save started two hours ago, as if the server had been down since. - var downtime = TimeSpan.FromHours(2); - World.SaveStartTime = Core.Now - downtime; - - foreach (var worker in workers) - { - worker.Wake(); - } - - source.SetOwner(persistence); - Assert.True(persistence.TrySnapshotEntries(out var slotCount)); - source.PushSlotRanges(persistence, slotCount); - - source.Flush(); - foreach (var worker in workers) - { - worker.Sleep(); - } - - persistence.WriteSnapshot(dir); - persistence.PostWorldSave(); - - loaded = new AnchoredPersistence(2101); - loaded.DeserializeIndexes(dir, null); - loaded.Deserialize(dir, null); - - var entity = loaded.EntitiesBySerial[serial]; - var expected = lastRested + downtime; - - Assert.True( - (entity.LastRested - expected).Duration() <= TimeSpan.FromSeconds(30), - $"Anchored timestamp must re-base by the downtime; expected ~{expected}, got {entity.LastRested}." - ); - } - finally - { - World.SaveStartTime = previousSaveStart; - persistence.Unregister(); - loaded?.Unregister(); - - foreach (var worker in workers) - { - worker.Exit(); - } - - World._threadWorkers = previousWorkers; - AssemblyHandler.Assemblies = previousAssemblies; - Directory.Delete(dir, true); - } - } -} diff --git a/Projects/Server/Items/Container.cs b/Projects/Server/Items/Container.cs index 06c676b25..f139da24b 100644 --- a/Projects/Server/Items/Container.cs +++ b/Projects/Server/Items/Container.cs @@ -44,10 +44,10 @@ public partial class Container : Item internal int _version; [SerializableField(3)] - [SaveFlag(nameof(ShouldSerializeLiftOverride))] [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _liftOverride; + [SerializableFieldSaveFlag(3)] private bool ShouldSerializeLiftOverride() => _liftOverride; public Container(int itemID) : base(itemID) @@ -84,7 +84,6 @@ public partial class Container : Item [EncodedInt] [SerializableProperty(0)] - [SaveFlag(nameof(ShouldSerializeMaxItems), nameof(MaxItemsDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int MaxItems { @@ -97,13 +96,14 @@ public partial class Container : Item } } + [SerializableFieldSaveFlag(0)] private bool ShouldSerializeMaxItems() => _maxItems != -1; + [SerializableFieldDefault(0)] private int MaxItemsDefaultValue() => -1; [EncodedInt] [SerializableProperty(1)] - [SaveFlag(nameof(ShouldSerializeGumpId), nameof(GumpIDDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int GumpID { @@ -115,13 +115,14 @@ public partial class Container : Item } } + [SerializableFieldSaveFlag(1)] private bool ShouldSerializeGumpId() => _gumpID != -1; + [SerializableFieldDefault(1)] private int GumpIDDefaultValue() => -1; [EncodedInt] [SerializableProperty(2)] - [SaveFlag(nameof(ShouldSerializeDropSound), nameof(DropSoundDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int DropSound { @@ -133,8 +134,10 @@ public partial class Container : Item } } + [SerializableFieldSaveFlag(2)] private bool ShouldSerializeDropSound() => _dropSound != -1; + [SerializableFieldDefault(2)] private int DropSoundDefaultValue() => -1; [CommandProperty(AccessLevel.GameMaster)] diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs index 13f249f2f..a2b63a821 100644 --- a/Projects/Server/Items/Item.cs +++ b/Projects/Server/Items/Item.cs @@ -863,7 +863,7 @@ public partial class Item : IHued, IComparable, ISpawnable, IObjectPropert public virtual void Serialize(IGenericWriter writer) { - writer.Write(11); // version + writer.Write(10); // version var flags = SaveFlag.None; @@ -1015,13 +1015,19 @@ public partial class Item : IHued, IComparable, ISpawnable, IObjectPropert writer.Write((int)flags); - // Anchored: shifted by downtime at load, so time-since-moved is preserved and the - // bytes are stable across saves while the item does not move. - writer.WriteAnchoredTime(LastMoved); + /* begin last moved time optimization */ + var ticks = LastMoved.Ticks; + var now = Core.Now.Ticks; + + var minutes = new TimeSpan(now - ticks).TotalMinutes; + + writer.WriteEncodedInt((int)Math.Clamp(minutes, int.MinValue, int.MaxValue)); + /* end */ if (GetSaveFlag(flags, SaveFlag.DecayReset)) { - writer.WriteAnchoredTime(info.m_DecayReset); + //TODO Use WriteAnchoredTime once the save-time anchor is ported + writer.WriteDeltaTime(info.m_DecayReset); } if (GetSaveFlag(flags, SaveFlag.Direction)) @@ -2766,7 +2772,6 @@ public partial class Item : IHued, IComparable, ISpawnable, IObjectPropert switch (version) { - case 11: case 10: case 9: case 8: @@ -2775,11 +2780,7 @@ public partial class Item : IHued, IComparable, ISpawnable, IObjectPropert { var flags = (SaveFlag)reader.ReadInt(); - if (version >= 11) - { - LastMoved = reader.ReadAnchoredTime(); - } - else if (version < 7) + if (version < 7) { LastMoved = reader.ReadDeltaTime(); } @@ -2799,10 +2800,10 @@ public partial class Item : IHued, IComparable, ISpawnable, IObjectPropert if (version >= 10 && GetSaveFlag(flags, SaveFlag.DecayReset)) { - var reset = version >= 11 ? reader.ReadAnchoredTime() : reader.ReadDeltaTime(); + var reset = reader.ReadDeltaTime(); - // Pre-v11 LastMoved was stored at whole-minute precision; keep the - // stamp only while it still extends the deadline. + // LastMoved is stored at whole-minute precision; keep the stamp only + // while it still extends the deadline. if (reset > LastMoved) { DecayResetTime = reset; diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index d1db8113b..0e88449e3 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -42,7 +42,7 @@ public delegate void PromptCallback(Mobile from, string text); public delegate void PromptStateCallback(Mobile from, string text, T state); -public class DamageEntry : IValueLinkListNode +public class DamageEntry { public DamageEntry(Mobile damager) => Damager = damager; @@ -57,11 +57,6 @@ public class DamageEntry : IValueLinkListNode public List Responsible { get; set; } public static TimeSpan ExpireDelay { get; set; } = TimeSpan.FromMinutes(2.0); - - // Intrusive links for Mobile._damageEntries. Sub-entries in Responsible never join a list. - public DamageEntry Next { get; set; } - public DamageEntry Previous { get; set; } - public bool OnLinkList { get; set; } } [Flags] @@ -382,6 +377,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro Aggressors = new List(); Aggressed = new List(); NextSkillTime = Core.TickCount; + DamageEntries = new List(); } // Sectors @@ -962,23 +958,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro public static VisibleDamageType VisibleDamageType { get; set; } - private ValueLinkList _damageEntries; - - /// - /// Damage entries ordered least recent (head) to most recent (tail). Expired entries are - /// pruned on access. Enumerate with foreach (ascending) or .ByDescending(). - /// Mutate only through and . - /// Calling a ValueLinkList mutator on this reference compiles, but operates on a defensive copy - /// while still unlinking the real nodes — it silently corrupts the list. - /// - public ref readonly ValueLinkList DamageEntries - { - get - { - PruneExpiredDamageEntries(); - return ref _damageEntries; - } - } + public List DamageEntries { get; private set; } [CommandProperty(AccessLevel.GameMaster)] public Mobile LastKiller { get; set; } @@ -1647,7 +1627,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro public virtual bool KeepsItemsOnDeath => m_AccessLevel > AccessLevel.Player; - public bool HasTrade => m_NetState?.Trades?.Count > 0; + public bool HasTrade => m_NetState?.Trades.Count > 0; public bool NoMoveHS { get; set; } @@ -2040,7 +2020,10 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro Aggressors[i].CanReportMurder = false; } - ClearDamageEntries(); // reset damage entries on full HP + if (DamageEntries.Count > 0) + { + DamageEntries.Clear(); // reset damage entries on full HP + } } else if (CanRegenHits) { @@ -2341,11 +2324,11 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro public virtual void Serialize(IGenericWriter writer) { - writer.Write(38); // version + writer.Write(37); // version - writer.WriteAnchoredTime(LastStrGain); - writer.WriteAnchoredTime(LastIntGain); - writer.WriteAnchoredTime(LastDexGain); + writer.WriteDeltaTime(LastStrGain); + writer.WriteDeltaTime(LastIntGain); + writer.WriteDeltaTime(LastDexGain); byte hairflag = 0x00; @@ -5762,54 +5745,24 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro } } - // Entries are kept in LastDamage order, so expired entries are always a head prefix. - private void PruneExpiredDamageEntries() - { -#if DEBUG - for (var node = _damageEntries._first; node != null; node = node.Next) - { - Debug.Assert( - node.Next == null || node.Next.LastDamage >= node.LastDamage, - "Damage entries must be ordered by LastDamage ascending." - ); - } -#endif - - var first = _damageEntries._first; - - if (first?.HasExpired != true) - { - return; - } - - var firstLive = first.Next; - - while (firstLive?.HasExpired == true) - { - firstLive = firstLive.Next; - } - - if (firstLive == null) - { - _damageEntries.RemoveAll(); - } - else - { - _damageEntries.RemoveAllBefore(firstLive); - } - } - - public void ClearDamageEntries() => _damageEntries.RemoveAll(); - public Mobile FindMostRecentDamager(bool allowSelf) => FindMostRecentDamageEntry(allowSelf)?.Damager; public DamageEntry FindMostRecentDamageEntry(bool allowSelf) { - PruneExpiredDamageEntries(); - - for (var de = _damageEntries._last; de != null; de = de.Previous) + for (var i = DamageEntries.Count - 1; i >= 0; --i) { - if (allowSelf || de.Damager != this) + if (i >= DamageEntries.Count) + { + continue; + } + + var de = DamageEntries[i]; + + if (de.HasExpired) + { + DamageEntries.RemoveAt(i); + } + else if (allowSelf || de.Damager != this) { return de; } @@ -5822,11 +5775,21 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro public DamageEntry FindLeastRecentDamageEntry(bool allowSelf) { - PruneExpiredDamageEntries(); - - for (var de = _damageEntries._first; de != null; de = de.Next) + for (var i = 0; i < DamageEntries.Count; ++i) { - if (allowSelf || de.Damager != this) + if (i < 0) + { + continue; + } + + var de = DamageEntries[i]; + + if (de.HasExpired) + { + DamageEntries.RemoveAt(i); + --i; + } + else if (allowSelf || de.Damager != this) { return de; } @@ -5837,17 +5800,24 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro public Mobile FindMostTotalDamager(bool allowSelf) => FindMostTotalDamageEntry(allowSelf)?.Damager; - // Walks most recent first with a strict comparison so the most recent entry wins ties, - // matching the previous reverse-indexed loop. public DamageEntry FindMostTotalDamageEntry(bool allowSelf) { - PruneExpiredDamageEntries(); - DamageEntry mostTotal = null; - for (var de = _damageEntries._last; de != null; de = de.Previous) + for (var i = DamageEntries.Count - 1; i >= 0; --i) { - if ((allowSelf || de.Damager != this) && (mostTotal == null || de.DamageGiven > mostTotal.DamageGiven)) + if (i >= DamageEntries.Count) + { + continue; + } + + var de = DamageEntries[i]; + + if (de.HasExpired) + { + DamageEntries.RemoveAt(i); + } + else if ((allowSelf || de.Damager != this) && (mostTotal == null || de.DamageGiven > mostTotal.DamageGiven)) { mostTotal = de; } @@ -5860,28 +5830,46 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro public DamageEntry FindLeastTotalDamageEntry(bool allowSelf) { - PruneExpiredDamageEntries(); + DamageEntry mostTotal = null; - DamageEntry leastTotal = null; - - for (var de = _damageEntries._last; de != null; de = de.Previous) + for (var i = DamageEntries.Count - 1; i >= 0; --i) { - if ((allowSelf || de.Damager != this) && (leastTotal == null || de.DamageGiven < leastTotal.DamageGiven)) + if (i >= DamageEntries.Count) { - leastTotal = de; + continue; + } + + var de = DamageEntries[i]; + + if (de.HasExpired) + { + DamageEntries.RemoveAt(i); + } + else if ((allowSelf || de.Damager != this) && (mostTotal == null || de.DamageGiven < mostTotal.DamageGiven)) + { + mostTotal = de; } } - return leastTotal; + return mostTotal; } public DamageEntry FindDamageEntryFor(Mobile m) { - PruneExpiredDamageEntries(); - - for (var de = _damageEntries._last; de != null; de = de.Previous) + for (var i = DamageEntries.Count - 1; i >= 0; --i) { - if (de.Damager == m) + if (i >= DamageEntries.Count) + { + continue; + } + + var de = DamageEntries[i]; + + if (de.HasExpired) + { + DamageEntries.RemoveAt(i); + } + else if (de.Damager == m) { return de; } @@ -5899,13 +5887,8 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro de.DamageGiven += amount; de.LastDamage = Core.Now; - // Move to the tail so the list stays in LastDamage order. - if (de.OnLinkList) - { - _damageEntries.Remove(de); - } - - _damageEntries.AddLast(de); + DamageEntries.Remove(de); + DamageEntries.Add(de); var master = from.GetDamageMaster(this); @@ -6167,7 +6150,6 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro switch (version) { - case 38: // Stat-gain stamps moved from delta time to anchored time case 37: // Decomposed hair into inline item id/hue (dropped the VirtualHairInfo object) case 36: // Moved virtues to VirtueSystem case 35: // Moved short term murders to PlayerMurderSystem @@ -6176,18 +6158,9 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro case 32: // Removed StuckMenu case 31: { - if (version >= 38) - { - LastStrGain = reader.ReadAnchoredTime(); - LastIntGain = reader.ReadAnchoredTime(); - LastDexGain = reader.ReadAnchoredTime(); - } - else - { - LastStrGain = reader.ReadDeltaTime(); - LastIntGain = reader.ReadDeltaTime(); - LastDexGain = reader.ReadDeltaTime(); - } + LastStrGain = reader.ReadDeltaTime(); + LastIntGain = reader.ReadDeltaTime(); + LastDexGain = reader.ReadDeltaTime(); goto case 30; } @@ -6495,6 +6468,9 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro m_DexLock = (StatLockType)reader.ReadByte(); m_IntLock = (StatLockType)reader.ReadByte(); + _statMods = new List(); + _skillMods = new List(); + if (version < 32) { if (reader.ReadBool()) @@ -7827,10 +7803,13 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro m_FollowersMax = 5; Skills = new Skills(this); Items = new List(); + _statMods = new List(); + _skillMods = new List(); Map = Map.Internal; AutoPageNotify = true; Aggressors = new List(); Aggressed = new List(); + DamageEntries = new List(); NextSkillTime = Core.TickCount; } diff --git a/Projects/Server/Mobiles/Mods/ResistanceMod.cs b/Projects/Server/Mobiles/Mods/ResistanceMod.cs index a5423e039..bd569f20e 100644 --- a/Projects/Server/Mobiles/Mods/ResistanceMod.cs +++ b/Projects/Server/Mobiles/Mods/ResistanceMod.cs @@ -21,15 +21,17 @@ namespace Server; [SerializationGenerator(0)] public partial class ResistanceMod : MobileMod { - [SerializableField(0, fieldChanged: nameof(OnTypeChanged))] + [SerializableField(0)] private ResistanceType _type; + [SerializableFieldChanged(0)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private void OnTypeChanged(ResistanceType oldValue, ResistanceType newValue) => Owner?.UpdateResistances(); - [SerializableField(1, fieldChanged: nameof(OnOffsetChanged))] + [SerializableField(1)] private int _offset; + [SerializableFieldChanged(1)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private void OnOffsetChanged(int oldValue, int newValue) => Owner?.UpdateResistances(); diff --git a/Projects/Server/Mobiles/Mods/SkillMod.cs b/Projects/Server/Mobiles/Mods/SkillMod.cs index cfe96c0c4..a78ec5ce6 100644 --- a/Projects/Server/Mobiles/Mods/SkillMod.cs +++ b/Projects/Server/Mobiles/Mods/SkillMod.cs @@ -21,29 +21,33 @@ namespace Server; [SerializationGenerator(0)] public abstract partial class SkillMod : MobileMod { - [SerializableField(0, fieldChanged: nameof(OnObeyCapChanged))] + [SerializableField(0)] private bool _obeyCap; + [SerializableFieldChanged(0)] [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void OnObeyCapChanged(bool oldValue, bool newValue) => Owner?.Skills[_skill]?.Update(); + private void OnObeCapChanged(bool oldValue, bool newValue) => Owner?.Skills[_skill]?.Update(); - [SerializableField(1, fieldChanged: nameof(OnSkillChanged))] + [SerializableField(1)] private SkillName _skill; + [SerializableFieldChanged(1)] private void OnSkillChanged(SkillName oldValue, SkillName newValue) { Owner?.Skills[newValue]?.Update(); Owner?.Skills[oldValue]?.Update(); } - [SerializableField(2, fieldChanged: nameof(OnRelativeChanged))] + [SerializableField(2)] private bool _relative; + [SerializableFieldChanged(2)] private void OnRelativeChanged(bool oldValue, bool newValue) => Owner?.Skills[_skill]?.Update(); - [SerializableField(3, fieldChanged: nameof(OnValueChanged))] + [SerializableField(3)] private double _value; + [SerializableFieldChanged(3)] private void OnValueChanged(double oldValue, double newValue) => Owner?.Skills[_skill]?.Update(); public SkillMod(Mobile owner) : base(owner) diff --git a/Projects/Server/Network/MovementThrottle.cs b/Projects/Server/Network/MovementThrottle.cs index 33ee318fc..c28ef2228 100644 --- a/Projects/Server/Network/MovementThrottle.cs +++ b/Projects/Server/Network/MovementThrottle.cs @@ -50,6 +50,9 @@ public static class MovementThrottle private const int ClientMaxUnackedMovements = 5; private const int MaxQueueWithUnmodifiedClient = ClientMaxUnackedMovements - 1; // 4 + // Debug logging - enable for testing speed hack detection + private static bool _debugLogging = false; + // Track NetStates with queued movements for efficient processing private static readonly HashSet _netStatesWithQueuedMovements = new(256); @@ -80,9 +83,15 @@ public static class MovementThrottle public static void Configure() { - _maxCredit = ServerConfiguration.GetOrUpdateSetting("movementThrottle.maxCredit", _maxCredit); - _maxRttBonus = ServerConfiguration.GetOrUpdateSetting("movementThrottle.maxRttBonus", _maxRttBonus); - _hardQueueLimit = ServerConfiguration.GetOrUpdateSetting("movementThrottle.hardQueueLimit", _hardQueueLimit); + _maxCredit = ServerConfiguration.GetOrUpdateSetting( + "movementThrottle.maxCredit", + _maxCredit + ); + + _hardQueueLimit = ServerConfiguration.GetOrUpdateSetting( + "movementThrottle.hardQueueLimit", + _hardQueueLimit + ); _movementHistorySize = ServerConfiguration.GetOrUpdateSetting( "movementThrottle.movementHistorySize", @@ -94,13 +103,6 @@ public static class MovementThrottle _minSamplesForRate ); - _maxChainGap = ServerConfiguration.GetOrUpdateSetting("movementThrottle.maxChainGap", _maxChainGap); - - _speedHackNotificationCooldown = ServerConfiguration.GetOrUpdateSetting( - "movementThrottle.speedHackNotificationCooldown", - _speedHackNotificationCooldown - ); - _suspiciousRateThreshold = (float)ServerConfiguration.GetOrUpdateSetting( "movementThrottle.suspiciousRateThreshold", _suspiciousRateThreshold @@ -110,6 +112,11 @@ public static class MovementThrottle "movementThrottle.definiteRateThreshold", _definiteRateThreshold ); + + _debugLogging = ServerConfiguration.GetOrUpdateSetting( + "movementThrottle.debugLogging", + _debugLogging + ); } /// @@ -184,16 +191,15 @@ public static class MovementThrottle // Credit can go negative up to -dynamicCredit (debt limit) if (ns._movementCredit - earlyAmount >= -dynamicCredit) { + var prevCredit = ns._movementCredit; // Use credit to cover early arrival ns._movementCredit -= earlyAmount; - if (ns._movementLogging) + if (_debugLogging && ns._movementLogging) { - var prevCredit = ns._movementCredit + earlyAmount; - logger.Debug( "[Credit] {Name}: delta={Delta}ms early={Early}ms credit={PrevCredit}->{Credit}/{MaxCredit} action=execute", - mobile, delta, earlyAmount, prevCredit, ns._movementCredit, dynamicCredit + mobile.RawName, delta, earlyAmount, prevCredit, ns._movementCredit, dynamicCredit ); } @@ -202,11 +208,11 @@ public static class MovementThrottle return; } - if (ns._movementLogging) + if (_debugLogging && ns._movementLogging) { logger.Debug( "[Credit] {Name}: delta={Delta}ms early={Early}ms credit={Credit}/{MaxCredit} EXHAUSTED -> queue", - mobile, delta, earlyAmount, ns._movementCredit, dynamicCredit + mobile.RawName, delta, earlyAmount, ns._movementCredit, dynamicCredit ); } @@ -221,11 +227,11 @@ public static class MovementThrottle var prevCredit = ns._movementCredit; ns._movementCredit = Math.Min(ns._movementCredit + delta, dynamicCredit); - if (ns._movementLogging && ns._movementCredit != prevCredit) + if (_debugLogging && ns._movementLogging && ns._movementCredit != prevCredit) { logger.Debug( "[Credit] {Name}: delta=+{Delta}ms credit={PrevCredit}->{Credit}/{MaxCredit} action=execute", - mobile, delta, prevCredit, ns._movementCredit, dynamicCredit + mobile.RawName, delta, prevCredit, ns._movementCredit, dynamicCredit ); } } @@ -241,9 +247,12 @@ public static class MovementThrottle { if (!mobile.Move(dir)) { - if (ns._movementLogging) + if (_debugLogging && ns._movementLogging) { - logger.Debug("[Execute] {Name}: Move FAILED dir={Dir} seq={Seq} -> reject+reset", mobile, dir, seq); + logger.Debug( + "[Execute] {Name}: Move FAILED dir={Dir} seq={Seq} -> reject+reset", + mobile.RawName, dir, seq + ); } // Movement failed (blocked, paralyzed, frozen, etc.) @@ -251,11 +260,11 @@ public static class MovementThrottle return; } - if (ns._movementLogging) + if (_debugLogging && ns._movementLogging) { logger.Debug( "[Execute] {Name}: Move OK dir={Dir} seq={Seq} nextMove={NextMove}ms", - mobile, dir, seq, ns._nextMovementTime - Core.TickCount + mobile.RawName, dir, seq, ns._nextMovementTime - Core.TickCount ); } @@ -295,11 +304,11 @@ public static class MovementThrottle ns._hasQueuedMovements = true; _netStatesWithQueuedMovements.Add(ns); - if (ns._movementLogging) + if (_debugLogging && ns._movementLogging) { logger.Debug( "[Queue] {Name}: enqueued dir={Dir} seq={Seq} (depth={Depth})", - ns.Mobile, dir, seq, ns._movementQueue.Count + ns.Mobile?.RawName, dir, seq, ns._movementQueue.Count ); } } @@ -311,6 +320,7 @@ public static class MovementThrottle { ns.SendMovementRej(seq, mobile); ns.ResetMovementState(); + _netStatesWithQueuedMovements.Remove(ns); } /// @@ -323,18 +333,20 @@ public static class MovementThrottle return; } - foreach (var ns in _netStatesWithQueuedMovements) + // Process each NetState with queued movements + // Use a snapshot to avoid modification during iteration + var toProcess = new List(_netStatesWithQueuedMovements); + + for (var i = 0; i < toProcess.Count; i++) { - if (ns.Running) + var ns = toProcess[i]; + if (!ns.Running) { - ProcessMovementQueue(ns); - if (ns._hasQueuedMovements) - { - continue; - } + _netStatesWithQueuedMovements.Remove(ns); + continue; } - _netStatesWithQueuedMovements.Remove(ns); + ProcessMovementQueue(ns); } } @@ -344,7 +356,6 @@ public static class MovementThrottle public static void ProcessMovementQueue(NetState ns) { var mobile = ns.Mobile; - if (mobile?.Deleted != false) { ClearQueue(ns); @@ -363,7 +374,7 @@ public static class MovementThrottle while (ns._movementQueue?.Count > 0) { // Check if it's time to execute - if (now - ns._nextMovementTime < 0) + if (now < ns._nextMovementTime) { // Not yet - leave remaining items in queue for next Slice break; @@ -383,11 +394,11 @@ public static class MovementThrottle // Execute the move if (!mobile.Move(movement.Direction)) { - if (ns._movementLogging) + if (_debugLogging && ns._movementLogging) { logger.Debug( "[Queue] {Name}: dequeued FAILED dir={Dir} (remaining={Remaining})", - mobile, movement.Direction, remaining + mobile.RawName, movement.Direction, remaining ); } @@ -396,12 +407,12 @@ public static class MovementThrottle return; } - if (ns._movementLogging) + if (_debugLogging && ns._movementLogging) { var waited = now - ns._nextMovementTime; logger.Debug( "[Queue] {Name}: dequeued OK dir={Dir} (remaining={Remaining}, waited={Waited}ms)", - mobile, movement.Direction, remaining, waited >= 0 ? waited : 0 + mobile.RawName, movement.Direction, remaining, waited >= 0 ? waited : 0 ); } @@ -419,6 +430,10 @@ public static class MovementThrottle // Update tracking ns._hasQueuedMovements = ns._movementQueue?.Count > 0; + if (!ns._hasQueuedMovements) + { + _netStatesWithQueuedMovements.Remove(ns); + } } /// @@ -454,6 +469,7 @@ public static class MovementThrottle { ns._movementQueue?.Clear(); ns._hasQueuedMovements = false; + _netStatesWithQueuedMovements.Remove(ns); } // Maximum expected packets per second (mounted running = 100ms = 10/sec, plus tolerance) @@ -468,7 +484,7 @@ public static class MovementThrottle logger.Information( "Movement queue overflow: {Character} ({Account}) | " + "Queue reached hard limit: {Limit} | IP: {IP}", - mobile, + mobile?.RawName ?? "Unknown", ns.Account?.Username ?? "Unknown", _hardQueueLimit, ns.Address @@ -500,7 +516,7 @@ public static class MovementThrottle private static void RecordMovement(NetState ns, long now, int cost, Direction dir, Mobile mobile) { // Calculate interval since last movement - var interval = ns._hasMovementRecord + var interval = ns._lastMovementRecordTime > 0 ? (int)(now - ns._lastMovementRecordTime) : -1; // -1 indicates first movement (no previous time) @@ -509,7 +525,6 @@ public static class MovementThrottle if (interval <= 0 || interval > _maxChainGap) { ns._lastMovementRecordTime = now; - ns._hasMovementRecord = true; // Use RTT to distinguish "stopped moving" vs "lagged" // - Stable low-latency connection with gap >> RTT → player stopped, reset history @@ -529,19 +544,19 @@ public static class MovementThrottle // A large gap followed by a burst of packets = likely lag recovery, not speed hack ns._lastGapDuration = interval; - if (ns._movementLogging) + if (_debugLogging && mobile?.RawName != null) { var action = shouldReset ? "history reset" : "history preserved (possible lag)"; logger.Debug( "[Movement] {Name}: SKIP recording (gap {Gap}ms > {MaxGap}ms, " + "RTT={RTT}ms stable={Stable} → {Action})", - mobile, interval, _maxChainGap, avgRtt, ns.HasStableConnection, action + mobile.RawName, interval, _maxChainGap, avgRtt, ns.HasStableConnection, action ); } } - else if (ns._movementLogging) + else if (_debugLogging && mobile?.RawName != null) { - logger.Debug("[Movement] {Name}: SKIP recording (first in chain)", mobile); + logger.Debug("[Movement] {Name}: SKIP recording (first in chain)", mobile.RawName); } return; @@ -557,9 +572,12 @@ public static class MovementThrottle // the next real move's interval artificially short, inflating rate. if (cost == 0) { - if (ns._movementLogging) + if (_debugLogging && mobile?.RawName != null) { - logger.Debug("[Movement] {Name}: SKIP direction-only change (preserves interval measurement)", mobile); + logger.Debug( + "[Movement] {Name}: SKIP direction-only change (preserves interval measurement)", + mobile.RawName + ); } return; } @@ -595,16 +613,15 @@ public static class MovementThrottle } ns._lastMovementRecordTime = now; - ns._hasMovementRecord = true; // Debug logging - if (ns._movementLogging) + if (_debugLogging && mobile?.RawName != null) { var historyCount = ns._movementHistoryFull ? _movementHistorySize : ns._movementHistoryIndex; logger.Debug( "[Movement] {Name}: interval={Interval}ms target={Target}ms queue={Queue} " + "flags={Flags} history={History}/{MaxHistory} RTT={RTT}ms", - mobile, interval, cost, record.QueueDepth, + mobile.RawName, interval, cost, record.QueueDepth, flags, historyCount, _movementHistorySize, ns.AverageRtt ); } @@ -797,7 +814,7 @@ public static class MovementThrottle var averageRtt = ns.AverageRtt; // Detailed rate breakdown for debugging - if (ns._movementLogging) + if (_debugLogging) { logger.Debug("[MovementAnalysis] Rate={Rate:F3}, Samples={Samples}, RTT={RTT}ms", rate, sampleCount, averageRtt); @@ -960,19 +977,19 @@ public static class MovementThrottle var verdict = AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence); // Debug logging - if (ns._movementLogging) + if (_debugLogging && ns.Mobile?.RawName != null) { var (burstSize, _) = DetectRecentBurst(ns); - var probeStatus = ns._rttProbePending ? "pending" : "idle"; + var probeStatus = ns._rttProbeTime > 0 ? "pending" : "idle"; var queueDepth = ns._movementQueue?.Count ?? 0; logger.Debug( "[RateCheck] {Name}: rate={Rate:F3} samples={Samples} verdict={Verdict} " + "confidence={Confidence:P0} queue={Queue} burst={Burst} sustained={Sustained}s", - ns.Mobile, rate, sampleCount, verdict, confidence, queueDepth, burstSize, ns._consecutiveHighRateSeconds + ns.Mobile.RawName, rate, sampleCount, verdict, confidence, queueDepth, burstSize, ns._consecutiveHighRateSeconds ); logger.Debug( " RTT: avg={Avg}ms last={Last}ms var={Var} samples={RttSamples} stable={Stable} probe={Probe}", - ns.AverageRtt, ns.LastRtt, ns._rttVariance, ns._rttSampleCount, ns.HasStableConnection, probeStatus + ns.AverageRtt, ns._lastRtt, ns._rttVariance, ns._rttSampleCount, ns.HasStableConnection, probeStatus ); } @@ -1008,11 +1025,11 @@ public static class MovementThrottle if (shouldNotify) { - if (ns._movementLogging) + if (_debugLogging) { logger.Debug( "[ALERT] {Urgency} - {Name}: rate={Rate:F3} verdict={Verdict} confidence={Confidence:P0}", - urgency, ns.Mobile, rate, verdict, confidence + urgency, ns.Mobile?.RawName, rate, verdict, confidence ); } NotifyStaff(ns, rate, sampleCount, confidence, verdict, urgency); @@ -1037,12 +1054,11 @@ public static class MovementThrottle var now = Core.TickCount; // Rate-limit notifications per player - if (ns._speedHackNotified && now - ns._lastSpeedHackNotification < _speedHackNotificationCooldown) + if (now - ns._lastSpeedHackNotification < _speedHackNotificationCooldown) { return; } - ns._speedHackNotified = true; ns._lastSpeedHackNotification = now; var mobile = ns.Mobile; @@ -1054,7 +1070,7 @@ public static class MovementThrottle "PacketRate: {PacketRate}/s (peak: {PeakRate}/s) | RTT: {Rtt}ms (stable: {Stable}) | " + "Sustained: {Sustained}s | Queue: {Queue} | Location: {Location} Map: {Map} | IP: {IP}", urgency, - mobile, + mobile?.RawName ?? "Unknown", ns.Account?.Username ?? "Unknown", rate, sampleCount, @@ -1122,7 +1138,7 @@ public static class MovementThrottle Verdict = verdict, Confidence = confidence, AverageRtt = ns.AverageRtt, - LastRtt = ns.LastRtt, + LastRtt = ns._lastRtt, RttVariance = ns._rttVariance, StableConnection = ns.HasStableConnection, RttSampleCount = ns._rttSampleCount, diff --git a/Projects/Server/Network/NetState/NetState.Movement.cs b/Projects/Server/Network/NetState/NetState.Movement.cs index c6f28fcd1..1cc79429e 100644 --- a/Projects/Server/Network/NetState/NetState.Movement.cs +++ b/Projects/Server/Network/NetState/NetState.Movement.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.InteropServices; using Server.Logging; @@ -69,24 +70,23 @@ public partial class NetState internal Queue _movementQueue; // Lazy initialized internal long _movementCredit; // Credit buffer for timing jitter internal long _nextMovementTime = Core.TickCount; // When next movement is allowed - internal long _lastQueueDepthCheck = Core.TickCount; // Throttle depth check frequency + internal int _sustainedQueueDepth; // Tracks sustained high queue depth + internal long _lastQueueDepthCheck; // Throttle depth check frequency internal bool _hasQueuedMovements; // Fast check for Slice() // Movement history for rate-based speed hack detection (lazy initialized) internal MovementRecord[] _movementHistory; // Circular buffer internal int _movementHistoryIndex; // Next write position (also serves as count until full) internal bool _movementHistoryFull; // True once buffer has wrapped - internal long _lastMovementRecordTime; // For calculating intervals (valid only when _hasMovementRecord) - internal bool _hasMovementRecord; // False until the first movement in a chain is seen + internal long _lastMovementRecordTime; // For calculating intervals // Detection state internal int _consecutiveHighRateSeconds; // Sustained detection counter - internal long _lastSpeedHackNotification; // Rate-limit notifications (valid only when _speedHackNotified) - internal bool _speedHackNotified; // False until the first notification is sent + internal long _lastSpeedHackNotification; // Rate-limit notifications internal int _lastGapDuration; // Duration of last gap > maxChainGap (for burst forgiveness) // Movement packet rate tracking (for speed hack detection) - internal long _movementWindowStart = Core.TickCount; // Start of current 1-second window + internal long _movementWindowStart; // Start of current 1-second window internal int _movementsInWindow; // Count in current window internal int _peakMovementRate; // Highest rate seen (packets/sec) @@ -100,9 +100,10 @@ public partial class NetState _nextMovementTime = Core.TickCount; _movementCredit = 0; _hasQueuedMovements = false; + _sustainedQueueDepth = 0; // Reset movement history - next movement starts a new chain - _hasMovementRecord = false; + _lastMovementRecordTime = 0; _movementHistoryIndex = 0; _movementHistoryFull = false; @@ -112,7 +113,7 @@ public partial class NetState _rttProbeInterval = RttProbeIntervalNormal; // Reset packet rate window - _movementWindowStart = Core.TickCount; + _movementWindowStart = 0; _movementsInWindow = 0; } @@ -164,19 +165,17 @@ public partial class NetState private const long MaxStableLatency = 200; // Max RTT (ms) for "stable" connection // RTT state - internal bool _rttProbePending; // True while waiting for a probe response - internal long _rttProbeTime; // When we sent the probe (valid only when _rttProbePending) + internal long _rttProbeTime; // When we sent the probe (0 = not waiting) + internal long _lastRtt; // Most recent RTT measurement internal long[] _rttHistory; // Rolling history (lazy init) internal int _rttHistoryIndex; // Current position in history internal int _rttSampleCount; // Number of samples collected (saturates at RttHistorySize) internal long _rttVariance; // Calculated variance for stability - internal long _nextRttProbe = Core.TickCount; // When to send next probe + internal long _nextRttProbe; // When to send next probe internal int _rttProbeInterval = RttProbeIntervalNormal; // Current probe interval - /// - /// Gets the most recent RTT measurement, or 0 if none has been recorded. - /// - public long LastRtt => _rttSampleCount > 0 ? _rttHistory[(_rttHistoryIndex - 1) & (RttHistorySize - 1)] : 0; + // High-resolution timestamp for RTT measurement (Stopwatch ticks, not game loop ticks) + private long _rttProbeTimestampHiRes; /// /// Sets the RTT probe interval based on suspicion level. @@ -207,22 +206,23 @@ public partial class NetState var now = Core.TickCount; // Don't send if we're still waiting for a response - if (_rttProbePending) + if (_rttProbeTime > 0) { // Timeout after 10 seconds - connection is probably dead or very laggy if (now - _rttProbeTime > 10000) { - _rttProbePending = false; + _rttProbeTime = 0; + _rttProbeTimestampHiRes = 0; } return; } // First probe: send immediately when player starts moving // Subsequent probes: send when interval has passed - if (now - _nextRttProbe >= 0) + if (_nextRttProbe == 0 || now >= _nextRttProbe) { - _rttProbePending = true; _rttProbeTime = now; + _rttProbeTimestampHiRes = Stopwatch.GetTimestamp(); _nextRttProbe = now + _rttProbeInterval + Utility.Random(RttProbeJitter); if (_movementLogging) @@ -242,9 +242,10 @@ public partial class NetState /// public void RecordRttMeasurement() { + var nowHiRes = Stopwatch.GetTimestamp(); var now = Core.TickCount; - if (!_rttProbePending) + if (_rttProbeTime <= 0) { // Not expecting a response (client-initiated version send) - ignore silently return; @@ -252,15 +253,19 @@ public partial class NetState var rtt = now - _rttProbeTime; + // High-resolution RTT in microseconds + var rttHiResUs = (nowHiRes - _rttProbeTimestampHiRes) * 1_000_000 / Stopwatch.Frequency; + if (_movementLogging) { movementLogger.Debug( - "[RTT-Response] {Account}: {Rtt}ms", - Account?.Username ?? _toString, rtt + "[RTT-Response] {Account}: {Rtt}ms (HiRes: {RttHiRes:F2}ms)", + Account?.Username ?? _toString, rtt, rttHiResUs / 1000.0 ); } - _rttProbePending = false; + _rttProbeTime = 0; + _rttProbeTimestampHiRes = 0; // Sanity check - RTT should be positive and reasonable if (rtt is <= 0 or > 10000) @@ -280,6 +285,7 @@ public partial class NetState // Update history _rttHistory[_rttHistoryIndex++ & (RttHistorySize - 1)] = rtt; + _lastRtt = rtt; // Track sample count (saturates at buffer size) if (_rttSampleCount < RttHistorySize) diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs index 04f1a5a76..7312603f7 100755 --- a/Projects/Server/Network/NetState/NetState.cs +++ b/Projects/Server/Network/NetState/NetState.cs @@ -44,7 +44,7 @@ public partial class NetState : IComparable, IValueLinkListNode _connectingQueue = new(2048); private static readonly HashSet _instances = new(2048); - public static HashSet Instances => _instances; + public static IReadOnlySet Instances => _instances; private readonly string _toString; private ClientVersion _version; @@ -109,6 +109,9 @@ public partial class NetState : IComparable, IValueLinkListNode, IValueLinkListNode Trades { get; private set; } + public List Trades { get; } public bool Seeded { get; set; } @@ -257,18 +260,8 @@ public partial class NetState : IComparable, IValueLinkListNode= 0; --i) { - if (Trades == null) - { - break; - } - if (i >= Trades.Count) { continue; @@ -287,18 +280,8 @@ public partial class NetState : IComparable, IValueLinkListNode= 0; --i) { - if (Trades != null) - { - break; - } - if (i < Trades.Count) { Trades[i].Cancel(); @@ -308,21 +291,11 @@ public partial class NetState : IComparable, IValueLinkListNode, IValueLinkListNode, IValueLinkListNode, IValueLinkListNode public long Position => _reader.Position; - public TimeSpan AnchoredTimeShift - { - get => _reader.AnchoredTimeShift; - set => _reader.AnchoredTimeShift = value; - } - public void Dispose() { _accessor?.SafeMemoryMappedViewHandle.ReleasePointer(); diff --git a/Projects/Server/Serialization/BufferReader.cs b/Projects/Server/Serialization/BufferReader.cs index bd37f4ff6..846e9e5dc 100644 --- a/Projects/Server/Serialization/BufferReader.cs +++ b/Projects/Server/Serialization/BufferReader.cs @@ -37,8 +37,6 @@ public class BufferReader : IGenericReader public long Position => _position; public long BufferSize => _buffer.Length; - public TimeSpan AnchoredTimeShift { get; set; } - public BufferReader(byte[] buffer, Dictionary typesDb = null, Encoding encoding = null) { _buffer = buffer; diff --git a/Projects/Server/Serialization/BufferWriter.cs b/Projects/Server/Serialization/BufferWriter.cs index ab53e2f2f..b811d68cb 100644 --- a/Projects/Server/Serialization/BufferWriter.cs +++ b/Projects/Server/Serialization/BufferWriter.cs @@ -384,7 +384,6 @@ public class BufferWriter : IGenericWriter } [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Obsolete("Delta time rewrites its bytes on every save. Write anchored time instead (WriteAnchoredTime, or [AnchoredDateTime] on generated fields); bump the containing type's version, as the wire format changes. Existing delta payloads remain readable through ReadDeltaTime in old-version fallbacks.")] public void WriteDeltaTime(DateTime value) { if (value == DateTime.MinValue) @@ -408,21 +407,6 @@ public class BufferWriter : IGenericWriter Write(value.Ticks - DateTime.UtcNow.Ticks); } - /// - /// Writes the absolute value; re-bases it - /// by the elapsed time since the save started, so downtime does not age it and an - /// unchanged value serializes to identical bytes. - /// - public void WriteAnchoredTime(DateTime value) - { - if (value.Kind == DateTimeKind.Local) - { - value = value.ToUniversalTime(); - } - - Write(value.Ticks); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(IPAddress value) { diff --git a/Projects/Server/Serialization/GenericEntityPersistence.cs b/Projects/Server/Serialization/GenericEntityPersistence.cs index c7323329f..a49f213eb 100644 --- a/Projects/Server/Serialization/GenericEntityPersistence.cs +++ b/Projects/Server/Serialization/GenericEntityPersistence.cs @@ -114,10 +114,9 @@ public class GenericEntityPersistence : GenericPersistence, IGenericEntityPer using var binFs = new FileStream( Path.Combine(dir, $"{Name}.bin"), FileMode.Create, FileAccess.Write, FileShare.None, 1024 * 1024 ); - // v4 records are fixed-width 26 bytes; the v5 header carries the save-start anchor - // and the type table (name lengths vary — 64 bytes per entry is a staging hint, not - // a contract). - var expectedIdxSize = 20 + 26L * EntitiesBySerial.Count + 64L * _typeTable.Count; + // v4 records are fixed-width 26 bytes; the header carries the type table + // (name lengths vary — 64 bytes per entry is a staging hint, not a contract). + var expectedIdxSize = 12 + 26L * EntitiesBySerial.Count + 64L * _typeTable.Count; using var idx = new FileBufferWriter(Path.Combine(dir, $"{Name}.idx"), expectedIdxSize); var binPosition = 0L; @@ -143,10 +142,7 @@ public class GenericEntityPersistence : GenericPersistence, IGenericEntityPer binPosition += _selfLength; } - idx.Write(5); // Version - - // One anchor for the whole save: the world is frozen from the moment it is stamped. - idx.Write(World.SaveStartTime.Ticks); + idx.Write(4); // Version // The type table is fully known at freeze (AddEntity diverts to the pending // queues while saving) and is written before the records so the loader can @@ -498,18 +494,6 @@ public class GenericEntityPersistence : GenericPersistence, IGenericEntityPer var version = dataReader.ReadInt(); - if (version >= 5) - { - // Re-base anchored timestamps by the elapsed time since the save started. - var anchor = new DateTime(dataReader.ReadLong(), DateTimeKind.Utc); - var shift = Core.Now - anchor; - _anchoredTimeShift = anchor.Ticks > 0 && shift > TimeSpan.Zero ? shift : TimeSpan.Zero; - - // The whole save shares one anchor. Publish it so payloads without their own - // (GenericPersistence bins) can shift too; indexes load before any of them. - World.LoadTimeShift = _anchoredTimeShift; - } - if (version >= 4) { DeserializeIndexesV4(dataReader, entities); @@ -676,9 +660,6 @@ public class GenericEntityPersistence : GenericPersistence, IGenericEntityPer private static List _toDelete; - // From the loaded idx (v5+); zero when the save predates the anchor. - private TimeSpan _anchoredTimeShift; - private unsafe void InternalDeserialize(string filePath, int index, Dictionary typesDb) { using var mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open); @@ -686,10 +667,7 @@ public class GenericEntityPersistence : GenericPersistence, IGenericEntityPer byte* ptr = null; accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); - var dataReader = new UnmanagedDataReader(ptr, accessor.Length, typesDb) - { - AnchoredTimeShift = _anchoredTimeShift - }; + var dataReader = new UnmanagedDataReader(ptr, accessor.Length, typesDb); Deserialize(dataReader); diff --git a/Projects/Server/Serialization/GenericPersistence.cs b/Projects/Server/Serialization/GenericPersistence.cs index 2268a86c6..5b8e79c29 100644 --- a/Projects/Server/Serialization/GenericPersistence.cs +++ b/Projects/Server/Serialization/GenericPersistence.cs @@ -98,13 +98,7 @@ public abstract class GenericPersistence : Persistence, IGenericSerializable byte* ptr = null; accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); - var dataReader = new UnmanagedDataReader(ptr, accessor.Length, typesDb) - { - // These payloads carry no anchor of their own; they inherit the save-wide - // shift stamped while the entity indexes were read (indexes always load - // before persistence payloads — see Persistence.Load). - AnchoredTimeShift = World.LoadTimeShift - }; + var dataReader = new UnmanagedDataReader(ptr, accessor.Length, typesDb); Deserialize(dataReader); error = dataReader.Position != fileLength diff --git a/Projects/Server/Serialization/IGenericReader.cs b/Projects/Server/Serialization/IGenericReader.cs index bfa302b39..0244a5166 100644 --- a/Projects/Server/Serialization/IGenericReader.cs +++ b/Projects/Server/Serialization/IGenericReader.cs @@ -43,12 +43,6 @@ public interface IGenericReader DateTime ReadDateTime() => new(ReadLong(), DateTimeKind.Utc); TimeSpan ReadTimeSpan() => new(ReadLong()); - /// - /// Decodes a legacy delta-time value. Only for reading old-version payloads (version - /// fallbacks and migration replays) — current formats store anchored time and read it - /// with . is - /// obsolete: no current-version format may write delta time. - /// DateTime ReadDeltaTime() { return ReadLong() switch @@ -58,37 +52,6 @@ public interface IGenericReader var delta => new DateTime(delta + DateTime.UtcNow.Ticks, DateTimeKind.Utc) }; } - - /// - /// Elapsed time between the loaded save starting and this load, applied by - /// . Zero when the source carries no anchor. - /// - TimeSpan AnchoredTimeShift => TimeSpan.Zero; - - DateTime ReadAnchoredTime() - { - var value = ReadDateTime(); - - if (value == DateTime.MinValue || value == DateTime.MaxValue) - { - return value; - } - - var shift = AnchoredTimeShift; - if (shift == TimeSpan.Zero) - { - return value; - } - - var ticks = value.Ticks + shift.Ticks; - - if (ticks >= DateTime.MaxValue.Ticks) - { - return DateTime.MaxValue; - } - - return ticks <= 0 ? DateTime.MinValue : new DateTime(ticks, DateTimeKind.Utc); - } decimal ReadDecimal() => new([ReadInt(), ReadInt(), ReadInt(), ReadInt()]); int ReadEncodedInt() { diff --git a/Projects/Server/Serialization/IGenericWriter.cs b/Projects/Server/Serialization/IGenericWriter.cs index 4162655de..9c9563139 100644 --- a/Projects/Server/Serialization/IGenericWriter.cs +++ b/Projects/Server/Serialization/IGenericWriter.cs @@ -40,11 +40,7 @@ public interface IGenericWriter void Write(decimal value); void WriteEncodedInt(int value); void Write(DateTime value); - - [Obsolete("Delta time rewrites its bytes on every save. Write anchored time instead (WriteAnchoredTime, or [AnchoredDateTime] on generated fields); bump the containing type's version, as the wire format changes. Existing delta payloads remain readable through ReadDeltaTime in old-version fallbacks.")] void WriteDeltaTime(DateTime value); - - void WriteAnchoredTime(DateTime value); void Write(IPAddress value); void Write(TimeSpan value); void Write(Point3D value); diff --git a/Projects/Server/Serialization/UnmanagedDataReader.cs b/Projects/Server/Serialization/UnmanagedDataReader.cs index aa3916ee9..7c8b5da1e 100644 --- a/Projects/Server/Serialization/UnmanagedDataReader.cs +++ b/Projects/Server/Serialization/UnmanagedDataReader.cs @@ -43,8 +43,6 @@ public unsafe class UnmanagedDataReader : IGenericReader /// public long Position { get; private set; } - public TimeSpan AnchoredTimeShift { get; set; } - /// /// Read bits of data raw from a serialized file using Little-endian. /// diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index fd69557b7..522701b78 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -39,8 +39,8 @@ - - + + diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index c00fc85f6..2c9e6d216 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -93,21 +93,6 @@ public static class World public static string SavePath { get; private set; } public static WorldState WorldState { get; private set; } public static bool Saving => WorldState == WorldState.Saving; - - /// - /// UTC time the current or most recent world save started. Written into save indexes so - /// anchored timestamps can be re-based by the downtime at load. - /// - public static DateTime SaveStartTime { get; internal set; } - - /// - /// The anchored-time shift for the save currently being loaded: the downtime between the - /// save's start and this load. Stamped while entity indexes are read (they all carry the - /// same anchor, since the whole save shares one ) and applied - /// to every reader of that save's files — including - /// payloads, which carry no anchor of their own. Zero for saves that predate the anchor. - /// - public static TimeSpan LoadTimeShift { get; internal set; } public static bool Running => WorldState is not WorldState.Loading and not WorldState.Initial; public static bool Loading => WorldState == WorldState.Loading; @@ -302,10 +287,6 @@ public static class World WorldState = WorldState.Saving; - // The world is frozen from here: one anchor for the whole save. Written into save - // indexes so anchored timestamps can be re-based by the downtime at load. - SaveStartTime = Core.Now; - Broadcast(0x35, true, "The world is saving, please wait."); logger.Information("Saving world"); diff --git a/Projects/UOContent.Tests/Fixtures/TestServerInitializer.cs b/Projects/UOContent.Tests/Fixtures/TestServerInitializer.cs index 5e4230b7c..a38af9c3d 100644 --- a/Projects/UOContent.Tests/Fixtures/TestServerInitializer.cs +++ b/Projects/UOContent.Tests/Fixtures/TestServerInitializer.cs @@ -103,7 +103,6 @@ internal static class TestServerInitializer // Registers the Accounts entity persistence; without it no test can construct an Account. Server.Accounting.Accounts.Configure(); RaceDefinitions.Configure(); - Server.Movement.Movement.Configure(); MovementImpl.Configure(); PathFollower.Configure(); World.Load(); diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/AcquisitionTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/AcquisitionTests.cs deleted file mode 100644 index 3ee516dcf..000000000 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/AcquisitionTests.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System; -using System.Collections.Generic; -using Server; -using Server.Mobiles; -using Xunit; - -namespace UOContent.Tests.Mobiles.AI; - -// Pins the reacquire gate and the AcquireOnApproachDelay gradient: every scan re-arms the -// full ReacquireDelay; enemy movement clamps the deadline to the approach delay (Zero = -// prodded scan); an illegal deadline self-heals. -[Collection("Sequential Pathfinding Tests")] -public class AcquisitionTests : IDisposable -{ - private readonly List _created = new(); - - public void Dispose() - { - foreach (var m in _created) - { - m?.Delete(); - } - - _created.Clear(); - } - - private sealed class WildStub : BaseCreature - { - public WildStub() : base(AIType.AI_Melee, FightMode.Closest, 16, 1) => Body = 0xC9; - - public override void GetSpeeds(out double activeSpeed, out double passiveSpeed) - { - activeSpeed = 0.3; - passiveSpeed = 0.6; - } - } - - private sealed class TargetStub : Mobile - { - public TargetStub() => Body = 0x190; - } - - private WildStub Spawn(Map map, Point3D loc) - { - var bc = new WildStub(); - bc.MoveToWorld(loc, map); - bc.AIObject.AITimer?.Stop(); - _created.Add(bc); - return bc; - } - - [Fact] - public void EmptyScan_HonorsReacquireDelay() - { - var map = Map.Maps[1]; - Assert.NotNull(map); - map.GetAverageZ(1500, 1600, out _, out var z, out _); - - var bc = Spawn(map, new Point3D(1500, 1600, (sbyte)z)); - bc.NextReacquireTime = Core.TickCount; - - Assert.False(bc.AIObject.AcquireFocusMob(bc.RangePerception, FightMode.Closest, false, false, true)); - Assert.InRange(bc.NextReacquireTime - Core.TickCount, 5000, 10000); - } - - [Fact] - public void WedgedGate_SelfHeals() - { - var map = Map.Maps[1]; - Assert.NotNull(map); - map.GetAverageZ(1500, 1600, out _, out var z, out _); - - var bc = Spawn(map, new Point3D(1500, 1600, (sbyte)z)); - - var target = new TargetStub(); - target.DefaultMobileInit(); - target.MoveToWorld(new Point3D(1497, 1600, (sbyte)z), map); - _created.Add(target); - - // Illegal deadline (beyond ReacquireDelay): must read as open, not block forever. - bc.NextReacquireTime = Core.TickCount + 60000; - - Assert.True(bc.AIObject.AcquireFocusMob(bc.RangePerception, FightMode.Closest, false, false, true)); - Assert.Equal(target, bc.FocusMob); - } - - [Theory] - [InlineData(false, 5, true)] // an enemy moving inside approach range (10) clamps the deadline - [InlineData(true, 5, false)] // a same-team wild creature is not an enemy — ignored - [InlineData(false, 12, false)] // inside RangePerception but outside approach range — poll only - [InlineData(false, 20, false)] // outside approach range (10) is ignored - public void MovementClampsScanDeadlineOnlyForEnemiesInRange(bool wildMover, int distance, bool notices) - { - var map = Map.Maps[1]; - Assert.NotNull(map); - map.GetAverageZ(1500, 1600, out _, out var z, out _); - - var bc = Spawn(map, new Point3D(1500, 1600, (sbyte)z)); - bc.NextReacquireTime = Core.TickCount + 8000; - - Mobile mover; - if (wildMover) - { - mover = Spawn(map, new Point3D(1500 - distance, 1600, (sbyte)z)); - } - else - { - mover = new TargetStub { Player = true }; - mover.DefaultMobileInit(); - mover.MoveToWorld(new Point3D(1500 - distance, 1600, (sbyte)z), map); - _created.Add(mover); - } - - bc.OnMovement(mover, new Point3D(1400, 1600, (sbyte)z)); - - var remaining = bc.NextReacquireTime - Core.TickCount; - - if (notices) - { - // Clamped to the approach delay (2s), never opened outright. - Assert.InRange(remaining, 1, (long)bc.AcquireOnApproachDelay.TotalMilliseconds); - } - else - { - Assert.True(remaining > 5000); - } - } - - private sealed class InstantStub : BaseCreature - { - public InstantStub() : base(AIType.AI_Melee, FightMode.Closest, 16, 1) => Body = 0xC9; - - public override TimeSpan AcquireOnApproachDelay => TimeSpan.Zero; - - public override void GetSpeeds(out double activeSpeed, out double passiveSpeed) - { - activeSpeed = 0.3; - passiveSpeed = 0.6; - } - } - - [Fact] - public void ZeroApproachDelay_OpensGateImmediately() - { - var map = Map.Maps[1]; - Assert.NotNull(map); - map.GetAverageZ(1500, 1600, out _, out var z, out _); - - var bc = new InstantStub(); - bc.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map); - bc.AIObject.AITimer?.Stop(); - _created.Add(bc); - bc.NextReacquireTime = Core.TickCount + 8000; - - var mover = new TargetStub { Player = true }; - mover.DefaultMobileInit(); - mover.MoveToWorld(new Point3D(1495, 1600, (sbyte)z), map); - _created.Add(mover); - - bc.OnMovement(mover, new Point3D(1400, 1600, (sbyte)z)); - - // Zero = the gate opens and the AI is prodded to think now; no direct engage. - Assert.True(Core.TickCount - bc.NextReacquireTime >= 0); - Assert.Null(bc.Combatant); - Assert.True(bc.AIObject.AITimer.Running); - } - - [Fact] - public void RepeatedMovement_DoesNotShortenBelowApproachDelay() - { - var map = Map.Maps[1]; - Assert.NotNull(map); - map.GetAverageZ(1500, 1600, out _, out var z, out _); - - var bc = Spawn(map, new Point3D(1500, 1600, (sbyte)z)); - bc.NextReacquireTime = Core.TickCount + 8000; - - var mover = new TargetStub { Player = true }; - mover.DefaultMobileInit(); - mover.MoveToWorld(new Point3D(1495, 1600, (sbyte)z), map); - _created.Add(mover); - - bc.OnMovement(mover, new Point3D(1400, 1600, (sbyte)z)); - var afterFirst = bc.NextReacquireTime; - - bc.OnMovement(mover, new Point3D(1496, 1600, (sbyte)z)); - - Assert.Equal(afterFirst, bc.NextReacquireTime); - } - - [Fact] - public void SuccessfulAcquire_HoldsFullDelay() - { - var map = Map.Maps[1]; - Assert.NotNull(map); - map.GetAverageZ(1500, 1600, out _, out var z, out _); - - var bc = Spawn(map, new Point3D(1500, 1600, (sbyte)z)); - - var target = new TargetStub(); - target.DefaultMobileInit(); - target.MoveToWorld(new Point3D(1497, 1600, (sbyte)z), map); - _created.Add(target); - - bc.NextReacquireTime = Core.TickCount; - - Assert.True(bc.AIObject.AcquireFocusMob(bc.RangePerception, FightMode.Closest, false, false, true)); - Assert.Equal(target, bc.FocusMob); - Assert.True(bc.NextReacquireTime - Core.TickCount > 5000); - } -} diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs index a952252a6..923f4aff9 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs @@ -40,7 +40,7 @@ public class ApproachTargetTests for (var i = 0; i < maxTicks; i++) { ai.NextMove = 0; - ai.WalkMobileRange(target, 1, 1, 2); + ai.WalkMobileRange(target, 1, false, 1, 2); if (bc.InRange(target, arriveDist)) { return true; @@ -123,7 +123,7 @@ public class ApproachTargetTests for (var i = 0; i < 200; i++) { ai.NextMove = 0; - ai.MoveTo(target, 1); + ai.MoveTo(target, false, 1); if (bc.InRange(target, 1)) { arrived = true; @@ -154,7 +154,7 @@ public class ApproachTargetTests for (var i = 0; i < 60; i++) { ai.NextMove = 0; - ai.MoveTo(target, 1); + ai.MoveTo(target, true, 1); // Target walks west every other tick for its first several steps, then stops, // so a same-speed chaser eventually closes the gap. @@ -214,7 +214,7 @@ public class ApproachTargetTests for (var i = 0; i < 120; i++) { ai.NextMove = 0; - ai.MoveTo(target, 1); + ai.MoveTo(target, false, 1); } // After giving up, the creature must idle (not oscillate) while the goal is still. @@ -223,7 +223,7 @@ public class ApproachTargetTests for (var i = 0; i < 20; i++) { ai.NextMove = 0; - ai.MoveTo(target, 1); + ai.MoveTo(target, false, 1); if (bc.Location != idleStart) { stayedIdle = false; diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFollowTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFollowTests.cs deleted file mode 100644 index db5225359..000000000 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFollowTests.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.Collections.Generic; -using Server; -using Server.Mobiles; -using Xunit; - -namespace UOContent.Tests.Mobiles.AI; - -// Guard-following may pathfind, so this shares the pathfinding collection. -[Collection("Sequential Pathfinding Tests")] -public class GuardFollowTests -{ - [Fact] - public void GuardFollow_StepsTowardMaster_AndRegistersMoveIntent() - { - var map = Map.Maps[1]; - Assert.NotNull(map); - map.GetAverageZ(1500, 1600, out _, out var z, out _); - - var master = new PlayerMobile(World.NewMobile); - master.DefaultMobileInit(); - master.MoveToWorld(new Point3D(1494, 1600, (sbyte)z), map); - - var pet = new PetTestStub(); - pet.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map); // 6 tiles east, open terrain - pet.SetControlMaster(master); - - var ai = pet.AIObject; - ai.AITimer?.Stop(); // drive manually - pet.ControlOrder = OrderType.Guard; - ai.AITimer?.Stop(); // the order change may restart the timer - - var start = pet.Location; - ai.NextMove = 0; - ai.Obey(); - - var moved = pet.Location != start; - var hasIntent = ai.TryGetMoveWake(out _); - var currentSpeed = pet.CurrentSpeed; - var currentMoveSpeed = pet.CurrentMoveSpeed; - - pet.Delete(); - master.Delete(); - - Assert.True(moved, "a guarding pet beyond guard range must step toward its master"); - // Without a move intent, guard-following only steps on the think grid. - Assert.True(hasIntent, "guard-following must register a move intent"); - - // AOS return sprint on both clocks; the per-step speed flip must not undo it. - Assert.Equal(0.1, currentSpeed); - Assert.Equal(0.1, currentMoveSpeed); - } - - [Fact] - public void GuardReturn_PreAOS_RunsActive() - { - var previous = Core.Expansion; - - try - { - Core.Expansion = Expansion.UOR; - - var map = Map.Maps[1]; - Assert.NotNull(map); - map.GetAverageZ(1500, 1600, out _, out var z, out _); - - var master = new PlayerMobile(World.NewMobile); - master.DefaultMobileInit(); - master.MoveToWorld(new Point3D(1494, 1600, (sbyte)z), map); - - var pet = new PetTestStub(); - pet.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map); - pet.SetControlMaster(master); - - var ai = pet.AIObject; - ai.AITimer?.Stop(); - pet.ControlOrder = OrderType.Guard; - ai.AITimer?.Stop(); - pet.SetCurrentSpeedToPassive(); // a stale passive state must not persist - - ai.NextMove = 0; - ai.Obey(); - - var currentSpeed = pet.CurrentSpeed; - - pet.Delete(); - master.Delete(); - - // No sprint pre-AOS: the return runs active. - Assert.Equal(0.2, currentSpeed); - } - finally - { - Core.Expansion = previous; - } - } -} diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardOrderTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardOrderTests.cs deleted file mode 100644 index 7d572786a..000000000 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardOrderTests.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Collections.Generic; -using Server; -using Server.Mobiles; -using Xunit; - -namespace UOContent.Tests.Mobiles.AI; - -// A guarding pet fights without leaving the Guard order, retargets toward the master's -// closest aggressor, and stands down when nothing threatens. Scene: the open -// (1495..1500, 1600) Trammel segment; targets are adjacent so no pathfinding runs. -[Collection("Sequential UOContent Tests")] -public class GuardOrderTests : IDisposable -{ - private readonly List _created = new(); - - private sealed class AggressorStub : Mobile - { - public AggressorStub() => Body = 0xC9; - } - - public void Dispose() - { - foreach (var m in _created) - { - m?.Delete(); - } - - _created.Clear(); - } - - private (PlayerMobile master, PetTestStub pet) SpawnGuardingPet(out Map map, out int z) - { - map = Map.Maps[1]; - Assert.NotNull(map); - map.GetAverageZ(1500, 1600, out _, out z, out _); - - var master = new PlayerMobile(World.NewMobile); - master.DefaultMobileInit(); - master.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map); - _created.Add(master); - - var pet = new PetTestStub(); - pet.MoveToWorld(new Point3D(1499, 1600, (sbyte)z), map); - pet.SetControlMaster(master); - _created.Add(pet); - - pet.AIObject.AITimer?.Stop(); // drive manually - pet.ControlOrder = OrderType.Guard; - pet.AIObject.AITimer?.Stop(); // the order change restarts the timer - - return (master, pet); - } - - private AggressorStub SpawnAggressor(PetTestStub pet, Point3D loc, Mobile attacking) - { - var aggr = new AggressorStub(); - aggr.MoveToWorld(loc, pet.Map); - _created.Add(aggr); - - // Setup guard: the scene must stay LOS-clear and the combatant must not be vetoed. - Assert.True(pet.InLOS(aggr), $"no LOS from pet to aggressor at {loc}"); - - if (attacking != null) - { - aggr.Combatant = attacking; - Assert.Same(attacking, aggr.Combatant); - } - - return aggr; - } - - [Fact] - public void GuardEngage_KeepsGuardOrder() - { - var (master, pet) = SpawnGuardingPet(out _, out var z); - var aggr = SpawnAggressor(pet, new Point3D(1498, 1600, (sbyte)z), master); - - pet.AIObject.Obey(); - - Assert.Same(aggr, pet.Combatant); - Assert.Equal(OrderType.Guard, pet.ControlOrder); - Assert.Equal(OrderType.Guard, pet.AIObject.PersistentOrder); - } - - [Fact] - public void Guard_RetargetsToAggressorClosestToMaster() - { - var (master, pet) = SpawnGuardingPet(out _, out var z); - var far = SpawnAggressor(pet, new Point3D(1495, 1600, (sbyte)z), master); - var near = SpawnAggressor(pet, new Point3D(1498, 1600, (sbyte)z), master); - - pet.Combatant = far; // already fighting the far aggressor - - pet.AIObject.Obey(); - - Assert.Same(near, pet.Combatant); // defends the master, not the current fight - Assert.Equal(OrderType.Guard, pet.ControlOrder); - } - - [Fact] - public void ExplicitAttack_ResumesGuard_WithoutChainingIntoAttack() - { - var (master, pet) = SpawnGuardingPet(out _, out var z); - - // Explicit kill order on a target that then becomes invalid. - var victim = SpawnAggressor(pet, new Point3D(1498, 1600, (sbyte)z), null); - pet.ControlTarget = victim; - pet.ControlOrder = OrderType.Attack; - victim.Hidden = true; - - // A second aggressor is still after the master; FightMode.Closest would chain it. - var aggr2 = SpawnAggressor(pet, new Point3D(1497, 1600, (sbyte)z), master); - - pet.AIObject.Obey(); // attack completes -> resume the persistent Guard - - Assert.Equal(OrderType.Guard, pet.ControlOrder); - - pet.AIObject.Obey(); // the guard scan engages the remaining aggressor in-order - - Assert.Same(aggr2, pet.Combatant); - Assert.Equal(OrderType.Guard, pet.ControlOrder); - } - - [Fact] - public void PeacefulGuard_StandsDown() - { - var (_, pet) = SpawnGuardingPet(out _, out _); - Assert.True(pet.Warmode); // the guard order opens in war stance - - pet.AIObject.Obey(); // nothing to guard against - - Assert.False(pet.Warmode); - Assert.Null(pet.Combatant); - Assert.Null(pet.FocusMob); - } -} diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs deleted file mode 100644 index 7d9243a29..000000000 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System; -using System.Collections.Generic; -using Server; -using Server.Mobiles; -using Xunit; - -namespace UOContent.Tests.Mobiles.AI; - -// Pins the CurrentMoveSpeed classification (verbatim active/passive maps to the matching -// move value; bespoke stays fused), SetSpeed's one-clock guarantee, and the v22 tail. -[Collection("Sequential UOContent Tests")] -public class MoveSpeedTests : IDisposable -{ - // Delete spawned stubs so they don't linger in the shared static World. - private readonly List _created = new(); - - public void Dispose() - { - for (var i = 0; i < _created.Count; i++) - { - _created[i].Delete(); - } - } - - private sealed class SpeedStub : BaseCreature - { - // Stands in for the npc-speeds table (unconfigured in the test fixture). - public double TableActiveMove; - public double TablePassiveMove; - - public SpeedStub() : base(AIType.AI_Animal) => Body = 0xC9; - - public SpeedStub(Serial serial) : base(serial) => Body = 0xC9; - - public override void GetSpeeds(out double activeSpeed, out double passiveSpeed) - { - activeSpeed = 0.3; - passiveSpeed = 0.6; - } - - public override void GetMoveSpeeds(out double activeMoveSpeed, out double passiveMoveSpeed) - { - activeMoveSpeed = TableActiveMove; - passiveMoveSpeed = TablePassiveMove; - } - } - - private SpeedStub NewCreature() - { - var bc = new SpeedStub(); - _created.Add(bc); - return bc; - } - - [Fact] - public void MoveSpeeds_InheritThinkValues_ByDefault() - { - var bc = NewCreature(); - - Assert.Equal(0.3, bc.ActiveMoveSpeed); - Assert.Equal(0.6, bc.PassiveMoveSpeed); - Assert.Equal(bc.CurrentSpeed, bc.CurrentMoveSpeed); - } - - [Fact] - public void CurrentMoveSpeed_ResolvesPerMode_WhenOverridden() - { - var bc = NewCreature(); - bc.SetMoveSpeed(0.45, 0.9); - - // SetSpeed left the creature passive; the think clock is untouched. - Assert.Equal(0.6, bc.CurrentSpeed); - Assert.Equal(0.9, bc.CurrentMoveSpeed); - - bc.SetCurrentSpeedToActive(); - Assert.Equal(0.3, bc.CurrentSpeed); - Assert.Equal(0.45, bc.CurrentMoveSpeed); - } - - [Fact] - public void CurrentMoveSpeed_BespokePace_StaysFused() - { - var bc = NewCreature(); - bc.SetMoveSpeed(0.45, 0.9); - - // Neither think value verbatim, so both clocks run it. - bc.CurrentSpeed = 0.11; - Assert.Equal(0.11, bc.CurrentMoveSpeed); - } - - [Fact] - public void SetSpeed_ClearsMoveOverrides() - { - var bc = NewCreature(); - bc.SetMoveSpeed(0.45, 0.9); - - bc.SetSpeed(0.2, 0.4); - - Assert.Equal(0.2, bc.ActiveMoveSpeed); - Assert.Equal(0.4, bc.PassiveMoveSpeed); - } - - [Fact] - public void NonPositiveMoveSpeed_ClearsThatOverride() - { - var bc = NewCreature(); - bc.SetMoveSpeed(0.45, 0.9); - - bc.ActiveMoveSpeed = 0; - - Assert.Equal(0.3, bc.ActiveMoveSpeed); // inheriting again - Assert.Equal(0.9, bc.PassiveMoveSpeed); // other override untouched - } - - [Fact] - public void ScaleMoveSpeed_ScalesOverrides_LeavesInheritAlone() - { - var bc = NewCreature(); - bc.ActiveMoveSpeed = 0.6; // passive left inheriting - - bc.ScaleMoveSpeed(1.0 / 1.2); - - Assert.Equal(0.5, bc.ActiveMoveSpeed); - Assert.Equal(bc.PassiveSpeed, bc.PassiveMoveSpeed); // still inheriting, not 0 * scalar - } - - [Fact] - public void Herding_DrivesMoveClock_ThinkUntouched() - { - var bc = NewCreature(); // think 0.3/0.6, passive - bc.SetMoveSpeed(0.45, 1.05); - - bc.TargetLocation = new Point2D(10, 10); - - Assert.Equal(0.6, bc.CurrentSpeed); // think clock unaffected by herding - Assert.Equal(0.3, bc.CurrentMoveSpeed); // fixed herding pace, not 1.05 - - bc.TargetLocation = null; - Assert.Equal(1.05, bc.CurrentMoveSpeed); - } - - [Fact] - public void SnapSpeedsToTable_UndoesScalingDrift_KeepsTunedValues() - { - var bc = NewCreature(); - bc.TableActiveMove = 0.45; - bc.TablePassiveMove = 0.9; - bc.SetMoveSpeed(0.45, 0.9); - - // 0.45 and 0.9 do not survive /1.2 then *1.2 bit-exactly. - bc.ScaleMoveSpeed(1.0 / 1.2); - bc.ScaleMoveSpeed(1.2); - Assert.NotEqual(0.45, bc.ActiveMoveSpeed); - - bc.SnapSpeedsToTable(); - Assert.Equal(0.45, bc.ActiveMoveSpeed); - Assert.Equal(0.9, bc.PassiveMoveSpeed); - - // A hand-tuned value is nowhere near the epsilon and must keep. - bc.SetMoveSpeed(0.7, 0.9); - bc.SnapSpeedsToTable(); - Assert.Equal(0.7, bc.ActiveMoveSpeed); - } - - [Fact] - public void Migration_MatchingThinkSpeeds_AdoptTableMoveValues() - { - var bc = NewCreature(); // think 0.3/0.6, matching its table entry - bc.TableActiveMove = 0.45; - bc.TablePassiveMove = 0.9; - - bc.MigrateMoveSpeeds(); - - Assert.Equal(0.45, bc.ActiveMoveSpeed); - Assert.Equal(0.9, bc.PassiveMoveSpeed); - } - - [Fact] - public void Migration_TunedThinkSpeeds_KeepInheriting() - { - var bc = NewCreature(); - bc.SetSpeed(0.35, 0.6); // hand-tuned: no longer matches the table entry - bc.TableActiveMove = 0.45; - bc.TablePassiveMove = 0.9; - - bc.MigrateMoveSpeeds(); - - Assert.Equal(0.35, bc.ActiveMoveSpeed); - Assert.Equal(0.6, bc.PassiveMoveSpeed); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void MoveSpeedOverrides_SurviveSerialization(bool overridden) - { - var bc = NewCreature(); - if (overridden) - { - bc.SetMoveSpeed(0.45, 0.9); - } - - var writer = new BufferWriter(true); - bc.Serialize(writer); - - var buffer = new byte[writer.Position]; - writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer); - - var copy = new SpeedStub(World.NewMobile); - _created.Add(copy); - var reader = new BufferReader(buffer); - copy.Deserialize(reader); - - // The v22 tail is the last block; exact consumption catches any offset mistake. - Assert.Equal(buffer.Length, reader.Position); - Assert.Equal(overridden ? 0.45 : 0.3, copy.ActiveMoveSpeed); - Assert.Equal(overridden ? 0.9 : 0.6, copy.PassiveMoveSpeed); - } -} diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetPacingTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetPacingTests.cs deleted file mode 100644 index 217049b3d..000000000 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetPacingTests.cs +++ /dev/null @@ -1,222 +0,0 @@ -using System; -using System.Collections.Generic; -using Server; -using Server.Mobiles; -using Xunit; - -namespace UOContent.Tests.Mobiles.AI; - -// Pet order handlers own the speed clocks; combat chases and herding keep their own pacing. -[Collection("Sequential UOContent Tests")] -public class PetPacingTests : IDisposable -{ - private readonly List _created = new(); - - private (PlayerMobile master, PetTestStub pet) Spawn(Point3D masterLoc, Point3D petLoc) - { - var pair = PetTestSetup.SpawnControlledPet(masterLoc, petLoc); - _created.Add(pair.master); - _created.Add(pair.pet); - return pair; - } - - public void Dispose() - { - foreach (var m in _created) - { - m?.Delete(); - } - - _created.Clear(); - } - - // Movement orders run active, resting orders run passive; the move clock follows. - [Fact] - public void OrderIssue_SetsThinkClock() - { - var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0)); - pet.SetMoveSpeed(0.3, 0.9); - pet.SetCurrentSpeedToPassive(); - - pet.ControlOrder = OrderType.Come; - Assert.Equal(0.2, pet.CurrentSpeed); - Assert.Equal(0.3, pet.CurrentMoveSpeed); // verbatim active -> activeMove - - pet.ControlOrder = OrderType.Stay; - Assert.Equal(0.4, pet.CurrentSpeed); - Assert.Equal(0.9, pet.CurrentMoveSpeed); - - pet.ControlTarget = master; - pet.ControlOrder = OrderType.Follow; - Assert.Equal(0.2, pet.CurrentSpeed); - - pet.ControlOrder = OrderType.Guard; - Assert.Equal(0.2, pet.CurrentSpeed); - } - - // AOS: following the master sprints at a bespoke 0.1 on both clocks. - [Fact] - public void FollowMaster_ObeySprints() - { - var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0)); - pet.SetMoveSpeed(0.3, 0.9); - pet.AIObject.AITimer?.Stop(); - - pet.ControlTarget = master; - pet.ControlOrder = OrderType.Follow; // fixture era is EJ - pet.AIObject.Obey(); - - Assert.Equal(0.1, pet.CurrentSpeed); - Assert.Equal(0.1, pet.CurrentMoveSpeed); - } - - // At the master's side a guarding pet stays active: no stale-warmode passive, no sprint. - [Fact] - public void GuardAtMastersSide_IsActive() - { - var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0)); - pet.SetMoveSpeed(0.3, 0.9); - pet.AIObject.AITimer?.Stop(); - pet.SetCurrentSpeedToPassive(); - - pet.ControlOrder = OrderType.Guard; - pet.AIObject.Obey(); // nothing to guard against, master adjacent - - Assert.Equal(0.2, pet.CurrentSpeed); - Assert.Equal(0.3, pet.CurrentMoveSpeed); - } - - // A pet chasing a combatant keeps the move table. - [Fact] - public void CombatChasingPet_KeepsMoveTable() - { - var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0)); - var target = new PetTestStub(); - target.MoveToWorld(new Point3D(1003, 1000, 0), Map.Felucca); - _created.Add(target); - - pet.SetMoveSpeed(0.3, 0.9); - pet.ControlOrder = OrderType.Guard; - pet.Combatant = target; - pet.SetCurrentSpeedToActive(); - - Assert.Equal(0.3, pet.CurrentMoveSpeed); - } - - // Herding overrides order pacing. - [Fact] - public void HerdedObeyingPet_KeepsHerdingPace() - { - var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0)); - pet.SetMoveSpeed(0.45, 0.9); - pet.SetCurrentSpeedToPassive(); - - pet.TargetLocation = new Point2D(1010, 1010); - - Assert.Equal(0.3, pet.CurrentMoveSpeed); // fixed herding pace - } - - private sealed class ThinkProbe : PetTestStub - { - public int Thinks; - - public override void OnThink() - { - Thinks++; - base.OnThink(); - } - } - - private (PlayerMobile master, ThinkProbe pet) SpawnProbe() - { - var master = new PlayerMobile(World.NewMobile); - master.DefaultMobileInit(); - master.MoveToWorld(new Point3D(1000, 1000, 0), Map.Felucca); - _created.Add(master); - - var pet = new ThinkProbe(); - pet.MoveToWorld(new Point3D(1001, 1000, 0), Map.Felucca); - pet.SetControlMaster(master); - _created.Add(pet); - - return (master, pet); - } - - // Advances time in 8ms lockstep so the wheel and Core.TickCount stay in sync. - private static void RunFor(long ms) - { - var deadline = Core._tickCount + ms; - - while (Core._tickCount < deadline) - { - Core._tickCount += 8; - Timer.Slice(Core._tickCount); - } - } - - private static bool RunUntil(Func condition, long maxMs) - { - var deadline = Core._tickCount + maxMs; - - while (Core._tickCount < deadline) - { - if (condition()) - { - return true; - } - - Core._tickCount += 8; - Timer.Slice(Core._tickCount); - } - - return condition(); - } - - // Runs past the spawn stagger; returns right after a think with the next 0.4s away. - private ThinkProbe SettledProbe(out PlayerMobile master) - { - Core._tickCount = 0; - Timer.Init(0); - - var (m, pet) = SpawnProbe(); - master = m; - pet.ForceIdle = true; // no wandering; pure cadence - pet.ControlOrder = OrderType.Stay; - - var settled = RunUntil(() => pet.Thinks >= 2, 8000); - Assert.True(settled, "the AI must reach a steady think cadence"); - - return pet; - } - - [Fact] - public void OrderChange_WakesStaleThinkTimer() - { - var pet = SettledProbe(out var master); - var thinksBefore = pet.Thinks; - - RunFor(200); // mid-wait, next think ~200ms out - Assert.Equal(thinksBefore, pet.Thinks); - - pet.ControlTarget = master; - pet.ControlOrder = OrderType.Follow; - - RunFor(80); - Assert.True(pet.Thinks > thinksBefore, "a fresh order must wake the AI promptly"); - } - - [Fact] - public void SpeedUp_ReschedulesPendingWake() - { - var pet = SettledProbe(out _); - var thinksBefore = pet.Thinks; - - RunFor(200); // mid-wait, next think ~200ms out - Assert.Equal(thinksBefore, pet.Thinks); - - pet.CurrentSpeed = 0.1; - - RunFor(120); - Assert.True(pet.Thinks > thinksBefore, "a speed-up must reschedule the pending wake"); - } -} diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/RunFlagTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/RunFlagTests.cs deleted file mode 100644 index a421aaba2..000000000 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/RunFlagTests.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System.Collections.Generic; -using Server; -using Server.Mobiles; -using Xunit; - -namespace UOContent.Tests.Mobiles.AI; - -// The Running bit is derived from the step pace: a step shorter than the client's walk -// interpolation (400ms on foot, 200ms mounted/flying) is flagged as a run. -[Collection("Sequential Pathfinding Tests")] -public class RunFlagTests : System.IDisposable -{ - private readonly List _created = new(); - - private PetTestStub Spawn(double activeMove) - { - var pet = new PetTestStub(); - pet.MoveToWorld(new Point3D(1000, 1000, 0), Map.Felucca); - pet.AIObject.AITimer?.Stop(); - pet.SetMoveSpeed(activeMove, activeMove * 3); - pet.SetCurrentSpeedToActive(); - pet.LastMoveTime = Core.TickCount; // mid-cadence unless a test says otherwise - _created.Add(pet); - return pet; - } - - public void Dispose() - { - foreach (var m in _created) - { - m?.Delete(); - } - - _created.Clear(); - } - - [Theory] - [InlineData(0.3, true)] - [InlineData(0.125, true)] - [InlineData(0.4, false)] - [InlineData(0.45, false)] - [InlineData(1.05, false)] - public void FootCreature_RunsOnlyWhenFasterThanWalk(double activeMove, bool expected) - { - var pet = Spawn(activeMove); - - Assert.Equal(activeMove, pet.CurrentMoveSpeed); - Assert.Equal(expected, pet.AIObject.ShouldRun()); - } - - [Theory] - [InlineData(0.3, false)] - [InlineData(0.15, true)] - public void FlyingCreature_UsesMountThresholds(double activeMove, bool expected) - { - var pet = Spawn(activeMove); - pet.Flying = true; - - Assert.Equal(expected, pet.AIObject.ShouldRun()); - } - - [Fact] - public void BadlyHurt_SlowsBelowWalk_DropsToWalk() - { - var pet = Spawn(0.35); - Assert.True(pet.AIObject.ShouldRun()); - - // The hurt inflation is on the observed step pace, so the flag follows it. - pet.SetHits(100); - pet.Hits = 5; - pet.SetStam(100); - pet.Stam = 5; - - Assert.False(pet.AIObject.ShouldRun()); - } - - [Theory] - [InlineData(0.3, true)] - [InlineData(0.45, false)] - public void DoMove_StampsRunningBit(double activeMove, bool expected) - { - var map = Map.Maps[1]; - Assert.NotNull(map); - map.GetAverageZ(1500, 1600, out _, out var z, out _); - - var pet = Spawn(activeMove); - pet.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map); - - var ai = pet.AIObject; - ai.NextMove = 0; - var start = pet.Location; - - Assert.True(ai.DoMove(Direction.West)); - Assert.NotEqual(start, pet.Location); - Assert.Equal(expected, (pet.Direction & Direction.Running) != 0); - } - - // An isolated step (after standing at least a walk interval) renders alone and darts - // if run-flagged, so it walks; continuing cadences and true sprinters keep the flag. - [Fact] - public void IsolatedStep_DropsToWalk() - { - var pet = Spawn(0.3); - pet.LastMoveTime = Core.TickCount - 1000; - - Assert.False(pet.AIObject.ShouldRun()); - } - - [Fact] - public void IsolatedStep_SprinterStillRuns() - { - var pet = Spawn(0.125); - pet.LastMoveTime = Core.TickCount - 1000; - - Assert.True(pet.AIObject.ShouldRun()); - } - - [Fact] - public void StallDoesNotBankCatchUpSteps() - { - var map = Map.Maps[1]; - Assert.NotNull(map); - map.GetAverageZ(1500, 1600, out _, out var z, out _); - - var pet = Spawn(0.3); - pet.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map); - - var ai = pet.AIObject; - ai.NextMove = Core.TickCount - 1000; - - Assert.True(ai.DoMove(Direction.West)); - - // A stall must restart the cadence at full pace: banked catch-up steps - // release as a burst the client renders as a sprint/teleport. - Assert.False(ai.CanMoveNow(out _)); - Assert.True(ai.NextMove - Core.TickCount > 250); - } - - [Fact] - public void LateStepDoesNotEarnAQuickerFollowUp() - { - var map = Map.Maps[1]; - Assert.NotNull(map); - map.GetAverageZ(1500, 1600, out _, out var z, out _); - - var pet = Spawn(0.3); - pet.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map); - - pet.Warmode = true; // keep the active move clock through the step - - var ai = pet.AIObject; - // The step lands 200ms past the budget — under one period, the reactive - // mirroring case (think grid vs budget deadline misalignment). - ai.NextMove = Core.TickCount - 200; - - Assert.True(ai.DoMove(Direction.West)); - - // The debt must not be repaid: a sub-period catch-up step follows ~100ms - // behind and renders as a dart pair beside the player. - Assert.True(ai.NextMove - Core.TickCount > 250); - } -} diff --git a/Projects/UOContent.Tests/Tests/Mobiles/LootingRightsTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/LootingRightsTests.cs deleted file mode 100644 index e02abece7..000000000 --- a/Projects/UOContent.Tests/Tests/Mobiles/LootingRightsTests.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System.Collections.Generic; -using Server.Mobiles; -using Xunit; - -namespace Server.Tests; - -/// -/// Pins the looting-rights rules that the inline damage entry list has to keep producing: the -/// returned stores are sorted by damage descending, the first (least recent) damager takes the -/// 1.25x bonus, the hitsMax band decides who clears the threshold, and a pet's damage is credited -/// to its damage master rather than to the pet. -/// -[Collection("Sequential UOContent Tests")] -public class LootingRightsTests -{ - private class TestMobile : Mobile - { - } - - private class PetMobile : Mobile - { - public Mobile Master { get; set; } - - public override Mobile GetDamageMaster(Mobile damagee) => Master; - } - - // GetLootingRights only ever credits mobiles flagged as players. - private static TestMobile NewPlayer() => new() { Player = true }; - - private static DamageStore FindStore(List rights, Mobile m) - { - for (var i = 0; i < rights.Count; i++) - { - if (rights[i].m_Mobile == m) - { - return rights[i]; - } - } - - return null; - } - - [Fact] - public void TwoPlayerDamagers_SortDescending_AndTheFirstDamagerTakesTheBonus() - { - var victim = new TestMobile(); - var first = NewPlayer(); - var second = NewPlayer(); - - try - { - victim.RegisterDamage(100, first); - victim.RegisterDamage(40, second); // second is the most recent, first is the "first damager" - - // hitsMax < 200 puts the bar at topDamage / 2. - var rights = BaseCreature.GetLootingRights(victim.DamageEntries, 100); - - Assert.Equal(2, rights.Count); - - // Sorted by damage descending. - Assert.True(rights[0].m_Damage >= rights[1].m_Damage); - Assert.Same(first, rights[0].m_Mobile); - Assert.Same(second, rights[1].m_Mobile); - - // The first damager - the least recent entry - gets the 1.25x bonus; nobody else does. - Assert.Equal(125, rights[0].m_Damage); - Assert.Equal(40, rights[1].m_Damage); - - // topDamage 125 / 2 = 62, so 40 is below the bar. - Assert.True(rights[0].m_HasRight); - Assert.False(rights[1].m_HasRight); - } - finally - { - victim.Delete(); - first.Delete(); - second.Delete(); - } - } - - [Fact] - public void HitsMaxBand_MovesTheRightsThreshold() - { - var victim = new TestMobile(); - var first = NewPlayer(); - var second = NewPlayer(); - - try - { - victim.RegisterDamage(100, first); - victim.RegisterDamage(40, second); - - // hitsMax >= 200 drops the bar to topDamage / 4 = 31, which 40 clears. - var rights = BaseCreature.GetLootingRights(victim.DamageEntries, 200); - - Assert.Equal(2, rights.Count); - Assert.True(rights[0].m_HasRight); - Assert.True(rights[1].m_HasRight); - Assert.Same(second, rights[1].m_Mobile); - } - finally - { - victim.Delete(); - first.Delete(); - second.Delete(); - } - } - - [Fact] - public void PetDamage_CreditsTheMaster_NotThePet() - { - var victim = new TestMobile(); - var master = NewPlayer(); - var pet = new PetMobile { Master = master }; - var wild = new TestMobile(); // no damage master, and not a player - - try - { - victim.RegisterDamage(50, pet); - victim.RegisterDamage(20, wild); - - var rights = BaseCreature.GetLootingRights(victim.DamageEntries, 100); - - // The master is credited through the entry's Responsible sub-entry, and is the only one. - Assert.Single(rights); - - var masterStore = FindStore(rights, master); - Assert.NotNull(masterStore); - Assert.Equal(62, masterStore.m_Damage); // 50, then the first-damager 1.25x bonus - Assert.True(masterStore.m_HasRight); - - // The pet's own damage was fully handed to the master, so it earns no store. - Assert.Null(FindStore(rights, pet)); - - // A non-player damager earns nothing even when its damage was never reassigned. - Assert.Null(FindStore(rights, wild)); - } - finally - { - victim.Delete(); - master.Delete(); - pet.Delete(); - wild.Delete(); - } - } -} diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BOBFilter.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BOBFilter.cs index f63e4f8d4..962c3d8e3 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/BOBFilter.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BOBFilter.cs @@ -6,27 +6,27 @@ namespace Server.Engines.BulkOrders; public partial class BOBFilter { [SerializableField(0)] - [SaveFlag(nameof(ShouldSerializeType))] private int _type; + [SerializableFieldSaveFlag(0)] private bool ShouldSerializeType() => _type != 0; [SerializableField(1)] - [SaveFlag(nameof(ShouldSerializeQuality))] private int _quality; + [SerializableFieldSaveFlag(1)] private bool ShouldSerializeQuality() => _quality != 0; [SerializableField(2)] - [SaveFlag(nameof(ShouldSerializeMaterial))] private int _material; + [SerializableFieldSaveFlag(2)] private bool ShouldSerializeMaterial() => _material != 0; [SerializableField(3)] - [SaveFlag(nameof(ShouldSerializeQuantity))] private int _quantity; + [SerializableFieldSaveFlag(3)] private bool ShouldSerializeQuantity() => _quantity != 0; private void Deserialize(IGenericReader reader, int version) diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionSkullBrazier.cs b/Projects/UOContent/Engines/CannedEvil/ChampionSkullBrazier.cs index 2632fd954..9ff8dc633 100755 --- a/Projects/UOContent/Engines/CannedEvil/ChampionSkullBrazier.cs +++ b/Projects/UOContent/Engines/CannedEvil/ChampionSkullBrazier.cs @@ -33,13 +33,17 @@ public partial class ChampionSkullBrazier : AddonComponent [SerializedCommandProperty(AccessLevel.GameMaster)] private ChampionSkullPlatform _platform; - [SerializableField(2, fieldChanged: nameof(OnSkullChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private Item _skull; - - private void OnSkullChanged(Item oldValue, Item newValue) + [SerializableProperty(2)] + [CommandProperty(AccessLevel.GameMaster)] + public Item Skull { - _platform?.Validate(); + get => _skull; + set + { + _skull = value; + this.MarkDirty(); + _platform?.Validate(); + } } public override int LabelNumber => 1049489 + (int)_type; diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs b/Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs index 3a024576b..a26bcc0f9 100755 --- a/Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs +++ b/Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs @@ -18,7 +18,6 @@ using System.Net; using System.Collections.Generic; using System.Runtime.InteropServices; using ModernUO.Serialization; -using Server.Collections; using Server.Engines.Virtues; using Server.Gumps; using Server.Items; @@ -28,44 +27,16 @@ using Server.Logging; namespace Server.Engines.CannedEvil; -[SerializationGenerator(11, false)] +[SerializationGenerator(10, false)] public partial class ChampionSpawn : Item { - private void MigrateFrom(V10Content content) - { - _level = content.Level; - _activatedByProximity = content.ActivatedByProximity; - _nextProximityTime = content.NextProximityTime; - _maxLevel = content.MaxLevel; - _activatedByValor = content.ActivatedByValor; - _damageEntries = content.DamageEntries; - _confinedRoaming = content.ConfinedRoaming; - _idol = content.Idol; - _hasBeenAdvanced = content.HasBeenAdvanced; - _spawnArea = content.SpawnArea; - _randomizeType = content.RandomizeType; - _kills = content.Kills; - _active = content.Active; - _type = content.Type; - _creatures = content.Creatures; - _redSkulls = content.RedSkulls; - _whiteSkulls = content.WhiteSkulls; - _platform = content.Platform; - _altar = content.Altar; - _expireDelay = content.ExpireDelay; - _expireTime = content.ExpireTime; - _champion = content.Champion; - _restartDelay = content.RestartDelay; - _restartTime = content.RestartTime; - } - private static readonly ILogger logger = LogFactory.GetLogger(typeof(ChampionSpawn)); [SerializableField(1)] [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _activatedByProximity; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(2)] [SerializedCommandProperty(AccessLevel.GameMaster)] private DateTime _nextProximityTime; @@ -125,7 +96,7 @@ public partial class ChampionSpawn : Item [SerializedCommandProperty(AccessLevel.GameMaster)] private TimeSpan _expireDelay; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(20)] [SerializedCommandProperty(AccessLevel.GameMaster)] private DateTime _expireTime; @@ -138,7 +109,7 @@ public partial class ChampionSpawn : Item [SerializedCommandProperty(AccessLevel.GameMaster)] private TimeSpan _restartDelay; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(23, setter: "private")] [SerializedCommandProperty(AccessLevel.GameMaster)] private DateTime _restartTime; @@ -232,38 +203,47 @@ public partial class ChampionSpawn : Item } } - [SerializableField(3, allowFieldChange: nameof(AllowMaxLevelChange))] - [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] - private int _maxLevel; - - private bool AllowMaxLevelChange(ref int value) + [SerializableProperty(3)] + [CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] + public int MaxLevel { - value = Math.Clamp(value, 0, 18); - return true; + get => _maxLevel; + set => _maxLevel = Math.Clamp(value, 0, 18); } - [SerializableField(9, fieldChanged: nameof(OnSpawnAreaChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private Rectangle2D _spawnArea; - - private void OnSpawnAreaChanged(Rectangle2D oldValue, Rectangle2D newValue) + [SerializableProperty(9)] + [CommandProperty(AccessLevel.GameMaster)] + public Rectangle2D SpawnArea { - UpdateRegion(); - } - - [SerializableField(11, fieldChanged: nameof(OnKillsChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _kills; - - private void OnKillsChanged(int oldValue, int newValue) - { - var n = _kills / (double)MaxKills; - var p = (int)(n * 100); - if (p < 90) + get => _spawnArea; + set { - SetWhiteSkullCount(p / 20); + _spawnArea = value; + this.MarkDirty(); + InvalidateProperties(); + UpdateRegion(); + } + } + + [SerializableProperty(11)] + [CommandProperty(AccessLevel.GameMaster)] + public int Kills + { + get => _kills; + set + { + _kills = value; + this.MarkDirty(); + + var n = _kills / (double)MaxKills; + var p = (int)(n * 100); + + if (p < 90) + { + SetWhiteSkullCount(p / 20); + } + + InvalidateProperties(); } } @@ -1182,6 +1162,11 @@ public partial class ChampionSpawn : Item foreach (var de in m.DamageEntries) { + if (de.HasExpired) + { + continue; + } + var damager = de.Damager; var master = damager.GetDamageMaster(m); diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionTitleContext.cs b/Projects/UOContent/Engines/CannedEvil/ChampionTitleContext.cs index 644e2b965..bfd00c0bf 100644 --- a/Projects/UOContent/Engines/CannedEvil/ChampionTitleContext.cs +++ b/Projects/UOContent/Engines/CannedEvil/ChampionTitleContext.cs @@ -51,9 +51,9 @@ public partial class ChampionTitleContext } [SerializableField(1)] - [SaveFlag(nameof(ShouldSerializeAbyss))] private ChampionTitle _abyss; + [SerializableFieldSaveFlag(1)] private bool ShouldSerializeAbyss() => _abyss != null; [CommandProperty(AccessLevel.GameMaster)] @@ -71,9 +71,9 @@ public partial class ChampionTitleContext } [SerializableField(2)] - [SaveFlag(nameof(ShouldSerializeArachnid))] private ChampionTitle _arachnid; + [SerializableFieldSaveFlag(2)] private bool ShouldSerializeArachnid() => _arachnid != null; [CommandProperty(AccessLevel.GameMaster)] @@ -91,9 +91,9 @@ public partial class ChampionTitleContext } [SerializableField(3)] - [SaveFlag(nameof(ShouldSerializeColdBlood))] private ChampionTitle _coldBlood; + [SerializableFieldSaveFlag(3)] private bool ShouldSerializeColdBlood() => _coldBlood != null; [CommandProperty(AccessLevel.GameMaster)] @@ -111,9 +111,9 @@ public partial class ChampionTitleContext } [SerializableField(4)] - [SaveFlag(nameof(ShouldSerializeForestLord))] private ChampionTitle _forestLord; + [SerializableFieldSaveFlag(4)] private bool ShouldSerializeForestLord() => _forestLord != null; [CommandProperty(AccessLevel.GameMaster)] @@ -131,9 +131,9 @@ public partial class ChampionTitleContext } [SerializableField(5)] - [SaveFlag(nameof(ShouldSerializeVerminHorde))] private ChampionTitle _verminHorde; + [SerializableFieldSaveFlag(5)] private bool ShouldSerializeVerminHorde() => _verminHorde != null; [CommandProperty(AccessLevel.GameMaster)] @@ -151,9 +151,9 @@ public partial class ChampionTitleContext } [SerializableField(6)] - [SaveFlag(nameof(ShouldSerializeUnholyTerror))] private ChampionTitle _unholyTerror; + [SerializableFieldSaveFlag(6)] private bool ShouldSerializeUnholyTerror() => _unholyTerror != null; [CommandProperty(AccessLevel.GameMaster)] @@ -171,9 +171,9 @@ public partial class ChampionTitleContext } [SerializableField(7)] - [SaveFlag(nameof(ShouldSerializeSleepingDragon))] private ChampionTitle _sleepingDragon; + [SerializableFieldSaveFlag(7)] private bool ShouldSerializeSleepingDragon() => _sleepingDragon != null; [CommandProperty(AccessLevel.GameMaster)] @@ -191,9 +191,9 @@ public partial class ChampionTitleContext } [SerializableField(8)] - [SaveFlag(nameof(ShouldSerializeCorrupt))] private ChampionTitle _corrupt; + [SerializableFieldSaveFlag(8)] private bool ShouldSerializeCorrupt() => _corrupt != null; [CommandProperty(AccessLevel.GameMaster)] @@ -211,9 +211,9 @@ public partial class ChampionTitleContext } [SerializableField(9)] - [SaveFlag(nameof(ShouldSerializeGlade))] private ChampionTitle _glade; + [SerializableFieldSaveFlag(9)] private bool ShouldSerializeGlade() => _glade != null; [CommandProperty(AccessLevel.GameMaster)] diff --git a/Projects/UOContent/Engines/CannedEvil/StarRoomGate.cs b/Projects/UOContent/Engines/CannedEvil/StarRoomGate.cs index 88acd587e..a5426e7a5 100644 --- a/Projects/UOContent/Engines/CannedEvil/StarRoomGate.cs +++ b/Projects/UOContent/Engines/CannedEvil/StarRoomGate.cs @@ -3,21 +3,15 @@ using ModernUO.Serialization; namespace Server.Items; -[SerializationGenerator(2, false)] +[SerializationGenerator(1, false)] public partial class StarRoomGate : Moongate { - private void MigrateFrom(V1Content content) - { - _decays = content.Decays; - _decayTime = content.DecayTime; - } - private static TimeSpan GateDuration = TimeSpan.FromMinutes(2.0); [SerializableField(0)] private bool _decays; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(1)] private DateTime _decayTime; diff --git a/Projects/UOContent/Engines/ConPVP/Games/BombingRun.cs b/Projects/UOContent/Engines/ConPVP/Games/BombingRun.cs index 6ab4fffea..f32168cbf 100644 --- a/Projects/UOContent/Engines/ConPVP/Games/BombingRun.cs +++ b/Projects/UOContent/Engines/ConPVP/Games/BombingRun.cs @@ -830,9 +830,10 @@ public partial class BRBomb : Item [SerializationGenerator(0, false)] public partial class BRGoal : BaseAddon { - [SerializableField(0, fieldChanged: nameof(OnNorthChanged))] + [SerializableField(0)] private bool _north; + [SerializableFieldChanged(0)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private void OnNorthChanged(bool oldValue, bool newValue) => Remake(); diff --git a/Projects/UOContent/Engines/ConPVP/Games/KingOfTheHill.cs b/Projects/UOContent/Engines/ConPVP/Games/KingOfTheHill.cs index 6aec6f328..6f73a0949 100644 --- a/Projects/UOContent/Engines/ConPVP/Games/KingOfTheHill.cs +++ b/Projects/UOContent/Engines/ConPVP/Games/KingOfTheHill.cs @@ -252,9 +252,10 @@ public partial class HillOfTheKing : Item public partial class KHBoard : Item { [SerializedCommandProperty(AccessLevel.GameMaster)] - [SerializableField(0, fieldChanged: nameof(OnControllerChanged))] + [SerializableField(0)] private KHController _controller; + [SerializableFieldChanged(0)] private void OnControllerChanged(KHController oldValue, KHController newValue) { oldValue?.RemoveBoard(this); diff --git a/Projects/UOContent/Engines/ConPVP/Trophy.cs b/Projects/UOContent/Engines/ConPVP/Trophy.cs index 603b52ef3..fa712de33 100644 --- a/Projects/UOContent/Engines/ConPVP/Trophy.cs +++ b/Projects/UOContent/Engines/ConPVP/Trophy.cs @@ -64,13 +64,17 @@ public partial class Trophy : Item UpdateStyle(); } - [SerializableField(1, fieldChanged: nameof(OnRankChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private TrophyRank _rank; - - private void OnRankChanged(TrophyRank oldValue, TrophyRank newValue) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public TrophyRank Rank { - UpdateStyle(); + get => _rank; + set + { + _rank = value; + UpdateStyle(); + this.MarkDirty(); + } } private void Deserialize(IGenericReader reader, int version) diff --git a/Projects/UOContent/Engines/Ethics/Core/Player.cs b/Projects/UOContent/Engines/Ethics/Core/Player.cs index d8aad9eb3..f21fe9eb9 100644 --- a/Projects/UOContent/Engines/Ethics/Core/Player.cs +++ b/Projects/UOContent/Engines/Ethics/Core/Player.cs @@ -5,20 +5,9 @@ using Server.Mobiles; namespace Server.Ethics; [PropertyObject] -[SerializationGenerator(2)] +[SerializationGenerator(1)] public partial class Player : EthicsEntity { - private void MigrateFrom(V1Content content) - { - _mobile = content.Mobile; - _power = content.Power; - _history = content.History; - _steed = content.Steed; - _familiar = content.Familiar; - _shield = content.Shield; - _ethic = content.Ethic; - } - [SerializableField(0, setter: "private")] private Mobile _mobile; @@ -38,7 +27,7 @@ public partial class Player : EthicsEntity [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] private Mobile _familiar; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(5, setter: "private")] private DateTime _shield; diff --git a/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs b/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs index f0fb2386f..32e8566cb 100644 --- a/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs +++ b/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs @@ -359,14 +359,14 @@ namespace Server.Factions { if (m_Mobile.InRange( m, 1 )) RunFrom( m ); - else if (!m_Mobile.InRange( m, m_Mobile.RangeFight > 2 ? m_Mobile.RangeFight : 2 ) && !MoveTo(m, 1)) + else if (!m_Mobile.InRange( m, m_Mobile.RangeFight > 2 ? m_Mobile.RangeFight : 2 ) && !MoveTo( m, true, 1 )) OnFailedMove(); } else {*/ if (!Mobile.InRange(m, Mobile.RangeFight)) { - if (!MoveTo(m, 1)) + if (!MoveTo(m, true, 1)) { OnFailedMove(); } diff --git a/Projects/UOContent/Engines/Harvest/Core/HarvestSystem.cs b/Projects/UOContent/Engines/Harvest/Core/HarvestSystem.cs index b67665d04..b82040134 100644 --- a/Projects/UOContent/Engines/Harvest/Core/HarvestSystem.cs +++ b/Projects/UOContent/Engines/Harvest/Core/HarvestSystem.cs @@ -219,7 +219,7 @@ namespace Server.Engines.Harvest } else { - bonusItem?.Delete(); + item.Delete(); } } diff --git a/Projects/UOContent/Engines/Khaldun/PuzzleChest.cs b/Projects/UOContent/Engines/Khaldun/PuzzleChest.cs index 53932c8f0..4548522b4 100644 --- a/Projects/UOContent/Engines/Khaldun/PuzzleChest.cs +++ b/Projects/UOContent/Engines/Khaldun/PuzzleChest.cs @@ -162,15 +162,10 @@ namespace Server.Items } } - [SerializationGenerator(1)] + [SerializationGenerator(0)] public partial class PuzzleChestSolutionAndTime : PuzzleChestSolution { - private void MigrateFrom(V0Content content) - { - _when = content.When; - } - - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(0)] private DateTime _when; @@ -242,12 +237,16 @@ namespace Server.Items } } - [SerializableField(0, fieldChanged: nameof(OnSolutionChanged))] - private PuzzleChestSolution _solution; - - private void OnSolutionChanged(PuzzleChestSolution oldValue, PuzzleChestSolution newValue) + [SerializableProperty(0)] + public PuzzleChestSolution Solution { - InitHints(); + get => _solution; + set + { + _solution = value; + InitHints(); + this.MarkDirty(); + } } public PuzzleChestCylinder FirstHint diff --git a/Projects/UOContent/Engines/ML Quests/MLQuestPersistence.cs b/Projects/UOContent/Engines/ML Quests/MLQuestPersistence.cs index d3e39c09f..5d846d595 100644 --- a/Projects/UOContent/Engines/ML Quests/MLQuestPersistence.cs +++ b/Projects/UOContent/Engines/ML Quests/MLQuestPersistence.cs @@ -19,7 +19,7 @@ namespace Server.Engines.MLQuests { base.Serialize(writer); - writer.Write(3); // version + writer.Write(2); // version writer.Write(MLQuestSystem.Contexts.Count); foreach (var context in MLQuestSystem.Contexts.Values) diff --git a/Projects/UOContent/Engines/ML Quests/Objectives/BaseObjective.cs b/Projects/UOContent/Engines/ML Quests/Objectives/BaseObjective.cs index 77c3350f5..2838c7736 100644 --- a/Projects/UOContent/Engines/ML Quests/Objectives/BaseObjective.cs +++ b/Projects/UOContent/Engines/ML Quests/Objectives/BaseObjective.cs @@ -119,7 +119,7 @@ namespace Server.Engines.MLQuests.Objectives if (IsTimed) { writer.Write(true); - writer.WriteAnchoredTime(EndTime); + writer.WriteDeltaTime(EndTime); } else { @@ -135,7 +135,7 @@ namespace Server.Engines.MLQuests.Objectives { if (reader.ReadBool()) { - var endTime = version >= 3 ? reader.ReadAnchoredTime() : reader.ReadDeltaTime(); + var endTime = reader.ReadDeltaTime(); if (objInstance != null) { diff --git a/Projects/UOContent/Engines/Pathing/PathFollower.cs b/Projects/UOContent/Engines/Pathing/PathFollower.cs index 04aaf0148..3a0a56fa5 100644 --- a/Projects/UOContent/Engines/Pathing/PathFollower.cs +++ b/Projects/UOContent/Engines/Pathing/PathFollower.cs @@ -83,7 +83,7 @@ public class PathFollower public static bool Check(Point3D loc, Point3D goal, int range) => Utility.InRange(loc, goal, range) && (range > 1 || (loc.Z - goal.Z).Abs() < 16); - public bool Follow(int range) + public bool Follow(bool run, int range) { var goal = GetGoalLocation(); Direction d; @@ -97,13 +97,13 @@ public class PathFollower if (!(Enabled && m_Path.Success)) { - d = m_From.GetDirectionTo(goal); + d = m_From.GetDirectionTo(goal, run); m_From.SetDirection(d); return Move(d) is MoveResult.Success or MoveResult.SuccessAutoTurn && Check(m_From.Location, goal, range); } - d = m_From.GetDirectionTo(m_Next); + d = m_From.GetDirectionTo(m_Next, run); m_From.SetDirection(d); var res = Move(d); diff --git a/Projects/UOContent/Engines/Plants/PlantItem.cs b/Projects/UOContent/Engines/Plants/PlantItem.cs index 5f66b1a7a..60451f53b 100644 --- a/Projects/UOContent/Engines/Plants/PlantItem.cs +++ b/Projects/UOContent/Engines/Plants/PlantItem.cs @@ -37,17 +37,17 @@ public partial class PlantItem : Item, ISecurable [SerializedIgnoreDupe] [SerializableField(0)] - [SaveFlag(nameof(ShouldSerializeSecureLevel))] [SerializedCommandProperty(AccessLevel.GameMaster)] private SecureLevel _level; + [SerializableFieldSaveFlag(0)] private bool ShouldSerializeSecureLevel() => (int)_level != 0; [SerializedIgnoreDupe] [SerializableField(5, setter: "private")] - [SaveFlag(nameof(ShouldSerializePlantSystem))] private PlantSystem _plantSystem; + [SerializableFieldSaveFlag(5)] private bool ShouldSerializePlantSystem() => _plantStatus < PlantStatus.DecorativePlant; // For clients older than 7.0.12.0 @@ -82,7 +82,6 @@ public partial class PlantItem : Item, ISecurable [CommandProperty(AccessLevel.GameMaster)] [SerializableProperty(1)] - [SaveFlag(nameof(ShouldSerializePlantStatus))] public PlantStatus PlantStatus { get => _plantStatus; @@ -121,38 +120,53 @@ public partial class PlantItem : Item, ISecurable } } + [SerializableFieldSaveFlag(1)] private bool ShouldSerializePlantStatus() => _plantStatus != PlantStatus.BowlOfDirt; - [SerializableField(2, fieldChanged: nameof(OnPlantTypeChanged))] - [SaveFlag(nameof(ShouldSerializePlantType))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private PlantType _plantType; - - private void OnPlantTypeChanged(PlantType oldValue, PlantType newValue) + [SerializableProperty(2)] + [CommandProperty(AccessLevel.GameMaster)] + public PlantType PlantType { - Update(); + get => _plantType; + set + { + _plantType = value; + Update(); + } } + [SerializableFieldSaveFlag(2)] private bool ShouldSerializePlantType() => (int)_plantType != 0; - [SerializableField(3, fieldChanged: nameof(OnPlantHueChanged))] - [SaveFlag(nameof(ShouldSerializePlantHue))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private PlantHue _plantHue; - - private void OnPlantHueChanged(PlantHue oldValue, PlantHue newValue) + [SerializableProperty(3)] + [CommandProperty(AccessLevel.GameMaster)] + public PlantHue PlantHue { - Update(); + get => _plantHue; + set + { + _plantHue = value; + Update(); + } } + [SerializableFieldSaveFlag(3)] private bool ShouldSerializePlantHue() => _plantHue != PlantHue.None; - [SerializableField(4)] - [SaveFlag(nameof(ShouldSerializeShowType))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private bool _showType; + [SerializableProperty(4)] + [CommandProperty(AccessLevel.GameMaster)] + public bool ShowType + { + get => _showType; + set + { + _showType = value; + InvalidateProperties(); + this.MarkDirty(); + } + } + [SerializableFieldSaveFlag(4)] private bool ShouldSerializeShowType() => _showType; [CommandProperty(AccessLevel.GameMaster)] diff --git a/Projects/UOContent/Engines/Plants/PlantSystem.cs b/Projects/UOContent/Engines/Plants/PlantSystem.cs index 4b202ef63..29b9f48be 100644 --- a/Projects/UOContent/Engines/Plants/PlantSystem.cs +++ b/Projects/UOContent/Engines/Plants/PlantSystem.cs @@ -33,24 +33,24 @@ namespace Server.Engines.Plants private PlantItem _plant; [SerializableField(0)] - [SaveFlag(nameof(ShouldSerializeFertileDirt))] private bool _fertileDirt; + [SerializableFieldSaveFlag(0)] private bool ShouldSerializeFertileDirt() => _fertileDirt; [SerializableField(1)] private DateTime _nextGrowth; [SerializableField(2, setter: "private")] - [SaveFlag(nameof(ShouldSerializeGrowthIndicator))] private PlantGrowthIndicator _growthIndicator; + [SerializableFieldSaveFlag(2)] private bool ShouldSerializeGrowthIndicator() => _growthIndicator != PlantGrowthIndicator.None; [SerializableField(13)] - [SaveFlag(nameof(ShouldSerializePollinated))] private bool _pollinated; + [SerializableFieldSaveFlag(13)] private bool ShouldSerializePollinated() => _pollinated; public PlantSystem(PlantItem plant) @@ -97,42 +97,45 @@ namespace Server.Engines.Plants public bool IsFullWater => _water >= 4; - [SerializableField(3, fieldChanged: nameof(OnWaterChanged), allowFieldChange: nameof(AllowWaterChange))] - [SaveFlag(nameof(ShouldSerializeWater))] - private int _water; - - private bool AllowWaterChange(ref int value) + [SerializableProperty(3)] + public int Water { - value = Math.Clamp(value, 0, 4); - return true; - } - - private void OnWaterChanged(int oldValue, int newValue) - { - Plant.InvalidateProperties(); + get => _water; + set + { + _water = Math.Clamp(value, 0, 4); + Plant.InvalidateProperties(); + MarkDirty(); + } } + [SerializableFieldSaveFlag(3)] private bool ShouldSerializeWater() => _water != 0; - [SerializableField(4, fieldChanged: nameof(OnHitsChanged), allowFieldChange: nameof(AllowHitsChange))] - [SaveFlag(nameof(ShouldSerializeHits))] - private int _hits; - - private bool AllowHitsChange(ref int value) + [SerializableProperty(4)] + public int Hits { - value = Math.Clamp(value, 0, MaxHits); - return true; - } - - private void OnHitsChanged(int oldValue, int newValue) - { - if (_hits == 0) + get => _hits; + set { - Plant.Die(); + if (_hits == value) + { + return; + } + + _hits = Math.Clamp(value, 0, MaxHits); + + if (_hits == 0) + { + Plant.Die(); + } + + Plant.InvalidateProperties(); + MarkDirty(); } - Plant.InvalidateProperties(); } + [SerializableFieldSaveFlag(4)] private bool ShouldSerializeHits() => _hits != 0; public int MaxHits => 10 + (int)Plant.PlantStatus * 2; @@ -146,108 +149,124 @@ namespace Server.Engines.Plants _ => PlantHealth.Vibrant }; - [SerializableField(5, allowFieldChange: nameof(AllowInfestationChange))] - [SaveFlag(nameof(ShouldSerializeInfestation))] - private int _infestation; - - private bool AllowInfestationChange(ref int value) + [SerializableProperty(5)] + public int Infestation { - value = Math.Clamp(value, 0, 2); - return true; + get => _infestation; + set + { + _infestation = Math.Clamp(value, 0, 2); + MarkDirty(); + } } + [SerializableFieldSaveFlag(5)] private bool ShouldSerializeInfestation() => _infestation != 0; - [SerializableField(6, allowFieldChange: nameof(AllowFungusChange))] - [SaveFlag(nameof(ShouldSerializeFungus))] - private int _fungus; - - private bool AllowFungusChange(ref int value) + [SerializableProperty(6)] + public int Fungus { - value = Math.Clamp(value, 0, 2); - return true; + get => _fungus; + set + { + _fungus = Math.Clamp(value, 0, 2); + MarkDirty(); + } } + [SerializableFieldSaveFlag(6)] private bool ShouldSerializeFungus() => _fungus != 0; - [SerializableField(7, allowFieldChange: nameof(AllowPoisonChange))] - [SaveFlag(nameof(ShouldSerializePoison))] - private int _poison; - - private bool AllowPoisonChange(ref int value) + [SerializableProperty(7)] + public int Poison { - value = Math.Clamp(value, 0, 2); - return true; + get => _poison; + set + { + _poison = Math.Clamp(value, 0, 2); + MarkDirty(); + } } + [SerializableFieldSaveFlag(7)] private bool ShouldSerializePoison() => _poison != 0; - [SerializableField(8, allowFieldChange: nameof(AllowDiseaseChange))] - [SaveFlag(nameof(ShouldSerializeDisease))] - private int _disease; - - private bool AllowDiseaseChange(ref int value) + [SerializableProperty(8)] + public int Disease { - value = Math.Clamp(value, 0, 2); - return true; + get => _disease; + set + { + _disease = Math.Clamp(value, 0, 2); + MarkDirty(); + } } + [SerializableFieldSaveFlag(8)] private bool ShouldSerializeDisease() => _disease != 0; public bool IsFullPoisonPotion => _poisonPotion >= 2; - [SerializableField(9, allowFieldChange: nameof(AllowPoisonPotionChange))] - [SaveFlag(nameof(ShouldSerializePoisonPotion))] - private int _poisonPotion; - - private bool AllowPoisonPotionChange(ref int value) + [SerializableProperty(9)] + public int PoisonPotion { - value = Math.Clamp(value, 0, 2); - return true; + get => _poisonPotion; + set + { + _poisonPotion = Math.Clamp(value, 0, 2); + MarkDirty(); + } } + [SerializableFieldSaveFlag(9)] private bool ShouldSerializePoisonPotion() => _poisonPotion != 0; public bool IsFullCurePotion => _curePotion >= 2; - [SerializableField(10, allowFieldChange: nameof(AllowCurePotionChange))] - [SaveFlag(nameof(ShouldSerializeCurePotion))] - private int _curePotion; - - private bool AllowCurePotionChange(ref int value) + [SerializableProperty(10)] + public int CurePotion { - value = Math.Clamp(value, 0, 2); - return true; + get => _curePotion; + set + { + _curePotion = Math.Clamp(value, 0, 2); + MarkDirty(); + } } + [SerializableFieldSaveFlag(10)] private bool ShouldSerializeCurePotion() => _curePotion != 0; public bool IsFullHealPotion => _healPotion >= 2; - [SerializableField(11, allowFieldChange: nameof(AllowHealPotionChange))] - [SaveFlag(nameof(ShouldSerializeHealPotion))] - private int _healPotion; - - private bool AllowHealPotionChange(ref int value) + [SerializableProperty(11)] + public int HealPotion { - value = Math.Clamp(value, 0, 2); - return true; + get => _healPotion; + set + { + _healPotion = Math.Clamp(value, 0, 2); + MarkDirty(); + } } + [SerializableFieldSaveFlag(11)] private bool ShouldSerializeHealPotion() => _healPotion != 0; public bool IsFullStrengthPotion => _strengthPotion >= 2; - [SerializableField(12, allowFieldChange: nameof(AllowStrengthPotionChange))] - [SaveFlag(nameof(ShouldSerializeStrengthPotion))] - private int _strengthPotion; - - private bool AllowStrengthPotionChange(ref int value) + [SerializableProperty(12)] + public int StrengthPotion { - value = Math.Clamp(value, 0, 2); - return true; + get => _strengthPotion; + set + { + _strengthPotion = Math.Clamp(value, 0, 2); + MarkDirty(); + } } + [SerializableFieldSaveFlag(12)] private bool ShouldSerializeStrengthPotion() => _strengthPotion != 0; public bool HasMaladies => Infestation > 0 || Fungus > 0 || Poison > 0 || Disease > 0 || Water != 2; @@ -255,7 +274,6 @@ namespace Server.Engines.Plants public bool PollenProducing => Plant.IsCrossable && Plant.PlantStatus >= PlantStatus.FullGrownPlant; [SerializableProperty(14)] - [SaveFlag(nameof(ShouldSerializeSeedType))] public PlantType SeedType { get => Pollinated ? _seedType : Plant.PlantType; @@ -266,10 +284,10 @@ namespace Server.Engines.Plants } } + [SerializableFieldSaveFlag(14)] private bool ShouldSerializeSeedType() => _pollinated; [SerializableProperty(15)] - [SaveFlag(nameof(ShouldSerializeSeedHue))] public PlantHue SeedHue { get => Pollinated ? _seedHue : Plant.PlantHue; @@ -280,58 +298,53 @@ namespace Server.Engines.Plants } } + [SerializableFieldSaveFlag(15)] private bool ShouldSerializeSeedHue() => _pollinated; - [SerializableField(16, allowFieldChange: nameof(AllowAvailableSeedsChange))] - [SaveFlag(nameof(ShouldSerializeAvailableSeeds))] - private int _availableSeeds; - - private bool AllowAvailableSeedsChange(ref int value) + [SerializableProperty(16)] + public int AvailableSeeds { - value = Math.Max(value, 0); - return true; + get => _availableSeeds; + set => _availableSeeds = Math.Max(value, 0); } + [SerializableFieldSaveFlag(16)] private bool ShouldSerializeAvailableSeeds() => _availableSeeds != 0; - [SerializableField(17, allowFieldChange: nameof(AllowLeftSeedsChange))] - [SaveFlag(nameof(ShouldSerializeLeftSeeds), nameof(LeftSeedsDefaultValue))] - private int _leftSeeds; - - private bool AllowLeftSeedsChange(ref int value) + [SerializableProperty(17)] + public int LeftSeeds { - value = Math.Max(value, 0); - return true; + get => _leftSeeds; + set => _leftSeeds = Math.Max(value, 0); } + [SerializableFieldSaveFlag(17)] private bool ShouldSerializeLeftSeeds() => _leftSeeds != 8; + [SerializableFieldDefault(17)] private int LeftSeedsDefaultValue() => 8; - [SerializableField(18, allowFieldChange: nameof(AllowAvailableResourcesChange))] - [SaveFlag(nameof(ShouldSerializeAvailableResources))] - private int _availableResources; - - private bool AllowAvailableResourcesChange(ref int value) + [SerializableProperty(18)] + public int AvailableResources { - value = Math.Max(value, 0); - return true; + get => _availableResources; + set => _availableResources = Math.Max(value, 0); } + [SerializableFieldSaveFlag(18)] private bool ShouldSerializeAvailableResources() => _availableResources != 0; - [SerializableField(19, allowFieldChange: nameof(AllowLeftResourcesChange))] - [SaveFlag(nameof(ShouldSerializeLeftResources), nameof(LeftResourcesDefaultValue))] - private int _leftResources; - - private bool AllowLeftResourcesChange(ref int value) + [SerializableProperty(19)] + public int LeftResources { - value = Math.Max(value, 0); - return true; + get => _leftResources; + set => _leftResources = Math.Max(value, 0); } + [SerializableFieldSaveFlag(19)] private bool ShouldSerializeLeftResources() => _leftResources != 8; + [SerializableFieldDefault(19)] private int LeftResourcesDefaultValue() => 8; public void Reset(bool potions) diff --git a/Projects/UOContent/Engines/Plants/Seed.cs b/Projects/UOContent/Engines/Plants/Seed.cs index ed6f13b7b..24a290f0b 100644 --- a/Projects/UOContent/Engines/Plants/Seed.cs +++ b/Projects/UOContent/Engines/Plants/Seed.cs @@ -35,14 +35,18 @@ public partial class Seed : Item public override double DefaultWeight => 1.0; - [SerializableField(1, fieldChanged: nameof(OnPlantHueChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private PlantHue _plantHue; - - private void OnPlantHueChanged(PlantHue oldValue, PlantHue newValue) + [CommandProperty(AccessLevel.GameMaster)] + [SerializableProperty(1)] + public PlantHue PlantHue { - Hue = PlantHueInfo.GetInfo(newValue).Hue; + get => _plantHue; + set + { + _plantHue = value; + Hue = PlantHueInfo.GetInfo(value).Hue; + InvalidateProperties(); + this.MarkDirty(); + } } public override int LabelNumber => 1060810; // seed diff --git a/Projects/UOContent/Engines/Player Murder System/MurderContext.cs b/Projects/UOContent/Engines/Player Murder System/MurderContext.cs index 384c91c98..cecd7bf7e 100644 --- a/Projects/UOContent/Engines/Player Murder System/MurderContext.cs +++ b/Projects/UOContent/Engines/Player Murder System/MurderContext.cs @@ -16,14 +16,12 @@ public partial class MurderContext [SerializedCommandProperty(AccessLevel.GameMaster)] private TimeSpan _longTermElapse; - [SerializableField(2, allowFieldChange: nameof(AllowShortTermMurdersChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private int _shortTermMurders; - - private bool AllowShortTermMurdersChange(ref int value) + [SerializableProperty(2)] + [CommandProperty(AccessLevel.GameMaster)] + public int ShortTermMurders { - value = Math.Max(value, 0); - return true; + get => _shortTermMurders; + set => _shortTermMurders = Math.Max(value, 0); } [SerializableField(3)] diff --git a/Projects/UOContent/Engines/Quests/The Summoning/Items/SummoningAltar.cs b/Projects/UOContent/Engines/Quests/The Summoning/Items/SummoningAltar.cs index 4e3e5e237..d5ec6f24a 100644 --- a/Projects/UOContent/Engines/Quests/The Summoning/Items/SummoningAltar.cs +++ b/Projects/UOContent/Engines/Quests/The Summoning/Items/SummoningAltar.cs @@ -12,12 +12,16 @@ public partial class SummoningAltar : AbbatoirAddon { } - [SerializableField(0, fieldChanged: nameof(OnDaemonChanged))] - private BoneDemon _daemon; - - private void OnDaemonChanged(BoneDemon oldValue, BoneDemon newValue) + [SerializableProperty(0)] + public BoneDemon Daemon { - CheckDaemon(); + get => _daemon; + set + { + _daemon = value; + CheckDaemon(); + this.MarkDirty(); + } } public void CheckDaemon() diff --git a/Projects/UOContent/Engines/Spawners/BaseSpawner.cs b/Projects/UOContent/Engines/Spawners/BaseSpawner.cs index 8efc54b8c..7ceeb00f6 100644 --- a/Projects/UOContent/Engines/Spawners/BaseSpawner.cs +++ b/Projects/UOContent/Engines/Spawners/BaseSpawner.cs @@ -54,10 +54,10 @@ public abstract partial class BaseSpawner : Item, ISpawner [SerializedCommandProperty(AccessLevel.Developer)] private Guid _guid; + [SerializableFieldSaveFlag(1)] private bool ShouldSerializeReturnOnDeactivate() => _returnOnDeactivate; [SerializableField(1)] - [SaveFlag(nameof(ShouldSerializeReturnOnDeactivate))] [SerializedCommandProperty(AccessLevel.Developer)] private bool _returnOnDeactivate; @@ -67,46 +67,48 @@ public abstract partial class BaseSpawner : Item, ISpawner private int _walkingRange = -1; + [SerializableFieldSaveFlag(4)] private bool ShouldSerializeWayPoint() => _wayPoint != null; [SerializableField(4)] - [SaveFlag(nameof(ShouldSerializeWayPoint))] [SerializedCommandProperty(AccessLevel.Developer)] private WayPoint _wayPoint; + [SerializableFieldSaveFlag(5)] private bool ShouldSerializeGroup() => _group; [InvalidateProperties] [SerializableField(5)] - [SaveFlag(nameof(ShouldSerializeGroup))] [SerializedCommandProperty(AccessLevel.Developer)] private bool _group; + [SerializableFieldSaveFlag(6)] private bool ShouldSerializeMinDelay() => _minDelay != DefaultMinDelay; + [SerializableFieldDefault(6)] private TimeSpan MinDelayDefault() => DefaultMinDelay; [InvalidateProperties] [SerializableField(6)] - [SaveFlag(nameof(ShouldSerializeMinDelay), nameof(MinDelayDefault))] [SerializedCommandProperty(AccessLevel.Developer)] private TimeSpan _minDelay; + [SerializableFieldSaveFlag(7)] private bool ShouldSerializeMaxDelay() => _maxDelay != DefaultMaxDelay; + [SerializableFieldDefault(7)] private TimeSpan MaxDelayDefault() => DefaultMaxDelay; [InvalidateProperties] [SerializableField(7)] - [SaveFlag(nameof(ShouldSerializeMaxDelay), nameof(MaxDelayDefault))] [SerializedCommandProperty(AccessLevel.Developer)] private TimeSpan _maxDelay; + [SerializableFieldSaveFlag(9)] private bool ShouldSerializeTeam() => _team != 0; [InvalidateProperties] [SerializableField(9)] - [SaveFlag(nameof(ShouldSerializeTeam))] [SerializedCommandProperty(AccessLevel.Developer)] private int _team; @@ -123,29 +125,29 @@ public abstract partial class BaseSpawner : Item, ISpawner /// If true, the home location of the spawn is the location where it spawned /// If false, the home location of the spawn is the location of the spawner /// + [SerializableFieldSaveFlag(11)] private bool ShouldSerializeSpawnLocationIsHome() => _spawnLocationIsHome; [InvalidateProperties] [SerializableField(11)] - [SaveFlag(nameof(ShouldSerializeSpawnLocationIsHome))] [SerializedCommandProperty(AccessLevel.Developer)] private bool _spawnLocationIsHome; + [SerializableFieldSaveFlag(12)] private bool ShouldSerializeEnd() => _end != default; [SerializableField(12)] - [SaveFlag(nameof(ShouldSerializeEnd))] [SerializedCommandProperty(AccessLevel.Developer)] private DateTime _end; /// /// Controls how spawn position optimization is handled. /// + [SerializableFieldSaveFlag(13)] private bool ShouldSerializeSpawnPositionMode() => _spawnPositionMode is not SpawnPositionMode.Automatic and not SpawnPositionMode.Abandoned; [SerializableField(13)] - [SaveFlag(nameof(ShouldSerializeSpawnPositionMode))] [SerializedCommandProperty(AccessLevel.Developer)] private SpawnPositionMode _spawnPositionMode; @@ -154,12 +156,13 @@ public abstract partial class BaseSpawner : Item, ISpawner /// /// Maximum number of random position attempts before engaging optimization. /// + [SerializableFieldSaveFlag(14)] private bool ShouldSerializeMaxSpawnAttempts() => _maxSpawnAttempts != DefaultMaxSpawnAttempts; + [SerializableFieldDefault(14)] private int MaxSpawnAttemptsDefault() => DefaultMaxSpawnAttempts; [SerializableField(14)] - [SaveFlag(nameof(ShouldSerializeMaxSpawnAttempts), nameof(MaxSpawnAttemptsDefault))] [SerializedCommandProperty(AccessLevel.Developer)] private int _maxSpawnAttempts; @@ -311,20 +314,26 @@ public abstract partial class BaseSpawner : Item, ISpawner } } - [SerializableField(8, fieldChanged: nameof(OnCountChanged))] - [SerializedCommandProperty(AccessLevel.Developer)] - [InvalidateProperties] - private int _count; - - private void OnCountChanged(int oldValue, int newValue) + [SerializableProperty(8)] + [CommandProperty(AccessLevel.Developer)] + public int Count { - if (IsFull) + get => _count; + set { - _timer?.Stop(); - } - else if (_timer?.Running != true) - { - DoTimer(); + _count = value; + + if (IsFull) + { + _timer?.Stop(); + } + else if (_timer?.Running != true) + { + DoTimer(); + } + + InvalidateProperties(); + this.MarkDirty(); } } diff --git a/Projects/UOContent/Engines/Spawners/Spawner.cs b/Projects/UOContent/Engines/Spawners/Spawner.cs index db25cab55..7c07d2681 100644 --- a/Projects/UOContent/Engines/Spawners/Spawner.cs +++ b/Projects/UOContent/Engines/Spawners/Spawner.cs @@ -10,17 +10,17 @@ public partial class Spawner : BaseSpawner /// When true, enables proactive spiral scanning to find valid spawn positions. /// Only relevant when SpawnPositionMode is Automatic or Enabled. /// + [SerializableFieldSaveFlag(0)] private bool ShouldSerializeUseSpiralScan() => _useSpiralScan; [SerializableField(0)] - [SaveFlag(nameof(ShouldSerializeUseSpiralScan))] [SerializedCommandProperty(AccessLevel.Developer)] private bool _useSpiralScan; + [SerializableFieldSaveFlag(1)] private bool ShouldSerializeSpawnBounds() => _spawnBounds != default; [SerializableProperty(1)] - [SaveFlag(nameof(ShouldSerializeSpawnBounds))] [CommandProperty(AccessLevel.Developer)] public override Rectangle3D SpawnBounds { diff --git a/Projects/UOContent/Engines/Stealables/StealableArtifacts.cs b/Projects/UOContent/Engines/Stealables/StealableArtifacts.cs index 4cd8d8c63..396b87dcd 100644 --- a/Projects/UOContent/Engines/Stealables/StealableArtifacts.cs +++ b/Projects/UOContent/Engines/Stealables/StealableArtifacts.cs @@ -248,7 +248,7 @@ public class StealableArtifacts : GenericPersistence public override void Serialize(IGenericWriter writer) { - writer.WriteEncodedInt(2); // version + writer.WriteEncodedInt(1); // version writer.Write(_enabled); @@ -261,7 +261,7 @@ public class StealableArtifacts : GenericPersistence var si = _artifacts[i]; writer.Write(si.Item); - writer.WriteAnchoredTime(si.NextRespawn); + writer.WriteDeltaTime(si.NextRespawn); } } } @@ -282,7 +282,7 @@ public class StealableArtifacts : GenericPersistence for (var i = 0; i < length; i++) { var item = reader.ReadEntity(); - var nextRespawn = version >= 2 ? reader.ReadAnchoredTime() : reader.ReadDeltaTime(); + var nextRespawn = reader.ReadDeltaTime(); if (i < _artifacts.Length) { diff --git a/Projects/UOContent/Engines/Treasures of Tokuno/GreaterArtifacts.cs b/Projects/UOContent/Engines/Treasures of Tokuno/GreaterArtifacts.cs index c42cfae4e..005688e1e 100644 --- a/Projects/UOContent/Engines/Treasures of Tokuno/GreaterArtifacts.cs +++ b/Projects/UOContent/Engines/Treasures of Tokuno/GreaterArtifacts.cs @@ -332,22 +332,27 @@ public partial class PigmentsOfTokuno : BasePigmentsOfTokuno [Constructible] public PigmentsOfTokuno(PigmentType type, int uses) : base(uses) => Type = type; - [SerializableField(0, fieldChanged: nameof(OnTypeChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private PigmentType _type; - - private void OnTypeChanged(PigmentType oldValue, PigmentType newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public PigmentType Type { - var v = (int)_type; - if (v >= 0 && v < _table.Length) + get => _type; + set { - Hue = _table[v][0]; - Label = _table[v][1]; - } - else - { - Hue = 0; - Label = -1; + _type = value; + + var v = (int)_type; + + if (v >= 0 && v < _table.Length) + { + Hue = _table[v][0]; + Label = _table[v][1]; + } + else + { + Hue = 0; + Label = -1; + } } } diff --git a/Projects/UOContent/Engines/Treasures of Tokuno/LesserArtifacts.cs b/Projects/UOContent/Engines/Treasures of Tokuno/LesserArtifacts.cs index 22c6af344..54d026df4 100644 --- a/Projects/UOContent/Engines/Treasures of Tokuno/LesserArtifacts.cs +++ b/Projects/UOContent/Engines/Treasures of Tokuno/LesserArtifacts.cs @@ -617,22 +617,27 @@ public partial class LesserPigmentsOfTokuno : BasePigmentsOfTokuno [Constructible] public LesserPigmentsOfTokuno(LesserPigmentType type) : base(1) => Type = type; - [SerializableField(0, fieldChanged: nameof(OnTypeChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private LesserPigmentType _type; - - private void OnTypeChanged(LesserPigmentType oldValue, LesserPigmentType newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public LesserPigmentType Type { - var v = (int)_type; - if (v >= 0 && v < _table.Length) + get => _type; + set { - Hue = _table[v][0]; - Label = _table[v][1]; - } - else - { - Hue = 0; - Label = -1; + _type = value; + + var v = (int)_type; + + if (v >= 0 && v < _table.Length) + { + Hue = _table[v][0]; + Label = _table[v][1]; + } + else + { + Hue = 0; + Label = -1; + } } } diff --git a/Projects/UOContent/Engines/Veteran Rewards/Character Statue Maker/CharacterStatue.cs b/Projects/UOContent/Engines/Veteran Rewards/Character Statue Maker/CharacterStatue.cs index 7a62cc735..ff665ec53 100644 --- a/Projects/UOContent/Engines/Veteran Rewards/Character Statue Maker/CharacterStatue.cs +++ b/Projects/UOContent/Engines/Veteran Rewards/Character Statue Maker/CharacterStatue.cs @@ -79,33 +79,45 @@ public partial class CharacterStatue : Mobile, IRewardItem InvalidateHues(); } - [SerializableField(0, fieldChanged: nameof(OnStatueTypeChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private StatueType _statueType; - - private void OnStatueTypeChanged(StatueType oldValue, StatueType newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public StatueType StatueType { - InvalidateHues(); - InvalidatePose(); + get => _statueType; + set + { + _statueType = value; + InvalidateHues(); + InvalidatePose(); + this.MarkDirty(); + } } - [SerializableField(1, fieldChanged: nameof(OnPoseChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private StatuePose _pose; - - private void OnPoseChanged(StatuePose oldValue, StatuePose newValue) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public StatuePose Pose { - InvalidatePose(); + get => _pose; + set + { + _pose = value; + InvalidatePose(); + this.MarkDirty(); + } } - [SerializableField(2, fieldChanged: nameof(OnMaterialChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private StatueMaterial _material; - - private void OnMaterialChanged(StatueMaterial oldValue, StatueMaterial newValue) + [SerializableProperty(2)] + [CommandProperty(AccessLevel.GameMaster)] + public StatueMaterial Material { - InvalidateHues(); - InvalidatePose(); + get => _material; + set + { + _material = value; + InvalidateHues(); + InvalidatePose(); + this.MarkDirty(); + } } public override void OnDoubleClick(Mobile from) diff --git a/Projects/UOContent/Engines/Veteran Rewards/Character Statue Maker/CharacterStatueMaker.cs b/Projects/UOContent/Engines/Veteran Rewards/Character Statue Maker/CharacterStatueMaker.cs index 48ae30ae9..a028b1c85 100644 --- a/Projects/UOContent/Engines/Veteran Rewards/Character Statue Maker/CharacterStatueMaker.cs +++ b/Projects/UOContent/Engines/Veteran Rewards/Character Statue Maker/CharacterStatueMaker.cs @@ -25,13 +25,17 @@ public partial class CharacterStatueMaker : Item, IRewardItem public override int LabelNumber => 1076173; // Character Statue Maker - [SerializableField(1, fieldChanged: nameof(OnStatueTypeChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private StatueType _statueType; - - private void OnStatueTypeChanged(StatueType oldValue, StatueType newValue) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public StatueType StatueType { - InvalidateHue(); + get => _statueType; + set + { + _statueType = value; + InvalidateHue(); + this.MarkDirty(); + } } public override void OnDoubleClick(Mobile from) diff --git a/Projects/UOContent/Engines/Virtues/VirtueContext.cs b/Projects/UOContent/Engines/Virtues/VirtueContext.cs index 7524f77ed..3b26b5294 100644 --- a/Projects/UOContent/Engines/Virtues/VirtueContext.cs +++ b/Projects/UOContent/Engines/Virtues/VirtueContext.cs @@ -5,121 +5,102 @@ using Server.Mobiles; namespace Server.Engines.Virtues; [PropertyObject] -[SerializationGenerator(1)] +[SerializationGenerator(0)] public partial class VirtueContext { - private void MigrateFrom(V0Content content) - { - // Save-flagged values arrive as nullables; unset flags fall back to the same - // defaults the old deserialize left in place. - _lastSacrificeGain = content.LastSacrificeGain ?? default; - _lastSacrificeLoss = content.LastSacrificeLoss ?? default; - _availableResurrects = content.AvailableResurrects ?? 0; - _lastJusticeLoss = content.LastJusticeLoss ?? default; - _lastCompassionLoss = content.LastCompassionLoss ?? default; - _nextCompassionDay = content.NextCompassionDay ?? default; - _compassionGains = content.CompassionGains ?? 0; - _lastValorLoss = content.LastValorLoss ?? default; - _lastHonorUse = content.LastHonorUse ?? default; - _honorActive = content.HonorActive; - _justiceProtection = content.JusticeProtection; - _justiceStatus = content.JusticeStatus ?? JusticeProtectorStatus.None; - _values = content.Values; - } - - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(0)] - [SaveFlag(nameof(ShouldSerializeLastSacrificeGain))] [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] private DateTime _lastSacrificeGain; + [SerializableFieldSaveFlag(0)] private bool ShouldSerializeLastSacrificeGain() => !SacrificeVirtue.CanGain(this); - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(1)] - [SaveFlag(nameof(ShouldSerializeLastSacrificeLoss))] [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] private DateTime _lastSacrificeLoss; + [SerializableFieldSaveFlag(1)] private bool ShouldSerializeLastSacrificeLoss() => !SacrificeVirtue.CanAtrophy(this); [SerializableField(2)] - [SaveFlag(nameof(ShouldSerializeAvailableResurrects))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _availableResurrects; + [SerializableFieldSaveFlag(2)] private bool ShouldSerializeAvailableResurrects() => _availableResurrects > 0; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(3)] - [SaveFlag(nameof(ShouldSerializeLastJusticeLoss))] [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] private DateTime _lastJusticeLoss; + [SerializableFieldSaveFlag(3)] private bool ShouldSerializeLastJusticeLoss() => !JusticeVirtue.CanAtrophy(this); - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(4)] - [SaveFlag(nameof(ShouldSerializeLastCompassionLoss))] [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] private DateTime _lastCompassionLoss; + [SerializableFieldSaveFlag(4)] private bool ShouldSerializeLastCompassionLoss() => !CompassionVirtue.CanAtrophy(this); - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(5)] - [SaveFlag(nameof(ShouldSerializeNextCompassionDay))] [SerializedCommandProperty(AccessLevel.GameMaster)] private DateTime _nextCompassionDay; + [SerializableFieldSaveFlag(5)] private bool ShouldSerializeNextCompassionDay() => _nextCompassionDay > Core.Now; [SerializableField(6)] - [SaveFlag(nameof(ShouldSerializeCompassionGains))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _compassionGains; + [SerializableFieldSaveFlag(6)] private bool ShouldSerializeCompassionGains() => _compassionGains > 0; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(7)] - [SaveFlag(nameof(ShouldSerializeValorLoss))] [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] private DateTime _lastValorLoss; + [SerializableFieldSaveFlag(7)] private bool ShouldSerializeValorLoss() => !ValorVirtue.CanAtrophy(this); - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(8)] - [SaveFlag(nameof(ShouldSerializeLastHonorUse))] [SerializedCommandProperty(AccessLevel.GameMaster)] private DateTime _lastHonorUse; + [SerializableFieldSaveFlag(8)] private bool ShouldSerializeLastHonorUse() => !HonorVirtue.CanUse(this); [SerializableField(9)] - [SaveFlag(nameof(ShouldSerializeHonorActive))] [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] private bool _honorActive; + [SerializableFieldSaveFlag(9)] private bool ShouldSerializeHonorActive() => _honorActive; [SerializableField(10)] - [SaveFlag(nameof(ShouldSerializeJusticeProtection))] private PlayerMobile _justiceProtection; + [SerializableFieldSaveFlag(10)] private bool ShouldSerializeJusticeProtection() => _justiceProtection != null && _justiceStatus != JusticeProtectorStatus.None; [SerializableField(11)] - [SaveFlag(nameof(ShouldSerializeJusticeStatus))] private JusticeProtectorStatus _justiceStatus; + [SerializableFieldSaveFlag(11)] private bool ShouldSerializeJusticeStatus() => _justiceProtection != null && _justiceStatus != JusticeProtectorStatus.None; [SerializableField(12, setter: "private")] - [SaveFlag(nameof(ShouldSerializeValues))] private int[] _values; + [SerializableFieldSaveFlag(12)] private bool ShouldSerializeValues() { if (_values == null) diff --git a/Projects/UOContent/Items/Addons/BaseAddon.cs b/Projects/UOContent/Items/Addons/BaseAddon.cs index f2a662f6a..6b2abbc66 100644 --- a/Projects/UOContent/Items/Addons/BaseAddon.cs +++ b/Projects/UOContent/Items/Addons/BaseAddon.cs @@ -63,14 +63,22 @@ namespace Server.Items } } - [SerializableField(1, fieldChanged: nameof(OnResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(_resource); + get => _resource; + set + { + if (_resource != value) + { + _resource = value; + Hue = CraftResources.GetHue(_resource); + + InvalidateProperties(); + this.MarkDirty(); + } + } } Item IAddon.Deed => Deed; diff --git a/Projects/UOContent/Items/Addons/BaseAddonContainer.cs b/Projects/UOContent/Items/Addons/BaseAddonContainer.cs index 8554ad4e5..ad655c24e 100644 --- a/Projects/UOContent/Items/Addons/BaseAddonContainer.cs +++ b/Projects/UOContent/Items/Addons/BaseAddonContainer.cs @@ -41,14 +41,22 @@ namespace Server.Items } } - [SerializableField(1, fieldChanged: nameof(OnResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(_resource); + get => _resource; + set + { + if (_resource != value) + { + _resource = value; + Hue = CraftResources.GetHue(_resource); + + InvalidateProperties(); + this.MarkDirty(); + } + } } public virtual bool RetainDeedHue => false; diff --git a/Projects/UOContent/Items/Addons/BaseAddonContainerDeed.cs b/Projects/UOContent/Items/Addons/BaseAddonContainerDeed.cs index 7d5014dee..c7e08e955 100644 --- a/Projects/UOContent/Items/Addons/BaseAddonContainerDeed.cs +++ b/Projects/UOContent/Items/Addons/BaseAddonContainerDeed.cs @@ -23,14 +23,22 @@ public abstract partial class BaseAddonContainerDeed : Item, ICraftable public abstract BaseAddonContainer Addon { get; } - [SerializableField(0, fieldChanged: nameof(OnResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(_resource); + get => _resource; + set + { + if (_resource != value) + { + _resource = value; + Hue = CraftResources.GetHue(_resource); + + InvalidateProperties(); + this.MarkDirty(); + } + } } public virtual int OnCraft( diff --git a/Projects/UOContent/Items/Addons/FlourMillEastAddon.cs b/Projects/UOContent/Items/Addons/FlourMillEastAddon.cs index decf3ca65..f84c959cf 100644 --- a/Projects/UOContent/Items/Addons/FlourMillEastAddon.cs +++ b/Projects/UOContent/Items/Addons/FlourMillEastAddon.cs @@ -48,19 +48,17 @@ namespace Server.Items [CommandProperty(AccessLevel.GameMaster)] public int MaxFlour => 2; - [SerializableField(0, fieldChanged: nameof(OnCurFlourChanged), allowFieldChange: nameof(AllowCurFlourChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private int _curFlour; - - private bool AllowCurFlourChange(ref int value) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int CurFlour { - value = Math.Clamp(value, 0, MaxFlour); - return true; - } - - private void OnCurFlourChanged(int oldValue, int newValue) - { - UpdateStage(); + get => _curFlour; + set + { + _curFlour = Math.Clamp(value, 0, MaxFlour); + UpdateStage(); + this.MarkDirty(); + } } public void StartWorking(Mobile from) diff --git a/Projects/UOContent/Items/Addons/FlourMillSouthAddon.cs b/Projects/UOContent/Items/Addons/FlourMillSouthAddon.cs index fa48aa83c..cbd0e2cc3 100644 --- a/Projects/UOContent/Items/Addons/FlourMillSouthAddon.cs +++ b/Projects/UOContent/Items/Addons/FlourMillSouthAddon.cs @@ -35,19 +35,16 @@ namespace Server.Items [CommandProperty(AccessLevel.GameMaster)] public int MaxFlour => 2; - [SerializableField(0, fieldChanged: nameof(OnCurFlourChanged), allowFieldChange: nameof(AllowCurFlourChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private int _curFlour; - - private bool AllowCurFlourChange(ref int value) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int CurFlour { - value = Math.Max(0, Math.Min(value, MaxFlour)); - return true; - } - - private void OnCurFlourChanged(int oldValue, int newValue) - { - UpdateStage(); + get => _curFlour; + set + { + _curFlour = Math.Max(0, Math.Min(value, MaxFlour)); + UpdateStage(); + } } public void StartWorking(Mobile from) diff --git a/Projects/UOContent/Items/Addons/SHTeleporter.cs b/Projects/UOContent/Items/Addons/SHTeleporter.cs index e6ce612b4..f34f9cb85 100644 --- a/Projects/UOContent/Items/Addons/SHTeleporter.cs +++ b/Projects/UOContent/Items/Addons/SHTeleporter.cs @@ -25,27 +25,35 @@ namespace Server.Items _teleOffset = offset; } - [SerializableField(0, fieldChanged: nameof(OnActiveChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private bool _active; - - private void OnActiveChanged(bool oldValue, bool newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public bool Active { - if (Addon is SHTeleporter sourceAddon) + get => _active; + set { - sourceAddon.ChangeActive(newValue); + _active = value; + + if (Addon is SHTeleporter sourceAddon) + { + sourceAddon.ChangeActive(value); + } } } - [SerializableField(1, fieldChanged: nameof(OnTeleDestChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private SHTeleComponent _teleDest; - - private void OnTeleDestChanged(SHTeleComponent oldValue, SHTeleComponent newValue) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public SHTeleComponent TeleDest { - if (Addon is SHTeleporter sourceAddon) + get => _teleDest; + set { - sourceAddon.ChangeDest(newValue); + _teleDest = value; + + if (Addon is SHTeleporter sourceAddon) + { + sourceAddon.ChangeDest(value); + } } } diff --git a/Projects/UOContent/Items/Aquarium/Aquarium.cs b/Projects/UOContent/Items/Aquarium/Aquarium.cs index 054fea588..749c677c2 100644 --- a/Projects/UOContent/Items/Aquarium/Aquarium.cs +++ b/Projects/UOContent/Items/Aquarium/Aquarium.cs @@ -31,9 +31,9 @@ namespace Server.Items private bool m_EvaluateDay; [SerializableField(0, setter: "private")] - [DeserializeTimer(nameof(DeserializeEvaluateTimer), wallClock: true)] private Timer _evaluateTimer; + [DeserializeTimerField(0)] private void DeserializeEvaluateTimer(TimeSpan delay) { _evaluateTimer = Timer.DelayCall(delay, EvaluationInterval, Evaluate); diff --git a/Projects/UOContent/Items/Aquarium/AquariumState.cs b/Projects/UOContent/Items/Aquarium/AquariumState.cs index 75fe6fc6f..cdd59d9d9 100644 --- a/Projects/UOContent/Items/Aquarium/AquariumState.cs +++ b/Projects/UOContent/Items/Aquarium/AquariumState.cs @@ -30,14 +30,19 @@ namespace Server.Items public AquariumState(Aquarium parent) => _aquarium = parent; - [SerializableField(0, allowFieldChange: nameof(AllowStateChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private int _state; - - private bool AllowStateChange(ref int value) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int State { - value = Math.Clamp(value, 0, 4); - return true; + get => _state; + set + { + if (_state != value) + { + _state = Math.Clamp(value, 0, 4); + MarkDirty(); + } + } } [SerializableField(1)] diff --git a/Projects/UOContent/Items/Armor/BaseArmor.cs b/Projects/UOContent/Items/Armor/BaseArmor.cs index f4062326d..6f91f3a71 100644 --- a/Projects/UOContent/Items/Armor/BaseArmor.cs +++ b/Projects/UOContent/Items/Armor/BaseArmor.cs @@ -18,92 +18,95 @@ namespace Server.Items { [SerializedIgnoreDupe] [SerializableField(0, setter: "private")] - [SaveFlag(nameof(ShouldSerializeAosAttributes), nameof(AttributesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosAttributes _attributes; + [SerializableFieldSaveFlag(0)] private bool ShouldSerializeAosAttributes() => !_attributes.IsEmpty; + [SerializableFieldDefault(0)] private AosAttributes AttributesDefaultValue() => new(this); [SerializedIgnoreDupe] [SerializableField(1, setter: "private")] - [SaveFlag(nameof(ShouldSerializeArmorAttributes), nameof(ArmorAttributesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosArmorAttributes _armorAttributes; + [SerializableFieldSaveFlag(1)] private bool ShouldSerializeArmorAttributes() => !_armorAttributes.IsEmpty; + [SerializableFieldDefault(1)] private AosArmorAttributes ArmorAttributesDefaultValue() => new(this); [EncodedInt] [InvalidateProperties] [SerializableField(2)] - [SaveFlag(nameof(ShouldSerializePhysicalBonus))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _physicalBonus; + [SerializableFieldSaveFlag(2)] private bool ShouldSerializePhysicalBonus() => _physicalBonus != 0; [EncodedInt] [InvalidateProperties] [SerializableField(3)] - [SaveFlag(nameof(ShouldSerializeFireBonus))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _fireBonus; + [SerializableFieldSaveFlag(3)] private bool ShouldSerializeFireBonus() => _fireBonus != 0; [EncodedInt] [InvalidateProperties] [SerializableField(4)] - [SaveFlag(nameof(ShouldSerializeColdBonus))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _coldBonus; + [SerializableFieldSaveFlag(4)] private bool ShouldSerializeColdBonus() => _coldBonus != 0; [EncodedInt] [InvalidateProperties] [SerializableField(5)] - [SaveFlag(nameof(ShouldSerializePoisonBonus))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _poisonBonus; + [SerializableFieldSaveFlag(5)] private bool ShouldSerializePoisonBonus() => _poisonBonus != 0; [EncodedInt] [InvalidateProperties] [SerializableField(6)] - [SaveFlag(nameof(ShouldSerializeEnergyBonus))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _energyBonus; + [SerializableFieldSaveFlag(6)] private bool ShouldSerializeEnergyBonus() => _energyBonus != 0; [SerializableField(7)] - [SaveFlag(nameof(ShouldSerializeIdentified))] [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _identified; + [SerializableFieldSaveFlag(7)] private bool ShouldSerializeIdentified() => _identified; [EncodedInt] [SerializableField(8)] - [SaveFlag(nameof(ShouldSerializeMaxHitPoints))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _maxHitPoints; + [SerializableFieldSaveFlag(8)] private bool ShouldSerializeMaxHitPoints() => _maxHitPoints != 0; [InvalidateProperties] [SerializableField(10)] - [SaveFlag(nameof(ShouldSerializeCrafter))] [SerializedCommandProperty(AccessLevel.GameMaster)] private string _crafter; + [SerializableFieldSaveFlag(10)] private bool ShouldSerializeCrafter() => !string.IsNullOrEmpty(_crafter); + [SerializableFieldSaveFlag(14)] private bool ShouldSerializeResource() => _resource != DefaultResource; // Field 15 @@ -132,12 +135,13 @@ namespace Server.Items [SerializedIgnoreDupe] [SerializableField(23, setter: "private")] - [SaveFlag(nameof(ShouldSerializeSkillBonuses), nameof(SkillBonusesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] public AosSkillBonuses _skillBonuses; + [SerializableFieldSaveFlag(23)] private bool ShouldSerializeSkillBonuses() => !_skillBonuses.IsEmpty; + [SerializableFieldDefault(23)] private AosSkillBonuses SkillBonusesDefaultValue() => new(this); private FactionItem m_FactionState; @@ -186,7 +190,6 @@ namespace Server.Items public virtual int OldIntReq => 0; [SerializableProperty(11)] - [SaveFlag(nameof(ShouldSerializeArmorQuality), nameof(QualityDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public ArmorQuality Quality { @@ -199,12 +202,13 @@ namespace Server.Items } } + [SerializableFieldSaveFlag(11)] private bool ShouldSerializeArmorQuality() => _quality != ArmorQuality.Regular; + [SerializableFieldDefault(11)] private ArmorQuality QualityDefaultValue() => ArmorQuality.Regular; [SerializableProperty(12)] - [SaveFlag(nameof(ShouldSerializeDurability))] [CommandProperty(AccessLevel.GameMaster)] public ArmorDurabilityLevel Durability { @@ -217,24 +221,33 @@ namespace Server.Items } } + [SerializableFieldSaveFlag(12)] private bool ShouldSerializeDurability() => _durability != ArmorDurabilityLevel.Regular; - [SerializableField(13, fieldChanged: nameof(OnProtectionLevelChanged))] - [SaveFlag(nameof(ShouldSerializeProtectionLevel))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private ArmorProtectionLevel _protectionLevel; - - private void OnProtectionLevelChanged(ArmorProtectionLevel oldValue, ArmorProtectionLevel newValue) + [SerializableProperty(13)] + [CommandProperty(AccessLevel.GameMaster)] + public ArmorProtectionLevel ProtectionLevel { - Invalidate(); - (Parent as Mobile)?.UpdateResistances(); + get => _protectionLevel; + set + { + if (_protectionLevel != value) + { + _protectionLevel = value; + + Invalidate(); + InvalidateProperties(); + + (Parent as Mobile)?.UpdateResistances(); + this.MarkDirty(); + } + } } + [SerializableFieldSaveFlag(13)] private bool ShouldSerializeProtectionLevel() => _protectionLevel != ArmorProtectionLevel.Regular; [SerializableProperty(14)] - [SaveFlag(nameof(ShouldSerializeResource), nameof(ResourceDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public CraftResource Resource { @@ -260,11 +273,11 @@ namespace Server.Items } } + [SerializableFieldDefault(14)] private CraftResource ResourceDefaultValue() => DefaultResource; [EncodedInt] [SerializableProperty(15, useField: nameof(_armorBase))] - [SaveFlag(nameof(ShouldSerializeArmorBase), nameof(ArmorBaseDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int BaseArmorRating { @@ -277,8 +290,10 @@ namespace Server.Items } } + [SerializableFieldSaveFlag(15)] private bool ShouldSerializeArmorBase() => _armorBase != -1; + [SerializableFieldDefault(15)] private int ArmorBaseDefaultValue() => -1; public double BaseArmorRatingScaled => BaseArmorRating * ArmorScalar; @@ -328,7 +343,6 @@ namespace Server.Items [EncodedInt] [SerializableProperty(16, useField: nameof(_strBonus))] - [SaveFlag(nameof(ShouldSerializeStrBonus), nameof(StrBonusDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int StrBonus { @@ -341,13 +355,14 @@ namespace Server.Items } } + [SerializableFieldSaveFlag(16)] private bool ShouldSerializeStrBonus() => _strBonus != -1; + [SerializableFieldDefault(16)] private int StrBonusDefaultValue() => -1; [EncodedInt] [SerializableProperty(17, useField: nameof(_dexBonus))] - [SaveFlag(nameof(ShouldSerializeDexBonus), nameof(DexBonusDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int DexBonus { @@ -360,13 +375,14 @@ namespace Server.Items } } + [SerializableFieldSaveFlag(17)] private bool ShouldSerializeDexBonus() => _dexBonus != -1; + [SerializableFieldDefault(17)] private int DexBonusDefaultValue() => -1; [EncodedInt] [SerializableProperty(18, useField: nameof(_intBonus))] - [SaveFlag(nameof(ShouldSerializeIntBonus), nameof(IntBonusDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int IntBonus { @@ -379,13 +395,14 @@ namespace Server.Items } } + [SerializableFieldSaveFlag(18)] private bool ShouldSerializeIntBonus() => _intBonus != -1; + [SerializableFieldDefault(18)] private int IntBonusDefaultValue() => -1; [EncodedInt] [SerializableProperty(19, useField: nameof(_strReq))] - [SaveFlag(nameof(ShouldSerializeStrReq), nameof(StrReqDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int StrRequirement { @@ -398,13 +415,14 @@ namespace Server.Items } } + [SerializableFieldSaveFlag(19)] private bool ShouldSerializeStrReq() => _strReq != -1; + [SerializableFieldDefault(19)] private int StrReqDefaultValue() => -1; [EncodedInt] [SerializableProperty(20, useField: nameof(_dexReq))] - [SaveFlag(nameof(ShouldSerializeDexReq), nameof(DexReqDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int DexRequirement { @@ -417,13 +435,14 @@ namespace Server.Items } } + [SerializableFieldSaveFlag(20)] private bool ShouldSerializeDexReq() => _dexReq != -1; + [SerializableFieldDefault(20)] private int DexReqDefaultValue() => -1; [EncodedInt] [SerializableProperty(21, useField: nameof(_intReq))] - [SaveFlag(nameof(ShouldSerializeIntReq), nameof(IntReqDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int IntRequirement { @@ -436,12 +455,13 @@ namespace Server.Items } } + [SerializableFieldSaveFlag(21)] private bool ShouldSerializeIntReq() => _intReq != -1; + [SerializableFieldDefault(21)] private int IntReqDefaultValue() => -1; [SerializableProperty(22, useField: nameof(_meditate))] - [SaveFlag(nameof(ShouldSerializeMeditationAllowance))] [CommandProperty(AccessLevel.GameMaster)] public AMA MeditationAllowance { @@ -453,6 +473,7 @@ namespace Server.Items } } + [SerializableFieldSaveFlag(22)] private bool ShouldSerializeMeditationAllowance() => _meditate >= AMA.All; public virtual double ArmorScalar @@ -668,7 +689,6 @@ namespace Server.Items [EncodedInt] [SerializableProperty(9)] - [SaveFlag(nameof(ShouldSerializeHitPoints))] [CommandProperty(AccessLevel.GameMaster)] public int HitPoints { @@ -696,6 +716,7 @@ namespace Server.Items } } + [SerializableFieldSaveFlag(9)] private bool ShouldSerializeHitPoints() => _hitPoints != 0; public virtual int InitMinHits => 0; diff --git a/Projects/UOContent/Items/Armor/Glasses/ElvenGlasses.cs b/Projects/UOContent/Items/Armor/Glasses/ElvenGlasses.cs index e44967f0b..4b0fb791f 100644 --- a/Projects/UOContent/Items/Armor/Glasses/ElvenGlasses.cs +++ b/Projects/UOContent/Items/Armor/Glasses/ElvenGlasses.cs @@ -31,12 +31,13 @@ namespace Server.Items public override ArmorMeditationAllowance DefMedAllowance => ArmorMeditationAllowance.All; [SerializableField(0, setter: "private")] - [SaveFlag(nameof(ShouldSerializeWeaponAttributes), nameof(WeaponAttributesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] public AosWeaponAttributes _weaponAttributes; + [SerializableFieldSaveFlag(0)] private bool ShouldSerializeWeaponAttributes() => !_weaponAttributes.IsEmpty; + [SerializableFieldDefault(0)] private AosWeaponAttributes WeaponAttributesDefaultValue() => new(this); public override void AppendChildNameProperties(IPropertyList list) diff --git a/Projects/UOContent/Items/Armor/Leather/LeafGloves.cs b/Projects/UOContent/Items/Armor/Leather/LeafGloves.cs index aa023cdc3..f9abd485b 100644 --- a/Projects/UOContent/Items/Armor/Leather/LeafGloves.cs +++ b/Projects/UOContent/Items/Armor/Leather/LeafGloves.cs @@ -33,26 +33,32 @@ namespace Server.Items public override ArmorMeditationAllowance DefMedAllowance => ArmorMeditationAllowance.All; - [SerializableField(0, fieldChanged: nameof(OnCurArcaneChargesChanged))] [EncodedInt] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _curArcaneCharges; - - private void OnCurArcaneChargesChanged(int oldValue, int newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int CurArcaneCharges { - Update(); + get => _curArcaneCharges; + set + { + _curArcaneCharges = value; + InvalidateProperties(); + Update(); + } } - [SerializableField(1, fieldChanged: nameof(OnMaxArcaneChargesChanged))] [EncodedInt] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _maxArcaneCharges; - - private void OnMaxArcaneChargesChanged(int oldValue, int newValue) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public int MaxArcaneCharges { - Update(); + get => _maxArcaneCharges; + set + { + _maxArcaneCharges = value; + InvalidateProperties(); + Update(); + } } [CommandProperty(AccessLevel.GameMaster)] diff --git a/Projects/UOContent/Items/Armor/Leather/LeatherGloves.cs b/Projects/UOContent/Items/Armor/Leather/LeatherGloves.cs index 297bd8ed1..a1ded151b 100644 --- a/Projects/UOContent/Items/Armor/Leather/LeatherGloves.cs +++ b/Projects/UOContent/Items/Armor/Leather/LeatherGloves.cs @@ -32,26 +32,32 @@ namespace Server.Items public override ArmorMeditationAllowance DefMedAllowance => ArmorMeditationAllowance.All; - [SerializableField(0, fieldChanged: nameof(OnCurArcaneChargesChanged))] [EncodedInt] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _curArcaneCharges; - - private void OnCurArcaneChargesChanged(int oldValue, int newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int CurArcaneCharges { - Update(); + get => _curArcaneCharges; + set + { + _curArcaneCharges = value; + InvalidateProperties(); + Update(); + } } - [SerializableField(1, fieldChanged: nameof(OnMaxArcaneChargesChanged))] [EncodedInt] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _maxArcaneCharges; - - private void OnMaxArcaneChargesChanged(int oldValue, int newValue) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public int MaxArcaneCharges { - Update(); + get => _maxArcaneCharges; + set + { + _maxArcaneCharges = value; + InvalidateProperties(); + Update(); + } } [CommandProperty(AccessLevel.GameMaster)] diff --git a/Projects/UOContent/Items/Books/BaseBook.cs b/Projects/UOContent/Items/Books/BaseBook.cs index 3d9b9f7b8..47f368754 100644 --- a/Projects/UOContent/Items/Books/BaseBook.cs +++ b/Projects/UOContent/Items/Books/BaseBook.cs @@ -19,38 +19,41 @@ namespace Server.Items [InternString] [InvalidateProperties] [SerializableField(1)] - [SaveFlag(nameof(ShouldSerializeTitle), nameof(TitleDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private string _title; + [SerializableFieldSaveFlag(1)] private bool ShouldSerializeTitle() => _title != DefaultContent?.Title; + [SerializableFieldDefault(1)] private string TitleDefaultValue() => DefaultContent?.Title; [InvalidateProperties] [SerializableField(2)] - [SaveFlag(nameof(ShouldSerializeAuthor), nameof(AuthorDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private string _author; + [SerializableFieldSaveFlag(2)] private bool ShouldSerializeAuthor() => _author != DefaultContent?.Author; + [SerializableFieldDefault(2)] private string AuthorDefaultValue() => DefaultContent?.Author; [SerializableField(3)] - [SaveFlag(nameof(ShouldSerializeWritable))] [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _writable; + [SerializableFieldSaveFlag(3)] private bool ShouldSerializeWritable() => _writable; [SerializedIgnoreDupe] [SerializableField(4, setter: "protected")] - [SaveFlag(nameof(ShouldSerializePages), nameof(PagesDefaultValue))] private BookPageInfo[] _pages; + [SerializableFieldSaveFlag(4)] private bool ShouldSerializePages() => DefaultContent?.IsMatch(_pages) != true; + [SerializableFieldDefault(4)] private BookPageInfo[] PagesDefaultValue() => DefaultContent?.Copy() ?? Array.Empty(); [Constructible] diff --git a/Projects/UOContent/Items/Clothing/BaseClothing.cs b/Projects/UOContent/Items/Clothing/BaseClothing.cs index 983568d2d..8246c8c46 100644 --- a/Projects/UOContent/Items/Clothing/BaseClothing.cs +++ b/Projects/UOContent/Items/Clothing/BaseClothing.cs @@ -26,71 +26,76 @@ namespace Server.Items public abstract partial class BaseClothing : Item, IDyable, IScissorable, IFactionItem, ICraftable, IWearableDurability, IAosItem { + [SerializableFieldSaveFlag(0)] private bool ShouldSerializeResource() => _resource != DefaultResource; [SerializedIgnoreDupe] [SerializableField(1, setter: "private")] - [SaveFlag(nameof(ShouldSerializeAttributes), nameof(AttributesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosAttributes _attributes; + [SerializableFieldSaveFlag(1)] private bool ShouldSerializeAttributes() => !_attributes.IsEmpty; + [SerializableFieldDefault(1)] private AosAttributes AttributesDefaultValue() => new(this); [SerializedIgnoreDupe] [SerializableField(2, setter: "private")] - [SaveFlag(nameof(ShouldSerializeClothingAttributes), nameof(ClothingAttributesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosArmorAttributes _clothingAttributes; + [SerializableFieldSaveFlag(2)] private bool ShouldSerializeClothingAttributes() => !_clothingAttributes.IsEmpty; + [SerializableFieldDefault(2)] private AosArmorAttributes ClothingAttributesDefaultValue() => new(this); [SerializedIgnoreDupe] [SerializableField(3, setter: "private")] - [SaveFlag(nameof(ShouldSerializeSkillBonuses), nameof(SkillBonusesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosSkillBonuses _skillBonuses; + [SerializableFieldSaveFlag(3)] private bool ShouldSerializeSkillBonuses() => !_skillBonuses.IsEmpty; + [SerializableFieldDefault(3)] private AosSkillBonuses SkillBonusesDefaultValue() => new(this); [SerializedIgnoreDupe] [SerializableField(4, setter: "private")] - [SaveFlag(nameof(ShouldSerializeResistances), nameof(ResistancesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosElementAttributes _resistances; + [SerializableFieldSaveFlag(4)] private bool ShouldSerializeResistances() => !_resistances.IsEmpty; + [SerializableFieldDefault(4)] private AosElementAttributes ResistancesDefaultValue() => new(this); [EncodedInt] [InvalidateProperties] [SerializableField(5)] - [SaveFlag(nameof(ShouldSerializeMaxHitPoints))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _maxHitPoints; + [SerializableFieldSaveFlag(5)] private bool ShouldSerializeMaxHitPoints() => _maxHitPoints != 0; [InvalidateProperties] [SerializableField(7)] - [SaveFlag(nameof(ShouldSerializeCrafter))] [SerializedCommandProperty(AccessLevel.GameMaster)] private string _crafter; + [SerializableFieldSaveFlag(7)] private bool ShouldSerializeCrafter() => !string.IsNullOrEmpty(_crafter); [InvalidateProperties] [SerializableField(8)] - [SaveFlag(nameof(ShouldSerializeQuality))] [SerializedCommandProperty(AccessLevel.GameMaster)] private ClothingQuality _quality = ClothingQuality.Regular; + [SerializableFieldSaveFlag(8)] private bool ShouldSerializeQuality() => _quality != ClothingQuality.Regular; // Field 9 @@ -113,19 +118,21 @@ namespace Server.Items Resistances = new AosElementAttributes(this); } - [SerializableField(0, fieldChanged: nameof(OnResourceChanged))] - [SaveFlag(nameof(ShouldSerializeResource))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(_resource); + get => _resource; + set + { + _resource = value; + Hue = CraftResources.GetHue(_resource); + InvalidateProperties(); + this.MarkDirty(); + } } [SerializableProperty(9, useField: nameof(_strReq))] - [SaveFlag(nameof(ShouldSerializeStrReq))] [CommandProperty(AccessLevel.GameMaster)] public int StrRequirement { @@ -138,6 +145,7 @@ namespace Server.Items } } + [SerializableFieldSaveFlag(9)] private bool ShouldSerializeStrReq() => _strReq != -1; public virtual CraftResource DefaultResource => CraftResource.None; @@ -291,7 +299,6 @@ namespace Server.Items [EncodedInt] [SerializableProperty(6)] - [SaveFlag(nameof(ShouldSerializeHitPoints))] [CommandProperty(AccessLevel.GameMaster)] public int HitPoints { @@ -317,6 +324,7 @@ namespace Server.Items } } + [SerializableFieldSaveFlag(6)] private bool ShouldSerializeHitPoints() => _hitPoints != 0; public virtual int InitMinHits => 0; diff --git a/Projects/UOContent/Items/Clothing/Cloaks.cs b/Projects/UOContent/Items/Clothing/Cloaks.cs index a92e8fc8c..67f572d9d 100644 --- a/Projects/UOContent/Items/Clothing/Cloaks.cs +++ b/Projects/UOContent/Items/Clothing/Cloaks.cs @@ -22,26 +22,34 @@ namespace Server.Items public override double DefaultWeight => 5.0; - [SerializableField(0, fieldChanged: nameof(OnCurArcaneChargesChanged))] [EncodedInt] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _curArcaneCharges; - - private void OnCurArcaneChargesChanged(int oldValue, int newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int CurArcaneCharges { - Update(); + get => _curArcaneCharges; + set + { + _curArcaneCharges = value; + this.MarkDirty(); + InvalidateProperties(); + Update(); + } } - [SerializableField(1, fieldChanged: nameof(OnMaxArcaneChargesChanged))] [EncodedInt] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _maxArcaneCharges; - - private void OnMaxArcaneChargesChanged(int oldValue, int newValue) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public int MaxArcaneCharges { - Update(); + get => _maxArcaneCharges; + set + { + _maxArcaneCharges = value; + this.MarkDirty(); + InvalidateProperties(); + Update(); + } } [CommandProperty(AccessLevel.GameMaster)] diff --git a/Projects/UOContent/Items/Clothing/OuterTorso.cs b/Projects/UOContent/Items/Clothing/OuterTorso.cs index f3b0b077f..4fda45bb5 100644 --- a/Projects/UOContent/Items/Clothing/OuterTorso.cs +++ b/Projects/UOContent/Items/Clothing/OuterTorso.cs @@ -37,22 +37,21 @@ namespace Server.Items public override double DefaultWeight => 3.0; } - [SerializationGenerator(4, false)] + [SerializationGenerator(3, false)] public partial class DeathRobe : Robe { private static readonly TimeSpan m_DefaultDecayTime = TimeSpan.FromMinutes(1.0); + [TimerDrift] [SerializableField(0)] - [DeserializeTimer(nameof(DeserializeDecayTimer))] private Timer _decayTimer; - private void DeserializeDecayTimer(TimeSpan delay) => BeginDecay(delay); - - private void MigrateFrom(V3Content content) + [DeserializeTimerField(0)] + private void DeserializeDecayTimer(TimeSpan delay) { - if (content.DecayTimerDelay != TimeSpan.MinValue) + if (delay != TimeSpan.MinValue) { - DeserializeDecayTimer(content.DecayTimerDelay); + BeginDecay(delay); } } @@ -325,26 +324,34 @@ namespace Server.Items public override double DefaultWeight => 3.0; - [SerializableField(0, fieldChanged: nameof(OnCurArcaneChargesChanged))] [EncodedInt] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _curArcaneCharges; - - private void OnCurArcaneChargesChanged(int oldValue, int newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int CurArcaneCharges { - Update(); + get => _curArcaneCharges; + set + { + _curArcaneCharges = value; + InvalidateProperties(); + Update(); + this.MarkDirty(); + } } - [SerializableField(1, fieldChanged: nameof(OnMaxArcaneChargesChanged))] [EncodedInt] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _maxArcaneCharges; - - private void OnMaxArcaneChargesChanged(int oldValue, int newValue) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public int MaxArcaneCharges { - Update(); + get => _maxArcaneCharges; + set + { + _maxArcaneCharges = value; + InvalidateProperties(); + Update(); + this.MarkDirty(); + } } [CommandProperty(AccessLevel.GameMaster)] diff --git a/Projects/UOContent/Items/Clothing/Shoes.cs b/Projects/UOContent/Items/Clothing/Shoes.cs index f791b3a62..4d80b4699 100644 --- a/Projects/UOContent/Items/Clothing/Shoes.cs +++ b/Projects/UOContent/Items/Clothing/Shoes.cs @@ -54,10 +54,11 @@ namespace Server.Items { [EncodedInt] [InvalidateProperties] - [SerializableField(0, fieldChanged: nameof(OnCurArcaneChargesChanged))] + [SerializableField(0)] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _curArcaneCharges; + [SerializableFieldChanged(0)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private void OnCurArcaneChargesChanged(int oldValue, int newValue) => Update(); @@ -70,15 +71,19 @@ namespace Server.Items public override CraftResource DefaultResource => CraftResource.RegularLeather; - [SerializableField(1, fieldChanged: nameof(OnMaxArcaneChargesChanged))] [EncodedInt] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _maxArcaneCharges; - - private void OnMaxArcaneChargesChanged(int oldValue, int newValue) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public int MaxArcaneCharges { - Update(); + get => _maxArcaneCharges; + set + { + _maxArcaneCharges = value; + InvalidateProperties(); + Update(); + this.MarkDirty(); + } } [CommandProperty(AccessLevel.GameMaster)] diff --git a/Projects/UOContent/Items/Construction/Doors/BaseDoor.cs b/Projects/UOContent/Items/Construction/Doors/BaseDoor.cs index ef779760b..dc3449c24 100644 --- a/Projects/UOContent/Items/Construction/Doors/BaseDoor.cs +++ b/Projects/UOContent/Items/Construction/Doors/BaseDoor.cs @@ -74,31 +74,43 @@ public abstract partial class BaseDoor : Item, ILockable, ITelekinesisable Movable = false; } - [SerializableField(1, fieldChanged: nameof(OnOpenChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private bool _open; - - private void OnOpenChanged(bool oldValue, bool newValue) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public bool Open { - ItemID = _open ? _openedId : _closedId; - if (_open) + get => _open; + set { - Location = new Point3D(X + _offset.X, Y + _offset.Y, Z + _offset.Z); - } - else - { - Location = new Point3D(X - _offset.X, Y - _offset.Y, Z - _offset.Z); - } - Effects.PlaySound(this, _open ? OpenedSound : ClosedSound); - if (_open) - { - _timer ??= new InternalTimer(this); - _timer.Start(); - } - else - { - _timer.Stop(); - _timer = null; + if (_open != value) + { + _open = value; + + ItemID = _open ? _openedId : _closedId; + + if (_open) + { + Location = new Point3D(X + _offset.X, Y + _offset.Y, Z + _offset.Z); + } + else + { + Location = new Point3D(X - _offset.X, Y - _offset.Y, Z - _offset.Z); + } + + Effects.PlaySound(this, _open ? OpenedSound : ClosedSound); + + if (_open) + { + _timer ??= new InternalTimer(this); + _timer.Start(); + } + else + { + _timer.Stop(); + _timer = null; + } + + this.MarkDirty(); + } } } diff --git a/Projects/UOContent/Items/Containers/Fillable Containers/FillableContainer.cs b/Projects/UOContent/Items/Containers/Fillable Containers/FillableContainer.cs index da4d8b77f..a9da67dbb 100644 --- a/Projects/UOContent/Items/Containers/Fillable Containers/FillableContainer.cs +++ b/Projects/UOContent/Items/Containers/Fillable Containers/FillableContainer.cs @@ -3,22 +3,19 @@ using ModernUO.Serialization; namespace Server.Items; -[SerializationGenerator(3, false)] +[SerializationGenerator(2, false)] public abstract partial class FillableContainer : LockableContainer { + [TimerDrift] [SerializableField(1)] - [DeserializeTimer(nameof(DeserializeRespawnTimer))] private Timer _respawnTimer; - private void DeserializeRespawnTimer(TimeSpan delay) => _respawnTimer = Timer.DelayCall(delay, Respawn); - - private void MigrateFrom(V2Content content) + [DeserializeTimerField(1)] + private void DeserializeRespawnTimer(TimeSpan delay) { - _contentType = content.ContentType; - - if (content.RespawnTimerDelay != TimeSpan.MinValue) + if (delay > TimeSpan.MinValue) { - DeserializeRespawnTimer(content.RespawnTimerDelay); + _respawnTimer = Timer.DelayCall(delay, Respawn); } } diff --git a/Projects/UOContent/Items/Containers/MarkContainer.cs b/Projects/UOContent/Items/Containers/MarkContainer.cs index 04e7c89ea..fa3f7e440 100644 --- a/Projects/UOContent/Items/Containers/MarkContainer.cs +++ b/Projects/UOContent/Items/Containers/MarkContainer.cs @@ -3,13 +3,14 @@ using ModernUO.Serialization; namespace Server.Items; -[SerializationGenerator(1, false)] +[SerializationGenerator(0, false)] public partial class MarkContainer : LockableContainer { + [TimerDrift] [SerializableField(1, getter: "private", setter: "private")] - [DeserializeTimer(nameof(DeserializeRelockTimer))] private InternalTimer _relockTimer; + [DeserializeTimerField(1)] private void DeserializeRelockTimer(TimeSpan delay) { if (!Locked && _autoLock) @@ -18,19 +19,6 @@ public partial class MarkContainer : LockableContainer } } - private void MigrateFrom(V0Content content) - { - _autoLock = content.AutoLock; - _targetMap = content.TargetMap; - _target = content.Target; - _description = content.Description; - - if (content.RelockTimerDelay != TimeSpan.MinValue) - { - DeserializeRelockTimer(content.RelockTimerDelay); - } - } - [SerializableField(2)] [SerializedCommandProperty(AccessLevel.GameMaster)] private Map _targetMap; @@ -62,19 +50,23 @@ public partial class MarkContainer : LockableContainer } } - [SerializableField(0, fieldChanged: nameof(OnAutoLockChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private bool _autoLock; - - private void OnAutoLockChanged(bool oldValue, bool newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public bool AutoLock { - if (!_autoLock) + get => _autoLock; + set { - StopTimer(); - } - else if (!Locked) - { - _relockTimer ??= new InternalTimer(this); + _autoLock = value; + + if (!_autoLock) + { + StopTimer(); + } + else if (!Locked) + { + _relockTimer ??= new InternalTimer(this); + } } } diff --git a/Projects/UOContent/Items/Containers/TreasureMapChest.cs b/Projects/UOContent/Items/Containers/TreasureMapChest.cs index a1aafa0b3..e0486182d 100644 --- a/Projects/UOContent/Items/Containers/TreasureMapChest.cs +++ b/Projects/UOContent/Items/Containers/TreasureMapChest.cs @@ -9,7 +9,7 @@ using Server.Network; namespace Server.Items; -[SerializationGenerator(4, false)] +[SerializationGenerator(3, false)] public partial class TreasureMapChest : LockableContainer { [Tidy] @@ -29,11 +29,12 @@ public partial class TreasureMapChest : LockableContainer [SerializedCommandProperty(AccessLevel.GameMaster)] private int _level; + [TimerDrift] [SerializableField(4)] [SerializedCommandProperty(AccessLevel.GameMaster)] - [DeserializeTimer(nameof(DeserializeExpireTimer))] private Timer _expireTimer; + [DeserializeTimerField(4)] private void DeserializeExpireTimer(TimeSpan delay) { if (!_temporary) @@ -42,20 +43,6 @@ public partial class TreasureMapChest : LockableContainer } } - private void MigrateFrom(V3Content content) - { - _guardians = content.Guardians; - _temporary = content.Temporary; - _owner = content.Owner; - _level = content.Level; - _lifted = content.Lifted; - - if (content.ExpireTimerDelay != TimeSpan.MinValue) - { - DeserializeExpireTimer(content.ExpireTimerDelay); - } - } - [Tidy] [CanBeNull] [SerializableField(5, setter: "private")] diff --git a/Projects/UOContent/Items/Deeds/DragonBardingDeed.cs b/Projects/UOContent/Items/Deeds/DragonBardingDeed.cs index 5ed370153..6599e6c56 100644 --- a/Projects/UOContent/Items/Deeds/DragonBardingDeed.cs +++ b/Projects/UOContent/Items/Deeds/DragonBardingDeed.cs @@ -28,14 +28,17 @@ public partial class DragonBardingDeed : Item, ICraftable public override int LabelNumber => _exceptional ? 1053181 : 1053012; // dragon barding deed - [SerializableField(2, fieldChanged: nameof(OnResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(2)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(newValue); + get => _resource; + set + { + _resource = value; + Hue = CraftResources.GetHue(value); + InvalidateProperties(); + } } public int OnCraft( diff --git a/Projects/UOContent/Items/Food/Beverage.cs b/Projects/UOContent/Items/Food/Beverage.cs index 85eecf17f..3a9f50297 100644 --- a/Projects/UOContent/Items/Food/Beverage.cs +++ b/Projects/UOContent/Items/Food/Beverage.cs @@ -324,21 +324,27 @@ public abstract partial class BaseBeverage : Item, IHasQuantity [CommandProperty(AccessLevel.GameMaster)] public bool IsFull => _quantity >= MaxQuantity; - [SerializableField(2, fieldChanged: nameof(OnContentChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private BeverageType _content; - - private void OnContentChanged(BeverageType oldValue, BeverageType newValue) + [SerializableProperty(2)] + [CommandProperty(AccessLevel.GameMaster)] + public BeverageType Content { - var itemID = ComputeItemID(); - if (itemID > 0) + get => _content; + set { - ItemID = itemID; - } - else - { - Delete(); + _content = value; + + InvalidateProperties(); + + var itemID = ComputeItemID(); + + if (itemID > 0) + { + ItemID = itemID; + } + else + { + Delete(); + } } } diff --git a/Projects/UOContent/Items/Food/Cooking.cs b/Projects/UOContent/Items/Food/Cooking.cs index cd010432d..6cff16d72 100644 --- a/Projects/UOContent/Items/Food/Cooking.cs +++ b/Projects/UOContent/Items/Food/Cooking.cs @@ -76,25 +76,25 @@ public partial class SackFlour : Item, IHasQuantity public override double DefaultWeight => 5.0; - [SerializableField(0, fieldChanged: nameof(OnQuantityChanged), allowFieldChange: nameof(AllowQuantityChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private int _quantity; - - private bool AllowQuantityChange(ref int value) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int Quantity { - value = Math.Min(20, Math.Max(0, value)); - return true; - } + get => _quantity; + set + { + _quantity = Math.Min(20, Math.Max(0, value)); - private void OnQuantityChanged(int oldValue, int newValue) - { - if (_quantity == 0) - { - Delete(); - } - else if (_quantity < 20 && ItemID is 0x1039 or 0x1045) - { - ++ItemID; + if (_quantity == 0) + { + Delete(); + } + else if (_quantity < 20 && ItemID is 0x1039 or 0x1045) + { + ++ItemID; + } + + this.MarkDirty(); } } diff --git a/Projects/UOContent/Items/Games/Mahjong/MahjongGame.cs b/Projects/UOContent/Items/Games/Mahjong/MahjongGame.cs index 89e4d6f8f..de7e663b7 100644 --- a/Projects/UOContent/Items/Games/Mahjong/MahjongGame.cs +++ b/Projects/UOContent/Items/Games/Mahjong/MahjongGame.cs @@ -55,33 +55,55 @@ public partial class MahjongGame : Item, ISecurable public override double DefaultWeight => 5.0; - [SerializableField(6, fieldChanged: nameof(OnShowScoresChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private bool _showScores; - - private void OnShowScoresChanged(bool oldValue, bool newValue) + [CommandProperty(AccessLevel.GameMaster)] + [SerializableProperty(6)] + public bool ShowScores { - if (newValue) + get => _showScores; + set { - _players.SendPlayersPacket(true, true); + if (_showScores == value) + { + return; + } + + _showScores = value; + + if (value) + { + _players.SendPlayersPacket(true, true); + } + + _players.SendGeneralPacket(true, true); + _players.SendLocalizedMessage(value ? 1062777 : 1062778); // The dealer has enabled/disabled score display. + this.MarkDirty(); } - _players.SendGeneralPacket(true, true); - _players.SendLocalizedMessage(newValue ? 1062777 : 1062778); // The dealer has enabled/disabled score display. } - [SerializableField(7, fieldChanged: nameof(OnSpectatorVisionChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private bool _spectatorVision; - - private void OnSpectatorVisionChanged(bool oldValue, bool newValue) + [CommandProperty(AccessLevel.GameMaster)] + [SerializableProperty(7)] + public bool SpectatorVision { - if (_players.IsInGamePlayer(_players.DealerPosition)) + get => _spectatorVision; + set { - _players.Dealer.NetState.SendMahjongGeneralInfo(this); + if (_spectatorVision == value) + { + return; + } + + _spectatorVision = value; + + if (_players.IsInGamePlayer(_players.DealerPosition)) + { + _players.Dealer.NetState.SendMahjongGeneralInfo(this); + } + + _players.SendTilesPacket(false, true); + _players.SendLocalizedMessage(value ? 1062715 : 1062716); // The dealer has enabled/disabled Spectator Vision. + InvalidateProperties(); + this.MarkDirty(); } - _players.SendTilesPacket(false, true); - _players.SendLocalizedMessage(newValue ? 1062715 : 1062716); // The dealer has enabled/disabled Spectator Vision. } private void BuildHorizontalWall( diff --git a/Projects/UOContent/Items/Jewels/BaseJewel.cs b/Projects/UOContent/Items/Jewels/BaseJewel.cs index e693c0aaa..4eb01cbe0 100644 --- a/Projects/UOContent/Items/Jewels/BaseJewel.cs +++ b/Projects/UOContent/Items/Jewels/BaseJewel.cs @@ -93,13 +93,16 @@ public abstract partial class BaseJewel : Item, ICraftable, IAosItem } } - [SerializableField(2, fieldChanged: nameof(OnResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(2)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(_resource); + get => _resource; + set + { + _resource = value; + Hue = CraftResources.GetHue(_resource); + } } public override int PhysicalResistance => Resistances.Physical; diff --git a/Projects/UOContent/Items/Lights/BaseLight.cs b/Projects/UOContent/Items/Lights/BaseLight.cs index 4bd63ba63..d3bff4af5 100644 --- a/Projects/UOContent/Items/Lights/BaseLight.cs +++ b/Projects/UOContent/Items/Lights/BaseLight.cs @@ -3,7 +3,7 @@ using ModernUO.Serialization; namespace Server.Items; -[SerializationGenerator(2, false)] +[SerializationGenerator(1, false)] public abstract partial class BaseLight : Item { public static readonly bool Burnout = false; @@ -16,10 +16,11 @@ public abstract partial class BaseLight : Item [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _protected; + [TimerDrift] [SerializableField(4, getter: "private", setter: "private")] - [DeserializeTimer(nameof(DeserializeTimer))] private Timer _burnTimer; + [DeserializeTimerField(4)] private void DeserializeTimer(TimeSpan delay) { if (_burning && _duration != TimeSpan.Zero) @@ -28,19 +29,6 @@ public abstract partial class BaseLight : Item } } - private void MigrateFrom(V1Content content) - { - _burntOut = content.BurntOut; - _burning = content.Burning; - _duration = content.Duration; - _protected = content.Protected; - - if (content.BurnTimerDelay != TimeSpan.MinValue) - { - DeserializeTimer(content.BurnTimerDelay); - } - } - [Constructible] public BaseLight(int itemID) : base(itemID) { diff --git a/Projects/UOContent/Items/Misc/CommunicationCrystals.cs b/Projects/UOContent/Items/Misc/CommunicationCrystals.cs index 07df233e9..a52b1a12d 100644 --- a/Projects/UOContent/Items/Misc/CommunicationCrystals.cs +++ b/Projects/UOContent/Items/Misc/CommunicationCrystals.cs @@ -275,16 +275,8 @@ public partial class BroadcastCrystal : Item [SerializationGenerator(0)] public partial class ReceiverCrystal : Item { - [SerializableField(0, fieldChanged: nameof(OnSenderChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] private BroadcastCrystal _sender; - private void OnSenderChanged(BroadcastCrystal oldValue, BroadcastCrystal newValue) - { - oldValue?.RemoveReceiver(this); - newValue?.AddReceiver(this); - } - [Constructible] public ReceiverCrystal() : base(0x1ED0) => Light = LightType.Circle150; @@ -305,6 +297,20 @@ public partial class ReceiverCrystal : Item } } + [SerializableProperty(0, useField: nameof(_sender))] + [CommandProperty(AccessLevel.GameMaster)] + public BroadcastCrystal Sender + { + get => _sender; + set + { + _sender?.RemoveReceiver(this); + _sender = value; + value?.AddReceiver(this); + this.MarkDirty(); + } + } + public override void GetProperties(IPropertyList list) { base.GetProperties(list); diff --git a/Projects/UOContent/Items/Misc/Corpses/Corpse.Migrations.cs b/Projects/UOContent/Items/Misc/Corpses/Corpse.Migrations.cs deleted file mode 100644 index bc9ce047e..000000000 --- a/Projects/UOContent/Items/Misc/Corpses/Corpse.Migrations.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System; - -namespace Server.Items; - -public partial class Corpse -{ - // Decay timer and TimeOfDeath moved from delta time to anchored time - private void MigrateFrom(V18Content content) - { - _restoreEquip = content.RestoreEquip; - _flags = content.Flags; - _timeOfDeath = content.TimeOfDeath; - _restoreTable = content.RestoreTable; - _looters = content.Looters; - _killer = content.Killer; - _aggressors = content.Aggressors; - _owner = content.Owner; - _corpseName = content.CorpseName; - _accessLevel = content.AccessLevel; - _guild = content.Guild; - _equipItems = content.EquipItems; - _hairItemId = content.HairItemId; - _hairHue = content.HairHue; - _facialHairItemId = content.FacialHairItemId; - _facialHairHue = content.FacialHairHue; - - if (content.DecayTimerDelay != TimeSpan.MinValue) - { - DeserializeDecayTimer(content.DecayTimerDelay); - } - } - - // Decay timer moved from [TimerDrift]/[DeserializeTimerField] to [DeserializeTimer] - private void MigrateFrom(V17Content content) - { - _restoreEquip = content.RestoreEquip; - _flags = content.Flags; - _timeOfDeath = content.TimeOfDeath; - _restoreTable = content.RestoreTable; - _looters = content.Looters; - _killer = content.Killer; - _aggressors = content.Aggressors; - _owner = content.Owner; - _corpseName = content.CorpseName; - _accessLevel = content.AccessLevel; - _guild = content.Guild; - _equipItems = content.EquipItems; - _hairItemId = content.HairItemId; - _hairHue = content.HairHue; - _facialHairItemId = content.FacialHairItemId; - _facialHairHue = content.FacialHairHue; - - if (content.DecayTimerDelay != TimeSpan.MinValue) - { - DeserializeDecayTimer(content.DecayTimerDelay); - } - } - - // Decomposed VirtualHairInfo into discrete int fields (hair/facial hair item id + hue) - private void MigrateFrom(V16Content content) - { - _restoreEquip = content.RestoreEquip; - _flags = content.Flags; - _timeOfDeath = content.TimeOfDeath; - _restoreTable = content.RestoreTable; - _decayTimer = new InternalTimer(this, content.DecayTimerDelay); - _decayTimer.Start(); - _looters = content.Looters; - _killer = content.Killer; - _aggressors = content.Aggressors; - _owner = content.Owner; - _corpseName = content.CorpseName; - _accessLevel = content.AccessLevel; - _guild = content.Guild; - _equipItems = content.EquipItems; - if (content.Hair != null) - { - _hairItemId = content.Hair.ItemId; - _hairHue = content.Hair.Hue; - } - - if (content.FacialHair != null) - { - _facialHairItemId = content.FacialHair.ItemId; - _facialHairHue = content.FacialHair.Hue; - } - } - - // Folded Murderer bool field into CorpseFlag.Murderer - private void MigrateFrom(V15Content content) - { - _restoreEquip = content.RestoreEquip; - _flags = content.Flags; - if (content.Murderer) - { - _flags |= CorpseFlag.Murderer; - } - _timeOfDeath = content.TimeOfDeath; - _restoreTable = content.RestoreTable; - _decayTimer = new InternalTimer(this, content.DecayTimerDelay); - _decayTimer.Start(); - _looters = content.Looters; - _killer = content.Killer; - _aggressors = content.Aggressors; - _owner = content.Owner; - _corpseName = content.CorpseName; - _accessLevel = content.AccessLevel; - _guild = content.Guild; - _equipItems = content.EquipItems; - if (content.Hair != null) - { - _hairItemId = content.Hair.ItemId; - _hairHue = content.Hair.Hue; - } - - if (content.FacialHair != null) - { - _facialHairItemId = content.FacialHair.ItemId; - _facialHairHue = content.FacialHair.Hue; - } - } - - // Replaced int Kills snapshot with bool Murderer snapshot - private void MigrateFrom(V14Content content) - { - _restoreEquip = content.RestoreEquip; - _flags = content.Flags; - if (content.Kills >= 5) - { - _flags |= CorpseFlag.Murderer; - } - _timeOfDeath = content.TimeOfDeath; - _restoreTable = content.RestoreTable; - _decayTimer = new InternalTimer(this, content.DecayTimerDelay); - _decayTimer.Start(); - _looters = content.Looters; - _killer = content.Killer; - _aggressors = content.Aggressors; - _owner = content.Owner; - _corpseName = content.CorpseName; - _accessLevel = content.AccessLevel; - _guild = content.Guild; - _equipItems = content.EquipItems; - if (content.Hair != null) - { - _hairItemId = content.Hair.ItemId; - _hairHue = content.Hair.Hue; - } - - if (content.FacialHair != null) - { - _facialHairItemId = content.FacialHair.ItemId; - _facialHairHue = content.FacialHair.Hue; - } - } - - // Added corpse hair and corpse facial hair - private void MigrateFrom(V13Content content) - { - _restoreEquip = content.RestoreEquip; - _flags = content.Flags; - if (content.Kills >= 5) - { - _flags |= CorpseFlag.Murderer; - } - _timeOfDeath = content.TimeOfDeath; - _restoreTable = content.RestoreTable; - _decayTimer = new InternalTimer(this, content.DecayTimerDelay); - _decayTimer.Start(); - _looters = content.Looters; - _killer = content.Killer; - _aggressors = content.Aggressors; - _owner = content.Owner; - _corpseName = content.CorpseName; - _accessLevel = content.AccessLevel; - _guild = content.Guild; - _equipItems = content.EquipItems; - } -} diff --git a/Projects/UOContent/Items/Misc/Corpses/Corpse.cs b/Projects/UOContent/Items/Misc/Corpses/Corpse.cs index 04e9806c9..1ee04170c 100644 --- a/Projects/UOContent/Items/Misc/Corpses/Corpse.cs +++ b/Projects/UOContent/Items/Misc/Corpses/Corpse.cs @@ -86,7 +86,7 @@ public enum CorpseFlag OwnerWasAnimatedDead = 0x00000800 } -[SerializationGenerator(19, false)] +[SerializationGenerator(17, false)] public partial class Corpse : Container, ICarvable { public static readonly TimeSpan MonsterLootRightSacrifice = TimeSpan.FromMinutes(2.0); @@ -106,7 +106,7 @@ public partial class Corpse : Container, ICarvable [SerializableField(1)] private CorpseFlag _flags; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(2)] [SerializedCommandProperty(AccessLevel.GameMaster)] private DateTime _timeOfDeath; @@ -114,10 +114,11 @@ public partial class Corpse : Container, ICarvable [SerializableField(3, getter: "private", setter: "private")] private Dictionary _restoreTable; + [TimerDrift] [SerializableField(4, getter: "private", setter: "private")] - [DeserializeTimer(nameof(DeserializeDecayTimer))] private Timer _decayTimer; + [DeserializeTimerField(4)] private void DeserializeDecayTimer(TimeSpan delay) => BeginDecay(delay); [SerializableField(5, setter: "private")] @@ -317,6 +318,127 @@ public partial class Corpse : Container, ICarvable DevourCorpse(); } + // Decomposed VirtualHairInfo into discrete int fields (hair/facial hair item id + hue) + private void MigrateFrom(V16Content content) + { + _restoreEquip = content.RestoreEquip; + _flags = content.Flags; + _timeOfDeath = content.TimeOfDeath; + _restoreTable = content.RestoreTable; + _decayTimer = new InternalTimer(this, content.DecayTimerDelay); + _decayTimer.Start(); + _looters = content.Looters; + _killer = content.Killer; + _aggressors = content.Aggressors; + _owner = content.Owner; + _corpseName = content.CorpseName; + _accessLevel = content.AccessLevel; + _guild = content.Guild; + _equipItems = content.EquipItems; + if (content.Hair != null) + { + _hairItemId = content.Hair.ItemId; + _hairHue = content.Hair.Hue; + } + + if (content.FacialHair != null) + { + _facialHairItemId = content.FacialHair.ItemId; + _facialHairHue = content.FacialHair.Hue; + } + } + + // Folded Murderer bool field into CorpseFlag.Murderer + private void MigrateFrom(V15Content content) + { + _restoreEquip = content.RestoreEquip; + _flags = content.Flags; + if (content.Murderer) + { + _flags |= CorpseFlag.Murderer; + } + _timeOfDeath = content.TimeOfDeath; + _restoreTable = content.RestoreTable; + _decayTimer = new InternalTimer(this, content.DecayTimerDelay); + _decayTimer.Start(); + _looters = content.Looters; + _killer = content.Killer; + _aggressors = content.Aggressors; + _owner = content.Owner; + _corpseName = content.CorpseName; + _accessLevel = content.AccessLevel; + _guild = content.Guild; + _equipItems = content.EquipItems; + if (content.Hair != null) + { + _hairItemId = content.Hair.ItemId; + _hairHue = content.Hair.Hue; + } + + if (content.FacialHair != null) + { + _facialHairItemId = content.FacialHair.ItemId; + _facialHairHue = content.FacialHair.Hue; + } + } + + // Replaced int Kills snapshot with bool Murderer snapshot + private void MigrateFrom(V14Content content) + { + _restoreEquip = content.RestoreEquip; + _flags = content.Flags; + if (content.Kills >= 5) + { + _flags |= CorpseFlag.Murderer; + } + _timeOfDeath = content.TimeOfDeath; + _restoreTable = content.RestoreTable; + _decayTimer = new InternalTimer(this, content.DecayTimerDelay); + _decayTimer.Start(); + _looters = content.Looters; + _killer = content.Killer; + _aggressors = content.Aggressors; + _owner = content.Owner; + _corpseName = content.CorpseName; + _accessLevel = content.AccessLevel; + _guild = content.Guild; + _equipItems = content.EquipItems; + if (content.Hair != null) + { + _hairItemId = content.Hair.ItemId; + _hairHue = content.Hair.Hue; + } + + if (content.FacialHair != null) + { + _facialHairItemId = content.FacialHair.ItemId; + _facialHairHue = content.FacialHair.Hue; + } + } + + // Added corpse hair and corpse facial hair + private void MigrateFrom(V13Content content) + { + _restoreEquip = content.RestoreEquip; + _flags = content.Flags; + if (content.Kills >= 5) + { + _flags |= CorpseFlag.Murderer; + } + _timeOfDeath = content.TimeOfDeath; + _restoreTable = content.RestoreTable; + _decayTimer = new InternalTimer(this, content.DecayTimerDelay); + _decayTimer.Start(); + _looters = content.Looters; + _killer = content.Killer; + _aggressors = content.Aggressors; + _owner = content.Owner; + _corpseName = content.CorpseName; + _accessLevel = content.AccessLevel; + _guild = content.Guild; + _equipItems = content.EquipItems; + } + [CommandProperty(AccessLevel.GameMaster)] public virtual bool InstancedCorpse => Core.SE && Core.Now < TimeOfDeath + InstancedCorpseTime; diff --git a/Projects/UOContent/Items/Misc/Corpses/DecayedCorpse.cs b/Projects/UOContent/Items/Misc/Corpses/DecayedCorpse.cs index 17267a6cc..ead812e2f 100644 --- a/Projects/UOContent/Items/Misc/Corpses/DecayedCorpse.cs +++ b/Projects/UOContent/Items/Misc/Corpses/DecayedCorpse.cs @@ -3,25 +3,18 @@ using ModernUO.Serialization; namespace Server.Items; -[SerializationGenerator(3, false)] +[SerializationGenerator(2, false)] public partial class DecayedCorpse : Container { private static TimeSpan _defaultDecayTime = TimeSpan.FromMinutes(7.0); + [TimerDrift] [SerializableField(0, getter: "private", setter: "private")] - [DeserializeTimer(nameof(DeserializeDecayTimer))] private Timer _decayTimer; + [DeserializeTimerField(0)] private void DeserializeDecayTimer(TimeSpan delay) => BeginDecay(delay); - private void MigrateFrom(V2Content content) - { - if (content.DecayTimerDelay != TimeSpan.MinValue) - { - DeserializeDecayTimer(content.DecayTimerDelay); - } - } - public DecayedCorpse(string name) : base(Utility.Random(0xECA, 9)) { Movable = false; diff --git a/Projects/UOContent/Items/Misc/MorphItem.cs b/Projects/UOContent/Items/Misc/MorphItem.cs index 7e5a74d8b..c5dd9c0de 100644 --- a/Projects/UOContent/Items/Misc/MorphItem.cs +++ b/Projects/UOContent/Items/Misc/MorphItem.cs @@ -30,24 +30,20 @@ public partial class MorphItem : Item _outsideRange = outRange; } - [SerializableField(0, allowFieldChange: nameof(AllowOutsideRangeChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private int _outsideRange; - - private bool AllowOutsideRangeChange(ref int value) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int OutsideRange { - value = Math.Clamp(value, 0, 18); - return true; + get => _outsideRange; + set => _outsideRange = Math.Clamp(value, 0, 18); } - [SerializableField(3, allowFieldChange: nameof(AllowInsideRangeChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private int _insideRange; - - private bool AllowInsideRangeChange(ref int value) + [SerializableProperty(3)] + [CommandProperty(AccessLevel.GameMaster)] + public int InsideRange { - value = Math.Clamp(value, 0, 18); - return true; + get => _insideRange; + set => _insideRange = Math.Clamp(value, 0, 18); } [CommandProperty(AccessLevel.GameMaster)] diff --git a/Projects/UOContent/Items/Misc/WarningItem.cs b/Projects/UOContent/Items/Misc/WarningItem.cs index 3779e4900..5d0b491a4 100644 --- a/Projects/UOContent/Items/Misc/WarningItem.cs +++ b/Projects/UOContent/Items/Misc/WarningItem.cs @@ -14,16 +14,8 @@ public partial class WarningItem : Item private TextDefinition _warningMessage; // Field 1 - [SerializableField(1, allowFieldChange: nameof(AllowRangeChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] private int _range; - private bool AllowRangeChange(ref int value) - { - value = Math.Min(value, 18); - return true; - } - [SerializableField(2)] private TimeSpan _resetDelay; @@ -47,6 +39,18 @@ public partial class WarningItem : Item _range = Math.Min(range, 18); } + [CommandProperty(AccessLevel.GameMaster)] + [SerializableProperty(1, useField: nameof(_range))] + public int Range + { + get => _range; + set + { + _range = Math.Min(value, 18); + this.MarkDirty(); + } + } + public virtual bool OnlyToTriggerer => false; public virtual int NeighborRange => 5; diff --git a/Projects/UOContent/Items/Quivers/BaseQuiver.cs b/Projects/UOContent/Items/Quivers/BaseQuiver.cs index 70824c9f6..427871f16 100644 --- a/Projects/UOContent/Items/Quivers/BaseQuiver.cs +++ b/Projects/UOContent/Items/Quivers/BaseQuiver.cs @@ -11,62 +11,64 @@ public partial class BaseQuiver : Container, ICraftable, IAosItem [SerializedIgnoreDupe] [SerializableField(0, setter: "private")] - [SaveFlag(nameof(ShouldSerializeAosAttributes), nameof(AttributesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosAttributes _attributes; + [SerializableFieldSaveFlag(0)] private bool ShouldSerializeAosAttributes() => !_attributes.IsEmpty; + [SerializableFieldDefault(0)] private AosAttributes AttributesDefaultValue() => new(this); [InvalidateProperties] [SerializableField(1)] - [SaveFlag(nameof(ShouldSerializeLowerAmmoCost))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _lowerAmmoCost; + [SerializableFieldSaveFlag(1)] private bool ShouldSerializeLowerAmmoCost() => _lowerAmmoCost != 0; [InvalidateProperties] [SerializableField(2)] - [SaveFlag(nameof(ShouldSerializeWeightReduction))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _weightReduction; + [SerializableFieldSaveFlag(2)] private bool ShouldSerializeWeightReduction() => _weightReduction != 0; [InvalidateProperties] [SerializableField(3)] - [SaveFlag(nameof(ShouldSerializeDamageIncrease))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _damageIncrease; + [SerializableFieldSaveFlag(3)] private bool ShouldSerializeDamageIncrease() => _damageIncrease != 0; [InvalidateProperties] [SerializableField(4)] - [SaveFlag(nameof(ShouldSerializeCrafter))] [SerializedCommandProperty(AccessLevel.GameMaster)] private string _crafter; + [SerializableFieldSaveFlag(4)] private bool ShouldSerializeCrafter() => !string.IsNullOrEmpty(_crafter); [InvalidateProperties] [SerializableField(5)] - [SaveFlag(nameof(ShouldSerializeQuality), nameof(QualityDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private ClothingQuality _quality; + [SerializableFieldSaveFlag(5)] private bool ShouldSerializeQuality() => _quality != ClothingQuality.Regular; + [SerializableFieldDefault(5)] private ClothingQuality QualityDefaultValue() => ClothingQuality.Regular; [InvalidateProperties] [SerializableField(6)] - [SaveFlag(nameof(ShouldSerializeCapacity))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _capacity; + [SerializableFieldSaveFlag(6)] private bool ShouldSerializeCapacity() => _capacity != 0; public BaseQuiver(int itemID = 0x2FB7) : base(itemID) diff --git a/Projects/UOContent/Items/Resources/Blacksmithing/Ingots.cs b/Projects/UOContent/Items/Resources/Blacksmithing/Ingots.cs index 8170b286d..ce772da40 100644 --- a/Projects/UOContent/Items/Resources/Blacksmithing/Ingots.cs +++ b/Projects/UOContent/Items/Resources/Blacksmithing/Ingots.cs @@ -16,14 +16,22 @@ public abstract partial class BaseIngot : Item, ICommodity public override double DefaultWeight => 0.1; - [SerializableField(0, fieldChanged: nameof(OnResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(newValue); + get => _resource; + set + { + if (_resource != value) + { + _resource = value; + Hue = CraftResources.GetHue(value); + + InvalidateProperties(); + this.MarkDirty(); + } + } } public override int LabelNumber diff --git a/Projects/UOContent/Items/Resources/Blacksmithing/Ore.cs b/Projects/UOContent/Items/Resources/Blacksmithing/Ore.cs index 6ae0c71c3..3116d7136 100644 --- a/Projects/UOContent/Items/Resources/Blacksmithing/Ore.cs +++ b/Projects/UOContent/Items/Resources/Blacksmithing/Ore.cs @@ -17,14 +17,22 @@ public abstract partial class BaseOre : Item _resource = resource; } - [SerializableField(0, fieldChanged: nameof(OnResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(newValue); + get => _resource; + set + { + if (_resource != value) + { + _resource = value; + Hue = CraftResources.GetHue(value); + + InvalidateProperties(); + this.MarkDirty(); + } + } } public override int LabelNumber diff --git a/Projects/UOContent/Items/Resources/Blacksmithing/Scales.cs b/Projects/UOContent/Items/Resources/Blacksmithing/Scales.cs index 277ed569d..fa63d902c 100644 --- a/Projects/UOContent/Items/Resources/Blacksmithing/Scales.cs +++ b/Projects/UOContent/Items/Resources/Blacksmithing/Scales.cs @@ -14,14 +14,22 @@ public abstract partial class BaseScales : Item, ICommodity _resource = resource; } - [SerializableField(0, fieldChanged: nameof(OnResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(newValue); + get => _resource; + set + { + if (_resource != value) + { + _resource = value; + Hue = CraftResources.GetHue(value); + + InvalidateProperties(); + this.MarkDirty(); + } + } } public override int LabelNumber => 1053139; // dragon scales diff --git a/Projects/UOContent/Items/Resources/Masonry/Granite.cs b/Projects/UOContent/Items/Resources/Masonry/Granite.cs index 089181ebe..ae4f215d7 100644 --- a/Projects/UOContent/Items/Resources/Masonry/Granite.cs +++ b/Projects/UOContent/Items/Resources/Masonry/Granite.cs @@ -15,14 +15,22 @@ public abstract partial class BaseGranite : Item public override double DefaultWeight => Core.ML ? 1.0 : 10.0; - [SerializableField(0, fieldChanged: nameof(OnResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(newValue); + get => _resource; + set + { + if (_resource != value) + { + _resource = value; + Hue = CraftResources.GetHue(value); + + InvalidateProperties(); + this.MarkDirty(); + } + } } public override int LabelNumber => 1044607; // high quality granite diff --git a/Projects/UOContent/Items/Resources/Tailor/Hides.cs b/Projects/UOContent/Items/Resources/Tailor/Hides.cs index d64eb2f0b..df6423fb2 100644 --- a/Projects/UOContent/Items/Resources/Tailor/Hides.cs +++ b/Projects/UOContent/Items/Resources/Tailor/Hides.cs @@ -15,14 +15,22 @@ public abstract partial class BaseHides : Item, ICommodity public override double DefaultWeight => 5.0; - [SerializableField(0, fieldChanged: nameof(OnResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(newValue); + get => _resource; + set + { + if (_resource != value) + { + _resource = value; + Hue = CraftResources.GetHue(value); + + InvalidateProperties(); + this.MarkDirty(); + } + } } public override int LabelNumber diff --git a/Projects/UOContent/Items/Resources/Tailor/Leathers.cs b/Projects/UOContent/Items/Resources/Tailor/Leathers.cs index afb757e1f..173e2dae0 100644 --- a/Projects/UOContent/Items/Resources/Tailor/Leathers.cs +++ b/Projects/UOContent/Items/Resources/Tailor/Leathers.cs @@ -15,14 +15,22 @@ public abstract partial class BaseLeather : Item, ICommodity public override double DefaultWeight => 1.0; - [SerializableField(0, fieldChanged: nameof(OnResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(newValue); + get => _resource; + set + { + if (_resource != value) + { + _resource = value; + Hue = CraftResources.GetHue(value); + + InvalidateProperties(); + this.MarkDirty(); + } + } } public override int LabelNumber diff --git a/Projects/UOContent/Items/Skill Items/Carpenter Items/Board.cs b/Projects/UOContent/Items/Skill Items/Carpenter Items/Board.cs index 53c8077f1..9ff0d8386 100644 --- a/Projects/UOContent/Items/Skill Items/Carpenter Items/Board.cs +++ b/Projects/UOContent/Items/Skill Items/Carpenter Items/Board.cs @@ -21,14 +21,22 @@ public partial class Board : Item, ICommodity Hue = CraftResources.GetHue(resource); } - [SerializableField(0, fieldChanged: nameof(OnResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(newValue); + get => _resource; + set + { + if (_resource != value) + { + _resource = value; + Hue = CraftResources.GetHue(value); + + InvalidateProperties(); + this.MarkDirty(); + } + } } int ICommodity.DescriptionNumber diff --git a/Projects/UOContent/Items/Skill Items/Fishing/Misc/MessageInABottle.cs b/Projects/UOContent/Items/Skill Items/Fishing/Misc/MessageInABottle.cs index bf626c2fd..5d6062099 100644 --- a/Projects/UOContent/Items/Skill Items/Fishing/Misc/MessageInABottle.cs +++ b/Projects/UOContent/Items/Skill Items/Fishing/Misc/MessageInABottle.cs @@ -25,14 +25,16 @@ public partial class MessageInABottle : Item public override int LabelNumber => 1041080; // a message in a bottle - [SerializableField(0, allowFieldChange: nameof(AllowLevelChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private int _level; - - private bool AllowLevelChange(ref int value) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int Level { - value = Math.Max(1, Math.Min(value, 4)); - return true; + get => _level; + set + { + _level = Math.Max(1, Math.Min(value, 4)); + this.MarkDirty(); + } } public static int GetRandomLevel() diff --git a/Projects/UOContent/Items/Skill Items/Fishing/Misc/SOS.cs b/Projects/UOContent/Items/Skill Items/Fishing/Misc/SOS.cs index 5374f8716..f8450e63f 100644 --- a/Projects/UOContent/Items/Skill Items/Fishing/Misc/SOS.cs +++ b/Projects/UOContent/Items/Skill Items/Fishing/Misc/SOS.cs @@ -103,20 +103,18 @@ public partial class SOS : Item [CommandProperty(AccessLevel.GameMaster)] public bool IsAncient => _level >= 4; - [SerializableField(0, fieldChanged: nameof(OnLevelChanged), allowFieldChange: nameof(AllowLevelChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _level; - - private bool AllowLevelChange(ref int value) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int Level { - value = Math.Max(1, Math.Min(value, 4)); - return true; - } - - private void OnLevelChanged(int oldValue, int newValue) - { - UpdateHue(); + get => _level; + set + { + _level = Math.Max(1, Math.Min(value, 4)); + UpdateHue(); + InvalidateProperties(); + this.MarkDirty(); + } } public void UpdateHue() => Hue = IsAncient ? 0x481 : 0; diff --git a/Projects/UOContent/Items/Skill Items/Lumberjack/Log.cs b/Projects/UOContent/Items/Skill Items/Lumberjack/Log.cs index 2a06f535a..2873957d5 100644 --- a/Projects/UOContent/Items/Skill Items/Lumberjack/Log.cs +++ b/Projects/UOContent/Items/Skill Items/Lumberjack/Log.cs @@ -28,14 +28,22 @@ public partial class Log : Item, ICommodity, IAxe public override double DefaultWeight => 2.0; - [SerializableField(0, fieldChanged: nameof(OnResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(newValue); + get => _resource; + set + { + if (_resource != value) + { + _resource = value; + Hue = CraftResources.GetHue(value); + + InvalidateProperties(); + this.MarkDirty(); + } + } } public virtual bool Axe(Mobile from, BaseAxe axe) => TryCreateBoards(from, 0, new Board()); diff --git a/Projects/UOContent/Items/Skill Items/Magical/Misc/RecallRune.cs b/Projects/UOContent/Items/Skill Items/Magical/Misc/RecallRune.cs index 14db7ac1e..68b6f324a 100644 --- a/Projects/UOContent/Items/Skill Items/Magical/Misc/RecallRune.cs +++ b/Projects/UOContent/Items/Skill Items/Magical/Misc/RecallRune.cs @@ -47,24 +47,36 @@ public partial class RecallRune : Item } } - [SerializableField(2, fieldChanged: nameof(OnMarkedChanged))] - [SerializedCommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] - [InvalidateProperties] - private bool _marked; - - private void OnMarkedChanged(bool oldValue, bool newValue) + [SerializableProperty(2)] + [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] + public bool Marked { - CalculateHue(); + get => _marked; + set + { + if (_marked != value) + { + _marked = value; + CalculateHue(); + InvalidateProperties(); + } + } } - [SerializableField(4, fieldChanged: nameof(OnTargetMapChanged))] - [SerializedCommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] - [InvalidateProperties] - private Map _targetMap; - - private void OnTargetMapChanged(Map oldValue, Map newValue) + [SerializableProperty(4)] + [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] + public Map TargetMap { - CalculateHue(); + get => _targetMap; + set + { + if (_targetMap != value) + { + _targetMap = value; + CalculateHue(); + InvalidateProperties(); + } + } } private void Deserialize(IGenericReader reader, int version) diff --git a/Projects/UOContent/Items/Skill Items/Magical/Runebook.cs b/Projects/UOContent/Items/Skill Items/Magical/Runebook.cs index f9ecdfa22..0a5a0f308 100644 --- a/Projects/UOContent/Items/Skill Items/Magical/Runebook.cs +++ b/Projects/UOContent/Items/Skill Items/Magical/Runebook.cs @@ -371,27 +371,27 @@ public partial class RunebookEntry private Runebook _runebook; [SerializableField(0)] - [SaveFlag(nameof(ShouldSerializeHouse))] private BaseHouse _house; + [SerializableFieldSaveFlag(0)] public bool ShouldSerializeHouse() => _house?.Deleted == false; [SerializableField(1)] - [SaveFlag(nameof(ShouldSerializeLocation))] private Point3D _location; + [SerializableFieldSaveFlag(1)] public bool ShouldSerializeLocation() => _house?.Deleted != false; [SerializableField(2)] - [SaveFlag(nameof(ShouldSerializeMap))] private Map _map; + [SerializableFieldSaveFlag(2)] public bool ShouldSerializeMap() => _house?.Deleted != false; [SerializableField(3)] - [SaveFlag(nameof(ShouldSerializeDesc))] private string _description; + [SerializableFieldSaveFlag(3)] public bool ShouldSerializeDesc() => _house?.Deleted != false; public RunebookEntry( diff --git a/Projects/UOContent/Items/Skill Items/Magical/Spellbook.cs b/Projects/UOContent/Items/Skill Items/Magical/Spellbook.cs index 9447fddfa..f1ced7308 100644 --- a/Projects/UOContent/Items/Skill Items/Magical/Spellbook.cs +++ b/Projects/UOContent/Items/Skill Items/Magical/Spellbook.cs @@ -133,19 +133,28 @@ public partial class Spellbook : Item, ICraftable, ISlayer, IAosItem public virtual int BookOffset => 0; public virtual int BookCount => 64; - [SerializableField(7, fieldChanged: nameof(OnContentChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private ulong _content; - - private void OnContentChanged(ulong oldValue, ulong newValue) + [CommandProperty(AccessLevel.GameMaster)] + [SerializableProperty(7)] + public ulong Content { - // This assignment will mark it as dirty - SpellCount = 0; - while (newValue > 0) + get => _content; + set { - _spellCount += (int)(newValue & 0x1); - newValue >>= 1; + if (_content != value) + { + _content = value; + + // This assignment will mark it as dirty + SpellCount = 0; + + while (value > 0) + { + _spellCount += (int)(value & 0x1); + value >>= 1; + } + + InvalidateProperties(); + } } } diff --git a/Projects/UOContent/Items/Skill Items/Misc/RepairDeed.cs b/Projects/UOContent/Items/Skill Items/Misc/RepairDeed.cs index e4d486d10..dbb8df68c 100644 --- a/Projects/UOContent/Items/Skill Items/Misc/RepairDeed.cs +++ b/Projects/UOContent/Items/Skill Items/Misc/RepairDeed.cs @@ -62,15 +62,17 @@ public partial class RepairDeed : Item public override bool DisplayLootType => false; - [SerializableField(1, allowFieldChange: nameof(AllowSkillLevelChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private double _skillLevel; - - private bool AllowSkillLevelChange(ref double value) + [CommandProperty(AccessLevel.GameMaster)] + [SerializableProperty(1)] + public double SkillLevel { - value = Math.Clamp(value, 0, 120.0); - return true; + get => _skillLevel; + set + { + _skillLevel = Math.Clamp(value, 0, 120.0); + InvalidateProperties(); + this.MarkDirty(); + } } public override void AddNameProperty(IPropertyList list) diff --git a/Projects/UOContent/Items/Skill Items/Musical Instruments/BaseInstrument.cs b/Projects/UOContent/Items/Skill Items/Musical Instruments/BaseInstrument.cs index f7716a4a1..b01416211 100644 --- a/Projects/UOContent/Items/Skill Items/Musical Instruments/BaseInstrument.cs +++ b/Projects/UOContent/Items/Skill Items/Musical Instruments/BaseInstrument.cs @@ -74,13 +74,16 @@ public abstract partial class BaseInstrument : Item, ICraftable, ISlayer } } - [SerializableField(1, fieldChanged: nameof(OnLastReplenishedChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private DateTime _lastReplenished; - - private void OnLastReplenishedChanged(DateTime oldValue, DateTime newValue) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public DateTime LastReplenished { - CheckReplenishUses(); + get => _lastReplenished; + set + { + _lastReplenished = value; + CheckReplenishUses(); + } } [SerializableProperty(3)] diff --git a/Projects/UOContent/Items/Skill Items/Tailor Items/Dyetubs/DyeTub.cs b/Projects/UOContent/Items/Skill Items/Tailor Items/Dyetubs/DyeTub.cs index e35cca471..6b557b7be 100644 --- a/Projects/UOContent/Items/Skill Items/Tailor Items/Dyetubs/DyeTub.cs +++ b/Projects/UOContent/Items/Skill Items/Tailor Items/Dyetubs/DyeTub.cs @@ -41,13 +41,20 @@ namespace Server.Items public virtual bool AllowDyables => true; - [SerializableField(2, allowFieldChange: nameof(AllowDyedHueChange), fieldChanged: nameof(OnDyedHueChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private int _dyedHue; - - private bool AllowDyedHueChange(ref int value) => _redyable; - - private void OnDyedHueChanged(int oldValue, int newValue) => Hue = newValue; + [SerializableProperty(2)] + [CommandProperty(AccessLevel.GameMaster)] + public int DyedHue + { + get => _dyedHue; + set + { + if (_redyable) + { + _dyedHue = value; + Hue = value; + } + } + } // Three metallic tubs now. public virtual bool MetallicHues => false; diff --git a/Projects/UOContent/Items/Skill Items/Tools/BaseRunicTool.cs b/Projects/UOContent/Items/Skill Items/Tools/BaseRunicTool.cs index 595b6b11d..6010ec376 100644 --- a/Projects/UOContent/Items/Skill Items/Tools/BaseRunicTool.cs +++ b/Projects/UOContent/Items/Skill Items/Tools/BaseRunicTool.cs @@ -60,14 +60,17 @@ public abstract partial class BaseRunicTool : BaseTool public BaseRunicTool(CraftResource resource, int uses, int itemID) : base(uses, itemID) => _resource = resource; - [SerializableField(0, fieldChanged: nameof(OnResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _resource; - - private void OnResourceChanged(CraftResource oldValue, CraftResource newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public CraftResource Resource { - Hue = CraftResources.GetHue(_resource); + get => _resource; + set + { + _resource = value; + Hue = CraftResources.GetHue(_resource); + InvalidateProperties(); + } } private void Deserialize(IGenericReader reader, int version) diff --git a/Projects/UOContent/Items/Special/8th Anniversary Items/FountainOfLife.cs b/Projects/UOContent/Items/Special/8th Anniversary Items/FountainOfLife.cs index 404fc9012..3bdfd00fa 100644 --- a/Projects/UOContent/Items/Special/8th Anniversary Items/FountainOfLife.cs +++ b/Projects/UOContent/Items/Special/8th Anniversary Items/FountainOfLife.cs @@ -31,7 +31,6 @@ public partial class FountainOfLife : BaseAddonContainer public const int MaxCharges = 10; [SerializableField(1)] - [DeserializeTimer(nameof(DeserializeTimer), wallClock: true)] private Timer _timer; [Constructible] @@ -40,6 +39,7 @@ public partial class FountainOfLife : BaseAddonContainer _charges = charges; } + [DeserializeTimerField(1)] private void DeserializeTimer(TimeSpan delay) { _timer = Timer.DelayCall(Utility.Max(delay, TimeSpan.Zero), RechargeTime, Recharge); @@ -54,15 +54,17 @@ public partial class FountainOfLife : BaseAddonContainer public override int DefaultDropSound => 66; public override int DefaultMaxItems => 125; - [SerializableField(0, allowFieldChange: nameof(AllowChargesChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _charges; - - private bool AllowChargesChange(ref int value) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int Charges { - value = Math.Min(value, MaxCharges); - return true; + get => _charges; + set + { + _charges = Math.Min(value, MaxCharges); + InvalidateProperties(); + this.MarkDirty(); + } } public override bool OnDragLift(Mobile from) => false; @@ -181,14 +183,16 @@ public partial class FountainOfLifeDeed : BaseAddonContainerDeed public override int LabelNumber => 1075197; // Fountain of Life public override BaseAddonContainer Addon => new FountainOfLife(_charges); - [SerializableField(0, allowFieldChange: nameof(AllowChargesChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _charges; - - private bool AllowChargesChange(ref int value) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int Charges { - value = Math.Min(value, FountainOfLife.MaxCharges); - return true; + get => _charges; + set + { + _charges = Math.Min(value, FountainOfLife.MaxCharges); + InvalidateProperties(); + this.MarkDirty(); + } } } diff --git a/Projects/UOContent/Items/Special/Heritage Items/FruitTrees.cs b/Projects/UOContent/Items/Special/Heritage Items/FruitTrees.cs index fabb756d8..6a7e5af6f 100644 --- a/Projects/UOContent/Items/Special/Heritage Items/FruitTrees.cs +++ b/Projects/UOContent/Items/Special/Heritage Items/FruitTrees.cs @@ -14,14 +14,12 @@ public abstract partial class BaseFruitTreeAddon : BaseAddon public abstract override BaseAddonDeed Deed { get; } public abstract Item Fruit { get; } - [SerializableField(0, allowFieldChange: nameof(AllowFruitsChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private int _fruits; - - private bool AllowFruitsChange(ref int value) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int Fruits { - value = Math.Max(value, 0); - return true; + get => _fruits; + set => _fruits = Math.Max(value, 0); } public override void OnComponentUsed(AddonComponent c, Mobile from) diff --git a/Projects/UOContent/Items/Special/House Raffle/HouseRaffleStone.cs b/Projects/UOContent/Items/Special/House Raffle/HouseRaffleStone.cs index 59e2b8277..c24985f9f 100644 --- a/Projects/UOContent/Items/Special/House Raffle/HouseRaffleStone.cs +++ b/Projects/UOContent/Items/Special/House Raffle/HouseRaffleStone.cs @@ -146,24 +146,34 @@ public partial class HouseRaffleStone : Item } } - [SerializableField(3, fieldChanged: nameof(OnPlotBoundsChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] - [InvalidateProperties] - private Rectangle2D _plotBounds; - - private void OnPlotBoundsChanged(Rectangle2D oldValue, Rectangle2D newValue) + [SerializableProperty(3)] + [CommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] + public Rectangle2D PlotBounds { - InvalidateRegion(); + get => _plotBounds; + set + { + _plotBounds = value; + + InvalidateRegion(); + InvalidateProperties(); + this.MarkDirty(); + } } - [SerializableField(4, fieldChanged: nameof(OnPlotFacetChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] - [InvalidateProperties] - private Map _plotFacet; - - private void OnPlotFacetChanged(Map oldValue, Map newValue) + [SerializableProperty(4)] + [CommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] + public Map PlotFacet { - InvalidateRegion(); + get => _plotFacet; + set + { + _plotFacet = value; + + InvalidateRegion(); + InvalidateProperties(); + this.MarkDirty(); + } } [CommandProperty(AccessLevel.GameMaster)] @@ -180,15 +190,17 @@ public partial class HouseRaffleStone : Item } } - [SerializableField(6, allowFieldChange: nameof(AllowTicketPriceChange))] - [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] - [InvalidateProperties] - private int _ticketPrice; - - private bool AllowTicketPriceChange(ref int value) + [SerializableProperty(6)] + [CommandProperty(AccessLevel.GameMaster, AccessLevel.Seer)] + public int TicketPrice { - value = Math.Max(0, value); - return true; + get => _ticketPrice; + set + { + _ticketPrice = Math.Max(0, value); + InvalidateProperties(); + this.MarkDirty(); + } } public override string DefaultName => "a house raffle stone"; diff --git a/Projects/UOContent/Items/Special/MonsterStatuette.cs b/Projects/UOContent/Items/Special/MonsterStatuette.cs index fd26a0060..8d49dd0e4 100644 --- a/Projects/UOContent/Items/Special/MonsterStatuette.cs +++ b/Projects/UOContent/Items/Special/MonsterStatuette.cs @@ -162,15 +162,21 @@ public partial class MonsterStatuette : Item, IRewardItem, IGumpToggleItem _ => fallback }; - [SerializableField(0, fieldChanged: nameof(OnTypeChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private MonsterStatuetteType _type; - - private void OnTypeChanged(MonsterStatuetteType oldValue, MonsterStatuetteType newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public MonsterStatuetteType Type { - ItemID = MonsterStatuetteInfo.GetInfo(_type).ItemID; - Hue = GetStatuetteHue(_type, Hue); + get => _type; + set + { + _type = value; + ItemID = MonsterStatuetteInfo.GetInfo(_type).ItemID; + + Hue = GetStatuetteHue(_type, Hue); + + InvalidateProperties(); + this.MarkDirty(); + } } public override int LabelNumber => MonsterStatuetteInfo.GetInfo(_type).LabelNumber; diff --git a/Projects/UOContent/Items/Special/Solen Items/BagOfSending.cs b/Projects/UOContent/Items/Special/Solen Items/BagOfSending.cs index 9441fd5c7..f2772b342 100644 --- a/Projects/UOContent/Items/Special/Solen Items/BagOfSending.cs +++ b/Projects/UOContent/Items/Special/Solen Items/BagOfSending.cs @@ -36,41 +36,50 @@ public partial class BagOfSending : Item, TranslocationItem public override int LabelNumber => 1054104; // a bag of sending - [SerializableField(0, fieldChanged: nameof(OnBagOfSendingHueChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private BagOfSendingHue _bagOfSendingHue; - - private void OnBagOfSendingHueChanged(BagOfSendingHue oldValue, BagOfSendingHue newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public BagOfSendingHue BagOfSendingHue { - Hue = newValue switch + get => _bagOfSendingHue; + set { - BagOfSendingHue.Yellow => 0x8A5, - BagOfSendingHue.Blue => 0x8AD, - BagOfSendingHue.Red => 0x89B, - _ => Hue - }; + _bagOfSendingHue = value; + + Hue = value switch + { + BagOfSendingHue.Yellow => 0x8A5, + BagOfSendingHue.Blue => 0x8AD, + BagOfSendingHue.Red => 0x89B, + _ => Hue + }; + this.MarkDirty(); + } } - [SerializableField(1, allowFieldChange: nameof(AllowChargesChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _charges; - - private bool AllowChargesChange(ref int value) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public int Charges { - value = Math.Clamp(value, 0, MaxCharges); - return true; + get => _charges; + set + { + _charges = Math.Clamp(value, 0, MaxCharges); + InvalidateProperties(); + this.MarkDirty(); + } } - [SerializableField(2, allowFieldChange: nameof(AllowRechargesChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _recharges; - - private bool AllowRechargesChange(ref int value) + [SerializableProperty(2)] + [CommandProperty(AccessLevel.GameMaster)] + public int Recharges { - value = Math.Clamp(value, 0, MaxRecharges); - return true; + get => _recharges; + set + { + _recharges = Math.Clamp(value, 0, MaxRecharges); + InvalidateProperties(); + this.MarkDirty(); + } } [CommandProperty(AccessLevel.GameMaster)] diff --git a/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs b/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs index 070a802b1..a76833e30 100644 --- a/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs +++ b/Projects/UOContent/Items/Special/Solen Items/BallOfSummoning.cs @@ -29,26 +29,30 @@ public partial class BallOfSummoning : Item, TranslocationItem public override double DefaultWeight => 10.0; - [SerializableField(0, allowFieldChange: nameof(AllowRechargesChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _recharges; - - private bool AllowRechargesChange(ref int value) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int Recharges { - value = Math.Clamp(value, 0, MaxRecharges); - return true; + get => _recharges; + set + { + _recharges = Math.Clamp(value, 0, MaxRecharges); + InvalidateProperties(); + this.MarkDirty(); + } } - [SerializableField(1, allowFieldChange: nameof(AllowChargesChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _charges; - - private bool AllowChargesChange(ref int value) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public int Charges { - value = Math.Clamp(value, 0, MaxCharges); - return true; + get => _charges; + set + { + _charges = Math.Clamp(value, 0, MaxCharges); + InvalidateProperties(); + this.MarkDirty(); + } } [SerializableProperty(2)] diff --git a/Projects/UOContent/Items/Special/Solen Items/BraceletOfBinding.cs b/Projects/UOContent/Items/Special/Solen Items/BraceletOfBinding.cs index 1d508c4ac..2f82791c2 100644 --- a/Projects/UOContent/Items/Special/Solen Items/BraceletOfBinding.cs +++ b/Projects/UOContent/Items/Special/Solen Items/BraceletOfBinding.cs @@ -27,26 +27,30 @@ public partial class BraceletOfBinding : BaseBracelet, TranslocationItem public override double DefaultWeight => 1.0; - [SerializableField(0, allowFieldChange: nameof(AllowRechargesChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _recharges; - - private bool AllowRechargesChange(ref int value) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int Recharges { - value = Math.Clamp(value, 0, MaxRecharges); - return true; + get => _recharges; + set + { + _recharges = Math.Clamp(value, 0, MaxRecharges); + InvalidateProperties(); + this.MarkDirty(); + } } - [SerializableField(1, allowFieldChange: nameof(AllowChargesChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _charges; - - private bool AllowChargesChange(ref int value) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public int Charges { - value = Math.Clamp(value, 0, MaxCharges); - return true; + get => _charges; + set + { + _charges = Math.Clamp(value, 0, MaxCharges); + InvalidateProperties(); + this.MarkDirty(); + } } [SerializableProperty(3)] diff --git a/Projects/UOContent/Items/Special/SoulStone.cs b/Projects/UOContent/Items/Special/SoulStone.cs index d26f6792c..bcad4a749 100644 --- a/Projects/UOContent/Items/Special/SoulStone.cs +++ b/Projects/UOContent/Items/Special/SoulStone.cs @@ -83,14 +83,20 @@ public partial class SoulStone : Item, ISecurable } } - [SerializableField(6, fieldChanged: nameof(OnSkillValueChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private double _skillValue; - - private void OnSkillValueChanged(double oldValue, double newValue) + [SerializableProperty(6)] + [CommandProperty(AccessLevel.GameMaster)] + public double SkillValue { - ItemID = IsEmpty ? _inactiveItemID : _activeItemID; + get => _skillValue; + set + { + _skillValue = value; + + ItemID = IsEmpty ? _inactiveItemID : _activeItemID; + + InvalidateProperties(); + this.MarkDirty(); + } } [CommandProperty(AccessLevel.GameMaster)] diff --git a/Projects/UOContent/Items/Suits/BaseSuit.cs b/Projects/UOContent/Items/Suits/BaseSuit.cs index dd1eb00f6..018c010a2 100644 --- a/Projects/UOContent/Items/Suits/BaseSuit.cs +++ b/Projects/UOContent/Items/Suits/BaseSuit.cs @@ -17,9 +17,20 @@ public abstract partial class BaseSuit : Item public override double DefaultWeight => 1.0; - [SerializableField(0, fieldChanged: nameof(OnAccessLevelChanged))] - [InvalidateProperties] - private AccessLevel _accessLevel; + [SerializableProperty(0)] + public AccessLevel AccessLevel + { + get => _accessLevel; + set + { + var oldAccessLevel = _accessLevel; + _accessLevel = value; + InvalidateProperties(); + this.MarkDirty(); + + OnAccessLevelChanged(oldAccessLevel, _accessLevel); + } + } public virtual void OnAccessLevelChanged(AccessLevel oldAccessLevel, AccessLevel accessLevel) { diff --git a/Projects/UOContent/Items/Talismans/BaseTalisman.cs b/Projects/UOContent/Items/Talismans/BaseTalisman.cs index b3f0c2ed4..632587523 100644 --- a/Projects/UOContent/Items/Talismans/BaseTalisman.cs +++ b/Projects/UOContent/Items/Talismans/BaseTalisman.cs @@ -128,131 +128,136 @@ public partial class BaseTalisman : Item, IAosItem [SerializedIgnoreDupe] [SerializableField(0, setter: "private")] - [SaveFlag(nameof(ShouldSerializeAttributes), nameof(AttributesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosAttributes _attributes; + [SerializableFieldSaveFlag(0)] public bool ShouldSerializeAttributes() => !_attributes.IsEmpty; + [SerializableFieldDefault(0)] private AosAttributes AttributesDefaultValue() => new(this); [SerializedIgnoreDupe] [SerializableField(1, setter: "private")] - [SaveFlag(nameof(ShouldSerializeSkillBonuses), nameof(SkillBonusesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosSkillBonuses _skillBonuses; + [SerializableFieldSaveFlag(1)] public bool ShouldSerializeSkillBonuses() => !_skillBonuses.IsEmpty; + [SerializableFieldDefault(1)] private AosSkillBonuses SkillBonusesDefaultValue() => new(this); [SerializedIgnoreDupe] [InvalidateProperties] [SerializableField(2)] - [SaveFlag(nameof(ShouldSerializeProtection), nameof(ProtectionDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private TalismanAttribute _protection; + [SerializableFieldSaveFlag(2)] public bool ShouldSerializeProtection() => !_protection.IsEmpty; + [SerializableFieldDefault(2)] private TalismanAttribute ProtectionDefaultValue() => new(); [SerializedIgnoreDupe] [InvalidateProperties] [SerializableField(3)] - [SaveFlag(nameof(ShouldSerializeKiller), nameof(KillerDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private TalismanAttribute _killer; + [SerializableFieldSaveFlag(3)] public bool ShouldSerializeKiller() => !_killer.IsEmpty; + [SerializableFieldDefault(3)] private TalismanAttribute KillerDefaultValue() => new(); [SerializedIgnoreDupe] [InvalidateProperties] [SerializableField(4)] - [SaveFlag(nameof(ShouldSerializeSummoner), nameof(SummonerDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private TalismanAttribute _summoner; + [SerializableFieldSaveFlag(4)] public bool ShouldSerializeSummoner() => !_summoner.IsEmpty; + [SerializableFieldDefault(4)] private TalismanAttribute SummonerDefaultValue() => new(); [InvalidateProperties] [SerializableField(5)] - [SaveFlag(nameof(ShouldSerializeRemoval))] [SerializedCommandProperty(AccessLevel.GameMaster)] private TalismanRemoval _removal; + [SerializableFieldSaveFlag(5)] public bool ShouldSerializeRemoval() => _removal != TalismanRemoval.None; [InvalidateProperties] [SerializableField(6)] - [SaveFlag(nameof(ShouldSerializeSkill))] [SerializedCommandProperty(AccessLevel.GameMaster)] private SkillName _skill; + [SerializableFieldSaveFlag(6)] public bool ShouldSerializeSkill() => (int)_skill != 0; [EncodedInt] [InvalidateProperties] [SerializableField(7)] - [SaveFlag(nameof(ShouldSerializeSuccessBonus))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _successBonus; + [SerializableFieldSaveFlag(7)] public bool ShouldSerializeSuccessBonus() => _successBonus != 0; [EncodedInt] [InvalidateProperties] [SerializableField(8)] - [SaveFlag(nameof(ShouldSerializeExceptionalBonus))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _exceptionalBonus; + [SerializableFieldSaveFlag(8)] public bool ShouldSerializeExceptionalBonus() => _exceptionalBonus != 0; [EncodedInt] [InvalidateProperties] [SerializableField(9)] - [SaveFlag(nameof(ShouldSerializeMaxCharges))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _maxCharges; + [SerializableFieldSaveFlag(9)] public bool ShouldSerializeMaxCharges() => _maxCharges != 0; [EncodedInt] [InvalidateProperties] [SerializableField(11)] - [SaveFlag(nameof(ShouldSerializeMaxChargeTime))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _maxChargeTime; + [SerializableFieldSaveFlag(11)] public bool ShouldSerializeMaxChargeTime() => _maxChargeTime != 0; [EncodedInt] [InvalidateProperties] [SerializableField(12)] - [SaveFlag(nameof(ShouldSerializeChargeTime))] private int _chargeTime; + [SerializableFieldSaveFlag(12)] public bool ShouldSerializeChargeTime() => _chargeTime != 0; [InvalidateProperties] [SerializableField(13)] - [SaveFlag(nameof(ShouldSerializeBlessed))] [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _blessed; + [SerializableFieldSaveFlag(13)] public bool ShouldSerializeBlessed() => _blessed; [InvalidateProperties] [SerializableField(14)] - [SaveFlag(nameof(ShouldSerializeSlayer))] [SerializedCommandProperty(AccessLevel.GameMaster)] private TalismanSlayerName _slayer; + [SerializableFieldSaveFlag(14)] public bool ShouldSerializeSlayer() => _slayer != TalismanSlayerName.None; private BaseCreature _creature; @@ -279,20 +284,26 @@ public partial class BaseTalisman : Item, IAosItem public override int LabelNumber => 1071023; // Talisman public virtual bool ForceShowName => false; // used to override default summoner/removal name - [SerializableField(10, fieldChanged: nameof(OnChargesChanged))] - [SaveFlag(nameof(ShouldSerializeCharges))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _charges; - - private void OnChargesChanged(int oldValue, int newValue) + [SerializableProperty(10)] + [CommandProperty(AccessLevel.GameMaster)] + public int Charges { - if (_chargeTime > 0) + get => _charges; + set { - StartTimer(); + _charges = value; + + if (_chargeTime > 0) + { + StartTimer(); + } + + InvalidateProperties(); + this.MarkDirty(); } } + [SerializableFieldSaveFlag(10)] public bool ShouldSerializeCharges() => _charges != 0; public static void Configure() diff --git a/Projects/UOContent/Items/Weapons/BaseWeapon.cs b/Projects/UOContent/Items/Weapons/BaseWeapon.cs index d48824670..1f03daf63 100644 --- a/Projects/UOContent/Items/Weapons/BaseWeapon.cs +++ b/Projects/UOContent/Items/Weapons/BaseWeapon.cs @@ -56,126 +56,130 @@ public abstract partial class BaseWeapon [InvalidateProperties] [SerializableField(0)] - [SaveFlag(nameof(ShouldSerializeDamageLevel))] [SerializedCommandProperty(AccessLevel.GameMaster)] private WeaponDamageLevel _damageLevel; + [SerializableFieldSaveFlag(0)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldSerializeDamageLevel() => _damageLevel != WeaponDamageLevel.Regular; [InvalidateProperties] [SerializableField(5)] - [SaveFlag(nameof(ShouldSerializeMaxHitPoints))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _maxHitPoints; + [SerializableFieldSaveFlag(5)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldSerializeMaxHitPoints() => _maxHitPoints != 0; [InvalidateProperties] [SerializableField(6)] - [SaveFlag(nameof(ShouldSerializeSlayer))] [SerializedCommandProperty(AccessLevel.GameMaster)] private SlayerName _slayer; + [SerializableFieldSaveFlag(6)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldSerializeSlayer() => _slayer != SlayerName.None; [InvalidateProperties] [SerializableField(7)] - [SaveFlag(nameof(ShouldSerializePoison))] [SerializedCommandProperty(AccessLevel.GameMaster)] private Poison _poison; + [SerializableFieldSaveFlag(7)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldSerializePoison() => _poison != null; [InvalidateProperties] [SerializableField(8)] - [SaveFlag(nameof(ShouldSerializePoisonCharges))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _poisonCharges; + [SerializableFieldSaveFlag(8)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldSerializePoisonCharges() => _poisonCharges > 0; [InvalidateProperties] [SerializableField(9)] - [SaveFlag(nameof(ShouldSerializeCrafter))] [SerializedCommandProperty(AccessLevel.GameMaster)] private string _crafter; + [SerializableFieldSaveFlag(9)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldSerializeCrafter() => !string.IsNullOrEmpty(_crafter); [InvalidateProperties] [SerializableField(10)] - [SaveFlag(nameof(ShouldSerializeIdentified))] [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _identified; + [SerializableFieldSaveFlag(10)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldSerializeIdentified() => _identified; [SerializedIgnoreDupe] [SerializableField(24, setter: "private")] - [SaveFlag(nameof(ShouldSerializeAttributes), nameof(AttributesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosAttributes _attributes; + [SerializableFieldSaveFlag(24)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldSerializeAttributes() => !_attributes.IsEmpty; + [SerializableFieldDefault(24)] private AosAttributes AttributesDefaultValue() => new(this); [SerializedIgnoreDupe] [SerializableField(25, setter: "private")] - [SaveFlag(nameof(ShouldSerializeWeaponAttributes), nameof(WeaponAttributesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosWeaponAttributes _weaponAttributes; + [SerializableFieldSaveFlag(25)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldSerializeWeaponAttributes() => !_weaponAttributes.IsEmpty; + [SerializableFieldDefault(25)] private AosWeaponAttributes WeaponAttributesDefaultValue() => new(this); [SerializedIgnoreDupe] [SerializableField(26, setter: "private")] - [SaveFlag(nameof(ShouldSerializeSkillBonuses), nameof(SkillBonusesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosSkillBonuses _skillBonuses; + [SerializableFieldSaveFlag(26)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldSerializeSkillBonuses() => !_skillBonuses.IsEmpty; + [SerializableFieldDefault(26)] private AosSkillBonuses SkillBonusesDefaultValue() => new(this); [InvalidateProperties] [SerializableField(27)] - [SaveFlag(nameof(ShouldSerializeSlayer2))] [SerializedCommandProperty(AccessLevel.GameMaster)] private SlayerName _slayer2; + [SerializableFieldSaveFlag(27)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldSerializeSlayer2() => _slayer2 != SlayerName.None; [SerializedIgnoreDupe] [SerializableField(28, setter: "private")] - [SaveFlag(nameof(ShouldSerializeElementAttributes), nameof(AosElementAttributesDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosElementAttributes _aosElementDamages; + [SerializableFieldSaveFlag(28)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldSerializeElementAttributes() => !_aosElementDamages.IsEmpty; + [SerializableFieldDefault(28)] private AosElementAttributes AosElementAttributesDefaultValue() => new(this); [InvalidateProperties] [SerializableField(29)] - [SaveFlag(nameof(ShouldSerializeEngravedText))] [SerializedCommandProperty(AccessLevel.GameMaster)] private string _engravedText; + [SerializableFieldSaveFlag(29)] [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldSerializeEngravedText() => !string.IsNullOrEmpty(_engravedText); @@ -282,7 +286,6 @@ public abstract partial class BaseWeapon public bool Consecrated { get; set; } [SerializableProperty(1)] - [SaveFlag(nameof(ShouldSerializeWeaponAccuracy))] [CommandProperty(AccessLevel.GameMaster)] public WeaponAccuracyLevel AccuracyLevel { @@ -318,10 +321,10 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(1)] private bool ShouldSerializeWeaponAccuracy() => _accuracyLevel != WeaponAccuracyLevel.Regular; [SerializableProperty(2)] - [SaveFlag(nameof(ShouldSerializeDurabilityLevel))] [CommandProperty(AccessLevel.GameMaster)] public WeaponDurabilityLevel DurabilityLevel { @@ -336,10 +339,10 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(2)] private bool ShouldSerializeDurabilityLevel() => _durabilityLevel != WeaponDurabilityLevel.Regular; [SerializableProperty(3)] - [SaveFlag(nameof(ShouldSerializeQuality), nameof(QualityDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public WeaponQuality Quality { @@ -354,12 +357,13 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(3)] private bool ShouldSerializeQuality() => _quality != WeaponQuality.Regular; + [SerializableFieldDefault(3)] private WeaponQuality QualityDefaultValue() => WeaponQuality.Regular; [SerializableProperty(4)] - [SaveFlag(nameof(ShouldSerializeHitPoints))] [CommandProperty(AccessLevel.GameMaster)] public int HitPoints { @@ -383,10 +387,10 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(4)] private bool ShouldSerializeHitPoints() => _hitPoints > 0; [SerializableProperty(11)] - [SaveFlag(nameof(ShouldSerializeStrReq), nameof(StrReqDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int StrRequirement { @@ -399,12 +403,13 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(11)] private bool ShouldSerializeStrReq() => _strRequirement != -1; + [SerializableFieldDefault(11)] private int StrReqDefaultValue() => -1; [SerializableProperty(12)] - [SaveFlag(nameof(ShouldSerializeDexReq), nameof(DexReqDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int DexRequirement { @@ -417,12 +422,13 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(12)] private bool ShouldSerializeDexReq() => _dexRequirement != -1; + [SerializableFieldDefault(12)] private int DexReqDefaultValue() => -1; [SerializableProperty(13)] - [SaveFlag(nameof(ShouldSerializeIntReq), nameof(IntReqDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int IntRequirement { @@ -435,12 +441,13 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(13)] private bool ShouldSerializeIntReq() => _intRequirement != -1; + [SerializableFieldDefault(13)] private int IntReqDefaultValue() => -1; [SerializableProperty(14)] - [SaveFlag(nameof(ShouldSerializeMinDamage), nameof(MinDamageDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int MinDamage { @@ -453,12 +460,13 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(14)] private bool ShouldSerializeMinDamage() => _minDamage != -1; + [SerializableFieldDefault(14)] private int MinDamageDefaultValue() => -1; [SerializableProperty(15)] - [SaveFlag(nameof(ShouldSerializeMaxDamage), nameof(MaxDamageDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int MaxDamage { @@ -471,12 +479,13 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(15)] private bool ShouldSerializeMaxDamage() => _maxDamage != -1; + [SerializableFieldDefault(15)] private int MaxDamageDefaultValue() => -1; [SerializableProperty(16)] - [SaveFlag(nameof(ShouldSerializeHitSound), nameof(HitSoundDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int HitSound { @@ -488,12 +497,13 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(16)] private bool ShouldSerializeHitSound() => _hitSound != -1; + [SerializableFieldDefault(16)] private int HitSoundDefaultValue() => -1; [SerializableProperty(17)] - [SaveFlag(nameof(ShouldSerializeMissSound), nameof(MissSoundDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int MissSound { @@ -505,12 +515,13 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(17)] private bool ShouldSerializeMissSound() => _missSound != -1; + [SerializableFieldDefault(17)] private int MissSoundDefaultValue() => -1; [SerializableProperty(18)] - [SaveFlag(nameof(ShouldSerializeSpeed), nameof(SpeedDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public float Speed { @@ -541,12 +552,13 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(18)] private bool ShouldSerializeSpeed() => _speed != -1; + [SerializableFieldDefault(18)] private float SpeedDefaultValue() => -1; [SerializableProperty(19)] - [SaveFlag(nameof(ShouldSerializeMaxRange), nameof(MaxRangeDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public int MaxRange { @@ -559,12 +571,13 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(19)] private bool ShouldSerializeMaxRange() => _maxRange != -1; + [SerializableFieldDefault(19)] private int MaxRangeDefaultValue() => -1; [SerializableProperty(20)] - [SaveFlag(nameof(ShouldSerializeSkill), nameof(SkillNameDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public SkillName Skill { @@ -577,12 +590,13 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(20)] private bool ShouldSerializeSkill() => _skill != (SkillName)(-1); + [SerializableFieldDefault(20)] private SkillName SkillNameDefaultValue() => (SkillName)(-1); [SerializableProperty(21)] - [SaveFlag(nameof(ShouldSerializeType), nameof(TypeDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public WeaponType Type { @@ -594,12 +608,13 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(21)] private bool ShouldSerializeType() => _type != (WeaponType)(-1); + [SerializableFieldDefault(21)] private WeaponType TypeDefaultValue() => (WeaponType)(-1); [SerializableProperty(22)] - [SaveFlag(nameof(ShouldSerializeAnimation), nameof(AnimationDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public WeaponAnimation Animation { @@ -611,12 +626,13 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(22)] private bool ShouldSerializeAnimation() => _animation != (WeaponAnimation)(-1); + [SerializableFieldDefault(22)] private WeaponAnimation AnimationDefaultValue() => (WeaponAnimation)(-1); [SerializableProperty(23)] - [SaveFlag(nameof(ShouldSerializeResource), nameof(ResourceDefaultValue))] [CommandProperty(AccessLevel.GameMaster)] public CraftResource Resource { @@ -632,8 +648,10 @@ public abstract partial class BaseWeapon } } + [SerializableFieldSaveFlag(23)] private bool ShouldSerializeResource() => _resource != CraftResource.Iron; + [SerializableFieldDefault(23)] private CraftResource ResourceDefaultValue() => CraftResource.Iron; public virtual int OnCraft( diff --git a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSpawn.v11.json b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSpawn.v11.json deleted file mode 100644 index defcbe100..000000000 --- a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSpawn.v11.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "version": 11, - "type": "Server.Engines.CannedEvil.ChampionSpawn", - "properties": [ - { - "name": "Level", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "ActivatedByProximity", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "NextProximityTime", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - }, - { - "name": "MaxLevel", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "ActivatedByValor", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "DamageEntries", - "type": "System.Collections.Generic.Dictionary\u003CServer.Mobile, int\u003E", - "rule": "DictionaryMigrationRule", - "ruleArguments": [ - "Server.Mobile", - "SerializableInterfaceMigrationRule", - "0", - "int", - "PrimitiveTypeMigrationRule", - "1", - "" - ] - }, - { - "name": "ConfinedRoaming", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "Idol", - "type": "Server.Engines.CannedEvil.IdolOfTheChampion", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "HasBeenAdvanced", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "SpawnArea", - "type": "Server.Rectangle2D", - "rule": "PrimitiveUOTypeMigrationRule", - "ruleArguments": [ - "Rect2D" - ] - }, - { - "name": "RandomizeType", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "Kills", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "Active", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "Type", - "type": "Server.Engines.CannedEvil.ChampionSpawnType", - "rule": "EnumMigrationRule" - }, - { - "name": "Creatures", - "type": "System.Collections.Generic.List\u003CServer.Mobile\u003E", - "rule": "ListMigrationRule", - "ruleArguments": [ - "@Tidy", - "Server.Mobile", - "SerializableInterfaceMigrationRule" - ] - }, - { - "name": "RedSkulls", - "type": "System.Collections.Generic.List\u003CServer.Item\u003E", - "rule": "ListMigrationRule", - "ruleArguments": [ - "@Tidy", - "Server.Item", - "SerializableInterfaceMigrationRule" - ] - }, - { - "name": "WhiteSkulls", - "type": "System.Collections.Generic.List\u003CServer.Item\u003E", - "rule": "ListMigrationRule", - "ruleArguments": [ - "@Tidy", - "Server.Item", - "SerializableInterfaceMigrationRule" - ] - }, - { - "name": "Platform", - "type": "Server.Engines.CannedEvil.ChampionPlatform", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "Altar", - "type": "Server.Engines.CannedEvil.ChampionAltar", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "ExpireDelay", - "type": "System.TimeSpan", - "rule": "PrimitiveTypeMigrationRule" - }, - { - "name": "ExpireTime", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - }, - { - "name": "Champion", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "RestartDelay", - "type": "System.TimeSpan", - "rule": "PrimitiveTypeMigrationRule" - }, - { - "name": "RestartTime", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Engines.Virtues.VirtueContext.v1.json b/Projects/UOContent/Migrations/Server.Engines.Virtues.VirtueContext.v1.json deleted file mode 100644 index 956de91db..000000000 --- a/Projects/UOContent/Migrations/Server.Engines.Virtues.VirtueContext.v1.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "version": 1, - "type": "Server.Engines.Virtues.VirtueContext", - "properties": [ - { - "name": "LastSacrificeGain", - "type": "System.DateTime", - "usesSaveFlag": true, - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - }, - { - "name": "LastSacrificeLoss", - "type": "System.DateTime", - "usesSaveFlag": true, - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - }, - { - "name": "AvailableResurrects", - "type": "int", - "usesSaveFlag": true, - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "LastJusticeLoss", - "type": "System.DateTime", - "usesSaveFlag": true, - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - }, - { - "name": "LastCompassionLoss", - "type": "System.DateTime", - "usesSaveFlag": true, - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - }, - { - "name": "NextCompassionDay", - "type": "System.DateTime", - "usesSaveFlag": true, - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - }, - { - "name": "CompassionGains", - "type": "int", - "usesSaveFlag": true, - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "LastValorLoss", - "type": "System.DateTime", - "usesSaveFlag": true, - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - }, - { - "name": "LastHonorUse", - "type": "System.DateTime", - "usesSaveFlag": true, - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - }, - { - "name": "HonorActive", - "type": "bool", - "usesSaveFlag": true, - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "JusticeProtection", - "type": "Server.Mobiles.PlayerMobile", - "usesSaveFlag": true, - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "JusticeStatus", - "type": "Server.Engines.Virtues.JusticeProtectorStatus", - "usesSaveFlag": true, - "rule": "EnumMigrationRule" - }, - { - "name": "Values", - "type": "int[]", - "usesSaveFlag": true, - "rule": "ArrayMigrationRule", - "ruleArguments": [ - "int", - "PrimitiveTypeMigrationRule", - "" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Ethics.Player.v2.json b/Projects/UOContent/Migrations/Server.Ethics.Player.v2.json deleted file mode 100644 index 04f4a0199..000000000 --- a/Projects/UOContent/Migrations/Server.Ethics.Player.v2.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "version": 2, - "type": "Server.Ethics.Player", - "properties": [ - { - "name": "Mobile", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "Power", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "History", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "Steed", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "Familiar", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "Shield", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - }, - { - "name": "Ethic", - "type": "Server.Ethics.Ethic", - "rule": "SerializableInterfaceMigrationRule" - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.BaseLight.v2.json b/Projects/UOContent/Migrations/Server.Items.BaseLight.v2.json deleted file mode 100644 index c04a03f3b..000000000 --- a/Projects/UOContent/Migrations/Server.Items.BaseLight.v2.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "version": 2, - "type": "Server.Items.BaseLight", - "properties": [ - { - "name": "BurntOut", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "Burning", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "Duration", - "type": "System.TimeSpan", - "rule": "PrimitiveTypeMigrationRule" - }, - { - "name": "Protected", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "BurnTimer", - "type": "Server.Timer", - "rule": "TimerMigrationRule", - "ruleArguments": [ - "@AnchoredTimer" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.Corpse.v18.json b/Projects/UOContent/Migrations/Server.Items.Corpse.v18.json deleted file mode 100644 index 452e3ba7c..000000000 --- a/Projects/UOContent/Migrations/Server.Items.Corpse.v18.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "version": 18, - "type": "Server.Items.Corpse", - "properties": [ - { - "name": "RestoreEquip", - "type": "System.Collections.Generic.List\u003CServer.Item\u003E", - "rule": "ListMigrationRule", - "ruleArguments": [ - "Server.Item", - "SerializableInterfaceMigrationRule" - ] - }, - { - "name": "Flags", - "type": "Server.Items.CorpseFlag", - "rule": "EnumMigrationRule" - }, - { - "name": "TimeOfDeath", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "DeltaTime" - ] - }, - { - "name": "RestoreTable", - "type": "System.Collections.Generic.Dictionary\u003CServer.Item, Server.Point3D\u003E", - "rule": "DictionaryMigrationRule", - "ruleArguments": [ - "Server.Item", - "SerializableInterfaceMigrationRule", - "0", - "Server.Point3D", - "PrimitiveUOTypeMigrationRule", - "1", - "Point3D" - ] - }, - { - "name": "DecayTimer", - "type": "Server.Timer", - "rule": "TimerMigrationRule", - "ruleArguments": [ - "@AnchoredTimer" - ] - }, - { - "name": "Looters", - "type": "System.Collections.Generic.HashSet\u003CServer.Mobile\u003E", - "rule": "HashSetMigrationRule", - "ruleArguments": [ - "Server.Mobile", - "SerializableInterfaceMigrationRule" - ] - }, - { - "name": "Killer", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "Aggressors", - "type": "System.Collections.Generic.List\u003CServer.Mobile\u003E", - "rule": "ListMigrationRule", - "ruleArguments": [ - "Server.Mobile", - "SerializableInterfaceMigrationRule" - ] - }, - { - "name": "Owner", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "CorpseName", - "type": "string", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "AccessLevel", - "type": "Server.AccessLevel", - "rule": "EnumMigrationRule" - }, - { - "name": "Guild", - "type": "Server.Guilds.Guild", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "EquipItems", - "type": "System.Collections.Generic.List\u003CServer.Item\u003E", - "rule": "ListMigrationRule", - "ruleArguments": [ - "Server.Item", - "SerializableInterfaceMigrationRule" - ] - }, - { - "name": "HairItemId", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "HairHue", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "FacialHairItemId", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "FacialHairHue", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.Corpse.v19.json b/Projects/UOContent/Migrations/Server.Items.Corpse.v19.json deleted file mode 100644 index 2cccb267a..000000000 --- a/Projects/UOContent/Migrations/Server.Items.Corpse.v19.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "version": 19, - "type": "Server.Items.Corpse", - "properties": [ - { - "name": "RestoreEquip", - "type": "System.Collections.Generic.List\u003CServer.Item\u003E", - "rule": "ListMigrationRule", - "ruleArguments": [ - "Server.Item", - "SerializableInterfaceMigrationRule" - ] - }, - { - "name": "Flags", - "type": "Server.Items.CorpseFlag", - "rule": "EnumMigrationRule" - }, - { - "name": "TimeOfDeath", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - }, - { - "name": "RestoreTable", - "type": "System.Collections.Generic.Dictionary\u003CServer.Item, Server.Point3D\u003E", - "rule": "DictionaryMigrationRule", - "ruleArguments": [ - "Server.Item", - "SerializableInterfaceMigrationRule", - "0", - "Server.Point3D", - "PrimitiveUOTypeMigrationRule", - "1", - "Point3D" - ] - }, - { - "name": "DecayTimer", - "type": "Server.Timer", - "rule": "TimerMigrationRule", - "ruleArguments": [ - "@AnchoredTimer" - ] - }, - { - "name": "Looters", - "type": "System.Collections.Generic.HashSet\u003CServer.Mobile\u003E", - "rule": "HashSetMigrationRule", - "ruleArguments": [ - "Server.Mobile", - "SerializableInterfaceMigrationRule" - ] - }, - { - "name": "Killer", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "Aggressors", - "type": "System.Collections.Generic.List\u003CServer.Mobile\u003E", - "rule": "ListMigrationRule", - "ruleArguments": [ - "Server.Mobile", - "SerializableInterfaceMigrationRule" - ] - }, - { - "name": "Owner", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "CorpseName", - "type": "string", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "AccessLevel", - "type": "Server.AccessLevel", - "rule": "EnumMigrationRule" - }, - { - "name": "Guild", - "type": "Server.Guilds.Guild", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "EquipItems", - "type": "System.Collections.Generic.List\u003CServer.Item\u003E", - "rule": "ListMigrationRule", - "ruleArguments": [ - "Server.Item", - "SerializableInterfaceMigrationRule" - ] - }, - { - "name": "HairItemId", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "HairHue", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "FacialHairItemId", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "FacialHairHue", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.DeathRobe.v4.json b/Projects/UOContent/Migrations/Server.Items.DeathRobe.v4.json deleted file mode 100644 index ef81d79cd..000000000 --- a/Projects/UOContent/Migrations/Server.Items.DeathRobe.v4.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": 4, - "type": "Server.Items.DeathRobe", - "properties": [ - { - "name": "DecayTimer", - "type": "Server.Timer", - "rule": "TimerMigrationRule", - "ruleArguments": [ - "@AnchoredTimer" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.DecayedCorpse.v3.json b/Projects/UOContent/Migrations/Server.Items.DecayedCorpse.v3.json deleted file mode 100644 index adcf02ff6..000000000 --- a/Projects/UOContent/Migrations/Server.Items.DecayedCorpse.v3.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": 3, - "type": "Server.Items.DecayedCorpse", - "properties": [ - { - "name": "DecayTimer", - "type": "Server.Timer", - "rule": "TimerMigrationRule", - "ruleArguments": [ - "@AnchoredTimer" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.FillableContainer.v3.json b/Projects/UOContent/Migrations/Server.Items.FillableContainer.v3.json deleted file mode 100644 index 2384676a3..000000000 --- a/Projects/UOContent/Migrations/Server.Items.FillableContainer.v3.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "version": 3, - "type": "Server.Items.FillableContainer", - "properties": [ - { - "name": "ContentType", - "type": "Server.Items.FillableContentType", - "rule": "EnumMigrationRule" - }, - { - "name": "RespawnTimer", - "type": "Server.Timer", - "rule": "TimerMigrationRule", - "ruleArguments": [ - "@AnchoredTimer" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.MarkContainer.v1.json b/Projects/UOContent/Migrations/Server.Items.MarkContainer.v1.json deleted file mode 100644 index 267ca4709..000000000 --- a/Projects/UOContent/Migrations/Server.Items.MarkContainer.v1.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "version": 1, - "type": "Server.Items.MarkContainer", - "properties": [ - { - "name": "AutoLock", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "RelockTimer", - "type": "Server.Items.MarkContainer.InternalTimer", - "rule": "TimerMigrationRule", - "ruleArguments": [ - "@AnchoredTimer" - ] - }, - { - "name": "TargetMap", - "type": "Server.Map", - "rule": "PrimitiveUOTypeMigrationRule", - "ruleArguments": [ - "Map" - ] - }, - { - "name": "Target", - "type": "Server.Point3D", - "rule": "PrimitiveUOTypeMigrationRule", - "ruleArguments": [ - "Point3D" - ] - }, - { - "name": "Description", - "type": "string", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.PuzzleChestSolutionAndTime.v1.json b/Projects/UOContent/Migrations/Server.Items.PuzzleChestSolutionAndTime.v1.json deleted file mode 100644 index 8424276f6..000000000 --- a/Projects/UOContent/Migrations/Server.Items.PuzzleChestSolutionAndTime.v1.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": 1, - "type": "Server.Items.PuzzleChestSolutionAndTime", - "properties": [ - { - "name": "When", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.StarRoomGate.v2.json b/Projects/UOContent/Migrations/Server.Items.StarRoomGate.v2.json deleted file mode 100644 index 0a61f7bfe..000000000 --- a/Projects/UOContent/Migrations/Server.Items.StarRoomGate.v2.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "version": 2, - "type": "Server.Items.StarRoomGate", - "properties": [ - { - "name": "Decays", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "DecayTime", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.TransientItem.v2.json b/Projects/UOContent/Migrations/Server.Items.TransientItem.v2.json deleted file mode 100644 index 3745c4428..000000000 --- a/Projects/UOContent/Migrations/Server.Items.TransientItem.v2.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": 2, - "type": "Server.Items.TransientItem", - "properties": [ - { - "name": "Expiration", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.TreasureMapChest.v4.json b/Projects/UOContent/Migrations/Server.Items.TreasureMapChest.v4.json deleted file mode 100644 index b4d1f85d4..000000000 --- a/Projects/UOContent/Migrations/Server.Items.TreasureMapChest.v4.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "version": 4, - "type": "Server.Items.TreasureMapChest", - "properties": [ - { - "name": "Guardians", - "type": "System.Collections.Generic.List\u003CServer.Mobile\u003E", - "rule": "ListMigrationRule", - "ruleArguments": [ - "@Tidy", - "@CanBeNull", - "Server.Mobile", - "SerializableInterfaceMigrationRule" - ] - }, - { - "name": "Temporary", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "Owner", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "Level", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "ExpireTimer", - "type": "Server.Timer", - "rule": "TimerMigrationRule", - "ruleArguments": [ - "@AnchoredTimer" - ] - }, - { - "name": "Lifted", - "type": "System.Collections.Generic.HashSet\u003CServer.Item\u003E", - "rule": "HashSetMigrationRule", - "ruleArguments": [ - "@Tidy", - "@CanBeNull", - "Server.Item", - "SerializableInterfaceMigrationRule" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Mobiles.BaseEscortable.v3.json b/Projects/UOContent/Migrations/Server.Mobiles.BaseEscortable.v3.json deleted file mode 100644 index 5149f4347..000000000 --- a/Projects/UOContent/Migrations/Server.Mobiles.BaseEscortable.v3.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "version": 3, - "type": "Server.Mobiles.BaseEscortable", - "properties": [ - { - "name": "DestinationString", - "type": "string", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "DeleteTimer", - "type": "Server.Timer", - "rule": "TimerMigrationRule", - "ruleArguments": [ - "@AnchoredTimer" - ] - }, - { - "name": "MlQuestType", - "type": "System.Type", - "rule": "PrimitiveTypeMigrationRule" - }, - { - "name": "MlQuestDestinationMessage", - "type": "Server.TextDefinition", - "rule": "PrimitiveUOTypeMigrationRule", - "ruleArguments": [ - "TextDefinition" - ] - }, - { - "name": "MlQuestPaymentMessage", - "type": "Server.TextDefinition", - "rule": "PrimitiveUOTypeMigrationRule", - "ruleArguments": [ - "TextDefinition" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Mobiles.PlayerVendor.v4.json b/Projects/UOContent/Migrations/Server.Mobiles.PlayerVendor.v4.json deleted file mode 100644 index 6b9c9eb1c..000000000 --- a/Projects/UOContent/Migrations/Server.Mobiles.PlayerVendor.v4.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "version": 4, - "type": "Server.Mobiles.PlayerVendor", - "properties": [ - { - "name": "ShopName", - "type": "string", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "NextPayTime", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - }, - { - "name": "House", - "type": "Server.Multis.BaseHouse", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "Owner", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "BankAccount", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "HoldGold", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "SellItems", - "type": "System.Collections.Generic.Dictionary\u003CServer.Item, Server.Mobiles.VendorItem\u003E", - "rule": "DictionaryMigrationRule", - "ruleArguments": [ - "Server.Item", - "SerializableInterfaceMigrationRule", - "0", - "Server.Mobiles.VendorItem", - "RawSerializableMigrationRule", - "1", - "" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Mobiles.RentedVendor.v1.json b/Projects/UOContent/Migrations/Server.Mobiles.RentedVendor.v1.json deleted file mode 100644 index b42fa33eb..000000000 --- a/Projects/UOContent/Migrations/Server.Mobiles.RentedVendor.v1.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "version": 1, - "type": "Server.Mobiles.RentedVendor", - "properties": [ - { - "name": "RentalDurationId", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "RentalPrice", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "LandlordRenew", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "RenterRenew", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "RenewalPrice", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "RentalGold", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "RentalExpireTime", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Mobiles.Sheep.v1.json b/Projects/UOContent/Migrations/Server.Mobiles.Sheep.v1.json deleted file mode 100644 index bd224f77f..000000000 --- a/Projects/UOContent/Migrations/Server.Mobiles.Sheep.v1.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": 1, - "type": "Server.Mobiles.Sheep", - "properties": [ - { - "name": "NextWoolTime", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Multis.BaseBoat.v5.json b/Projects/UOContent/Migrations/Server.Multis.BaseBoat.v5.json deleted file mode 100644 index 140d3dd64..000000000 --- a/Projects/UOContent/Migrations/Server.Multis.BaseBoat.v5.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "version": 5, - "type": "Server.Multis.BaseBoat", - "properties": [ - { - "name": "MapItem", - "type": "Server.Items.MapItem", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "NextNavPoint", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "Facing", - "type": "Server.Direction", - "rule": "EnumMigrationRule" - }, - { - "name": "TimeOfDecay", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - }, - { - "name": "Owner", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "PPlank", - "type": "Server.Items.Plank", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "SPlank", - "type": "Server.Items.Plank", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "TillerMan", - "type": "Server.Items.TillerMan", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "Hold", - "type": "Server.Items.Hold", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "Anchored", - "type": "bool", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "ShipName", - "type": "string", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Multis.BaseCamp.v2.json b/Projects/UOContent/Migrations/Server.Multis.BaseCamp.v2.json deleted file mode 100644 index 471c645f1..000000000 --- a/Projects/UOContent/Migrations/Server.Multis.BaseCamp.v2.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "version": 2, - "type": "Server.Multis.BaseCamp", - "properties": [ - { - "name": "Items", - "type": "System.Collections.Generic.List\u003CServer.Item\u003E", - "rule": "ListMigrationRule", - "ruleArguments": [ - "@Tidy", - "Server.Item", - "SerializableInterfaceMigrationRule" - ] - }, - { - "name": "Mobiles", - "type": "System.Collections.Generic.List\u003CServer.Mobile\u003E", - "rule": "ListMigrationRule", - "ruleArguments": [ - "@Tidy", - "Server.Mobile", - "SerializableInterfaceMigrationRule" - ] - }, - { - "name": "DecayTime", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Spells.Fifth.PoisonField.v1.json b/Projects/UOContent/Migrations/Server.Spells.Fifth.PoisonField.v1.json deleted file mode 100644 index 1f16a4d7f..000000000 --- a/Projects/UOContent/Migrations/Server.Spells.Fifth.PoisonField.v1.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "version": 1, - "type": "Server.Spells.Fifth.PoisonField", - "properties": [ - { - "name": "Caster", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "End", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Spells.Fourth.FireFieldItem.v1.json b/Projects/UOContent/Migrations/Server.Spells.Fourth.FireFieldItem.v1.json deleted file mode 100644 index 3e91c7a4d..000000000 --- a/Projects/UOContent/Migrations/Server.Spells.Fourth.FireFieldItem.v1.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "version": 1, - "type": "Server.Spells.Fourth.FireFieldItem", - "properties": [ - { - "name": "Damage", - "type": "int", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "" - ] - }, - { - "name": "Caster", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "End", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Spells.Seventh.EnergyField.v2.json b/Projects/UOContent/Migrations/Server.Spells.Seventh.EnergyField.v2.json deleted file mode 100644 index dd202da03..000000000 --- a/Projects/UOContent/Migrations/Server.Spells.Seventh.EnergyField.v2.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "version": 2, - "type": "Server.Spells.Seventh.EnergyField", - "properties": [ - { - "name": "Caster", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "End", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Spells.Sixth.ParalyzeField.v1.json b/Projects/UOContent/Migrations/Server.Spells.Sixth.ParalyzeField.v1.json deleted file mode 100644 index 3e834a6b1..000000000 --- a/Projects/UOContent/Migrations/Server.Spells.Sixth.ParalyzeField.v1.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "version": 1, - "type": "Server.Spells.Sixth.ParalyzeField", - "properties": [ - { - "name": "Caster", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "End", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Spells.Third.WallOfStone.v1.json b/Projects/UOContent/Migrations/Server.Spells.Third.WallOfStone.v1.json deleted file mode 100644 index eafcc5945..000000000 --- a/Projects/UOContent/Migrations/Server.Spells.Third.WallOfStone.v1.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "version": 1, - "type": "Server.Spells.Third.WallOfStone", - "properties": [ - { - "name": "Caster", - "type": "Server.Mobile", - "rule": "SerializableInterfaceMigrationRule" - }, - { - "name": "End", - "type": "System.DateTime", - "rule": "PrimitiveTypeMigrationRule", - "ruleArguments": [ - "AnchoredTime" - ] - } - ] -} \ No newline at end of file diff --git a/Projects/UOContent/Misc/ShardPoller.cs b/Projects/UOContent/Misc/ShardPoller.cs index 8516e6678..762a0be9c 100644 --- a/Projects/UOContent/Misc/ShardPoller.cs +++ b/Projects/UOContent/Misc/ShardPoller.cs @@ -36,14 +36,16 @@ public partial class ShardPoller : Item Movable = false; } - [SerializableField(0, allowFieldChange: nameof(AllowTitleChange))] - [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] - private string _title; - - private bool AllowTitleChange(ref string value) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] + public string Title { - value = ShardPollPrompt.UrlToHref(value); - return true; + get => _title; + set + { + _title = ShardPollPrompt.UrlToHref(value); + this.MarkDirty(); + } } [CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] @@ -52,20 +54,31 @@ public partial class ShardPoller : Item ? TimeSpan.Zero : Utility.Max(StartTime + Duration - Core.Now, TimeSpan.Zero); - [SerializableField(3, fieldChanged: nameof(OnActiveChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] - private bool _active; - - private void OnActiveChanged(bool oldValue, bool newValue) + [SerializableProperty(3)] + [CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] + public bool Active { - if (_active) + get => _active; + set { - StartTime = Core.Now; - _activePollers.Add(this); - } - else - { - _activePollers.Remove(this); + if (_active == value) + { + return; + } + + _active = value; + + if (_active) + { + StartTime = Core.Now; + _activePollers.Add(this); + } + else + { + _activePollers.Remove(this); + } + + this.MarkDirty(); } } @@ -237,12 +250,15 @@ public partial class ShardPollOption } } - [SerializableField(0, fieldChanged: nameof(OnTitleChanged))] - private string _title; - - private void OnTitleChanged(string oldValue, string newValue) + [SerializableProperty(0)] + public string Title { - _lineBreaks = -1; + get => _title; + set + { + _title = value; + _lineBreaks = -1; + } } public int Votes => Voters.Length; diff --git a/Projects/UOContent/Mobiles/AI/AnimalAI.cs b/Projects/UOContent/Mobiles/AI/AnimalAI.cs index e5a1670e3..a9d410483 100644 --- a/Projects/UOContent/Mobiles/AI/AnimalAI.cs +++ b/Projects/UOContent/Mobiles/AI/AnimalAI.cs @@ -38,7 +38,7 @@ public class AnimalAI : BaseAI return true; } - if (!WalkMobileRange(combatant, 1, Mobile.RangeFight, Mobile.RangeFight)) + if (!WalkMobileRange(combatant, 1, false, Mobile.RangeFight, Mobile.RangeFight)) { if (Mobile.GetDistanceToSqrt(combatant) > Mobile.RangePerception + 1) { diff --git a/Projects/UOContent/Mobiles/AI/ArcherAI.cs b/Projects/UOContent/Mobiles/AI/ArcherAI.cs index 803587ceb..dd461a03f 100644 --- a/Projects/UOContent/Mobiles/AI/ArcherAI.cs +++ b/Projects/UOContent/Mobiles/AI/ArcherAI.cs @@ -17,7 +17,7 @@ public class ArcherAI : BaseAI if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true)) { - this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name}, attacking"); + this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name} and I will attack"); Mobile.Combatant = Mobile.FocusMob; Action = ActionType.Combat; @@ -43,11 +43,11 @@ public class ArcherAI : BaseAI return true; } - if (!WalkMobileRange(combatant, 1, Mobile.RangeFight, Mobile.Weapon.MaxRange)) + if (!WalkMobileRange(combatant, 1, false, Mobile.RangeFight, Mobile.Weapon.MaxRange)) { this.DebugSayFormatted($"I am still not in range of {combatant.Name}"); - if (!Mobile.InRange(combatant, Mobile.ChaseLeashRange)) + if ((int)Mobile.GetDistanceToSqrt(combatant) > Mobile.RangePerception + 1) { this.DebugSayFormatted($"I have lost {combatant.Name}"); diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/AIGroupMovement.cs b/Projects/UOContent/Mobiles/AI/BaseAI/AIGroupMovement.cs index 4463bf4de..08a745296 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/AIGroupMovement.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/AIGroupMovement.cs @@ -36,34 +36,12 @@ public abstract partial class BaseAI } } - /// - /// Crowding refinement for the final approach: engages only near the target when allies - /// contest the ring, so creatures spread instead of stacking. Chasing any real distance - /// always uses the pathfinding approach primitive. - /// - private bool UseGroupMovement(Mobile target, int range) => + private bool UseGroupMovement(Mobile target) => Mobile.Combatant == target && !Mobile.Controlled - && Mobile.InRange(target, range + 2) - && CountCrowdingAllies(target, range) > 0; + && CountNearbyAllies(target) > 0; - private int CountCrowdingAllies(Mobile target, int range) - { - var crowding = 0; - - foreach (var m in target.GetMobilesInRange(range + 1)) - { - if (m != Mobile && m.Combatant == target && m is BaseCreature { Controlled: false } bc - && bc.Team == Mobile.Team) - { - crowding++; - } - } - - return crowding; - } - - public static bool MoveToWithGroup(BaseAI ai, Mobile target, int range) + public static bool MoveToWithGroup(BaseAI ai, Mobile target, bool run, int range) { if (Core.TickCount - _lastGroupUpdateTime > 1000) { @@ -78,8 +56,7 @@ public abstract partial class BaseAI { if (optimalPosition == Point3D.Zero) { - - return ai.MoveToWithCollisionAvoidance(target, range); + return ai.MoveToWithCollisionAvoidance(target, run, range); } _reservedPositions[mobile] = optimalPosition; @@ -91,15 +68,7 @@ public abstract partial class BaseAI direction = GetAdjustedDirection(direction); } - var res = ai.DoMoveImpl(direction, true); - - if (res is MoveResult.Success or MoveResult.BadState) - { - return true; - } - - // A blocked or wall-slid step is not progress — route around the obstacle. - return ai.ApproachTarget(target, range); + return ai.DoMove(direction, true); } finally { @@ -107,6 +76,21 @@ public abstract partial class BaseAI } } + private int CountNearbyAllies(Mobile target) + { + var allies = 0; + foreach (var m in Mobile.GetMobilesInRange(8)) + { + if (m != Mobile && m.Combatant == target && m is BaseCreature { Controlled: false } bc + && bc.Team == Mobile.Team) + { + allies++; + } + } + + return allies; + } + private PooledRefList GetNearbyAllies(Mobile target) { var allies = PooledRefList.Create(); diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/AIMovement.cs b/Projects/UOContent/Mobiles/AI/BaseAI/AIMovement.cs index 8b2a16cf2..1063dd70a 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/AIMovement.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/AIMovement.cs @@ -13,12 +13,10 @@ * along with this program. If not, see . * ************************************************************************/ -using System; using System.Runtime.CompilerServices; using Server.Collections; using Server.Items; using MoveImpl = Server.Movement.MovementImpl; -using Moves = Server.Movement.Movement; namespace Server.Mobiles; @@ -39,17 +37,7 @@ public abstract partial class BaseAI private bool _approachGaveUp; private Point3D _approachGaveUpGoalLoc; - // --- Move intent (see ContinueMove) ------------------------------------------------ - // Durable movement goal renewed by en-route ApproachTarget/MoveToPoint calls; while - // live, the AITimer wakes at NextMove between think ticks to advance the step. - private Mobile _moveIntentTarget; - private IPoint3D _moveIntentPoint; - private int _moveIntentRange; - private long _moveIntentExpire; - - // Inflates a step delay while badly hurt; computed from the passed base so it cannot - // compound across steps. Damage slows steps, never decisions. - public static double BadlyHurtMoveDelay(BaseCreature bc, double delay) + public static double BadlyHurtMoveDelay(BaseCreature bc) { var statMin = Core.HS ? bc.Stam : bc.Hits; var statMax = Core.HS ? bc.StamMax : bc.HitsMax; @@ -57,59 +45,20 @@ public abstract partial class BaseAI if (!bc.IsDeadPet && (bc.ReduceSpeedWithDamage || bc.IsSubdued) && statMax > 0 && statMin < statMax * 0.3) { - var stat = (double)statMin / statMax; + var hits = (double)statMin / statMax; - if (stat < 0.1) { return delay + 0.15; } - if (stat < 0.2) { return delay + 0.1; } - - return delay + 0.05; + if (hits < 0.1) { return bc.CurrentSpeed + 0.15; } + if (hits < 0.2) { return bc.CurrentSpeed + 0.1; } + if (hits < 0.3) { return bc.CurrentSpeed + 0.05; } } - return delay; + return bc.CurrentSpeed; } public bool CanMoveNow(out double delay) { delay = 0.0; - return Core.TickCount - NextMove >= 0; - } - - // Seconds per step as the client observes it: the move clock plus the hurt inflation. - private double EffectiveStepDelay() - { - var stepDelay = Mobile.CurrentMoveSpeed; - - return Core.AOS && IsFollowingMaster() ? stepDelay : BadlyHurtMoveDelay(Mobile, stepDelay); - } - - // The Running bit only selects the client's per-step interpolation (walk 400ms / run - // 200ms on foot, 200/100 mounted). A step shorter than the walk time must run or the - // client falls behind and snaps — but an isolated step (after standing at least a walk - // interval) renders alone and darts if run-flagged, so it goes out as a walk. A true - // sprinter always runs: a walk-rendered first step would flood the client's queue. - public bool ShouldRun() - { - var mounted = Mobile.Mounted || Mobile.Flying; - var walkDelay = mounted ? Moves.WalkMountDelay : Moves.WalkFootDelay; - var pace = EffectiveStepDelay() * 1000; - - if (pace >= walkDelay) - { - return false; - } - - var runDelay = mounted ? Moves.RunMountDelay : Moves.RunFootDelay; - - return pace < runDelay || Core.TickCount - Mobile.LastMoveTime < walkDelay; - } - - // One step per period, paced from the step just taken — no debt accrual: repaying a - // late step with a quicker follow-up puts two steps ~100ms apart, which renders as a - // dart. In continuous pursuit the move-wake lands within wheel resolution of this - // deadline, so the only cost is single-digit-ms drift per step. - private void ConsumeMoveBudget() - { - NextMove = Core.TickCount + Math.Max(50, (long)(EffectiveStepDelay() * 1000)); + return Core.TickCount >= NextMove; } public virtual bool CheckMove() => !(Mobile.Deleted || Mobile.DisallowAllMoves); @@ -127,8 +76,6 @@ public abstract partial class BaseAI return MoveResult.BadState; } - d = (d & Direction.Mask) | (ShouldRun() ? Direction.Running : 0); - if ((Mobile.Direction & Direction.Mask) != (d & Direction.Mask)) { Mobile.Direction = d; @@ -139,20 +86,22 @@ public abstract partial class BaseAI if (TryMove(d)) { - // Obeying pets are paced by their order handlers. - if (!IsObeyingMoveOrder()) + if (Core.AOS && IsFollowingMaster()) { - if (Mobile.Warmode || Mobile.Combatant != null) - { - Mobile.SetCurrentSpeedToActive(); - } - else - { - Mobile.SetCurrentSpeedToPassive(); - } + Mobile.CurrentSpeed = 0.1; + } + else if (Mobile.Hits < Mobile.HitsMax * 0.3) + { + Mobile.CurrentSpeed = BadlyHurtMoveDelay(Mobile); + } + else if (Mobile.Warmode || Mobile.Combatant != null) + { + Mobile.CurrentSpeed = Mobile.ActiveSpeed; + } + else + { + Mobile.CurrentSpeed = Mobile.PassiveSpeed; } - - ConsumeMoveBudget(); return MoveResult.Success; } @@ -202,7 +151,6 @@ public abstract partial class BaseAI if (Mobile.Move(Mobile.Direction)) { - ConsumeMoveBudget(); return MoveResult.SuccessAutoTurn; } } @@ -355,18 +303,16 @@ public abstract partial class BaseAI /// best-distance stall counter idles the creature if an in-range goal is genuinely /// unreachable, without ever abandoning a real chase or detour. /// - protected bool ApproachTarget(Mobile target, int range) + protected bool ApproachTarget(Mobile target, bool run, int range) { if (Mobile.Deleted || Mobile.DisallowAllMoves || target?.Deleted != false) { - ClearMoveIntent(); return false; } if (Mobile.InRange(target, range)) { ResetApproach(); - ClearMoveIntent(); return true; } @@ -375,15 +321,12 @@ public abstract partial class BaseAI { if (target.Location == _approachGaveUpGoalLoc) { - ClearMoveIntent(); return false; } ResetApproach(); // target moved — try again fresh } - RenewMoveIntent(target, null, range); - // FAST PATH: greedy step toward the target, counted as success ONLY when the move // fully succeeded (not an auto-turn sidestep) and actually got us closer. An // auto-turn sidestep can reduce Euclidean distance while moving in the wrong @@ -394,87 +337,35 @@ public abstract partial class BaseAI if (Path == null && Mobile.InLOS(target)) { var distBefore = Mobile.GetDistanceToSqrt(target); - var res = DoMoveImpl(Mobile.GetDirectionTo(target), true); + var res = DoMoveImpl(Mobile.GetDirectionTo(target, run), true); if (res == MoveResult.BadState) { - return true; // not allowed to move this tick (frozen/casting/throttled); not a failure + return false; // not allowed to move this tick; not a stall } if (res == MoveResult.Success && Mobile.GetDistanceToSqrt(target) < distBefore) { - ResetApproach(); - return true; // healthy en-route progress + return Mobile.InRange(target, range); } - // else: fall through; let the PathFollower route around the obstacle. } // PLANNING PATH: a persistent PathFollower, never discarded by a greedy step. if (Path == null || Path.Goal != target) { - Path = new PathFollower(Mobile, target) { Mover = DoMoveImpl }; } - // Sample move-eligibility BEFORE the attempt: a successful step consumes the move - // budget, which would mask stall accounting and the progress signal. - var couldMove = CanMoveNow(out _) && !IsInBadState(); - var locBefore = Mobile.Location; - - if (Path.Follow(range)) + if (Path.Follow(run, range)) { ResetApproach(); return true; } - TrackApproachProgress(target, couldMove); - - // En-route progress is success; failure only when a move-eligible tick took no step - // (no working path), or the approach has given up. - var progressed = !_approachGaveUp && (Mobile.Location != locBefore || !couldMove); - - return progressed; - } - - /// - /// Walks toward a fixed point (e.g. a target's last-known position), pathfinding around - /// obstacles. Returns false on arrival or when genuinely unable to make progress. - /// - public bool MoveToPoint(IPoint3D goal) - { - if (Mobile.Deleted || Mobile.DisallowAllMoves || goal == null) - { - ClearMoveIntent(); - return false; - } - - if (Path?.Goal != goal) - { - Path = new PathFollower(Mobile, goal) { Mover = DoMoveImpl }; - } - - RenewMoveIntent(null, goal, 1); - - var couldMove = CanMoveNow(out _) && !IsInBadState(); - var locBefore = Mobile.Location; - - if (Path.Follow(1)) - { - Path = null; - ClearMoveIntent(); - return false; // arrived - } - - var progressed = Mobile.Location != locBefore || !couldMove; - - if (!progressed) - { - ClearMoveIntent(); - } - - return progressed; + TrackApproachProgress(target); + return false; } /// @@ -485,11 +376,11 @@ public abstract partial class BaseAI /// gives up and idles. A MOVING goal (an active chase) resets the baseline every tick, /// so chases never give up even when the gap holds constant. /// - private void TrackApproachProgress(Mobile target, bool couldMove) + private void TrackApproachProgress(Mobile target) { - if (!couldMove) + if (!CanMoveNow(out _)) { - return; // a tick that was never allowed to move (stun, stall) is not a stall + return; // a not-yet-due move (stun) is not a stall } var dist = Mobile.GetDistanceToSqrt(target); @@ -520,7 +411,6 @@ public abstract partial class BaseAI _approachGaveUp = true; _approachGaveUpGoalLoc = goalLoc; Path = null; - ClearMoveIntent(); } } @@ -536,73 +426,30 @@ public abstract partial class BaseAI _approachGaveUp = false; } - private void RenewMoveIntent(Mobile target, IPoint3D point, int range) - { - _moveIntentTarget = target; - _moveIntentPoint = point; - _moveIntentRange = range; - - // A live pursuit renews every think tick; unrenewed intent dies on its own. - _moveIntentExpire = Core.TickCount + (long)(Mobile.CurrentSpeed * 2000) + 250; - } - - public void ClearMoveIntent() - { - _moveIntentTarget = null; - _moveIntentPoint = null; - } - - /// - /// True while a durable movement goal is live; is the tick - /// the movement budget elapses. - /// - public bool TryGetMoveWake(out long nextMove) - { - nextMove = NextMove; - - return (_moveIntentTarget != null || _moveIntentPoint != null) && Core.TickCount - _moveIntentExpire < 0; - } - - /// - /// Advances the current pursuit/investigation by one step on a movement-clock wake; - /// no decisions run. - /// - public void ContinueMove() - { - if (!TryGetMoveWake(out var nextMove) || Core.TickCount - nextMove < 0) - { - return; - } - - if (_moveIntentTarget != null) - { - ApproachTarget(_moveIntentTarget, _moveIntentRange); - } - else - { - MoveToPoint(_moveIntentPoint); - } - } - - public virtual bool MoveTo(Mobile m, int range) + public virtual bool MoveTo(Mobile m, bool run, int range) { if (Mobile.Deleted || Mobile.DisallowAllMoves || m?.Deleted != false) { return false; } + var distance = (int)Mobile.GetDistanceToSqrt(m); + var distanceThreshold = Core.AOS && IsFollowingMaster() ? 1 : 5; + + var shouldRun = run && distance > distanceThreshold; + if (Mobile.InRange(m, range)) { ResetApproach(); return true; } - if (UseGroupMovement(m, range)) + if (UseGroupMovement(m)) { - return MoveToWithGroup(this, m, range); + return MoveToWithGroup(this, m, shouldRun, range); } - return ApproachTarget(m, range); + return ApproachTarget(m, shouldRun, range); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -612,22 +459,15 @@ public abstract partial class BaseAI Mobile.ControlTarget == Mobile.ControlMaster && Mobile.Combatant == null; - // A pet executing a movement order outside combat; its order handler owns its speed. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool IsObeyingMoveOrder() => - Mobile.Controlled && - Mobile.Combatant == null && - Mobile.ControlOrder is OrderType.Come or OrderType.Follow or OrderType.Guard; - - private bool MoveToWithCollisionAvoidance(Mobile target, int range) + private bool MoveToWithCollisionAvoidance(Mobile target, bool run, int range) { + var distance = (int)Mobile.GetDistanceToSqrt(target); + + var shouldRun = run && distance > 5; + var direction = Mobile.GetDirectionTo(target); - // Wall-slide auto-turns must not count as progress, or a creature pinned on - // geometry reports success forever. - var res = DoMoveImpl(direction, true); - - if (res is MoveResult.Success or MoveResult.BadState) + if (DoMove(direction, true)) { return true; } @@ -636,14 +476,14 @@ public abstract partial class BaseAI { var clockwise = (Direction)(((int)direction + i) % 8); - if (DoMoveImpl(clockwise, true) == MoveResult.Success) + if (DoMove(clockwise, true)) { return true; } var counterclockwise = (Direction)(((int)direction - i + 8) % 8); - if (DoMoveImpl(counterclockwise, true) == MoveResult.Success) + if (DoMove(counterclockwise, true)) { return true; } @@ -651,10 +491,10 @@ public abstract partial class BaseAI // Tactical sidesteps exhausted — route around the obstacle via the centralized // approach primitive (persistent PathFollower, no oscillation). - return ApproachTarget(target, range); + return ApproachTarget(target, shouldRun, range); } - public virtual bool WalkMobileRange(Mobile m, int iSteps, int iWantDistMin, int iWantDistMax) + public virtual bool WalkMobileRange(Mobile m, int iSteps, bool run, int iWantDistMin, int iWantDistMax) { if (Mobile.Deleted || Mobile.DisallowAllMoves || m == null) { @@ -665,12 +505,14 @@ public abstract partial class BaseAI { var iCurrDist = (int)Mobile.GetDistanceToSqrt(m); + var shouldRun = run && iCurrDist > 5; + if (iCurrDist >= iWantDistMin && iCurrDist <= iWantDistMax) { return true; } - if (!MoveTowardsOrAwayFrom(m, iCurrDist, iWantDistMax)) + if (!MoveTowardsOrAwayFrom(m, shouldRun, iCurrDist, iWantDistMax)) { return false; } @@ -681,16 +523,18 @@ public abstract partial class BaseAI return dist >= iWantDistMin && dist <= iWantDistMax; } - private bool MoveTowardsOrAwayFrom(Mobile m, int iCurrDist, int iWantDistMax) + private bool MoveTowardsOrAwayFrom(Mobile m, bool run, int iCurrDist, int iWantDistMax) { + var shouldRun = run && iCurrDist > 5; + if (iCurrDist > iWantDistMax) { // Too far: approach via the centralized progress-based primitive. - return ApproachTarget(m, iWantDistMax); + return ApproachTarget(m, shouldRun, iWantDistMax); } // Too close: back away. Retreat keeps the simple greedy behavior (out of scope). - if (DoMove(m.GetDirectionTo(Mobile), true)) + if (DoMove(m.GetDirectionTo(Mobile, shouldRun), true)) { Path = null; return true; diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/AITimer.cs b/Projects/UOContent/Mobiles/AI/BaseAI/AITimer.cs index e11268e87..3c08d944b 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/AITimer.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/AITimer.cs @@ -17,116 +17,26 @@ using System; namespace Server.Mobiles; -/// -/// Drives an AI on two clocks: decisions at , plus -/// move-only wakes at while a pursuit is live. Each tick -/// schedules the earlier of the two deadlines. -/// public sealed class AITimer : Timer { private readonly BaseAI _owner; - private long _nextThink; - private long _nextWake; // when the pending wheel entry fires - private bool _inTick; private int _detectHiddenMinDelay; private int _detectHiddenMaxDelay; - // The initial delay is irrelevant: Activate is the only start path and sets its own. - public AITimer(BaseAI owner) : base(TimeSpan.Zero, TimeSpan.FromSeconds(owner.Mobile.CurrentSpeed)) + public AITimer(BaseAI owner) : base(TimeSpan.FromMilliseconds(Utility.Random(3000)), + TimeSpan.FromSeconds(owner.Mobile.CurrentSpeed)) { _owner = owner; _owner._nextDetectHidden = Core.TickCount; - _nextThink = Core.TickCount; } public void Activate() { - _nextThink = Core.TickCount; - - if (Running) - { - return; - } - - // Short random spread: the creature responds within a think while a sector's - // worth of timers avoids a same-tick burst; the idle think jitter keeps the - // cohort apart from there. - Delay = TimeSpan.FromMilliseconds(Utility.Random(256)); + Interval = TimeSpan.FromSeconds(_owner.Mobile.CurrentSpeed); Start(); - _nextWake = Core.TickCount + (long)Delay.TotalMilliseconds; - } - - // Think now. A think grants no action: steps, swings, casts, and abilities keep their own gates. - public void Prod() - { - _nextThink = Core.TickCount; - - if (Running) - { - Reschedule(); - return; - } - - Delay = TimeSpan.Zero; - Start(); - _nextWake = Core.TickCount + (long)Delay.TotalMilliseconds; - } - - // A speed-up must not wait out a stale, longer think deadline. - public void OnSpeedChanged() - { - var candidate = Core.TickCount + (long)(_owner.Mobile.CurrentSpeed * 1000); - - if (candidate - _nextThink < 0) - { - _nextThink = candidate; - Reschedule(); - } - } - - // Moves the pending wake earlier. Interval is only read after the next fire, - // so this needs Stop, Delay = remaining, Start. - private void Reschedule() - { - if (_inTick || !Running) - { - return; // ScheduleNext handles it at tick end - } - - var now = Core.TickCount; - var deadline = _nextThink; - - if (_owner.TryGetMoveWake(out var nextMove) && nextMove - now > 0 && nextMove - deadline < 0) - { - deadline = nextMove; - } - - if (deadline - _nextWake >= 0) - { - return; // pending wake is already early enough - } - - Stop(); - Delay = TimeSpan.FromMilliseconds(Math.Max(0, deadline - now)); - Start(); - _nextWake = now + (long)Delay.TotalMilliseconds; } protected override void OnTick() - { - _inTick = true; - - try - { - OnTickCore(); - } - finally - { - _inTick = false; - } - } - - private void OnTickCore() { if (ShouldStop()) { @@ -134,64 +44,23 @@ public sealed class AITimer : Timer return; } - if (Core.TickCount - _nextThink >= 0) + _owner.Mobile.OnThink(); + + if (ShouldStop()) { - _owner.Mobile.OnThink(); - - if (ShouldStop()) - { - Stop(); - return; - } - - HandleBardEffects(); - - if (_owner.Mobile.Controlled ? _owner.Obey() : _owner.Think()) - { - HandleDetectHidden(); - } - - // Cadence from the post-decision speed (decisions may flip active/passive). - var period = (long)(_owner.Mobile.CurrentSpeed * 1000); - _nextThink = Core.TickCount + period; - - // Idle cadence drifts: a zero-mean jitter random-walks think phases apart, so - // creatures spawned or woken together cannot stay in lock-step (a one-shot - // spread can collide and identical periods never separate). Engaged cadence - // stays exact — pursuit timing anchors to real step times. - if (_owner.Mobile.CurrentSpeed == _owner.Mobile.PassiveSpeed) - { - var jitter = (int)(period >> 3); - _nextThink += Utility.RandomMinMax(-jitter, jitter); - } - } - else - { - _owner.ContinueMove(); + Stop(); + return; } - ScheduleNext(); - } + Interval = TimeSpan.FromSeconds(_owner.Mobile.CurrentSpeed); + HandleBardEffects(); - private void ScheduleNext() - { - var now = Core.TickCount; - var delay = _nextThink - now; - - if (_owner.TryGetMoveWake(out var nextMove)) + if (_owner.Mobile.Controlled ? !_owner.Obey() : !_owner.Think()) { - var moveDelay = nextMove - now; - - // Only a future budget is a wake — a blocked creature must not spin the timer. - if (moveDelay > 0 && moveDelay < delay) - { - delay = moveDelay; - } + return; } - // The wheel rounds up to its 8ms resolution; a non-positive delay becomes one turn. - Interval = TimeSpan.FromMilliseconds(delay); - _nextWake = now + (long)Interval.TotalMilliseconds; + HandleDetectHidden(); } private bool ShouldStop() diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs index 4b8753d10..57b6f6a25 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs @@ -26,26 +26,11 @@ namespace Server.Mobiles; public abstract partial class BaseAI { - // Last-known-position tracking: recorded while the combatant is in LOS; drives the - // guard-time investigation and the instant re-engage. - private const int GuardGraceDuration = 10_000; - private const int LkpFreshDuration = 30_000; - private const int InvestigateDuration = 15_000; - private ActionType _action; public long _nextDetectHidden; public DateTime _lastOrder = DateTime.MinValue; public Mobile _commandIssuer; - private Mobile _lkpTarget; - private Point3D _lkpLocation; - private IPoint3D _lkpGoal; // boxed _lkpLocation handed to the PathFollower - private IPoint3D _herdGoal; // boxed herding goal handed to the PathFollower - private long _lkpExpireTick; - private long _guardStopTick; - private long _investigateStopTick; - private bool _investigating; - public PathFollower Path { get; protected set; } public AITimer AITimer { get; } public long NextMove { get; set; } @@ -59,12 +44,11 @@ public abstract partial class BaseAI public BaseAI(BaseCreature m) { Mobile = m; - NextMove = Core.TickCount; AITimer = new AITimer(this); if (!m.PlayerRangeSensitive || !World.Loading && m.Map != null && m.Map != Map.Internal && m.Map.GetSector(m.Location).Active) { - AITimer.Activate(); + AITimer.Start(); } if (Action != ActionType.Wander) @@ -249,15 +233,6 @@ public abstract partial class BaseAI return true; } - if (_action == ActionType.Combat) - { - UpdateLastKnownLocation(); - } - else if (_action is ActionType.Wander or ActionType.Guard) - { - TryReengageLastKnown(); - } - switch (Action) { case ActionType.Wander: @@ -297,9 +272,6 @@ public abstract partial class BaseAI public virtual void OnActionChanged() { - // A change of course invalidates between-think movement continuation. - ClearMoveIntent(); - switch (Action) { case ActionType.Wander: @@ -351,15 +323,6 @@ public abstract partial class BaseAI { Mobile.Warmode = true; Mobile.Combatant = null; - - // Investigate a fresh last-seen position that is not already in view; the guard - // grace period begins once the investigation ends. - _investigating = _lkpTarget != null && Core.TickCount - _lkpExpireTick < 0 && - !(Mobile.InRange(_lkpLocation, 1) || - Mobile.InLOS(_lkpLocation) && Mobile.InRange(_lkpLocation, Mobile.RangePerception)); - _investigateStopTick = Core.TickCount + InvestigateDuration; - _guardStopTick = Core.TickCount + GuardGraceDuration; - _lkpGoal = null; } private void HandleFleeAction() @@ -442,7 +405,7 @@ public abstract partial class BaseAI var master = Mobile.SummonMaster; if (master != null && master.Map == Mobile.Map && master.InRange(Mobile, Mobile.RangePerception)) { - MoveTo(master, 1); + MoveTo(master, false, 1); } } @@ -490,118 +453,18 @@ public abstract partial class BaseAI public virtual bool DoActionGuard() { - if (_investigating) + if (Mobile.Combatant == null) { - if (InvestigateLastKnown()) - { - return true; - } - - _investigating = false; - _guardStopTick = Core.TickCount + GuardGraceDuration; + DebugSay("No threats found. Going home..."); + Action = ActionType.Wander; } - if (Core.TickCount - _guardStopTick < 0) - { - DebugSay("I am on guard."); - - if (Utility.Random(8) == 0) - { - Mobile.Direction = (Direction)Utility.Random(8); - } - - return true; - } - - DebugSay("I stopped being on guard. Going home..."); + DebugSay("I stopped being on guard."); Action = ActionType.Wander; return true; } - /// - /// Records the combatant's position while it is visible and in line of sight. - /// - private void UpdateLastKnownLocation() - { - var combatant = Mobile.Combatant; - - if (combatant?.Deleted == false && combatant.Map == Mobile.Map && - Mobile.CanSee(combatant) && Mobile.InLOS(combatant)) - { - _lkpTarget = combatant; - _lkpLocation = combatant.Location; - _lkpExpireTick = Core.TickCount + LkpFreshDuration; - } - } - - /// - /// Re-engages the last-seen target when it returns to view within perception range, - /// bypassing the reacquire throttle. - /// - private bool TryReengageLastKnown() - { - var target = _lkpTarget; - - if (target == null) - { - return false; - } - - if (target.Deleted || !target.Alive || target.Map != Mobile.Map || - target is BaseCreature { IsDeadPet: true } || Core.TickCount - _lkpExpireTick >= 0) - { - ClearLastKnown(); - return false; - } - - if (Mobile.Controlled || Mobile.BardPacified || Mobile.BardProvoked || Mobile.FightMode == FightMode.None) - { - return false; - } - - if (!Mobile.InRange(target, Mobile.RangePerception) || !Mobile.CanSee(target) || - !Mobile.InLOS(target) || !Mobile.CanBeHarmful(target, false)) - { - return false; - } - - DebugSay("There you are!"); - Mobile.Combatant = target; - Mobile.FocusMob = null; - Action = ActionType.Combat; - return true; - } - - /// - /// Walks toward the last-seen position until it is in view, reached, timed out, or - /// unreachable. Returns false when the investigation is finished. - /// - private bool InvestigateLastKnown() - { - if (_lkpTarget == null || Core.TickCount - _investigateStopTick >= 0) - { - return false; - } - - if (Mobile.InRange(_lkpLocation, 1) || - Mobile.InLOS(_lkpLocation) && Mobile.InRange(_lkpLocation, Mobile.RangePerception)) - { - DebugSay("They truly disappeared..."); - return false; - } - - _lkpGoal ??= _lkpLocation; - return MoveToPoint(_lkpGoal); - } - - private void ClearLastKnown() - { - _lkpTarget = null; - _lkpGoal = null; - _investigating = false; - } - public virtual bool DoActionFlee() { var from = Mobile.Combatant; @@ -628,7 +491,6 @@ public abstract partial class BaseAI if (target == null) { - _herdGoal = null; return false; } @@ -636,15 +498,7 @@ public abstract partial class BaseAI if (distance >= 1 && distance <= 15) { - // A cached boxed goal keeps the PathFollower persistent across ticks; walking - // through MoveToPoint paces herding on the movement clock and paths around - // obstacles. - if (_herdGoal == null || _herdGoal.X != target.X || _herdGoal.Y != target.Y) - { - _herdGoal = new Point3D(target.X, target.Y, Mobile.Map?.GetAverageZ(target.X, target.Y) ?? Mobile.Z); - } - - MoveToPoint(_herdGoal); + DoMove(Mobile.GetDirectionTo(target)); return true; } @@ -654,7 +508,6 @@ public abstract partial class BaseAI } Mobile.TargetLocation = null; - _herdGoal = null; return false; } @@ -798,7 +651,7 @@ public abstract partial class BaseAI { if (AcquireFocusMob(Mobile.RangePerception * 2, FightMode.Closest, true, false, true)) { - if (WalkMobileRange(Mobile.FocusMob, 1, Mobile.RangePerception, Mobile.RangePerception * 2)) + if (WalkMobileRange(Mobile.FocusMob, 1, false, Mobile.RangePerception, Mobile.RangePerception * 2)) { DebugSay("I backed off to safety. Wandering..."); @@ -850,24 +703,22 @@ public abstract partial class BaseAI return false; } - var reacquireDelay = (long)Mobile.ReacquireDelay.TotalMilliseconds; - var gateRemaining = Mobile.NextReacquireTime - Core.TickCount; - - if (gateRemaining > 0 && gateRemaining <= reacquireDelay) + if (Core.TickCount - Mobile.NextReacquireTime < 0) { Mobile.FocusMob = null; return false; } - DebugSay("Acquiring new target...", 0); + Mobile.NextReacquireTime = Core.TickCount + (int)Mobile.ReacquireDelay.TotalMilliseconds; - var acquired = AcquireNewFocusMob(Mobile.Map, iRange, acqType, bPlayerOnly, bFacFriend, bFacFoe); + DebugSay("Acquiring new target..."); - // Reaction time is the approach path (BaseCreature.ScheduleAcquireOnApproach), - // not this poll — every scan honors the full delay. - Mobile.NextReacquireTime = Core.TickCount + reacquireDelay; + if (Mobile.Map == null) + { + return Mobile.FocusMob != null; + } - return acquired; + return AcquireNewFocusMob(Mobile.Map, iRange, acqType, bPlayerOnly, bFacFriend, bFacFoe); } private bool HandleBardProvoked() @@ -943,10 +794,8 @@ public abstract partial class BaseAI private bool AcquireNewFocusMob(Map map, int iRange, FightMode acqType, bool bPlayerOnly, bool bFacFriend, bool bFacFoe) { - Mobile newFocusMob = null; - Mobile enemySummonMob = null; - var val = double.MinValue; - var enemySummonVal = double.MinValue; + Mobile newFocusMob = null, enemySummonMob = null; + double val = double.MinValue, enemySummonVal = double.MinValue; foreach (var m in map.GetMobilesInRange(Mobile.Location, iRange)) { @@ -1150,6 +999,6 @@ public abstract partial class BaseAI public virtual void OnCurrentSpeedChanged() { - AITimer.OnSpeedChanged(); + AITimer.Interval = TimeSpan.FromSeconds(Mobile.CurrentSpeed); } } diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/PetOrderHandlers.cs b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrderHandlers.cs index 862c78649..ecb456d2e 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/PetOrderHandlers.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrderHandlers.cs @@ -26,7 +26,7 @@ public abstract partial class BaseAI return; } - AITimer.Prod(); + Activate(); switch (Mobile.ControlOrder) { @@ -36,10 +36,6 @@ public abstract partial class BaseAI break; } case OrderType.Come: - { - Mobile.SetCurrentSpeedToActive(); - break; - } case OrderType.Drop: case OrderType.Friend: case OrderType.Unfriend: @@ -139,7 +135,6 @@ public abstract partial class BaseAI Mobile.FocusMob = null; Mobile.Warmode = false; Mobile.Combatant = null; - Mobile.SetCurrentSpeedToPassive(); } private void HandleTransferOrder() @@ -153,7 +148,6 @@ public abstract partial class BaseAI Mobile.FocusMob = null; Mobile.Warmode = false; Mobile.Combatant = null; - Mobile.SetCurrentSpeedToPassive(); Mobile.PlaySound(Mobile.GetIdleSound()); _commandIssuer = null; } @@ -168,16 +162,9 @@ public abstract partial class BaseAI _commandIssuer?.RevealingAction(); Mobile.FocusMob = null; Mobile.Warmode = true; - Mobile.SetCurrentSpeedToActive(); - - // Resuming the persistent order must not replay the flourish. - if (!_resolvingOrder) - { - Mobile.PlaySound(Mobile.GetAttackSound()); - Mobile.ControlMaster?.SendLocalizedMessage(1049671, Mobile.Name); - // ~1_NAME~ is now guarding you. - } - + Mobile.PlaySound(Mobile.GetAttackSound()); + Mobile.ControlMaster?.SendLocalizedMessage(1049671, Mobile.Name); + // ~1_NAME~ is now guarding you. _commandIssuer = null; } @@ -204,7 +191,6 @@ public abstract partial class BaseAI } Mobile.Warmode = true; - Mobile.SetCurrentSpeedToActive(); Mobile.PlaySound(Mobile.GetAttackSound()); _commandIssuer = null; } @@ -220,7 +206,6 @@ public abstract partial class BaseAI Mobile.FocusMob = null; Mobile.Warmode = false; Mobile.Combatant = null; - Mobile.SetCurrentSpeedToActive(); Mobile.PlaySound(Mobile.GetIdleSound()); _commandIssuer = null; } @@ -236,7 +221,6 @@ public abstract partial class BaseAI Mobile.FocusMob = null; Mobile.Warmode = false; Mobile.Combatant = null; - Mobile.SetCurrentSpeedToPassive(); Mobile.PlaySound(Mobile.GetIdleSound()); _commandIssuer = null; // Home (the stay anchor) is owned by SetPersistentOrder, not this handler. diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs index e6c48850d..1b7139a96 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs @@ -84,7 +84,7 @@ public abstract partial class BaseAI return true; } - WalkMobileRange(Mobile.ControlMaster, 1, 1, 2); + WalkMobileRange(Mobile.ControlMaster, 1, false, 1, 2); if (Mobile.GetDistanceToSqrt(Mobile.ControlMaster) <= 2) { @@ -128,15 +128,9 @@ public abstract partial class BaseAI this.DebugSayFormatted($"I am ordered to follow {Mobile.ControlTarget?.Name}."); - // AOS: sprint after the master (bespoke 0.1 paces both clocks). - if (Core.AOS && Mobile.ControlTarget == Mobile.ControlMaster && Mobile.Combatant == null) - { - Mobile.CurrentSpeed = 0.1; - } - if (currentDistance > 1) { - WalkMobileRange(Mobile.ControlTarget, 1, 1, 2); + WalkMobileRange(Mobile.ControlTarget, 1, currentDistance > 2, 1, 2); } } @@ -297,13 +291,14 @@ public abstract partial class BaseAI return true; } - var combatant = FindGuardTarget(); + FindCombatant(); - if (combatant != null) + if (IsValidCombatant(Mobile.Combatant)) { + var combatant = Mobile.Combatant; + this.DebugSayFormatted($"Attacking target: {combatant.Name}"); - // Engage without leaving the Guard order so tags, recall handling, and retargeting persist. Mobile.Combatant = combatant; Mobile.FocusMob = combatant; Action = ActionType.Combat; @@ -314,30 +309,16 @@ public abstract partial class BaseAI { this.DebugSayFormatted($"Guarding my master, {controlMaster.Name}."); - // Stand down; a stale Warmode would skew the return pace. - Mobile.FocusMob = null; - Mobile.Warmode = false; - Mobile.Combatant = null; + var guardLocation = controlMaster.Location; - var distance = (int)Mobile.GetDistanceToSqrt(controlMaster); + var distance = (int)Mobile.GetDistanceToSqrt(guardLocation); if (distance > 3) { - // AOS: sprint back (bespoke 0.1 paces both clocks); earlier eras run active. - if (Core.AOS) - { - Mobile.CurrentSpeed = 0.1; - } - else - { - Mobile.SetCurrentSpeedToActive(); - } - - WalkMobileRange(controlMaster, 1, 1, 3); + DoMove(Mobile.GetDirectionTo(guardLocation)); } else { - Mobile.SetCurrentSpeedToActive(); // alert at the master's side WalkRandom(3, 1, 1); } } @@ -378,83 +359,67 @@ public abstract partial class BaseAI Mobile.ControlTarget = Mobile.ControlMaster; ResumePersistentOrder(); - // A resumed Guard engages through its own scan; other fallbacks chain an explicit Attack. - if (Mobile.ControlOrder == OrderType.Guard || - Mobile.FightMode is not (FightMode.Closest or FightMode.Aggressor)) + if (Mobile.FightMode is FightMode.Closest or FightMode.Aggressor) { - return; - } - - var next = FindGuardTarget(); - - if (next != null) - { - Mobile.ControlTarget = next; - Mobile.ControlOrder = OrderType.Attack; - Mobile.Combatant = next; - - this.DebugSayFormatted($"{next.Name} is still hostile! Engaging..."); - - Think(); + FindCombatant(); } } - /// - /// Selects the aggressor closest to the master. The current combatant is kept - /// unless a strictly closer one exists. Never mutates order state. - /// - private Mobile FindGuardTarget() + private void FindCombatant() { var controlMaster = Mobile.ControlMaster; - var anchor = controlMaster ?? Mobile; - - var current = Mobile.Combatant; - var best = current != controlMaster && IsValidCombatant(current) ? current : null; - var bestDist = best?.GetDistanceToSqrt(anchor) ?? double.MaxValue; foreach (var aggr in Mobile.GetMobilesInRange(Mobile.RangePerception)) { - if (aggr == best || aggr == Mobile || aggr == controlMaster || - aggr.IsDeadBondedPet || !aggr.Alive || - aggr.Combatant != Mobile && (controlMaster == null || aggr.Combatant != controlMaster)) + if (!Mobile.CanSee(aggr) || aggr.IsDeadBondedPet || !aggr.Alive) { continue; } - var dist = aggr.GetDistanceToSqrt(anchor); + var isAttackingPet = aggr.Combatant == Mobile; + var isAttackingMaster = controlMaster != null && aggr.Combatant == controlMaster; - if (dist < bestDist && Mobile.CanSee(aggr) && Mobile.InLOS(aggr)) + if (isAttackingPet || isAttackingMaster) { - best = aggr; - bestDist = dist; + if (Mobile.InLOS(aggr)) + { + Mobile.ControlTarget = aggr; + Mobile.ControlOrder = OrderType.Attack; + Mobile.Combatant = aggr; + + var target = isAttackingMaster ? "master" : "me"; + this.DebugSayFormatted($"{aggr.Name} is attacking my {target}! Engaging..."); + + Think(); + return; + } } } - var aggressors = controlMaster?.Aggressors; - - if (aggressors != null) + if (controlMaster?.Aggressors != null) { - for (var i = 0; i < aggressors.Count; i++) + for (var i = 0; i < controlMaster.Aggressors.Count; i++) { - var aggressor = aggressors[i].Attacker; + var aggressor = controlMaster.Aggressors[i].Attacker; - if (aggressor == best || aggressor?.Deleted != false || !aggressor.Alive || - aggressor.IsDeadBondedPet || !Mobile.InRange(aggressor, Mobile.RangePerception)) + if (aggressor?.Deleted != false || !aggressor.Alive || aggressor.IsDeadBondedPet) { continue; } - var dist = aggressor.GetDistanceToSqrt(anchor); - - if (dist < bestDist && Mobile.CanSee(aggressor) && Mobile.InLOS(aggressor)) + if (Mobile.InRange(aggressor, Mobile.RangePerception) && Mobile.CanSee(aggressor) && Mobile.InLOS(aggressor)) { - best = aggressor; - bestDist = dist; + Mobile.ControlTarget = aggressor; + Mobile.ControlOrder = OrderType.Attack; + Mobile.Combatant = aggressor; + + this.DebugSayFormatted($"{aggressor.Name} recently attacked my master! Retaliating..."); + + Think(); + return; } } } - - return best; } public virtual bool DoOrderRelease() diff --git a/Projects/UOContent/Mobiles/AI/BerserkAI.cs b/Projects/UOContent/Mobiles/AI/BerserkAI.cs index 8a2015f1a..4663ae8d0 100644 --- a/Projects/UOContent/Mobiles/AI/BerserkAI.cs +++ b/Projects/UOContent/Mobiles/AI/BerserkAI.cs @@ -12,7 +12,7 @@ public class BerserkAI : BaseAI if (AcquireFocusMob(Mobile.RangePerception, FightMode.Closest, false, true, true)) { - this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name}, attacking"); + this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name} and I will attack"); Mobile.Combatant = Mobile.FocusMob; Action = ActionType.Combat; @@ -38,7 +38,7 @@ public class BerserkAI : BaseAI return true; } - if (!WalkMobileRange(combatant, 1, Mobile.RangeFight, Mobile.RangeFight)) + if (!WalkMobileRange(combatant, 1, false, Mobile.RangeFight, Mobile.RangeFight)) { this.DebugSayFormatted($"I am still not in range of {combatant.Name}"); diff --git a/Projects/UOContent/Mobiles/AI/HealerAI.cs b/Projects/UOContent/Mobiles/AI/HealerAI.cs index cc54c73c3..2a1127b43 100644 --- a/Projects/UOContent/Mobiles/AI/HealerAI.cs +++ b/Projects/UOContent/Mobiles/AI/HealerAI.cs @@ -81,7 +81,7 @@ public class HealerAI : BaseAI return true; } - WalkMobileRange(Mobile.FocusMob, 1, 4, 7); + WalkMobileRange(Mobile.FocusMob, 1, false, 4, 7); // TODO: Should it be able to do this? if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, Mobile.Combatant)) diff --git a/Projects/UOContent/Mobiles/AI/MageAI.cs b/Projects/UOContent/Mobiles/AI/MageAI.cs index 1ed8cbdda..e9450db98 100644 --- a/Projects/UOContent/Mobiles/AI/MageAI.cs +++ b/Projects/UOContent/Mobiles/AI/MageAI.cs @@ -171,7 +171,7 @@ public class MageAI : BaseAI { if (!SmartAI) { - if (!MoveTo(m, Mobile.RangeFight)) + if (!MoveTo(m, false, Mobile.RangeFight)) { OnFailedMove(); } @@ -185,14 +185,14 @@ public class MageAI : BaseAI { RunFrom(m); } - else if (!Mobile.InRange(m, Math.Max(Mobile.RangeFight, 2)) && !MoveTo(m, 1)) + else if (!Mobile.InRange(m, Math.Max(Mobile.RangeFight, 2)) && !MoveTo(m, false, 1)) { OnFailedMove(); } } else if (!Mobile.InRange(m, Mobile.RangeFight)) { - if (!MoveTo(m, 1)) + if (!MoveTo(m, false, 1)) { OnFailedMove(); } @@ -679,7 +679,7 @@ public class MageAI : BaseAI Mobile.Combatant = Mobile.FocusMob; Mobile.FocusMob = null; } - else if (!Mobile.InRange(c, Mobile.ChaseLeashRange)) + else if (!Mobile.InRange(c, Mobile.RangePerception * 3)) { Mobile.Combatant = null; } @@ -695,23 +695,6 @@ public class MageAI : BaseAI } } - // Geometry (not hiding — CanSee passed above) is blocking the shot: close in until - // line of sight returns. Poisoned mages still fall through to cure. - if (!Mobile.Poisoned && Mobile.Spell?.IsCasting != true && !Mobile.InLOS(c)) - { - DebugSay("I cannot see my target, moving to regain line of sight"); - - if (!MoveTo(c, 1)) - { - OnFailedMove(); - } - - _lastTarget = c; - _lastTargetLoc = c.Location; - - return true; - } - if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, c)) { DebugSay("I used my abilities!"); @@ -1035,16 +1018,7 @@ public class MageAI : BaseAI if (toTarget != null) { - // Without line of sight the stand-off is pointless — close in so the held - // target can be invoked. - if (!Mobile.InLOS(toTarget)) - { - MoveTo(toTarget, 1); - } - else - { - RunTo(toTarget); - } + RunTo(toTarget); } } diff --git a/Projects/UOContent/Mobiles/AI/MeleeAI.cs b/Projects/UOContent/Mobiles/AI/MeleeAI.cs index a6d0a9bf9..aa262caee 100644 --- a/Projects/UOContent/Mobiles/AI/MeleeAI.cs +++ b/Projects/UOContent/Mobiles/AI/MeleeAI.cs @@ -1,5 +1,3 @@ -using System.Runtime.CompilerServices; - namespace Server.Mobiles; public class MeleeAI : BaseAI @@ -16,7 +14,6 @@ public class MeleeAI : BaseAI if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true)) { this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name}, attacking"); - Mobile.Combatant = Mobile.FocusMob; Action = ActionType.Combat; } @@ -68,9 +65,13 @@ public class MeleeAI : BaseAI return true; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsValidCombatant(Mobile combatant) => - combatant?.Deleted == false && combatant.Map == Mobile.Map && combatant.Alive && !combatant.IsDeadBondedPet; + private bool IsValidCombatant(Mobile combatant) + { + return combatant?.Deleted == false + && combatant.Map == Mobile.Map + && combatant.Alive + && !combatant.IsDeadBondedPet; + } private bool HandleOutOfRangeCombatant(Mobile combatant) { @@ -81,7 +82,7 @@ public class MeleeAI : BaseAI return true; } - if (!Mobile.InRange(combatant, Mobile.ChaseLeashRange)) + if (!Mobile.InRange(combatant, Mobile.RangePerception * 3)) { Mobile.Combatant = null; } @@ -98,7 +99,7 @@ public class MeleeAI : BaseAI private bool AttemptMoveToCombatant(Mobile combatant) { - if (MoveTo(combatant, Mobile.RangeFight)) + if (MoveTo(combatant, false, Mobile.RangeFight)) { return true; } @@ -126,8 +127,7 @@ public class MeleeAI : BaseAI { if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true)) { - this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name}, attacking"); - + this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name}, attacking."); Mobile.Combatant = Mobile.FocusMob; Action = ActionType.Combat; } diff --git a/Projects/UOContent/Mobiles/AI/PredatorAI.cs b/Projects/UOContent/Mobiles/AI/PredatorAI.cs index b1ea3a638..5e0e01520 100644 --- a/Projects/UOContent/Mobiles/AI/PredatorAI.cs +++ b/Projects/UOContent/Mobiles/AI/PredatorAI.cs @@ -41,7 +41,7 @@ public class PredatorAI : BaseAI return true; } - if (!WalkMobileRange(combatant, 1, Mobile.RangeFight, Mobile.RangeFight)) + if (!WalkMobileRange(combatant, 1, false, Mobile.RangeFight, Mobile.RangeFight)) { if (Mobile.GetDistanceToSqrt(combatant) > Mobile.RangePerception + 1) { @@ -70,7 +70,7 @@ public class PredatorAI : BaseAI } else if (AcquireFocusMob(Mobile.RangePerception * 2, FightMode.Closest, true, false, true)) { - if (WalkMobileRange(Mobile.FocusMob, 1, Mobile.RangePerception, Mobile.RangePerception * 2)) + if (WalkMobileRange(Mobile.FocusMob, 1, false, Mobile.RangePerception, Mobile.RangePerception * 2)) { DebugSay("Well, here I am safe"); diff --git a/Projects/UOContent/Mobiles/AI/ThiefAI.cs b/Projects/UOContent/Mobiles/AI/ThiefAI.cs index a9209dfbb..0fa37bbc9 100644 --- a/Projects/UOContent/Mobiles/AI/ThiefAI.cs +++ b/Projects/UOContent/Mobiles/AI/ThiefAI.cs @@ -43,7 +43,7 @@ public class ThiefAI : BaseAI return true; } - if (!WalkMobileRange(combatant, 1, Mobile.RangeFight, Mobile.RangeFight)) + if (!WalkMobileRange(combatant, 1, false, Mobile.RangeFight, Mobile.RangeFight)) { this.DebugSayFormatted($"I should be closer to {combatant.Name}"); } diff --git a/Projects/UOContent/Mobiles/Animals/Misc/Sheep.cs b/Projects/UOContent/Mobiles/Animals/Misc/Sheep.cs index 741093110..b43c34272 100644 --- a/Projects/UOContent/Mobiles/Animals/Misc/Sheep.cs +++ b/Projects/UOContent/Mobiles/Animals/Misc/Sheep.cs @@ -5,14 +5,9 @@ using System.Runtime.CompilerServices; namespace Server.Mobiles { - [SerializationGenerator(1, false)] + [SerializationGenerator(0, false)] public partial class Sheep : BaseCreature, ICarvable { - private void MigrateFrom(V0Content content) - { - _nextWoolTime = content.NextWoolTime; - } - [Constructible] public Sheep() : base(AIType.AI_Animal, FightMode.Aggressor) { @@ -48,14 +43,18 @@ namespace Server.Mobiles public override string CorpseName => "a sheep corpse"; - [SerializableField(0, fieldChanged: nameof(OnNextWoolTimeChanged))] - [AnchoredDateTime] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private DateTime _nextWoolTime; - - private void OnNextWoolTimeChanged(DateTime oldValue, DateTime newValue) + [DeltaDateTime] + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public DateTime NextWoolTime { - SheepBody(); + get => _nextWoolTime; + set + { + _nextWoolTime = value; + SheepBody(); + this.MarkDirty(); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/Ethereals.cs b/Projects/UOContent/Mobiles/Animals/Mounts/Ethereals.cs index f6dc13b40..f13cebcbe 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/Ethereals.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/Ethereals.cs @@ -11,19 +11,19 @@ namespace Server.Mobiles public partial class EtherealMount : Item, IMount, IMountItem, IRewardItem { [SerializableField(0)] - [SaveFlag(nameof(ShouldSerializeIsDonationItem))] [SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] public bool _isDonationItem; [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SerializableFieldSaveFlag(0)] public bool ShouldSerializeIsDonationItem() => _isDonationItem; [SerializableField(1)] - [SaveFlag(nameof(ShouldSerializeIsRewardItem))] [SerializedCommandProperty(AccessLevel.GameMaster)] public bool _isRewardItem; [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SerializableFieldSaveFlag(1)] public bool ShouldSerializeIsRewardItem() => _isRewardItem; [Constructible] @@ -40,27 +40,43 @@ namespace Server.Mobiles public override double DefaultWeight => 1.0; - [SerializableField(2, fieldChanged: nameof(OnMountedIDChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private int _mountedID; - - private void OnMountedIDChanged(int oldValue, int newValue) + [SerializableProperty(2)] + [CommandProperty(AccessLevel.GameMaster)] + public int MountedID { - if (_rider != null) + get => _mountedID; + set { - ItemID = newValue; + if (_mountedID != value) + { + _mountedID = value; + + if (_rider != null) + { + ItemID = value; + } + this.MarkDirty(); + } } } - [SerializableField(3, fieldChanged: nameof(OnRegularIDChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private int _regularID; - - private void OnRegularIDChanged(int oldValue, int newValue) + [SerializableProperty(3)] + [CommandProperty(AccessLevel.GameMaster)] + public int RegularID { - if (_rider == null) + get => _regularID; + set { - ItemID = newValue; + if (_regularID != value) + { + _regularID = value; + + if (_rider == null) + { + ItemID = value; + } + this.MarkDirty(); + } } } @@ -71,7 +87,6 @@ namespace Server.Mobiles public virtual int EtherealHue => 0x4001; [SerializableProperty(4)] - [SaveFlag(nameof(ShouldSerializeRider))] [CommandProperty(AccessLevel.GameMaster)] public Mobile Rider { @@ -109,19 +124,22 @@ namespace Server.Mobiles } } + [SerializableFieldSaveFlag(4)] private bool ShouldSerializeRider() => _rider != null; - [SerializableField(5, allowFieldChange: nameof(AllowStepsChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [SaveFlag(nameof(ShouldSerializeSteps))] - private int _steps; - - private bool AllowStepsChange(ref int value) + [CommandProperty(AccessLevel.GameMaster)] + [SerializableProperty(5)] + public int Steps { - value = Math.Clamp(value, 0, StepsMax); - return true; + get => _steps; + set + { + _steps = Math.Clamp(value, 0, StepsMax); + this.MarkDirty(); + } } + [SerializableFieldSaveFlag(5)] private bool ShouldSerializeSteps() => _steps != StepsMax; public virtual int StepsMax => 3840; // Should be same as horse diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/SwampDragon.cs b/Projects/UOContent/Mobiles/Animals/Mounts/SwampDragon.cs index 19a061af2..69bd9ce56 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/SwampDragon.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/SwampDragon.cs @@ -62,37 +62,48 @@ namespace Server.Mobiles [SerializableField(3)] private int _bardingHP; - [SerializableField(2, fieldChanged: nameof(OnHasBardingChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private bool _hasBarding; - - private void OnHasBardingChanged(bool oldValue, bool newValue) + [CommandProperty(AccessLevel.GameMaster)] + [SerializableProperty(2)] + public bool HasBarding { - if (_hasBarding) + get => _hasBarding; + set { - Hue = CraftResources.GetHue(_bardingResource); - Body = 0x31F; - ItemID = 0x3EBE; - } - else - { - Hue = 0x851; - Body = 0x31A; - ItemID = 0x3EBD; + _hasBarding = value; + + if (_hasBarding) + { + Hue = CraftResources.GetHue(_bardingResource); + Body = 0x31F; + ItemID = 0x3EBE; + } + else + { + Hue = 0x851; + Body = 0x31A; + ItemID = 0x3EBD; + } + InvalidateProperties(); + this.MarkDirty(); } } - [SerializableField(4, fieldChanged: nameof(OnBardingResourceChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private CraftResource _bardingResource; - - private void OnBardingResourceChanged(CraftResource oldValue, CraftResource newValue) + [CommandProperty(AccessLevel.GameMaster)] + [SerializableProperty(4)] + public CraftResource BardingResource { - if (_hasBarding) + get => _bardingResource; + set { - Hue = CraftResources.GetHue(newValue); + _bardingResource = value; + + if (_hasBarding) + { + Hue = CraftResources.GetHue(value); + } + + InvalidateProperties(); + this.MarkDirty(); } } diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 404aaede8..af85a195a 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -265,12 +265,8 @@ namespace Server.Mobiles private double _passiveSpeed; private double _currentSpeed; - // Movement clock (seconds per step); 0 = inherit the matching think value. - private double _activeMoveSpeed; - private double _passiveMoveSpeed; - - // Herding - forces the mob to walk to a specific location, paced by the movement - // clock at HerdingMoveSpeed. Thinking is unaffected. + // Herding - Overrides the AI to force the mob to move to a specific location + // Thinking: 0.3s, Movement: 0.6s. private IPoint2D _targetLocation; private int m_DamageMax = -1; @@ -346,7 +342,6 @@ namespace Server.Mobiles FightMode = mode; GetSpeeds(out var activeSpeed, out var passiveSpeed); - GetMoveSpeeds(out _activeMoveSpeed, out _passiveMoveSpeed); ActiveSpeed = activeSpeed; PassiveSpeed = passiveSpeed; @@ -665,20 +660,12 @@ namespace Server.Mobiles [CommandProperty(AccessLevel.GameMaster)] public int RangePerception { get; set; } - /// - /// How far a chase may stretch before the creature gives up its combatant. Between - /// RangePerception and this leash it keeps chasing but may switch to closer targets. - /// - [CommandProperty(AccessLevel.GameMaster)] - public virtual int ChaseLeashRange => RangePerception * 2; - [CommandProperty(AccessLevel.GameMaster)] public int RangeFight { get; set; } [CommandProperty(AccessLevel.GameMaster)] public int RangeHome { get; set; } = 10; - /// Seconds per AI decision while engaged; see for movement pace. [CommandProperty(AccessLevel.GameMaster)] public virtual double ActiveSpeed { @@ -692,7 +679,6 @@ namespace Server.Mobiles } } - /// Seconds per AI decision while idle; see for movement pace. [CommandProperty(AccessLevel.GameMaster)] public virtual double PassiveSpeed { @@ -707,37 +693,21 @@ namespace Server.Mobiles } } - /// Seconds per step while engaged. Inherits ; set 0 to re-inherit. - [CommandProperty(AccessLevel.GameMaster)] - public virtual double ActiveMoveSpeed - { - get => _activeMoveSpeed > 0 ? _activeMoveSpeed : _activeSpeed; - set => _activeMoveSpeed = value > 0 ? value : 0; - } - - /// Seconds per step while idle. Inherits ; set 0 to re-inherit. - [CommandProperty(AccessLevel.GameMaster)] - public virtual double PassiveMoveSpeed - { - get => _passiveMoveSpeed > 0 ? _passiveMoveSpeed : _passiveSpeed; - set => _passiveMoveSpeed = value > 0 ? value : 0; - } - - // Herded creatures walk at a fixed standard pace regardless of their own speed - // (RunUO's forced 0.3, without its TransformMoveDelay inflation to 0.6). - private const double HerdingMoveSpeed = 0.3; - [CommandProperty(AccessLevel.GameMaster)] public IPoint2D TargetLocation { get => _targetLocation; - set => _targetLocation = value; + set + { + _targetLocation = value; + AIObject?.OnCurrentSpeedChanged(); + } } [CommandProperty(AccessLevel.GameMaster)] public double CurrentSpeed { - get => _currentSpeed; + get => _targetLocation != null ? 0.3 : _currentSpeed; set { if (Math.Abs(_currentSpeed - value) > 0.0001) @@ -748,27 +718,8 @@ namespace Server.Mobiles } } - /// - /// Resolved seconds per step: a verbatim active/passive - /// maps to the matching movement value; a bespoke pace (e.g. the pet-order 0.1 sprint) - /// stays fused to both clocks. A herded creature is always driven at - /// . - /// [CommandProperty(AccessLevel.GameMaster)] - public double CurrentMoveSpeed - { - get - { - if (_targetLocation != null) - { - return HerdingMoveSpeed; - } - - return _currentSpeed == _activeSpeed ? ActiveMoveSpeed - : _currentSpeed == _passiveSpeed ? PassiveMoveSpeed - : _currentSpeed; - } - } + public double MoveSpeedMod { get; set; } [CommandProperty(AccessLevel.GameMaster)] public Point3D Home @@ -846,8 +797,6 @@ namespace Server.Mobiles [CommandProperty(AccessLevel.GameMaster)] public Point3D ControlDest { get; set; } - // Fires on every assignment, not only changes: a reissued order is a command - // (retarget, break off combat, re-anchor Home). Handlers receive the previous order. [CommandProperty(AccessLevel.GameMaster)] public OrderType ControlOrder { @@ -936,49 +885,20 @@ namespace Server.Mobiles public virtual bool GivesMLMinorArtifact => false; + /* To save on cpu usage, RunUO creatures only reacquire creatures under the following circumstances: + * - 10 seconds have elapsed since the last time it tried + * - The creature was attacked + * - Some creatures, like dragons, will reacquire when they see someone move + * + * This functionality appears to be implemented on OSI as well + */ + public long NextReacquireTime { get; set; } public virtual TimeSpan ReacquireDelay => TimeSpan.FromSeconds(10.0); - - // Reaction-time gradient: an enemy moving inside AcquireOnApproachRange pulls the - // next scan to at most this far away. Zero (paragons) scans on the very next - // think; larger is dumber; pure ReacquireDelay is the oblivious floor. - public virtual TimeSpan AcquireOnApproachDelay => m_Paragon ? TimeSpan.Zero : TimeSpan.FromSeconds(2.0); - - // Reactive range is tighter than the periodic scan's RangePerception: approach - // aggro starts on-screen; the ReacquireDelay poll keeps the wide ambient sweep. - public virtual int AcquireOnApproachRange => 10; - - // Clamps the scan deadline rather than opening the gate: repeated steps cannot - // shorten it further, so an armed creature scans once per delay period. - private void ScheduleAcquireOnApproach() - { - var delay = (long)AcquireOnApproachDelay.TotalMilliseconds; - var deadline = Core.TickCount + delay; - - if (deadline - NextReacquireTime < 0) - { - NextReacquireTime = deadline; - } - - if (delay <= 0) - { - // Zero: think now — the ranked scan engages within a wheel turn. Prod is - // spam-safe; the Combatant == null guard stops the prods once engaged. - AIObject?.AITimer?.Prod(); - } - } - - // IsEnemy first — it cheaply rejects the common case (a same-team wild creature - // wandering past); CanBeHarmful covers hidden movers via CanSee. - private bool ShouldAcquireOnApproach(Mobile m) => - Combatant == null && - !Controlled && !Summoned && !BardPacified && - FightMode != FightMode.None && FightMode != FightMode.Aggressor && - InRange(m.Location, AcquireOnApproachRange) && - IsEnemy(m) && CanBeHarmful(m, false); - public virtual bool ReacquireOnMovement => false; + public virtual bool AcquireOnApproach => m_Paragon; + public virtual int AcquireOnApproachRange => 10; public static bool Summoning { get; set; } @@ -1923,7 +1843,7 @@ namespace Server.Mobiles { base.Serialize(writer); - writer.Write(22); // version + writer.Write(20); // version writer.Write((int)m_CurrentAI); writer.Write((int)m_DefaultAI); @@ -1960,7 +1880,7 @@ namespace Server.Mobiles if (_summoned) { - writer.WriteAnchoredTime(SummonEnd); + writer.WriteDeltaTime(SummonEnd); } writer.Write(ControlSlots); @@ -2043,18 +1963,12 @@ namespace Server.Mobiles // Version 19 writer.Write(HomeMap); - - // Version 22 (0 = inherit the matching think value) - writer.Write(_activeMoveSpeed); - writer.Write(_passiveMoveSpeed); } public override void Deserialize(IGenericReader reader) { base.Deserialize(reader); - NextReacquireTime = Core.TickCount; - var version = reader.ReadInt(); m_CurrentAI = (AIType)reader.ReadInt(); @@ -2121,7 +2035,7 @@ namespace Server.Mobiles if (_summoned) { - SummonEnd = version >= 21 ? reader.ReadAnchoredTime() : reader.ReadDeltaTime(); + SummonEnd = reader.ReadDeltaTime(); new UnsummonTimer(this, SummonEnd - Core.Now).Start(); } @@ -2252,16 +2166,6 @@ namespace Server.Mobiles HomeMap = reader.ReadMap(); } - if (version >= 22) - { - _activeMoveSpeed = reader.ReadDouble(); - _passiveMoveSpeed = reader.ReadDouble(); - } - else - { - MigrateMoveSpeeds(); - } - if (version <= 14 && m_Paragon && Hue == 0x31) { Hue = Paragon.Hue; // Paragon hue fixed, should now be 0x501. @@ -2886,9 +2790,15 @@ namespace Server.Mobiles public override void OnMovement(Mobile m, Point3D oldLocation) { - if (ShouldAcquireOnApproach(m)) + if (AcquireOnApproach && !Controlled && !Summoned && !BardPacified && FightMode != FightMode.Aggressor) { - ScheduleAcquireOnApproach(); + if (InRange(m.Location, AcquireOnApproachRange) && !InRange(oldLocation, AcquireOnApproachRange) && + CanBeHarmful(m) && IsEnemy(m)) + { + Combatant = FocusMob = m; + AIObject?.MoveTo(m, true, 1); + DoHarmful(m); + } } else if (ReacquireOnMovement) { @@ -3123,12 +3033,14 @@ namespace Server.Mobiles return base.OnBeforeDeath(); } - public int ComputeBonusDamage(in ValueLinkList list, Mobile m) + public int ComputeBonusDamage(List list, Mobile m) { var bonus = 0; - foreach (var de in list.ByDescending()) + for (var i = list.Count - 1; i >= 0; --i) { + var de = list[i]; + if (de.Damager == m || de.Damager is not BaseCreature bc) { continue; @@ -3165,15 +3077,26 @@ namespace Server.Mobiles Combatant is PlayerMobile || Combatant is BaseCreature { Controlled: true } bc && bc.GetMaster() is PlayerMobile; - // Iterates most recent first, matching the previous reverse-indexed loop. The list is - // already pruned of expired entries by the Mobile.DamageEntries getter. - public static List GetLootingRights(in ValueLinkList damageEntries, int hitsMax) + public static List GetLootingRights(List damageEntries, int hitsMax) { var rights = new List(); DamageStore firstDamager = null; - foreach (var de in damageEntries.ByDescending()) + for (var i = damageEntries.Count - 1; i >= 0; --i) { + if (i >= damageEntries.Count) + { + continue; + } + + var de = damageEntries[i]; + + if (de.HasExpired) + { + damageEntries.RemoveAt(i); + continue; + } + var damage = de.DamageGiven; var respList = de.Responsible; @@ -4656,78 +4579,13 @@ namespace Server.Mobiles return false; } - /// - /// Sets the think clock and clears movement overrides (legacy one-clock semantics); - /// use for an independent movement pace. - /// public void SetSpeed(double active, double passive, bool isPassive = true) { ActiveSpeed = active; PassiveSpeed = passive; - ClearMoveSpeed(); CurrentSpeed = isPassive ? PassiveSpeed : ActiveSpeed; } - /// Sets only the movement clock (seconds per step). - public void SetMoveSpeed(double active, double passive) - { - ActiveMoveSpeed = active; - PassiveMoveSpeed = passive; - } - - /// Clears movement overrides; steps pace off the think clock again. - public void ClearMoveSpeed() - { - _activeMoveSpeed = 0; - _passiveMoveSpeed = 0; - } - - /// - /// Scales movement overrides (paragon and similar buffs). Inheriting values stay - /// inheriting — they already follow the scaled think clock. - /// - public void ScaleMoveSpeed(double scalar) - { - if (_activeMoveSpeed > 0) - { - _activeMoveSpeed *= scalar; - } - - if (_passiveMoveSpeed > 0) - { - _passiveMoveSpeed *= scalar; - } - } - - /// - /// Snaps speeds within rounding distance of the creature's table values back to - /// exact. A scaling buff that divides then multiplies can drift by an ulp (e.g. - /// 0.9 and 0.45 through 1.2), which would read as hand-tuned; call after undoing - /// such a buff. Genuinely tuned speeds are nowhere near the epsilon and keep. - /// - public void SnapSpeedsToTable() - { - GetSpeeds(out var activeSpeed, out var passiveSpeed); - - if (Math.Abs(_activeSpeed - activeSpeed) < 0.0001 && Math.Abs(_passiveSpeed - passiveSpeed) < 0.0001) - { - _activeSpeed = activeSpeed; - _passiveSpeed = passiveSpeed; - } - - GetMoveSpeeds(out var activeMoveSpeed, out var passiveMoveSpeed); - - if (activeMoveSpeed > 0 && Math.Abs(_activeMoveSpeed - activeMoveSpeed) < 0.0001) - { - _activeMoveSpeed = activeMoveSpeed; - } - - if (passiveMoveSpeed > 0 && Math.Abs(_passiveMoveSpeed - passiveMoveSpeed) < 0.0001) - { - _passiveMoveSpeed = passiveMoveSpeed; - } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetCurrentSpeedToActive() => CurrentSpeed = ActiveSpeed; @@ -5041,25 +4899,6 @@ namespace Server.Mobiles NPCSpeeds.GetSpeeds(this, out activeSpeed, out passiveSpeed); } - public virtual void GetMoveSpeeds(out double activeMoveSpeed, out double passiveMoveSpeed) - { - NPCSpeeds.GetMoveSpeeds(this, out activeMoveSpeed, out passiveMoveSpeed); - } - - // Pre-v22 saves carry no movement clock. A creature whose serialized think speeds - // still match what it would spawn with today was never hand-tuned: adopt today's - // move values so existing worlds (and pets) pick up npc-speeds pacing without a - // respawn. Tuned creatures keep movement inheriting their think clock. - internal void MigrateMoveSpeeds() - { - GetSpeeds(out var activeSpeed, out var passiveSpeed); - - if (_activeSpeed == activeSpeed && _passiveSpeed == passiveSpeed) - { - GetMoveSpeeds(out _activeMoveSpeed, out _passiveMoveSpeed); - } - } - public virtual void DropBackpack() { var backpack = Backpack; diff --git a/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs b/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs index 46d939f3b..d3b119523 100644 --- a/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs +++ b/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs @@ -92,7 +92,7 @@ public abstract partial class BaseFamiliar : BaseCreature Hidden = m_LastHidden = master.Hidden; } - if (AIObject?.WalkMobileRange(master, 5, 1, 1) == true) + if (AIObject?.WalkMobileRange(master, 5, false, 1, 1) == true) { Warmode = master.Warmode; Combatant = master.Combatant; diff --git a/Projects/UOContent/Mobiles/Hireables/BaseHire.cs b/Projects/UOContent/Mobiles/Hireables/BaseHire.cs index 3f96e5d36..e3fbadd97 100644 --- a/Projects/UOContent/Mobiles/Hireables/BaseHire.cs +++ b/Projects/UOContent/Mobiles/Hireables/BaseHire.cs @@ -23,14 +23,19 @@ public partial class BaseHire : BaseCreature public int GoldOnDeath { get; set; } - [SerializableField(1, fieldChanged: nameof(OnIsHiredChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private bool _isHired; - - private void OnIsHiredChanged(bool oldValue, bool newValue) + [SerializableProperty(1)] + [CommandProperty(AccessLevel.GameMaster)] + public bool IsHired { - Delta(MobileDelta.Noto); + get => _isHired; + set + { + _isHired = value; + + Delta(MobileDelta.Noto); + InvalidateProperties(); + this.MarkDirty(); + } } public BaseHire(AIType AI) : base(AI, FightMode.Aggressor) diff --git a/Projects/UOContent/Mobiles/Monsters/LBR/Meers/EnragedCreatures.cs b/Projects/UOContent/Mobiles/Monsters/LBR/Meers/EnragedCreatures.cs index ce4f3526d..91367a5f0 100644 --- a/Projects/UOContent/Mobiles/Monsters/LBR/Meers/EnragedCreatures.cs +++ b/Projects/UOContent/Mobiles/Monsters/LBR/Meers/EnragedCreatures.cs @@ -108,7 +108,7 @@ namespace Server.Mobiles */ else if (!Combat(this)) { - AIObject?.MoveTo(SummonMaster, 5); + AIObject?.MoveTo(SummonMaster, false, 5); } /* On OSI, if the summon attacks a mobile, the summoner meer also diff --git a/Projects/UOContent/Mobiles/NPCSpeeds.cs b/Projects/UOContent/Mobiles/NPCSpeeds.cs index 8f5e908bb..a61ce70c6 100644 --- a/Projects/UOContent/Mobiles/NPCSpeeds.cs +++ b/Projects/UOContent/Mobiles/NPCSpeeds.cs @@ -38,22 +38,6 @@ public static class NPCSpeeds passiveSpeed = sp.PassiveSpeed; } - // Move speeds are optional (0 = inherit), so this tolerates a missing entry or table. - public static void GetMoveSpeeds(BaseCreature bc, out double activeMoveSpeed, out double passiveMoveSpeed) - { - if ((bc.SpeedClass == SpeedLevel.None || !_speedsByLevel.TryGetValue(bc.SpeedClass, out var sp)) && - !_speedsByType.TryGetValue(bc.GetType(), out sp) && - !_speedsByLevel.TryGetValue(SpeedLevel.Medium, out sp)) - { - activeMoveSpeed = 0; - passiveMoveSpeed = 0; - return; - } - - activeMoveSpeed = sp.ActiveMoveSpeed; - passiveMoveSpeed = sp.PassiveMoveSpeed; - } - public static void RegisterSpeed(SpeedClassEntry entry) { _speedsByLevel[entry.Level] = entry; @@ -94,13 +78,6 @@ public static class NPCSpeeds [JsonPropertyName("passive")] public double PassiveSpeed { get; init; } - // Movement clock (seconds per step); absent/0 = inherit the matching think value. - [JsonPropertyName("activeMove")] - public double ActiveMoveSpeed { get; init; } - - [JsonPropertyName("passiveMove")] - public double PassiveMoveSpeed { get; init; } - [JsonPropertyName("types")] public HashSet Types { get; init; } } diff --git a/Projects/UOContent/Mobiles/Special/Harrower.cs b/Projects/UOContent/Mobiles/Special/Harrower.cs index 8d87943bc..ee3fa1873 100644 --- a/Projects/UOContent/Mobiles/Special/Harrower.cs +++ b/Projects/UOContent/Mobiles/Special/Harrower.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using ModernUO.Serialization; -using Server.Collections; using Server.Engines.CannedEvil; using Server.Engines.Virtues; using Server.Items; diff --git a/Projects/UOContent/Mobiles/Special/Paragon.cs b/Projects/UOContent/Mobiles/Special/Paragon.cs index cbd0a30c7..825609e22 100644 --- a/Projects/UOContent/Mobiles/Special/Paragon.cs +++ b/Projects/UOContent/Mobiles/Special/Paragon.cs @@ -79,7 +79,6 @@ public static class Paragon bc.PassiveSpeed /= SpeedBuff; bc.ActiveSpeed /= SpeedBuff; - bc.ScaleMoveSpeed(1.0 / SpeedBuff); bc.CurrentSpeed = bc.PassiveSpeed; bc.DamageMin += DamageBuff; @@ -144,8 +143,6 @@ public static class Paragon bc.PassiveSpeed *= SpeedBuff; bc.ActiveSpeed *= SpeedBuff; - bc.ScaleMoveSpeed(SpeedBuff); - bc.SnapSpeedsToTable(); // an ulp of scaling drift must not read as hand-tuned bc.CurrentSpeed = bc.PassiveSpeed; bc.DamageMin -= DamageBuff; diff --git a/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs b/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs index cf439a76c..462307be3 100644 --- a/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs +++ b/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs @@ -17,7 +17,7 @@ using EDI = Server.Mobiles.EscortDestinationInfo; namespace Server.Mobiles; -[SerializationGenerator(3, false)] +[SerializationGenerator(2, false)] public partial class BaseEscortable : BaseCreature { private static readonly ILogger logger = LogFactory.GetLogger(typeof(BaseEscortable)); @@ -158,22 +158,16 @@ public partial class BaseEscortable : BaseCreature [SerializableField(0, setter: "private")] private string _destinationString; + [TimerDrift] [SerializableField(1)] - [DeserializeTimer(nameof(DeserializeDeleteTimer))] private Timer _deleteTimer; - private void DeserializeDeleteTimer(TimeSpan delay) => Timer.DelayCall(delay, Delete); - - private void MigrateFrom(V2Content content) + [DeserializeTimerField(1)] + private void DeserializeDeleteTimer(TimeSpan delay) { - _destinationString = content.DestinationString; - _mlQuestType = content.MlQuestType; - _mlQuestDestinationMessage = content.MlQuestDestinationMessage; - _mlQuestPaymentMessage = content.MlQuestPaymentMessage; - - if (content.DeleteTimerDelay != TimeSpan.MinValue) + if (delay >= TimeSpan.Zero) { - DeserializeDeleteTimer(content.DeleteTimerDelay); + Timer.DelayCall(delay, Delete); } } diff --git a/Projects/UOContent/Mobiles/Vendors/Barkeeper/PlayerBarkeeper.cs b/Projects/UOContent/Mobiles/Vendors/Barkeeper/PlayerBarkeeper.cs index bbbc3ff6f..d80c60dcc 100644 --- a/Projects/UOContent/Mobiles/Vendors/Barkeeper/PlayerBarkeeper.cs +++ b/Projects/UOContent/Mobiles/Vendors/Barkeeper/PlayerBarkeeper.cs @@ -148,13 +148,18 @@ public partial class PlayerBarkeeper : BaseVendor LoadSBInfo(); } - [SerializableField(0, fieldChanged: nameof(OnHouseChanged))] - private BaseHouse _house; - - private void OnHouseChanged(BaseHouse oldValue, BaseHouse newValue) + [SerializableProperty(0)] + public BaseHouse House { - oldValue?.PlayerBarkeepers.Remove(this); - newValue?.PlayerBarkeepers.Add(this); + get => _house; + set + { + _house?.PlayerBarkeepers.Remove(this); + value?.PlayerBarkeepers.Add(this); + + _house = value; + this.MarkDirty(); + } } public override bool IsActiveBuyer => false; diff --git a/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs b/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs index 9b8f5a2f3..2ff6eb812 100644 --- a/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs +++ b/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs @@ -22,20 +22,9 @@ public class PlayerVendorTargetAttribute : Attribute; * Next, uncomment the MigrateFrom function and change the `V3Content` type to match the serialization version * before it was bumped. Then run publish.cmd to generate the migration file. */ -[SerializationGenerator(4, false)] +[SerializationGenerator(3, false)] public partial class PlayerVendor : Mobile { - private void MigrateFrom(V3Content content) - { - _shopName = content.ShopName; - _nextPayTime = content.NextPayTime; - _house = content.House; - _owner = content.Owner; - _bankAccount = content.BankAccount; - _holdGold = content.HoldGold; - _sellItems = content.SellItems; - } - private Timer _payTimer; [InvalidateProperties] @@ -43,7 +32,7 @@ public partial class PlayerVendor : Mobile [SerializedCommandProperty(AccessLevel.GameMaster)] private string _shopName; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(1, setter: "private")] [SerializedCommandProperty(AccessLevel.GameMaster)] private DateTime _nextPayTime; @@ -105,13 +94,18 @@ public partial class PlayerVendor : Mobile public PlayerVendorPlaceholder Placeholder { get; set; } - [SerializableField(2, fieldChanged: nameof(OnHouseChanged))] - private BaseHouse _house; - - private void OnHouseChanged(BaseHouse oldValue, BaseHouse newValue) + [SerializableProperty(2)] + public BaseHouse House { - oldValue?.PlayerVendors.Remove(this); - newValue?.PlayerVendors.Add(this); + get => _house; + set + { + _house?.PlayerVendors.Remove(this); + value?.PlayerVendors.Add(this); + + _house = value; + this.MarkDirty(); + } } public int ChargePerDay diff --git a/Projects/UOContent/Mobiles/Vendors/RentedVendor.cs b/Projects/UOContent/Mobiles/Vendors/RentedVendor.cs index d7d0a3ab6..99d818826 100644 --- a/Projects/UOContent/Mobiles/Vendors/RentedVendor.cs +++ b/Projects/UOContent/Mobiles/Vendors/RentedVendor.cs @@ -46,20 +46,9 @@ public class VendorRentalDuration } } -[SerializationGenerator(1)] +[SerializationGenerator(0)] public partial class RentedVendor : PlayerVendor { - private void MigrateFrom(V0Content content) - { - _rentalDurationId = content.RentalDurationId; - _rentalPrice = content.RentalPrice; - _landlordRenew = content.LandlordRenew; - _renterRenew = content.RenterRenew; - _renewalPrice = content.RenewalPrice; - _rentalGold = content.RentalGold; - _rentalExpireTime = content.RentalExpireTime; - } - private Timer _rentalExpireTimer; public RentedVendor( @@ -104,7 +93,7 @@ public partial class RentedVendor : PlayerVendor [SerializedCommandProperty(AccessLevel.GameMaster)] private int _rentalGold; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(6)] [SerializedCommandProperty(AccessLevel.GameMaster)] private DateTime _rentalExpireTime; diff --git a/Projects/UOContent/Mobiles/Vendors/VendorInventory.cs b/Projects/UOContent/Mobiles/Vendors/VendorInventory.cs index 8f0a1fba8..d4d5ce4bd 100644 --- a/Projects/UOContent/Mobiles/Vendors/VendorInventory.cs +++ b/Projects/UOContent/Mobiles/Vendors/VendorInventory.cs @@ -37,7 +37,7 @@ namespace Server.Mobiles Items = reader.ReadEntityList(); Gold = reader.ReadInt(); - ExpireTime = version >= 1 ? reader.ReadAnchoredTime() : reader.ReadDeltaTime(); + ExpireTime = reader.ReadDeltaTime(); if (Items.Count == 0 && Gold == 0) { @@ -88,7 +88,7 @@ namespace Server.Mobiles public void Serialize(IGenericWriter writer) { - writer.WriteEncodedInt(1); // version + writer.WriteEncodedInt(0); // version writer.Write(Owner); writer.Write(VendorName); @@ -98,7 +98,7 @@ namespace Server.Mobiles writer.Write(Items); writer.Write(Gold); - writer.WriteAnchoredTime(ExpireTime); + writer.WriteDeltaTime(ExpireTime); } private class ExpireTimer : Timer diff --git a/Projects/UOContent/Mobiles/Vendors/VendorItem.cs b/Projects/UOContent/Mobiles/Vendors/VendorItem.cs index 99d1446e6..14af3656a 100644 --- a/Projects/UOContent/Mobiles/Vendors/VendorItem.cs +++ b/Projects/UOContent/Mobiles/Vendors/VendorItem.cs @@ -32,20 +32,18 @@ public partial class VendorItem public string FormattedPrice => Core.ML ? Price.ToString("N0", CultureInfo.GetCultureInfo("en-US")) : Price.ToString(); - [SerializableField(2, fieldChanged: nameof(OnDescriptionChanged), allowFieldChange: nameof(AllowDescriptionChange))] - private string _description; - - private bool AllowDescriptionChange(ref string value) + [SerializableProperty(2)] + public string Description { - value = value ?? ""; - return true; - } - - private void OnDescriptionChanged(string oldValue, string newValue) - { - if (Valid) + get => _description; + set { - Item.InvalidateProperties(); + _description = value ?? ""; + + if (Valid) + { + Item.InvalidateProperties(); + } } } diff --git a/Projects/UOContent/Multis/Boats/BaseBoat.cs b/Projects/UOContent/Multis/Boats/BaseBoat.cs index ed618b91a..2082056da 100644 --- a/Projects/UOContent/Multis/Boats/BaseBoat.cs +++ b/Projects/UOContent/Multis/Boats/BaseBoat.cs @@ -19,24 +19,9 @@ namespace Server.Multis Single } - [SerializationGenerator(5, false)] + [SerializationGenerator(4, false)] public abstract partial class BaseBoat : BaseMulti { - private void MigrateFrom(V4Content content) - { - _mapItem = content.MapItem; - _nextNavPoint = content.NextNavPoint; - _facing = content.Facing; - _timeOfDecay = content.TimeOfDecay; - _owner = content.Owner; - _pPlank = content.PPlank; - _sPlank = content.SPlank; - _tillerMan = content.TillerMan; - _hold = content.Hold; - _anchored = content.Anchored; - _shipName = content.ShipName; - } - public enum DryDockResult { Valid, @@ -151,23 +136,31 @@ namespace Server.Multis } } - [SerializableField(3, fieldChanged: nameof(OnTimeOfDecayChanged))] - [AnchoredDateTime] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private DateTime _timeOfDecay; - - private void OnTimeOfDecayChanged(DateTime oldValue, DateTime newValue) + [DeltaDateTime] + [SerializableProperty(3)] + [CommandProperty(AccessLevel.GameMaster)] + public DateTime TimeOfDecay { - TillerMan?.InvalidateProperties(); + get => _timeOfDecay; + set + { + _timeOfDecay = value; + TillerMan?.InvalidateProperties(); + this.MarkDirty(); + } } - [SerializableField(10, fieldChanged: nameof(OnShipNameChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private string _shipName; - - private void OnShipNameChanged(string oldValue, string newValue) + [SerializableProperty(10)] + [CommandProperty(AccessLevel.GameMaster)] + public string ShipName { - TillerMan?.InvalidateProperties(); + get => _shipName; + set + { + _shipName = value; + TillerMan?.InvalidateProperties(); + this.MarkDirty(); + } } [CommandProperty(AccessLevel.GameMaster)] diff --git a/Projects/UOContent/Multis/Camps/BaseCamp.cs b/Projects/UOContent/Multis/Camps/BaseCamp.cs index e60e3e46e..e3d10dd0b 100644 --- a/Projects/UOContent/Multis/Camps/BaseCamp.cs +++ b/Projects/UOContent/Multis/Camps/BaseCamp.cs @@ -6,16 +6,9 @@ using Server.Mobiles; namespace Server.Multis; -[SerializationGenerator(2, false)] +[SerializationGenerator(1, false)] public abstract partial class BaseCamp : BaseMulti { - private void MigrateFrom(V1Content content) - { - _items = content.Items; - _mobiles = content.Mobiles; - _decayTime = content.DecayTime; - } - [Tidy] [SerializableField(0, setter: "private")] private List _items; @@ -24,7 +17,7 @@ public abstract partial class BaseCamp : BaseMulti [SerializableField(1, setter: "private")] private List _mobiles; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(2, setter: "private")] private DateTime _decayTime; diff --git a/Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs b/Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs index 384e468b9..a6297837d 100644 --- a/Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs +++ b/Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs @@ -86,17 +86,11 @@ public static class IncomingPlayerPackets public static void HuePickerResponse(NetState state, SpanReader reader) { var serial = reader.ReadUInt32(); - reader.ReadInt16(); // Item ID + _ = reader.ReadInt16(); // Item ID var hue = Utility.ClipDyedHue(reader.ReadInt16() & 0x3FFF); - if (state.HuePickers == null) + foreach (var huePicker in state.HuePickers) { - return; - } - - for (var i = 0; i < state.HuePickers.Count; i++) - { - var huePicker = state.HuePickers[i]; if (huePicker.Serial == serial) { state.RemoveHuePicker(huePicker); @@ -293,18 +287,13 @@ public static class IncomingPlayerPackets public static void MenuResponse(NetState state, SpanReader reader) { var serial = reader.ReadUInt32(); - reader.ReadInt16(); // menu id + int menuID = reader.ReadInt16(); int index = reader.ReadInt16(); - reader.ReadInt16(); // item id - reader.ReadInt16(); // hue + int itemID = reader.ReadInt16(); + int hue = reader.ReadInt16(); index -= 1; // convert from 1-based to 0-based - if (state.Menus == null) - { - return; - } - for (var i = 0; i < state.Menus.Count; i++) { var menu = state.Menus[i]; diff --git a/Projects/UOContent/Regions/BaseRegion.cs b/Projects/UOContent/Regions/BaseRegion.cs index 776ec269d..b18c7fa18 100644 --- a/Projects/UOContent/Regions/BaseRegion.cs +++ b/Projects/UOContent/Regions/BaseRegion.cs @@ -113,7 +113,7 @@ public class BaseRegion : Region m_RectBuffer2.RemoveAt(k); var sz = rect.Start.Z; - var ez = rect.End.Z; + var ez = rect.End.X; if (l1 < l2) { diff --git a/Projects/UOContent/Spells/Fifth/PoisonField.cs b/Projects/UOContent/Spells/Fifth/PoisonField.cs index 92a797f19..3c5d2f609 100644 --- a/Projects/UOContent/Spells/Fifth/PoisonField.cs +++ b/Projects/UOContent/Spells/Fifth/PoisonField.cs @@ -64,19 +64,13 @@ public class PoisonFieldSpell : MagerySpell, ITargetingSpell } [DispellableField] -[SerializationGenerator(1, false)] +[SerializationGenerator(0, false)] public partial class PoisonField : Item { - private void MigrateFrom(V0Content content) - { - _caster = content.Caster; - _end = content.End; - } - [SerializableField(0)] private Mobile _caster; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(1)] private DateTime _end; diff --git a/Projects/UOContent/Spells/Fourth/FireField.cs b/Projects/UOContent/Spells/Fourth/FireField.cs index 8f20f995b..0b0a22129 100644 --- a/Projects/UOContent/Spells/Fourth/FireField.cs +++ b/Projects/UOContent/Spells/Fourth/FireField.cs @@ -67,23 +67,16 @@ public class FireFieldSpell : MagerySpell, ITargetingSpell } [DispellableField] -[SerializationGenerator(1, false)] +[SerializationGenerator(0, false)] public partial class FireFieldItem : Item { - private void MigrateFrom(V0Content content) - { - _damage = content.Damage; - _caster = content.Caster; - _end = content.End; - } - [SerializableField(0)] private int _damage; [SerializableField(1)] private Mobile _caster; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(2)] private DateTime _end; private Timer _timer; diff --git a/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs b/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs index 63788aa69..6d85b9d73 100644 --- a/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs +++ b/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs @@ -238,7 +238,10 @@ namespace Server.Mobiles if (master?.Map == Mobile.Map && master?.InRange(Mobile, Mobile.RangePerception) == true) { - WalkMobileRange(master, 2, 0, 1); + var iCurrDist = (int)Mobile.GetDistanceToSqrt(master); + var bRun = iCurrDist > 5; + + WalkMobileRange(master, 2, bRun, 0, 1); } else { diff --git a/Projects/UOContent/Spells/Seventh/EnergyField.cs b/Projects/UOContent/Spells/Seventh/EnergyField.cs index dbfece36d..5e3ea1149 100644 --- a/Projects/UOContent/Spells/Seventh/EnergyField.cs +++ b/Projects/UOContent/Spells/Seventh/EnergyField.cs @@ -77,19 +77,13 @@ public class EnergyFieldSpell : MagerySpell, ITargetingSpell } [DispellableField] -[SerializationGenerator(2, false)] +[SerializationGenerator(1, false)] public partial class EnergyField : Item { - private void MigrateFrom(V1Content content) - { - _caster = content.Caster; - _end = content.End; - } - [SerializableField(0)] private Mobile _caster; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(1)] private DateTime _end; diff --git a/Projects/UOContent/Spells/Sixth/ParalyzeField.cs b/Projects/UOContent/Spells/Sixth/ParalyzeField.cs index 301cee507..830f16dfe 100644 --- a/Projects/UOContent/Spells/Sixth/ParalyzeField.cs +++ b/Projects/UOContent/Spells/Sixth/ParalyzeField.cs @@ -77,19 +77,13 @@ public class ParalyzeFieldSpell : MagerySpell, ITargetingSpell } [DispellableField] -[SerializationGenerator(1, false)] +[SerializationGenerator(0, false)] public partial class ParalyzeField : Item { - private void MigrateFrom(V0Content content) - { - _caster = content.Caster; - _end = content.End; - } - [SerializableField(0)] private Mobile _caster; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(1)] private DateTime _end; diff --git a/Projects/UOContent/Spells/Spellweaving/Items/TransientItem.cs b/Projects/UOContent/Spells/Spellweaving/Items/TransientItem.cs index 49894cc95..54ac52030 100644 --- a/Projects/UOContent/Spells/Spellweaving/Items/TransientItem.cs +++ b/Projects/UOContent/Spells/Spellweaving/Items/TransientItem.cs @@ -3,17 +3,12 @@ using ModernUO.Serialization; namespace Server.Items; -[SerializationGenerator(2, false)] +[SerializationGenerator(1, false)] public partial class TransientItem : Item { - private void MigrateFrom(V1Content content) - { - _expiration = content.Expiration; - } - private TimerExecutionToken _timerToken; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(0)] [SerializedCommandProperty(AccessLevel.GameMaster)] private DateTime _expiration; diff --git a/Projects/UOContent/Spells/Third/WallOfStone.cs b/Projects/UOContent/Spells/Third/WallOfStone.cs index 1b1dd04d3..7cae3b848 100644 --- a/Projects/UOContent/Spells/Third/WallOfStone.cs +++ b/Projects/UOContent/Spells/Third/WallOfStone.cs @@ -63,19 +63,13 @@ public class WallOfStoneSpell : MagerySpell, ITargetingSpell } [DispellableField] -[SerializationGenerator(1, false)] +[SerializationGenerator(0, false)] public partial class WallOfStone : Item { - private void MigrateFrom(V0Content content) - { - _caster = content.Caster; - _end = content.End; - } - [SerializableField(0)] private Mobile _caster; - [AnchoredDateTime] + [DeltaDateTime] [SerializableField(1)] private DateTime _end; diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index 60f28ac6f..5c7b74214 100644 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -50,8 +50,8 @@ - - + + diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md index 7310c104e..8edf70a72 100644 --- a/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md +++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md @@ -29,9 +29,6 @@ description: > - `BaseCreature(AI, Fight, 10, 1, 0.2, 0.4)` -> `BaseCreature(AI, Fight)` (extra params default) - `Name = "text"` -> `public override string DefaultName => "text";` - Expression-bodied overrides: `public override int Meat { get { return 1; } }` -> `public override int Meat => 1;` -- AI movement calls lose the `run` flag: `MoveTo(m, true, range)` -> `MoveTo(m, range)` (also `WalkMobileRange`, `ApproachTarget`, `MoveToPoint`, `PathFollower.Follow`); the Running bit is derived from step pace -> `dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md` § AI Movement -- `AcquireOnApproach` (bool) -> `AcquireOnApproachDelay` (TimeSpan; `Zero` = old instant behavior) -> same doc § Target Acquisition -- `DamageEntries` is an inline `ref readonly ValueLinkList`, not a `List`: indexer/`Add`/`Remove`/`Clear` -> `foreach` / `.ByDescending()` (needs `using Server.Collections;`) and `ClearDamageEntries()`; `GetLootingRights` takes it by `in` -> same doc § Damage Entries ## Anti-Patterns - Using `_field--` instead of `Property--` (bypasses MarkDirty tracking) diff --git a/dev-docs/claude-skills/modernuo-content-patterns.md b/dev-docs/claude-skills/modernuo-content-patterns.md index 6e0902b64..12c9515ef 100644 --- a/dev-docs/claude-skills/modernuo-content-patterns.md +++ b/dev-docs/claude-skills/modernuo-content-patterns.md @@ -23,22 +23,6 @@ description: > 4. **Clean up timers and references in `OnDelete()`/`OnAfterDelete()`** 5. **No LINQ** in game logic -- use loops and `PooledRefList` 6. **File placement** matters -- follow the directory conventions below -7. **Creature speeds are delays in seconds, on two clocks** -- think - (`ActiveSpeed`/`PassiveSpeed`, seconds per AI decision) and move - (`ActiveMoveSpeed`/`PassiveMoveSpeed`, seconds per step; inherits think until - overridden). Prefer `npc-speeds.json` buckets (`SpeedClass`); `SetSpeed()` sets think - AND clears move overrides, `SetMoveSpeed()` sets move only. The client `Running` bit is - derived from the step pace (`BaseAI.ShouldRun`); movement APIs take no run argument -- - see `dev-docs/content-patterns.md` § Creature Speeds. Reaction time to approaching - enemies is `AcquireOnApproachDelay` (TimeSpan gradient; `Zero` = paragon snap, 2s - default, `ReacquireDelay`-only = oblivious) -- see § Target Acquisition -8. **`OnThink` overrides must be excess-call tolerant** -- it fires more often than the - think cadence (player commands prod it; speed-ups reschedule it). Gate consequential - work on a tick-count deadline (subtraction form) or make it idempotent; bare per-call - random rolls are cosmetics-only. `MonsterAbility` is under the same contract: the - trigger cooldown is the rate limit, `ChanceToTrigger` is per-sample jitter, and a - zero-cooldown `Think`/`CombatAction` ability triggers every sampled think -- see - `dev-docs/content-patterns.md` § OnThink: the excess-call contract ## New Item Template diff --git a/dev-docs/claude-skills/modernuo-serialization.md b/dev-docs/claude-skills/modernuo-serialization.md index 91e3d5e95..c96244656 100644 --- a/dev-docs/claude-skills/modernuo-serialization.md +++ b/dev-docs/claude-skills/modernuo-serialization.md @@ -40,31 +40,21 @@ public partial class MyItem : Item { } public partial class MigratedItem : Item { } ``` -### [SerializableField(index, getter, setter, isVirtual, fieldChanged, allowFieldChange)] +### [SerializableField(index, setter, saveIf)] Applied to `_camelCase` private fields. Generates `PascalCase` property. - `index`: Serialization order (0+) -- `getter`/`setter`: Access level -- `"private"`, `"internal"`, or omit for public -- `isVirtual`: Generate a virtual property -- `fieldChanged`: `nameof` of `void Method(T oldValue, T newValue)`, invoked by the generated setter after assignment -- `allowFieldChange`: `nameof` of `bool Method(ref T value)`, invoked before assignment -- coerce through the `ref` parameter or return `false` to reject - -Generated setter pipeline: equality check → `allowFieldChange` → assignment → `MarkDirty` → `InvalidateProperties` (if declared) → `fieldChanged`. The field still holds the old value while the gate runs. Hooks require a generated setter (SG3018 on readonly/setterless fields); a missing or wrong-shaped named method is SG3015. +- `setter`: Access level -- `"private"`, `"internal"`, or omit for public +- `saveIf`: Condition method name for conditional serialization ```csharp -[SerializableField(0, allowFieldChange: nameof(AllowChargesChange))] +[SerializableField(0)] [SerializedCommandProperty(AccessLevel.GameMaster)] -[InvalidateProperties] private int _charges; - -private bool AllowChargesChange(ref int value) -{ - value = Math.Clamp(value, 0, MaxCharges); - return true; -} +// Generates: public int Charges { get; set; } ``` ### [SerializableProperty(index, useField)] -Applied to properties with **custom getters** (fallback defaults, lazy/self-healing reads) or setter semantics the field hooks cannot express. For setters that only coerce, veto, or run post-change side effects, use `[SerializableField]` with `allowFieldChange`/`fieldChanged` instead. +Applied to properties with custom get/set logic. - `index`: Serialization order - `useField`: Backing field name if auto-detection fails @@ -73,12 +63,12 @@ Applied to properties with **custom getters** (fallback defaults, lazy/self-heal [CommandProperty(AccessLevel.GameMaster)] public int MaxItems { - get => _maxItems == -1 ? DefaultMaxItems : _maxItems; // custom getter: the reason this is a property + get => _maxItems == -1 ? DefaultMaxItems : _maxItems; set { _maxItems = value; InvalidateProperties(); - this.MarkDirty(); // REQUIRED in custom setters + this.MarkDirty(); } } ``` @@ -99,11 +89,8 @@ Exposes field to `[Props` gump for in-game editing. ### [EncodedInt] Variable-length int encoding (saves space for small values). -### [AnchoredDateTime] -Stores the absolute UTC instant; shifted by downtime at load so remaining time is preserved. Byte-stable across idle saves. Prefer for deadlines/elapsed-while-running values. - ### [DeltaDateTime] -Stores DateTime as offset from current time (handles server restarts). Legacy: rewrites bytes every save; prefer `[AnchoredDateTime]` for new fields. Converting between the two changes the wire format (version bump). +Stores DateTime as offset from current time (handles server restarts). ### [InternString] Interns strings to reduce memory for repeated values. @@ -141,32 +128,28 @@ private void AfterDeserialization() } ``` -### [DeserializeTimer(nameof(Method), wallClock)] -Required on every serializable `Timer` member (SG3008 otherwise). By default the next tick is stored as **anchored time** (downtime does not consume the remaining delay; idle saves byte-stable); `wallClock: true` stores an absolute deadline instead (delay negative if it passed during downtime). The method -- `void Method(TimeSpan delay)` -- is invoked **only when a timer was running at save**; there is no sentinel to check. +### [DeserializeTimerField(fieldIndex)] +Custom timer deserialization. Timer is saved as remaining TimeSpan. ```csharp [SerializableField(0, setter: "private")] -[DeserializeTimer(nameof(DeserializeEvaluateTimer), wallClock: true)] private Timer _evaluateTimer; +[DeserializeTimerField(0)] private void DeserializeEvaluateTimer(TimeSpan delay) { _evaluateTimer = Timer.DelayCall(delay, EvaluationInterval, Evaluate); } ``` -Switching a timer between drifting and `wallClock` changes the wire format: bump the class version and add `MigrateFrom` -- the old content struct exposes `XxxNext` (`DateTime`) and `XxxDelay` (`TimeSpan`, `TimeSpan.MinValue` when no timer ran). - -### [SaveFlag(nameof(ShouldSerializeMethod), nameof(DefaultValueMethod))] -On the serializable field/property itself. Conditional serialization -- skip fields with default values. Second method optional; when omitted, the field keeps its default at load. +### [SerializableFieldSaveFlag(fieldIndex)] / [SerializableFieldDefault(fieldIndex)] +Conditional serialization -- skip fields with default values. ```csharp -[SerializableField(0)] -[SaveFlag(nameof(ShouldSerializeMaxItems), nameof(MaxItemsDefaultValue))] -private int _maxItems; - +[SerializableFieldSaveFlag(0)] private bool ShouldSerializeMaxItems() => _maxItems != -1; +[SerializableFieldDefault(0)] private int MaxItemsDefaultValue() => -1; ``` @@ -233,35 +216,28 @@ public partial class ChargedItem : Item } ``` -### Item with Setter Hooks (coerce + side effects) +### Item with Custom Properties ```csharp [SerializationGenerator(2)] public partial class BagOfSending : Item { - [SerializableField(0, fieldChanged: nameof(OnBagOfSendingHueChanged))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - private BagOfSendingHue _bagOfSendingHue; - - private void OnBagOfSendingHueChanged(BagOfSendingHue oldValue, BagOfSendingHue newValue) + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public BagOfSendingHue BagOfSendingHue { - Hue = newValue switch + get => _bagOfSendingHue; + set { - BagOfSendingHue.Yellow => 0x8A5, - BagOfSendingHue.Blue => 0x8AD, - BagOfSendingHue.Red => 0x89B, - _ => Hue - }; - } - - [SerializableField(1, allowFieldChange: nameof(AllowChargesChange))] - [SerializedCommandProperty(AccessLevel.GameMaster)] - [InvalidateProperties] - private int _charges; - - private bool AllowChargesChange(ref int value) - { - value = Math.Clamp(value, 0, MaxCharges); - return true; + _bagOfSendingHue = value; + Hue = value switch + { + BagOfSendingHue.Yellow => 0x8A5, + BagOfSendingHue.Blue => 0x8AD, + BagOfSendingHue.Red => 0x89B, + _ => Hue + }; + this.MarkDirty(); + } } } ``` @@ -352,13 +328,11 @@ public partial class MagicGem ## Real Examples - Simple creature: `Projects/UOContent/Mobiles/Animals/Bears/BlackBear.cs` - Serialized fields + timer: `Projects/UOContent/Items/Weapons/Ranged/BaseRanged.cs` -- Setter hooks (allowFieldChange + fieldChanged): `Projects/UOContent/Items/Special/Solen Items/BagOfSending.cs` -- Custom getters (era fallbacks, the [SerializableProperty] use case): `Projects/UOContent/Items/Weapons/BaseWeapon.cs` +- Custom properties: `Projects/UOContent/Items/Special/Solen Items/BagOfSending.cs` - Complex with AfterDeserialization: `Projects/UOContent/Accounting/Account.cs` -- Timer deserialization (wall-clock): `Projects/UOContent/Items/Aquarium/Aquarium.cs` -- Timer deserialization (drifting/anchored + timer MigrateFrom): `Projects/UOContent/Items/Lights/BaseLight.cs` +- Timer deserialization: `Projects/UOContent/Items/Aquarium/Aquarium.cs` - Tidy + DeltaDateTime: `Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs` -- Conditional serialization ([SaveFlag]): `Projects/Server/Items/Container.cs` +- Conditional serialization: `Projects/Server/Items/Container.cs` ## Version Migration Migration schemas are JSON files in `Projects/Server/Migrations/` and `Projects/UOContent/Migrations/`: diff --git a/dev-docs/claude-skills/modernuo-timers.md b/dev-docs/claude-skills/modernuo-timers.md index f366b3569..2c5c8168e 100644 --- a/dev-docs/claude-skills/modernuo-timers.md +++ b/dev-docs/claude-skills/modernuo-timers.md @@ -147,27 +147,18 @@ public partial class DecayingItem : Item } ``` -### [DeserializeTimer] Pattern (for Timer fields) -Required on every serializable `Timer` member. Drifting by default: the next tick is stored -as anchored time, so server downtime does not consume the remaining delay. Use -`wallClock: true` for absolute deadlines (delay is negative if it passed during downtime). -The method is invoked **only when a timer was running at save** — no sentinel to check. - +### [DeserializeTimerField] Pattern (for Timer fields) ```csharp [SerializableField(0, setter: "private")] -[DeserializeTimer(nameof(DeserializeEvaluateTimer), wallClock: true)] private Timer _evaluateTimer; +[DeserializeTimerField(0)] private void DeserializeEvaluateTimer(TimeSpan delay) { _evaluateTimer = Timer.DelayCall(delay, EvaluationInterval, Evaluate); } ``` -Switching an existing timer between drifting and `wallClock` changes the wire format — bump -the class's `[SerializationGenerator]` version and add a `MigrateFrom` (the old content -struct exposes `XxxDelay`, `TimeSpan.MinValue` when no timer ran). - ### Custom Timer Class (When You Need Complex Logic) ```csharp private class DecayTimer : Timer diff --git a/dev-docs/content-patterns.md b/dev-docs/content-patterns.md index 6205ec2d4..30bb942f3 100644 --- a/dev-docs/content-patterns.md +++ b/dev-docs/content-patterns.md @@ -254,120 +254,6 @@ public override int TreasureMapLevel => 3; // Drops treasure map public override double WeaponAbilityChance => 0.4; // Weapon ability chance ``` -### Creature Speeds (think vs move clocks) - -All "speed" values are **delays in seconds** (smaller = faster). A creature runs two clocks: - -- **Think clock** — `ActiveSpeed`/`PassiveSpeed`/`CurrentSpeed`: seconds per AI decision - (combat decisions, target acquisition, spell timing). -- **Move clock** — `ActiveMoveSpeed`/`PassiveMoveSpeed`/`CurrentMoveSpeed`: seconds per - step. Inherits the matching think value until overridden, so a creature configured with - only think speeds behaves as one clock. Any value is legal — steps are scheduled - independently of think ticks, so the two need not divide evenly. - -Speeds normally come from `Distribution/Data/npc-speeds.json` (via `SpeedClass` or type -lists); `activeMove`/`passiveMove` are optional per bucket. Prefer data over code: - -```csharp -public override SpeedLevel SpeedClass => SpeedLevel.Slow; // bucket in npc-speeds.json -``` - -Code-level overrides for special cases: - -```csharp -SetSpeed(0.5, 2.0); // think clock; ALSO clears move overrides (one-clock legacy semantics) -SetMoveSpeed(0.45, 0.9); // move clock only — call after SetSpeed if both are wanted -ClearMoveSpeed(); // back to inheriting the think clock -``` - -All four are `[props`-tunable per instance (move values: set `0` to re-inherit); per-instance -move overrides serialize. Being badly hurt slows steps, never decisions (RunUO parity). - -The client's `Running` bit is derived from the step pace, never passed by callers -(`BaseAI.ShouldRun`, stamped in `DoMoveImpl`): a step shorter than the client's walk -interpolation — 400 ms on foot, 200 ms mounted/flying (`Movement.WalkFootDelay` / -`WalkMountDelay`) — is flagged as a run, or the client falls behind and snaps. An isolated -step (resuming after at least a walk interval standing) goes out as a walk regardless of -pace — the client renders each step alone, so a run-flagged single step darts — unless the -pace beats the run interpolation (a true sprinter), where a walk-rendered first step would -flood the client's step queue. Movement APIs (`MoveTo`, `WalkMobileRange`, -`ApproachTarget`, `MoveToPoint`) take no run argument; to make a creature run, make it -fast. Creatures step at most once per `CurrentMoveSpeed` period, paced from the step just -taken — a stall never banks catch-up steps, so a resumed chase restarts at full pace. - -### Target Acquisition: the reaction-time gradient - -Acquisition is event-driven, not polled. The periodic scan (`AcquireFocusMob`) is gated by -`ReacquireDelay` (10 s default) and every scan re-arms it in full, success or failure — it -is target stickiness plus the fallback for what movement cannot signal (reveals, doors, -summons). Reaction time comes from `BaseCreature.OnMovement`: an enemy moving inside -`AcquireOnApproachRange` (10 — on-screen; the periodic scan keeps the wider -`RangePerception`) clamps the next scan to -at most **`AcquireOnApproachDelay`** — the intelligence gradient. `TimeSpan.Zero` -(paragons) also prods the AI, so the ranked scan engages within a timer-wheel turn; the -2 s default reads as "took a beat to notice you"; larger is dumber; a creature that -overrides the delay above `ReacquireDelay` is effectively oblivious to approach. Repeated -steps cannot shorten the clamp, so an armed creature scans once per delay period, not once -per step or think. `ReacquireOnMovement` remains the broader hook (any mover, no enemy -check, scan next think). The gate self-heals: a deadline further out than `ReacquireDelay` -is illegal and reads as open, so no wedged or wrapped value can silence acquisition beyond -one delay period. - -### OnThink: the excess-call contract - -`OnThink()` is a scheduler pass, not an action. The AI timer calls it *at least* at the -think cadence (`CurrentSpeed`), but it can and does fire more often: a player command -wakes the AI immediately (`AITimer.Prod()`), a speed-up reschedules the pending wake, and -players run command macros that drive extra thinks deliberately (order spam is spam-safe -by design — reaction, never action). RunUO had the same property (its timer restarted -with a random delay on every speed change), so this has never been a fixed-rate callback. - -**Every `OnThink` override must be excess-call tolerant.** An extra call must never grant -an extra action: - -- Gate consequential work on its own deadline field, compared in subtraction form - (`Core.TickCount - _nextX >= 0` — see `tick-counts.md`), or make it idempotent. -- Never pace a consequential action with a bare per-call `Utility.RandomDouble()` roll — - its frequency then scales with think rate, which players can influence. Per-call rolls - are acceptable only for pure cosmetics (idle animations, flavor sounds). -- The engine already gates the expensive things: steps (the `NextMove` budget), weapon - swings, spell casts, detect-hidden, and the base `BaseCreature.OnThink` actions (heal, - rummage, aura) all carry their own clocks. Follow that pattern. - -```csharp -private long _nextSpecial; - -public override void OnThink() -{ - base.OnThink(); - - if (Core.TickCount - _nextSpecial >= 0) - { - DoSpecial(); - _nextSpecial = Core.TickCount + 5000; // the real rate limit lives here - } -} -``` - -### MonsterAbility: same contract - -`MonsterAbility.CanTrigger` is sampled once per think for `Think`- and -`CombatAction`-triggered abilities, so abilities live under the same rule: - -- **`MinTriggerCooldown`/`MaxTriggerCooldown` is the real rate limit** — the floor holds - no matter how often thinks fire. Always give a triggered ability a real cooldown. -- **`ChanceToTrigger` is a per-sample roll**: above the cooldown floor, the expected - trigger delay shrinks as think rate rises. Treat the chance as flavor jitter, never as - the rate limiter, and keep cooldowns long relative to the think interval so the jitter - stays negligible (fire breath — chance 0.5, cooldown 30–45s — varies under 1% between - natural and spammed think rates). -- A **zero-cooldown ability records no cooldown at all** and triggers on every sampled - think that passes its chance — only ever correct for passive alteration hooks, never - for `Think`/`CombatAction` triggers. -- An ability that breaks pet orders (fear-style effects) must own its duration explicitly - (a hold state, or a "refuses orders until" deadline checked in the order handlers) — - pets react to re-issued commands immediately, so think latency is not a hold. - --- ## New Spell diff --git a/dev-docs/runuo-migration-docs/02-serialization.md b/dev-docs/runuo-migration-docs/02-serialization.md index 666ebfdb4..1470792af 100644 --- a/dev-docs/runuo-migration-docs/02-serialization.md +++ b/dev-docs/runuo-migration-docs/02-serialization.md @@ -457,24 +457,16 @@ set ``` Without this, changes won't be saved. -Most RunUO custom setters only clamp the value or run side effects after assignment. Those -convert to a plain `[SerializableField]` with the `allowFieldChange`/`fieldChanged` hooks, -which handle the equality check and `MarkDirty()` for you -- reserve `[SerializableProperty]` -for custom getters (see `dev-docs/serialization.md`). - ### 3. Field Ordering The `[SerializableField(N)]` index determines serialization order. Choose a logical order and don't change it after the first save — or increment the version. ### 4. Conditional Serialization -Use `[SaveFlag]` on the serializable field to skip default values (the second method is -optional -- omit it and the field keeps its default at load): +Use `[SerializableFieldSaveFlag]` and `[SerializableFieldDefault]` to skip default values: ```csharp -[SerializableField(0)] -[SaveFlag(nameof(ShouldSerializeMaxItems), nameof(MaxItemsDefaultValue))] -private int _maxItems; - +[SerializableFieldSaveFlag(0)] private bool ShouldSerializeMaxItems() => _maxItems != -1; +[SerializableFieldDefault(0)] private int MaxItemsDefaultValue() => -1; ``` @@ -487,15 +479,12 @@ private List _followers; ``` ### 6. DateTime Fields -Use `[AnchoredDateTime]` to survive server restarts -- the value is shifted by downtime at -load, so the remaining time is preserved and idle saves stay byte-stable: +Use `[DeltaDateTime]` to survive server restarts: ```csharp -[AnchoredDateTime] +[DeltaDateTime] [SerializableField(0)] private DateTime _expireTime; ``` -(`[DeltaDateTime]` is the legacy equivalent; it rewrites bytes on every save. Converting an -existing field between the two changes the wire format and requires a version bump.) ### 7. Keeping Manual Serialization (Rare) Some edge cases still need manual serialization. If a type has complex conditional logic that can't be expressed with attributes, you can implement `ISerializable` manually. But this is rare — try attributes first. diff --git a/dev-docs/runuo-migration-docs/03-timers.md b/dev-docs/runuo-migration-docs/03-timers.md index 13596a079..29f3d1282 100644 --- a/dev-docs/runuo-migration-docs/03-timers.md +++ b/dev-docs/runuo-migration-docs/03-timers.md @@ -305,7 +305,7 @@ In RunUO, timers are commonly started in `Deserialize()`. In ModernUO, use `[Aft `_token.Cancel()` can be called on a default token, a stopped token, or an already-cancelled token. No null checks needed. ### 4. Timer.DelayCall Still Exists -`Timer.DelayCall()` is still available and returns a `Timer` object. Use it when you need the `Timer` reference (e.g., for a serialized timer field with `[DeserializeTimer]`) or state-carrying overloads. +`Timer.DelayCall()` is still available and returns a `Timer` object. Use it when you need the `Timer` reference (e.g., for `[DeserializeTimerField]`) or state-carrying overloads. ### 5. Custom Timer Classes Are Still Possible For complex timer logic (e.g., `Corpse.DecayTimer`), you can still subclass `Timer` with `OnTick()`. But prefer the fire-and-forget pattern for simple cases. diff --git a/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md b/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md index def2ecf48..c5029131c 100644 --- a/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md +++ b/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md @@ -474,88 +474,6 @@ The extra parameters (RangePerception, RangeFight, ActiveSpeed, PassiveSpeed) ha | `Name = "a creature"` in constructor | `public override string DefaultName => "a creature";` | | `get { return value; }` | `=> value;` expression-bodied | -## AI Movement: No `run` Argument - -RunUO's movement calls took a `run` flag that callers set inconsistently (`true` in -combat, `false` for pets, gated by `dist > 5` inside `MoveTo`). The flag only selects the -client's per-step animation time, so ModernUO derives it from the creature's step pace -(`BaseAI.ShouldRun`) and the parameter is gone: - -```csharp -// RunUO -MoveTo(combatant, true, m_Mobile.RangeFight); -WalkMobileRange(m_Mobile.ControlMaster, 1, false, 0, 1); - -// ModernUO -MoveTo(combatant, Mobile.RangeFight); -WalkMobileRange(Mobile.ControlMaster, 1, 0, 1); -``` - -`ApproachTarget`, `MoveToPoint` and `PathFollower.Follow` lose the argument the same way. -To make a creature run, make it fast (`SetMoveSpeed` / `npc-speeds.json`), not flagged. -An isolated step (after the creature stood for at least a walk interval) goes out as a -walk regardless of pace — only a continuing cadence, or a pace faster than the run -interpolation, flags run. - -## Target Acquisition: `AcquireOnApproach` Is a Delay - -RunUO's `AcquireOnApproach` bool (paragon insta-aggro on approach) is now -`AcquireOnApproachDelay`, a `TimeSpan` reaction-time gradient that applies to every -creature — enemy movement inside `AcquireOnApproachRange` schedules a scan within the -delay instead of waiting out the 10 s `ReacquireDelay` poll: - -```csharp -// RunUO -public override bool AcquireOnApproach => true; - -// ModernUO — Zero is the old instant behavior; larger values are dumber -public override TimeSpan AcquireOnApproachDelay => TimeSpan.Zero; -``` - -`AcquireOnApproachRange` stays 10 for all creatures (reactive aggro is on-screen; the -periodic `ReacquireDelay` scan still sweeps the full `RangePerception`). The -acquired target comes from the normal FightMode-ranked scan, not from whichever mobile -happened to move. See `content-patterns.md` § Target Acquisition. - -## Damage Entries: Inline `ValueLinkList`, Not `List` - -RunUO's `Mobile.DamageEntries` was a `List` allocated for every mobile. -ModernUO keeps damage entries in an inline `ValueLinkList` struct held by -the mobile itself, ordered least recent → most recent, so a mobile that never takes -damage owns no list object and `RegisterDamage` relinks in O(1). The property is -`ref readonly`; expired entries are pruned when it is read. - -```csharp -// RunUO -for (var i = m.DamageEntries.Count - 1; i >= 0; --i) -{ - var de = m.DamageEntries[i]; // indexer - ... -} -m.DamageEntries.Clear(); -var rights = BaseCreature.GetLootingRights(m.DamageEntries, m.HitsMax); // List - -// ModernUO — needs `using Server.Collections;` for the enumerator extensions -foreach (var de in m.DamageEntries.ByDescending()) // most recent first -{ - ... -} -foreach (var de in m.DamageEntries) // least recent first -{ - ... -} -m.ClearDamageEntries(); -var rights = BaseCreature.GetLootingRights(m.DamageEntries, m.HitsMax); // in ValueLinkList -``` - -What no longer compiles: the indexer, `.Add`, `.Remove`, `.RemoveAt`, `.Clear`, and -passing the property where a `List` is expected. `.Count`, -`FindDamageEntryFor`, `FindMostRecentDamager` and the other `Find*` methods, and -`RegisterDamage` are unchanged. `DamageEntry` now carries `Next`/`Previous`/`OnLinkList` -link fields; never set them yourself, and never call a `ValueLinkList` mutator on the -`ref readonly` property — it compiles against a copy and corrupts the node's link state. -Mutate only through `RegisterDamage` and `ClearDamageEntries`. - ## Item Name Changes ```csharp diff --git a/dev-docs/runuo-migration-docs/11-api-reference.md b/dev-docs/runuo-migration-docs/11-api-reference.md index b6c12bfe8..215dd3b06 100644 --- a/dev-docs/runuo-migration-docs/11-api-reference.md +++ b/dev-docs/runuo-migration-docs/11-api-reference.md @@ -130,13 +130,6 @@ Alphabetical by RunUO API name. Use Ctrl+F / Cmd+F to search. | `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 | -| `m.DamageEntries` (`List`) | `m.DamageEntries` (`ref readonly ValueLinkList`) | Inline, least→most recent; `foreach` / `.ByDescending()` only, needs `using Server.Collections;`; no indexer, `Add`, `Remove`, `Clear` | -| `m.DamageEntries.Clear()` | `m.ClearDamageEntries()` | | -| `GetLootingRights(List, int)` | `GetLootingRights(in ValueLinkList, int)` | Callers passing `m.DamageEntries` compile unchanged | ## Networking diff --git a/dev-docs/serialization.md b/dev-docs/serialization.md index 059fbff69..151d9d717 100644 --- a/dev-docs/serialization.md +++ b/dev-docs/serialization.md @@ -117,7 +117,7 @@ public partial class MyItem : Item { } See `dev-docs/runuo-migration-docs/02-serialization.md` for complete migration guidance. -### [SerializableField(index, getter, setter, isVirtual, fieldChanged, allowFieldChange)] +### [SerializableField(index, setter, saveIf)] **Target**: Private field (`_camelCase`) **Generates**: Public `PascalCase` property with get/set @@ -125,11 +125,8 @@ See `dev-docs/runuo-migration-docs/02-serialization.md` for complete migration g | Parameter | Type | Default | Description | |---|---|---|---| | `index` | `int` | Required | Serialization order (0-based) | -| `getter` | `string` | `"public"` | Getter accessibility | -| `setter` | `string` | `"public"` | `"private"` or `"internal"` to restrict setter | -| `isVirtual` | `bool` | `false` | Generate a `virtual` property | -| `fieldChanged` | `string` | `null` | `nameof` of a `void Method(T oldValue, T newValue)` invoked by the generated setter after assignment | -| `allowFieldChange` | `string` | `null` | `nameof` of a `bool Method(ref T value)` invoked before assignment; coerce the value through the `ref` parameter, or return `false` to reject the change | +| `setter` | `string` | `null` (public) | `"private"` or `"internal"` to restrict setter | +| `saveIf` | `string` | `null` | Method name returning bool for conditional save | ```csharp [SerializableField(0)] // Public property @@ -147,57 +144,14 @@ The generated property for `_charges` would be: public int Charges { get => _charges; - set - { - if (value != _charges) - { - _charges = value; - this.MarkDirty(); - } - } + set { _charges = value; this.MarkDirty(); } } ``` -**Setter hooks** replace most hand-written `[SerializableProperty]` setters. The generated -pipeline is: equality check → `allowFieldChange` (coerce/veto) → assignment → `MarkDirty` → -`InvalidateProperties` (if declared) → `fieldChanged`. The gate runs before assignment, so -the field itself still holds the old value inside it. - -```csharp -[SerializableField(0, allowFieldChange: nameof(AllowChargesChange))] -[SerializedCommandProperty(AccessLevel.GameMaster)] -[InvalidateProperties] -private int _charges; - -private bool AllowChargesChange(ref int value) -{ - value = Math.Clamp(value, 0, MaxCharges); // coerce, or return false to veto - return true; -} - -[SerializableField(1, fieldChanged: nameof(OnOwnerChanged))] -private Mobile _owner; - -// oldValue makes unsubscribe/resubscribe patterns trivial -private void OnOwnerChanged(Mobile oldValue, Mobile newValue) -{ - oldValue?.Followers.Remove(this); - newValue?.Followers.Add(this); -} -``` - -Both hooks require a generated setter — declaring one on a `readonly` field or with -`setter: null` is a compile-time error (SG3018), and a named method that is missing or has -the wrong signature is too (SG3015). - ### [SerializableProperty(index, useField)] **Target**: Property with custom get/set logic -**Use when**: You need a **custom getter** (fallback defaults, lazy or self-healing reads) -or setter semantics the field hooks cannot express (work that must run on *equal* -assignment, pre-assignment state capture). For setters that only coerce, veto, or run -post-change side effects, prefer `[SerializableField]` with `allowFieldChange`/`fieldChanged` -instead — the generated setter handles equality, `MarkDirty`, and ordering for you. +**Use when**: You need non-trivial getter/setter logic | Parameter | Type | Default | Description | |---|---|---|---| @@ -209,7 +163,7 @@ instead — the generated setter handles equality, `MarkDirty`, and ordering for [CommandProperty(AccessLevel.GameMaster)] public int MaxItems { - get => _maxItems == -1 ? DefaultMaxItems : _maxItems; // custom getter: the reason this is a property + get => _maxItems == -1 ? DefaultMaxItems : _maxItems; set { _maxItems = value; @@ -219,10 +173,6 @@ public int MaxItems } ``` -Note: the `fieldChanged`/`allowFieldChange` hooks are `[SerializableField]` arguments and -cannot be declared on a `[SerializableProperty]` — its setter is your own code, so call your -methods from the setter directly. - ### [InvalidateProperties] **Target**: `[SerializableField]`-decorated field @@ -258,32 +208,12 @@ Overloads: Best for fields that are usually small values (counts, IDs, indexes). -### [AnchoredDateTime] - -**Target**: `DateTime` field -**Effect**: Stores the absolute UTC instant; at load it is shifted forward by the downtime -between the save and the load (using the save-start anchor in the save's index file), so -server downtime does not consume the remaining time. `DateTime.MinValue`/`MaxValue` -sentinels pass through unshifted. - -Prefer this for deadlines and "elapsed while running" values. Unlike `[DeltaDateTime]`, the -stored bytes do not change on every save when the value is unchanged, keeping idle saves -byte-stable. - -```csharp -[AnchoredDateTime] -[SerializableField(0)] -private DateTime _expireTime; -``` - ### [DeltaDateTime] **Target**: `DateTime` field **Effect**: Stores as offset from current time rather than absolute timestamp. -Legacy encoding for surviving restarts: it rewrites the bytes on every save even when the -value has not changed. Prefer `[AnchoredDateTime]` for new fields; converting an existing -field between the two changes the wire format and requires a version bump. +This ensures timers and expiration dates survive server restarts correctly. ```csharp [DeltaDateTime] @@ -359,76 +289,40 @@ private void AfterDeserialization() } ``` -### [DeserializeTimer(nameof(Method), wallClock)] +### [DeserializeTimerField(fieldIndex)] -**Target**: `Timer`-typed `[SerializableField]` or `[SerializableProperty]` member -**Effect**: Declares how the timer is stored and restored. Required on every serializable -timer (SG3008 otherwise). - -By default the timer's next tick is stored as **anchored time**: server downtime does not -consume the remaining delay, and idle saves are byte-stable. Pass `wallClock: true` to store -an absolute deadline instead (the delay is then negative when the deadline passed during -downtime). - -The named method — `void Method(TimeSpan delay)` — is invoked **only when a timer was -actually running at save**, with the remaining delay. There is no sentinel value to check. +**Target**: Method taking `TimeSpan` parameter +**Effect**: Custom deserialization for Timer fields. The timer is saved as remaining delay. ```csharp [SerializableField(0, setter: "private")] -[DeserializeTimer(nameof(DeserializeDecayTimer))] private Timer _decayTimer; -private void DeserializeDecayTimer(TimeSpan delay) => _decayTimer = Timer.DelayCall(delay, Delete); -``` - -Switching an existing timer between drifting and `wallClock` changes the wire format — bump -the class version and add a `MigrateFrom`. The old-version content struct exposes the -timer's `XxxNext` (`DateTime`) and `XxxDelay` (`TimeSpan`, `TimeSpan.MinValue` when no timer -was running): - -```csharp -private void MigrateFrom(V3Content content) +[DeserializeTimerField(0)] +private void DeserializeDecayTimer(TimeSpan delay) { - if (content.DecayTimerDelay != TimeSpan.MinValue) - { - DeserializeDecayTimer(content.DecayTimerDelay); - } + _decayTimer = Timer.DelayCall(delay, Delete); + _decayTimer.Start(); } ``` -### [SaveFlag(nameof(ShouldSerializeMethod), nameof(DefaultValueMethod))] +### [SerializableFieldSaveFlag(fieldIndex)] / [SerializableFieldDefault(fieldIndex)] -**Target**: the serializable field or property itself **Conditional serialization** -- skip fields that have their default value. -The first method (`bool Method()`) decides whether the value is written. The optional second -method (returning the field's type, no parameters) supplies the value at load when it was -not written; when omitted, the field keeps its default value. - -```csharp -[SerializableField(0)] -[SaveFlag(nameof(ShouldSerializeCharges), nameof(ChargesDefaultValue))] -private int _charges; - -private bool ShouldSerializeCharges() => _charges != -1; - -private int ChargesDefaultValue() => -1; -``` - -Works on `[SerializableProperty]` members the same way: - ```csharp [EncodedInt] [SerializableProperty(0)] -[SaveFlag(nameof(ShouldSerializeMaxItems), nameof(MaxItemsDefaultValue))] public int MaxItems { get => _maxItems == -1 ? DefaultMaxItems : _maxItems; set { _maxItems = value; this.MarkDirty(); } } +[SerializableFieldSaveFlag(0)] private bool ShouldSerializeMaxItems() => _maxItems != -1; +[SerializableFieldDefault(0)] private int MaxItemsDefaultValue() => -1; ``` diff --git a/dev-docs/timers.md b/dev-docs/timers.md index 2c051cca6..c10f31bd6 100644 --- a/dev-docs/timers.md +++ b/dev-docs/timers.md @@ -188,19 +188,15 @@ public partial class TimedItem : Item ``` ### Pattern 4: Serializable Timer Field -Every serializable `Timer` member declares `[DeserializeTimer(nameof(Method))]` on the -field. By default the next tick is stored as anchored time (downtime does not consume the -remaining delay); pass `wallClock: true` for absolute deadlines. The method runs **only when -a timer was running at save**, with the remaining delay. - ```csharp [SerializableField(0, setter: "private")] -[DeserializeTimer(nameof(DeserializeDecayTimer))] private Timer _decayTimer; +[DeserializeTimerField(0)] private void DeserializeDecayTimer(TimeSpan delay) { _decayTimer = Timer.DelayCall(delay, Delete); + _decayTimer.Start(); } public void BeginDecay(TimeSpan delay)