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:
parent
a8d3d2773e
commit
9c7cb5d778
34 changed files with 502 additions and 248 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue