fix: Fixes dupe property copying. Adds IgnoreDupe (#1811)

## Summary

### Changes
- Adds `[IgnoreDupe]` and `[SerializedIgnoreDupe]`
- Updates all _known_ classes that need the attribute. Some might be missing, please helps us find them!
- Adds `Item.Dupe()` command and encapsulates `CopyProperties` and `OnAfterDuped`. This is also overridable.
- Updates Dupe command to use the new logic.
- Fixes duping multiple kinds of objects that used to be outright broken.

### Bug Fixes
- Fixes issue with durability after duping
- Fixes issue with hue after duping

> [!Note]
> **Developer Note**
> Customizing how duping an item works now requires two steps:
> 1. Add `[IgnoreDupe]` or `[SerializedIgnoreDupe]` to the property/field
> 2. Add custom logic in an `OnAfterDuped` override
>
> When do you need to do this?
> *When the property being copied is not a primitive, and you need to manually deep-clone the contents of the property such as with Lists, Dictionaries, or sub classes.*
This commit is contained in:
Kamron Batman 2024-06-02 15:04:54 -07:00 • committed by GitHub
parent a8d3d2773e
commit 9c7cb5d778
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 502 additions and 248 deletions

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using ModernUO.Serialization;
using Server.ContextMenus;
using Server.Gumps;
using Server.Items;
using Server.Mobiles;
using Server.Multis;
using Server.Prompts;
@ -16,6 +15,7 @@ public partial class BulkOrderBook : Item, ISecurable
[SerializableField(0)]
private int _itemCount;
[SerializedIgnoreDupe]
[InvalidateProperties]
[SerializableField(1)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
@ -26,10 +26,12 @@ public partial class BulkOrderBook : Item, ISecurable
[SerializedCommandProperty(AccessLevel.GameMaster)]
private string _bookName;
[SerializedIgnoreDupe]
[SerializableField(3)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private BOBFilter _filter;
[SerializedIgnoreDupe]
[SerializableField(4)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private List<IBOBEntry> _entries;
@ -40,12 +42,39 @@ public partial class BulkOrderBook : Item, ISecurable
Weight = 1.0;
LootType = LootType.Blessed;
_entries = new List<IBOBEntry>();
_entries = [];
_filter = new BOBFilter();
_level = SecureLevel.CoOwners;
}
public override void OnAfterDuped(Item newItem)
{
if (newItem is not BulkOrderBook book)
{
return;
}
var filter = book._filter;
filter.Material = Filter.Material;
filter.Quality = Filter.Quality;
filter.Quantity = Filter.Quantity;
filter.Type = Filter.Type;
for (var i = 0; i < Entries.Count; i++)
{
// Recreate the BOD
var bod = Entries[i].Reconstruct();
// Recreate the entry
IBOBEntry newEntry = bod is LargeBOD largeBod ? new BOBLargeEntry(largeBod) : new BOBSmallEntry((SmallBOD)bod);
book.AddToEntries(newEntry);
// Delete the new BOD
bod.Delete();
}
}
public override void OnDoubleClick(Mobile from)
{
if (!from.InRange(GetWorldLocation(), 2))
@ -169,13 +198,21 @@ public partial class BulkOrderBook : Item, ISecurable
}
}
public void InvalidateContainers(IEntity parent)
public static void InvalidateContainers(IEntity parent)
{
if (parent is Container c)
do
{
c.InvalidateProperties();
InvalidateContainers(c.Parent);
}
if (parent is Item item)
{
item.InvalidateProperties();
parent = item.Parent;
}
else if (parent is Mobile m)
{
m.InvalidateProperties();
return;
}
} while (parent != null);
}
private void Deserialize(IGenericReader reader, int version)

View file

@ -195,11 +195,15 @@ namespace Server.Engines.Plants
switch (value)
{
case 1:
AddLabel(x, y, 0x35, "+");
break;
{
AddLabel(x, y, 0x35, "+");
break;
}
case 2:
AddLabel(x, y, 0x21, "+");
break;
{
AddLabel(x, y, 0x21, "+");
break;
}
}
}
@ -208,17 +212,25 @@ namespace Server.Engines.Plants
switch (value)
{
case 0:
AddLabel(x, y, 0x21, "-");
break;
{
AddLabel(x, y, 0x21, "-");
break;
}
case 1:
AddLabel(x, y, 0x35, "-");
break;
{
AddLabel(x, y, 0x35, "-");
break;
}
case 3:
AddLabel(x, y, 0x35, "+");
break;
{
AddLabel(x, y, 0x35, "+");
break;
}
case 4:
AddLabel(x, y, 0x21, "+");
break;
{
AddLabel(x, y, 0x21, "+");
break;
}
}
}
@ -236,21 +248,33 @@ namespace Server.Engines.Plants
switch (m_Plant.PlantSystem.GrowthIndicator)
{
default:
case PlantGrowthIndicator.None:
case PlantGrowthIndicator.InvalidLocation:
AddLabel(x, y, 0x21, "!");
break;
{
AddLabel(x, y, 0x21, "!");
break;
}
case PlantGrowthIndicator.NotHealthy:
AddLabel(x, y, 0x21, "-");
break;
{
AddLabel(x, y, 0x21, "-");
break;
}
case PlantGrowthIndicator.Delay:
AddLabel(x, y, 0x35, "-");
break;
{
AddLabel(x, y, 0x35, "-");
break;
}
case PlantGrowthIndicator.Grown:
AddLabel(x, y, 0x3, "+");
break;
{
AddLabel(x, y, 0x3, "+");
break;
}
case PlantGrowthIndicator.DoubleGrown:
AddLabel(x, y, 0x3F, "+");
break;
{
AddLabel(x, y, 0x3F, "+");
break;
}
}
}

View file

@ -32,6 +32,9 @@ public enum PlantStatus
[SerializationGenerator(3, false)]
public partial class PlantItem : Item, ISecurable
{
public static List<PlantItem> Plants { get; } = [];
[SerializedIgnoreDupe]
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private SecureLevel _level;
@ -39,6 +42,7 @@ public partial class PlantItem : Item, ISecurable
[SerializableFieldSaveFlag(0)]
private bool ShouldSerializeSecureLevel() => (int)_level != 0;
[SerializedIgnoreDupe]
[SerializableField(5, setter: "private")]
private PlantSystem _plantSystem;
@ -192,7 +196,10 @@ public partial class PlantItem : Item, ISecurable
[CommandProperty(AccessLevel.GameMaster)]
public bool Reproduces => PlantHueInfo.CanReproduce(PlantHue) && PlantTypeInfo.CanReproduce(PlantType);
public static List<PlantItem> Plants { get; } = new();
public override void OnAfterDuped(Item newItem)
{
PlantSystem.OnAfterDuped(newItem);
}
public override void OnSingleClick(Mobile from)
{
@ -231,7 +238,7 @@ public partial class PlantItem : Item, ISecurable
};
}
public int GetLocalizedContainerType() => 1150435;
public static int GetLocalizedContainerType() => 1150435;
private void Update()
{

View file

@ -66,9 +66,9 @@ namespace Server.Engines.Plants
private void Deserialize(IGenericReader reader, int version)
{
_fertileDirt = reader.ReadBool();
NextGrowth = reader.ReadDateTime();
_nextGrowth = reader.ReadDateTime();
GrowthIndicator = (PlantGrowthIndicator)reader.ReadInt();
_growthIndicator = (PlantGrowthIndicator)reader.ReadInt();
_water = reader.ReadInt();
@ -139,16 +139,14 @@ namespace Server.Engines.Plants
public int MaxHits => 10 + (int)Plant.PlantStatus * 2;
public PlantHealth Health
{
get => (_hits * 100 / MaxHits) switch
public PlantHealth Health =>
(_hits * 100 / MaxHits) switch
{
< 33 => PlantHealth.Dying,
< 66 => PlantHealth.Wilted,
< 100 => PlantHealth.Healthy,
_ => PlantHealth.Vibrant
};
}
[SerializableProperty(5)]
public int Infestation
@ -369,6 +367,41 @@ namespace Server.Engines.Plants
LeftResources = 8;
}
public void OnAfterDuped(Item newItem)
{
if (newItem is not PlantItem plant)
{
return;
}
// Copy all properties from this.PlantSystem to plantItem.PlantSystem
var plantSystem = plant.PlantSystem;
plantSystem.Water = Water;
plantSystem.Hits = Hits;
plantSystem.Infestation = Infestation;
plantSystem.Fungus = Fungus;
plantSystem.PoisonPotion = PoisonPotion;
plantSystem.Disease = Disease;
plantSystem.PoisonPotion = PoisonPotion;
plantSystem.CurePotion = CurePotion;
plantSystem.HealPotion = HealPotion;
plantSystem.StrengthPotion = StrengthPotion;
plantSystem.Pollinated = Pollinated;
// Do not use getter since it has computed logic
plantSystem.SeedType = _seedType;
plantSystem.SeedHue = _seedHue;
plantSystem.AvailableSeeds = AvailableSeeds;
plantSystem.LeftSeeds = LeftSeeds;
plantSystem.AvailableResources = AvailableResources;
plantSystem.LeftResources = LeftResources;
plantSystem.FertileDirt = FertileDirt;
plantSystem.NextGrowth = NextGrowth;
plantSystem.GrowthIndicator = GrowthIndicator;
}
public int GetLocalizedDirtStatus() =>
Water switch
{

View file

@ -173,9 +173,8 @@ public partial class Seed : Item
return;
}
newSeed.PlantType = _plantType;
newSeed.PlantHue = _plantHue;
newSeed.ShowType = _showType;
newSeed.Hue = Hue;
}
private void Deserialize(IGenericReader reader, int version)

View file

@ -14,6 +14,7 @@ namespace Server.Engines.Spawners;
[SerializationGenerator(10, false)]
public abstract partial class BaseSpawner : Item, ISpawner
{
[SerializedIgnoreDupe]
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.Developer)]
private Guid _guid;
@ -22,6 +23,7 @@ public abstract partial class BaseSpawner : Item, ISpawner
[SerializedCommandProperty(AccessLevel.Developer)]
private bool _returnOnDeactivate;
[SerializedIgnoreDupe]
[SerializableField(2, setter: "private")]
private List<SpawnerEntry> _entries;
@ -141,6 +143,7 @@ public abstract partial class BaseSpawner : Item, ISpawner
public bool IsFull => Spawned?.Count >= _count;
public bool IsEmpty => Spawned?.Count == 0;
[IgnoreDupe]
public Dictionary<ISpawnable, SpawnerEntry> Spawned { get; private set; }
[SerializableProperty(8)]
@ -269,17 +272,18 @@ public abstract partial class BaseSpawner : Item, ISpawner
{
newSpawner._guid = Guid.NewGuid();
newSpawner.Spawned = new Dictionary<ISpawnable, SpawnerEntry>();
newSpawner.Entries = new List<SpawnerEntry>();
newSpawner.Entries = [];
for (var i = 0; i < Entries.Count; i++)
{
var entry = Entries[i];
newSpawner.AddEntry(
Entries[i].SpawnedName,
Entries[i].SpawnedProbability,
Entries[i].SpawnedMaxCount,
entry.SpawnedName,
entry.SpawnedProbability,
entry.SpawnedMaxCount,
false,
Entries[i].Properties,
Entries[i].Parameters
entry.Properties,
entry.Parameters
);
}
}
@ -295,7 +299,7 @@ public abstract partial class BaseSpawner : Item, ISpawner
)
{
var entry = new SpawnerEntry(this, creaturename, probability, amount, properties, parameters);
Entries.Add(entry);
AddToEntries(entry);
if (dotimer)
{
DoTimer(TimeSpan.FromSeconds(1));
@ -315,7 +319,7 @@ public abstract partial class BaseSpawner : Item, ISpawner
_count = amount;
_team = team;
_homeRange = homeRange;
Entries = new List<SpawnerEntry>();
Entries = [];
Spawned = new Dictionary<ISpawnable, SpawnerEntry>();
DoTimer(TimeSpan.FromSeconds(1));
@ -394,7 +398,7 @@ public abstract partial class BaseSpawner : Item, ISpawner
public void Defrag()
{
Entries ??= new List<SpawnerEntry>();
Entries ??= [];
for (var i = 0; i < Entries.Count; ++i)
{
@ -590,7 +594,7 @@ public abstract partial class BaseSpawner : Item, ISpawner
IEntity entity = null;
var propargs = string.IsNullOrEmpty(entry.Properties)
? Array.Empty<string>()
? []
: CommandSystem.Split(entry.Properties.Trim());
var props = FormatProperties(propargs);
@ -604,7 +608,7 @@ public abstract partial class BaseSpawner : Item, ISpawner
}
var paramargs = string.IsNullOrEmpty(entry.Parameters)
? Array.Empty<string>()
? []
: entry.Parameters.Trim().Split(' ');
if (paramargs.Length == 0)