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

@ -21,19 +21,13 @@ using ModernUO.Serialization;
namespace Server;
[AttributeUsage(AttributeTargets.Property)]
public class HueAttribute : Attribute
{
}
public class HueAttribute : Attribute;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class PropertyObjectAttribute : Attribute
{
}
public class PropertyObjectAttribute : Attribute;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class NoSortAttribute : Attribute
{
}
public class NoSortAttribute : Attribute;
[AttributeUsage(AttributeTargets.Method)]
public class CallPriorityAttribute : Attribute
@ -193,3 +187,9 @@ public class SerializedCommandPropertyAttribute : SerializedPropertyAttrAttribut
public bool ReadOnly { get; }
public bool CanModify { get; }
}
[AttributeUsage(AttributeTargets.Property)]
public class IgnoreDupeAttribute : Attribute;
[AttributeUsage(AttributeTargets.Field)]
public class SerializedIgnoreDupeAttribute : SerializedPropertyAttrAttribute<IgnoreDupeAttribute>;

View file

@ -44,14 +44,18 @@ public abstract class BaseGuild : ISerializable
public bool Deleted => Disbanded;
[IgnoreDupe]
[CommandProperty(AccessLevel.Counselor)]
public Serial Serial { get; }
[IgnoreDupe]
[CommandProperty(AccessLevel.GameMaster, readOnly: true)]
public DateTime Created { get; set; } = Core.Now;
[IgnoreDupe]
public long SavePosition { get; set; } = -1;
[IgnoreDupe]
public BufferWriter SaveBuffer { get; set; }
public abstract void Serialize(IGenericWriter writer);

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using Server.Collections;
using Server.ContextMenus;
@ -254,8 +255,13 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
}
// Sectors
[IgnoreDupe]
public Item Next { get; set; }
[IgnoreDupe]
public Item Previous { get; set; }
[IgnoreDupe]
public bool OnLinkList { get; set; }
/// <summary>
@ -586,6 +592,7 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
}
// Note: Setting the parent via command/props causes problems.
[IgnoreDupe]
[CommandProperty(AccessLevel.GameMaster, readOnly: true)]
public IEntity Parent
{
@ -754,6 +761,7 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
public int CompareTo(Item other) => other == null ? -1 : Serial.CompareTo(other.Serial);
public virtual int HuedItemID => m_ItemID;
public ObjectPropertyList PropertyList => m_PropertyList ??= InitializePropertyList(new ObjectPropertyList(this));
/// <summary>
@ -768,13 +776,17 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
AddNameProperties(list);
}
[IgnoreDupe]
[CommandProperty(AccessLevel.GameMaster, readOnly: true)]
public DateTime Created { get; set; } = Core.Now;
[IgnoreDupe]
public long SavePosition { get; set; } = -1;
[IgnoreDupe]
public BufferWriter SaveBuffer { get; set; }
[IgnoreDupe]
[CommandProperty(AccessLevel.Counselor)]
public Serial Serial { get; }
@ -1445,6 +1457,7 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
ClearProperties();
}
[IgnoreDupe]
public ISpawner Spawner
{
get => LookupCompactInfo()?.m_Spawner;
@ -3341,21 +3354,41 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
}
}
private static readonly HashSet<string> _excludedProperties =
[
"SaveBuffer",
"Parent",
"Next",
"Previous",
"OnLinkList"
];
public virtual bool DupeExcludedProperty(string propertyName) => _excludedProperties.Contains(propertyName);
public virtual void OnAfterDuped(Item newItem)
{
}
// Warning: This uses reflection and is slow!
public virtual void Dupe(Item newItem)
{
CopyProperties(this, newItem);
OnAfterDuped(newItem);
}
// Warning: This uses reflection and is slow!
public static void CopyProperties(Item src, Item dest)
{
var props = src.GetType().GetProperties();
for (var i = 0; i < props.Length; i++)
{
var p = props[i];
if (p.GetCustomAttribute(typeof(IgnoreDupeAttribute), true) != null || !p.CanRead || !p.CanWrite)
{
continue;
}
try
{
p.SetValue(dest, p.GetValue(src, null), null);
}
catch
{
// ignored
}
}
}
public virtual bool OnDragLift(Mobile from) => true;
public virtual bool OnEquip(Mobile from) => true;

View file

@ -2256,13 +2256,17 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
AddNameProperties(list);
}
[IgnoreDupe]
[CommandProperty(AccessLevel.GameMaster, readOnly: true)]
public DateTime Created { get; set; } = Core.Now;
[IgnoreDupe]
public long SavePosition { get; set; } = -1;
[IgnoreDupe]
public BufferWriter SaveBuffer { get; set; }
[IgnoreDupe]
[CommandProperty(AccessLevel.Counselor)]
public Serial Serial { get; }

View file

@ -11,7 +11,7 @@ public class TestBook : BaseBook
{
public TestBook(int itemID, int pageCount = 20, bool writable = true) : base(itemID, pageCount, writable)
{
}
}
public TestBook(int itemID, string title, string author, int pageCount, bool writable) : base(itemID, title, author, pageCount, writable)
{

View file

@ -2,162 +2,137 @@ using System;
using Server.Items;
using Server.Targeting;
namespace Server.Commands
namespace Server.Commands;
public static class Dupe
{
public static class Dupe
public static void Configure()
{
public static void Configure()
CommandSystem.Register("Dupe", AccessLevel.GameMaster, Dupe_OnCommand);
CommandSystem.Register("DupeInBag", AccessLevel.GameMaster, DupeInBag_OnCommand);
}
[Usage("Dupe [amount]")]
[Description("Dupes a targeted item.")]
private static void Dupe_OnCommand(CommandEventArgs e)
{
var amount = 1;
if (e.Length >= 1)
{
CommandSystem.Register("Dupe", AccessLevel.GameMaster, Dupe_OnCommand);
CommandSystem.Register("DupeInBag", AccessLevel.GameMaster, DupeInBag_OnCommand);
amount = e.GetInt32(0);
}
[Usage("Dupe [amount]")]
[Description("Dupes a targeted item.")]
private static void Dupe_OnCommand(CommandEventArgs e)
e.Mobile.Target = new DupeTarget(false, amount > 0 ? amount : 1);
e.Mobile.SendMessage("What do you wish to dupe?");
}
[Usage("DupeInBag <count>")]
[Description("Dupes an item at it's current location (count) number of times.")]
private static void DupeInBag_OnCommand(CommandEventArgs e)
{
var amount = 1;
if (e.Length >= 1)
{
var amount = 1;
if (e.Length >= 1)
amount = e.GetInt32(0);
}
e.Mobile.Target = new DupeTarget(true, amount > 0 ? amount : 1);
e.Mobile.SendMessage("What do you wish to dupe?");
}
private class DupeTarget : Target
{
private readonly int m_Amount;
private readonly bool m_InBag;
public DupeTarget(bool inbag, int amount)
: base(15, false, TargetFlags.None)
{
m_InBag = inbag;
m_Amount = amount;
}
protected override void OnTarget(Mobile from, object targ)
{
var done = false;
if (targ is not Item copy)
{
amount = e.GetInt32(0);
from.SendMessage("You can only dupe items.");
return;
}
e.Mobile.Target = new DupeTarget(false, amount > 0 ? amount : 1);
e.Mobile.SendMessage("What do you wish to dupe?");
}
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} duping {CommandLogging.Format(copy)} (inBag={m_InBag}; amount={m_Amount})"
);
[Usage("DupeInBag <count>")]
[Description("Dupes an item at it's current location (count) number of times.")]
private static void DupeInBag_OnCommand(CommandEventArgs e)
{
var amount = 1;
if (e.Length >= 1)
Container pack = null;
if (m_InBag)
{
amount = e.GetInt32(0);
pack = copy.Parent switch
{
Container cont => cont,
Mobile m => m.Backpack,
_ => null
};
}
else
{
pack = from.Backpack;
}
e.Mobile.Target = new DupeTarget(true, amount > 0 ? amount : 1);
e.Mobile.SendMessage("What do you wish to dupe?");
}
public static void CopyProperties(Item dest, Item src)
{
var props = src.GetType().GetProperties();
for (var i = 0; i < props.Length; i++)
var c = copy.GetType().GetConstructor(out var paramCount);
if (c != null)
{
var p = props[i];
var args = paramCount == 0 ? null : new object[paramCount];
if (args != null)
{
Array.Fill(args, Type.Missing);
}
try
{
// Do not set the parent since it screws up mobile/container totals and weights.
if (p.CanRead && p.CanWrite && !src.DupeExcludedProperty(p.Name) && !dest.DupeExcludedProperty(p.Name))
from.SendMessage($"Duping {m_Amount}...");
for (var i = 0; i < m_Amount; i++)
{
p.SetValue(dest, p.GetValue(src, null), null);
if (c.Invoke(args) is Item newItem)
{
copy.Dupe(newItem);
if (pack != null)
{
pack.DropItem(newItem);
}
else
{
newItem.MoveToWorld(from.Location, from.Map);
}
newItem.UpdateTotals();
newItem.InvalidateProperties();
newItem.Delta(ItemDelta.Update);
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} duped {CommandLogging.Format(copy)} creating {CommandLogging.Format(newItem)}"
);
}
}
from.SendMessage("Done");
done = true;
}
catch
{
// ignored
}
}
}
private class DupeTarget : Target
{
private readonly int m_Amount;
private readonly bool m_InBag;
public DupeTarget(bool inbag, int amount)
: base(15, false, TargetFlags.None)
{
m_InBag = inbag;
m_Amount = amount;
}
protected override void OnTarget(Mobile from, object targ)
{
var done = false;
if (!(targ is Item))
{
from.SendMessage("You can only dupe items.");
from.SendMessage("Error!");
return;
}
}
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} duping {CommandLogging.Format(targ)} (inBag={m_InBag}; amount={m_Amount})"
);
var copy = (Item)targ;
Container pack = null;
if (m_InBag)
{
pack = copy.Parent switch
{
Container cont => cont,
Mobile m => m.Backpack,
_ => pack
};
}
else
{
pack = from.Backpack;
}
var c = copy.GetType().GetConstructor(out var paramCount);
if (c != null)
{
var args = paramCount == 0 ? null : new object[paramCount];
if (args != null)
{
Array.Fill(args, Type.Missing);
}
try
{
from.SendMessage($"Duping {m_Amount}...");
for (var i = 0; i < m_Amount; i++)
{
if (c.Invoke(args) is Item newItem)
{
CopyProperties(newItem, copy);
copy.OnAfterDuped(newItem);
if (pack != null)
{
pack.DropItem(newItem);
}
else
{
newItem.MoveToWorld(from.Location, from.Map);
}
newItem.UpdateTotals();
newItem.InvalidateProperties();
newItem.Delta(ItemDelta.Update);
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} duped {CommandLogging.Format(targ)} creating {CommandLogging.Format(newItem)}"
);
}
}
from.SendMessage("Done");
done = true;
}
catch
{
from.SendMessage("Error!");
return;
}
}
if (!done)
{
from.SendMessage("Unable to dupe. Item must have a constructor with zero required parameters.");
}
if (!done)
{
from.SendMessage("Unable to dupe. Item must have a constructor with zero required parameters.");
}
}
}

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)

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using ModernUO.Serialization;
using Server.ContextMenus;
using Server.Multis;
@ -13,7 +14,7 @@ namespace Server.Items
public static readonly TimeSpan EvaluationInterval = TimeSpan.FromDays(1);
private static readonly Type[] m_Decorations =
{
[
typeof(FishBones),
typeof(WaterloggedBoots),
typeof(CaptainBlackheartsFishingPole),
@ -23,7 +24,7 @@ namespace Server.Items
typeof(IslandStatue),
typeof(Shell),
typeof(ToyBoat)
};
];
private bool m_EvaluateDay;
@ -45,16 +46,19 @@ namespace Server.Items
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _vacationLeft;
[SerializedIgnoreDupe]
[InvalidateProperties]
[SerializableField(3, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private AquariumState _food;
[SerializedIgnoreDupe]
[InvalidateProperties]
[SerializableField(4, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private AquariumState _water;
[SerializedIgnoreDupe]
[SerializableField(5, setter: "private")]
private List<int> _events;
@ -90,7 +94,7 @@ namespace Server.Items
_water.Maintain = Utility.RandomMinMax(1, 3);
_events = new List<int>();
_events = [];
_evaluateTimer = Timer.DelayCall(EvaluationInterval, EvaluationInterval, Evaluate);
}
@ -145,9 +149,9 @@ namespace Server.Items
public override double DefaultWeight => 10.0;
private static int[] FishHues =
{
[
0x1C2, 0x1C3, 0x2A3, 0x47E, 0x51D
};
];
public override void OnDelete()
{
@ -471,6 +475,29 @@ namespace Server.Items
}
}
public override void OnAfterDuped(Item newItem)
{
if (newItem is not Aquarium aquarium)
{
return;
}
aquarium.Food.Added = Food.Added;
aquarium.Food.Improve = Food.Improve;
aquarium.Food.Maintain = Food.Maintain;
aquarium.Food.State = Food.State;
aquarium.Water.Added = Water.Added;
aquarium.Water.Improve = Water.Improve;
aquarium.Water.Maintain = Water.Maintain;
aquarium.Water.State = Water.State;
for (var i = 0; i < _events.Count; i++)
{
aquarium.AddToEvents(_events[i]);
}
}
private void Deserialize(IGenericReader reader, int version)
{
switch (version)
@ -500,7 +527,7 @@ namespace Server.Items
_water = new AquariumState(this);
_water.Deserialize(reader);
_events = new List<int>();
_events = [];
var count = reader.ReadInt();
for (var i = 0; i < count; i++)
{
@ -856,7 +883,8 @@ namespace Server.Items
from.PlaySound(0x5A4);
}
public virtual bool AddFish(BaseFish fish) => AddFish(null, fish);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool AddFish(BaseFish fish) => AddFish(null, fish);
public virtual bool AddFish(Mobile from, BaseFish fish)
{
@ -877,16 +905,15 @@ namespace Server.Items
LiveCreatures += 1;
from?.SendLocalizedMessage(
1073632,
$"#{fish.LabelNumber}"
); // You add the following creature to your aquarium: ~1_FISH~
// You add the following creature to your aquarium: ~1_FISH~
from?.SendLocalizedMessage(1073632, $"#{fish.LabelNumber}");
InvalidateProperties();
return true;
}
public virtual bool AddDecoration(Item item) => AddDecoration(null, item);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool AddDecoration(Item item) => AddDecoration(null, item);
public virtual bool AddDecoration(Mobile from, Item item)
{

View file

@ -40,7 +40,7 @@ namespace Server.Items
if (_state != value)
{
_state = Math.Clamp(value, 0, 4);
this.MarkDirty();
MarkDirty();
}
}
}

View file

@ -13,6 +13,7 @@ namespace Server.Items
[SerializationGenerator(9, false)]
public abstract partial class BaseArmor : Item, IScissorable, IFactionItem, ICraftable, IWearableDurability, IAosItem
{
[SerializedIgnoreDupe]
[SerializableField(0, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosAttributes _attributes;
@ -23,6 +24,7 @@ namespace Server.Items
[SerializableFieldDefault(0)]
private AosAttributes AttributesDefaultValue() => new(this);
[SerializedIgnoreDupe]
[SerializableField(1, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosArmorAttributes _armorAttributes;
@ -128,6 +130,7 @@ namespace Server.Items
// Field 22
private AMA _meditate = (AMA)(-1);
[SerializedIgnoreDupe]
[SerializableField(23, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
public AosSkillBonuses _skillBonuses;
@ -788,6 +791,12 @@ namespace Server.Items
armor.Attributes = new AosAttributes(newItem, Attributes);
armor.ArmorAttributes = new AosArmorAttributes(newItem, ArmorAttributes);
armor.SkillBonuses = new AosSkillBonuses(newItem, SkillBonuses);
// Set hue again because of resource
armor.Hue = Hue;
// Set HP/Max again because of durability
armor.HitPoints = HitPoints;
armor.MaxHitPoints = MaxHitPoints;
}
public int ComputeStatReq(StatType type)

View file

@ -11,6 +11,7 @@ namespace Server.Items
[SerializationGenerator(5, false)]
public partial class BaseBook : Item, ISecurable
{
[SerializedIgnoreDupe]
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private SecureLevel _level;
@ -45,6 +46,7 @@ namespace Server.Items
[SerializableFieldSaveFlag(3)]
private bool ShouldSerializeWritable() => _writable;
[SerializedIgnoreDupe]
[SerializableField(4, setter: "protected")]
private BookPageInfo[] _pages;
@ -52,7 +54,7 @@ namespace Server.Items
private bool ShouldSerializePages() => DefaultContent?.IsMatch(_pages) != true;
[SerializableFieldDefault(4)]
private BookPageInfo[] PagesDefaultvalue() => DefaultContent?.Copy() ?? Array.Empty<BookPageInfo>();
private BookPageInfo[] PagesDefaultValue() => DefaultContent?.Copy() ?? Array.Empty<BookPageInfo>();
[Constructible]
public BaseBook(int itemID, int pageCount = 20, bool writable = true) : this(itemID, null, null, pageCount, writable)
@ -91,6 +93,29 @@ namespace Server.Items
[CommandProperty(AccessLevel.GameMaster)]
public int PagesCount => _pages.Length;
public override void OnAfterDuped(Item newItem)
{
if (newItem is not BaseBook book)
{
return;
}
book.Pages = new BookPageInfo[book.PagesCount];
for (var i = 0; i < _pages.Length; ++i)
{
var oldPage = _pages[i];
var oldLines = _pages[i].Lines;
var lines = new string[oldPage.Lines.Length];
for (var j = 0; j < oldLines.Length; j++)
{
lines[j] = oldLines[j];
}
book._pages[i] = new BookPageInfo(lines);
}
}
public virtual BookContent DefaultContent => null;
public string ContentAsString

View file

@ -1,35 +1,32 @@
using System;
namespace Server.Items;
namespace Server.Items
public class BookPageInfo
{
public class BookPageInfo
public BookPageInfo() => Lines = [];
public BookPageInfo(params string[] lines) => Lines = lines;
public BookPageInfo(IGenericReader reader)
{
public BookPageInfo() => Lines = Array.Empty<string>();
var length = reader.ReadInt();
public BookPageInfo(params string[] lines) => Lines = lines;
Lines = new string[length];
public BookPageInfo(IGenericReader reader)
for (var i = 0; i < Lines.Length; ++i)
{
var length = reader.ReadInt();
Lines = new string[length];
for (var i = 0; i < Lines.Length; ++i)
{
Lines[i] = reader.ReadString().Intern();
}
Lines[i] = reader.ReadString().Intern();
}
}
public string[] Lines { get; set; }
public string[] Lines { get; set; }
public void Serialize(IGenericWriter writer)
public void Serialize(IGenericWriter writer)
{
writer.Write(Lines.Length);
for (var i = 0; i < Lines.Length; ++i)
{
writer.Write(Lines.Length);
for (var i = 0; i < Lines.Length; ++i)
{
writer.Write(Lines[i]);
}
writer.Write(Lines[i]);
}
}
}

View file

@ -29,6 +29,7 @@ namespace Server.Items
[SerializableFieldSaveFlag(0)]
private bool ShouldSerializeResource() => _resource != DefaultResource;
[SerializedIgnoreDupe]
[SerializableField(1, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosAttributes _attributes;
@ -39,6 +40,7 @@ namespace Server.Items
[SerializableFieldDefault(1)]
private AosAttributes AttributesDefaultValue() => new(this);
[SerializedIgnoreDupe]
[SerializableField(2, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosArmorAttributes _clothingAttributes;
@ -49,6 +51,7 @@ namespace Server.Items
[SerializableFieldDefault(2)]
private AosArmorAttributes ClothingAttributesDefaultValue() => new(this);
[SerializedIgnoreDupe]
[SerializableField(3, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosSkillBonuses _skillBonuses;
@ -59,6 +62,7 @@ namespace Server.Items
[SerializableFieldDefault(3)]
private AosSkillBonuses SkillBonusesDefaultValue() => new(this);
[SerializedIgnoreDupe]
[SerializableField(4, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosElementAttributes _resistances;
@ -637,6 +641,12 @@ namespace Server.Items
clothing.Resistances = new AosElementAttributes(newItem, Resistances);
clothing.SkillBonuses = new AosSkillBonuses(newItem, SkillBonuses);
clothing.ClothingAttributes = new AosArmorAttributes(newItem, ClothingAttributes);
// Set hue again because of resource
clothing.Hue = Hue;
// Set HP/Max again because of durability
clothing.HitPoints = HitPoints;
clothing.MaxHitPoints = MaxHitPoints;
}
public override bool AllowEquippedCast(Mobile from) =>

View file

@ -58,6 +58,7 @@ public partial class GenericHouseDoor : BaseHouseDoor
[SerializationGenerator(2, false)]
public abstract partial class BaseHouseDoor : BaseDoor, ISecurable
{
[SerializedIgnoreDupe]
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private SecureLevel _level;

View file

@ -11,6 +11,7 @@ namespace Server.Items;
[SerializationGenerator(2, false)]
public abstract partial class BaseBoard : Container, ISecurable
{
[SerializedIgnoreDupe]
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private SecureLevel _level;

View file

@ -13,22 +13,28 @@ public partial class MahjongGame : Item, ISecurable
public const int MaxPlayers = 4;
public const int BaseScore = 30000;
[SerializedIgnoreDupe]
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private SecureLevel _level;
[SerializedIgnoreDupe]
[SerializableField(1, setter: "private")]
private MahjongTile[] _tiles;
[SerializedIgnoreDupe]
[SerializableField(2, setter: "private")]
private MahjongDealerIndicator _dealerIndicator;
[SerializedIgnoreDupe]
[SerializableField(3, setter: "private")]
private MahjongWallBreakIndicator _wallBreakIndicator;
[SerializedIgnoreDupe]
[SerializableField(4, setter: "private")]
private MahjongDices _dices;
[SerializedIgnoreDupe]
[SerializableField(5, setter: "private")]
private MahjongPlayers _players;

View file

@ -32,14 +32,17 @@ public abstract partial class BaseJewel : Item, ICraftable, IAosItem
[SerializedCommandProperty(AccessLevel.GameMaster)]
private GemType _gemType;
[SerializedIgnoreDupe]
[SerializableField(4, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosAttributes _attributes;
[SerializedIgnoreDupe]
[SerializableField(5, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosElementAttributes _resistances;
[SerializedIgnoreDupe]
[SerializableField(6, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosSkillBonuses _skillBonuses;
@ -193,6 +196,9 @@ public abstract partial class BaseJewel : Item, ICraftable, IAosItem
jewel.Attributes = new AosAttributes(newItem, Attributes);
jewel.Resistances = new AosElementAttributes(newItem, Resistances);
jewel.SkillBonuses = new AosSkillBonuses(newItem, SkillBonuses);
// Set hue again because of resource
jewel.Hue = Hue;
}
public override void OnAdded(IEntity parent)

View file

@ -31,6 +31,7 @@ public partial class PlayerBBEast : BasePlayerBB
[SerializationGenerator(0, false)]
public abstract partial class BasePlayerBB : Item, ISecurable
{
[SerializedIgnoreDupe]
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private SecureLevel _level;
@ -40,10 +41,12 @@ public abstract partial class BasePlayerBB : Item, ISecurable
private string _title;
[CanBeNull]
[SerializedIgnoreDupe]
[SerializableField(2)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private PlayerBBMessage _greeting;
[SerializedIgnoreDupe]
[SerializableField(3)]
private List<PlayerBBMessage> _messages;
@ -53,6 +56,25 @@ public abstract partial class BasePlayerBB : Item, ISecurable
_level = SecureLevel.Anyone;
}
public override void OnAfterDuped(Item newItem)
{
if (newItem is not BasePlayerBB board)
{
return;
}
if (_greeting != null)
{
board.Greeting = new PlayerBBMessage(_greeting.Time, _greeting.Poster, _greeting.Message);
}
for (var i = 0; i < _messages.Count; i++)
{
var message = _messages[i];
board.AddToMessages(new PlayerBBMessage(message.Time, message.Poster, message.Message));
}
}
public override void GetContextMenuEntries(Mobile from, List<ContextMenuEntry> list)
{
base.GetContextMenuEntries(from, list);

View file

@ -9,6 +9,7 @@ public partial class BaseQuiver : Container, ICraftable, IAosItem
{
private static Type[] m_Ammo = { typeof(Arrow), typeof(Bolt) };
[SerializedIgnoreDupe]
[SerializableField(0, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosAttributes _attributes;

View file

@ -24,10 +24,12 @@ public partial class Runebook : Item, ISecurable, ICraftable
[SerializedCommandProperty(AccessLevel.GameMaster)]
private string _crafter;
[SerializedIgnoreDupe]
[SerializableField(2)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private SecureLevel _level;
[SerializedIgnoreDupe]
[SerializableField(3, setter: "private")]
private List<RunebookEntry> _entries;
@ -281,7 +283,7 @@ public partial class Runebook : Item, ISecurable, ICraftable
return;
}
book.Entries = new List<RunebookEntry>();
book.Entries = [];
for (var i = 0; i < Entries.Count; i++)
{

View file

@ -96,10 +96,12 @@ public partial class Spellbook : Item, ICraftable, ISlayer, IAosItem
[SerializedCommandProperty(AccessLevel.GameMaster)]
private SlayerName _slayer2;
[SerializedIgnoreDupe]
[SerializableField(5, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosAttributes _attributes;
[SerializedIgnoreDupe]
[SerializableField(6, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosSkillBonuses _skillBonuses;
@ -539,13 +541,6 @@ public partial class Spellbook : Item, ICraftable, ISlayer, IAosItem
book.Attributes = new AosAttributes(newItem, _attributes);
book.SkillBonuses = new AosSkillBonuses(newItem, _skillBonuses);
book.Content = _content;
book.SpellCount = _spellCount;
book.Slayer = _slayer;
book.Slayer2 = _slayer2;
book.Quality = _quality;
book.EngravedText = _engravedText;
book.Crafter = _crafter;
}
public override void OnAdded(IEntity parent)

View file

@ -15,6 +15,7 @@ namespace Server.Items
[SerializationGenerator(2, false)]
public partial class DyeTub : Item, ISecurable
{
[SerializedIgnoreDupe]
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private SecureLevel _level;

View file

@ -99,9 +99,11 @@ public partial class DawnsMusicBox : Item, ISecurable
MusicName.SelimsBar, MusicName.SerpentIsleCombat_U7, MusicName.ValoriaShips
};
[SerializedIgnoreDupe]
[SerializableField(0, setter: "private")]
private List<MusicName> _tracks;
[SerializedIgnoreDupe]
[SerializableField(1)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private SecureLevel _level;

View file

@ -18,6 +18,7 @@ public partial class RoseOfTrinsic : Item, ISecurable
[SerializableField(1, setter: "private")]
private DateTime _nextSpawnTime;
[SerializedIgnoreDupe]
[SerializableField(2)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private SecureLevel _level;
@ -34,6 +35,16 @@ public partial class RoseOfTrinsic : Item, ISecurable
public override int LabelNumber => 1062913; // Rose of Trinsic
public override void OnAfterDuped(Item newItem)
{
if (newItem is not RoseOfTrinsic rose)
{
return;
}
rose.NextSpawnTime = NextSpawnTime;
}
[SerializableProperty(0)]
[CommandProperty(AccessLevel.GameMaster)]
public int Petals

View file

@ -10,6 +10,7 @@ namespace Server.Items;
[SerializationGenerator(1)]
public partial class TapestryOfSosaria : Item, ISecurable
{
[SerializedIgnoreDupe]
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private SecureLevel _level;

View file

@ -17,6 +17,7 @@ public partial class SoulStone : Item, ISecurable
[SerializedCommandProperty(AccessLevel.GameMaster)]
private string _lastUserName;
[SerializedIgnoreDupe]
[SerializableField(1)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private SecureLevel _level;

View file

@ -125,6 +125,7 @@ public partial class BaseTalisman : Item, IAosItem
SkillName.Tinkering
};
[SerializedIgnoreDupe]
[SerializableField(0, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosAttributes _attributes;
@ -135,6 +136,7 @@ public partial class BaseTalisman : Item, IAosItem
[SerializableFieldDefault(0)]
private AosAttributes AttributesDefaultValue() => new(this);
[SerializedIgnoreDupe]
[SerializableField(1, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosSkillBonuses _skillBonuses;
@ -145,6 +147,7 @@ public partial class BaseTalisman : Item, IAosItem
[SerializableFieldDefault(1)]
private AosSkillBonuses SkillBonusesDefaultValue() => new(this);
[SerializedIgnoreDupe]
[InvalidateProperties]
[SerializableField(2)]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
@ -156,6 +159,7 @@ public partial class BaseTalisman : Item, IAosItem
[SerializableFieldDefault(2)]
private TalismanAttribute ProtectionDefaultValue() => new();
[SerializedIgnoreDupe]
[InvalidateProperties]
[SerializableField(3)]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
@ -167,6 +171,7 @@ public partial class BaseTalisman : Item, IAosItem
[SerializableFieldDefault(3)]
private TalismanAttribute KillerDefaultValue() => new();
[SerializedIgnoreDupe]
[InvalidateProperties]
[SerializableField(4)]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]

View file

@ -115,6 +115,7 @@ public abstract partial class BaseWeapon : Item, IWeapon, IFactionItem, ICraftab
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool ShouldSerializeIdentified() => _identified;
[SerializedIgnoreDupe]
[SerializableField(24, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosAttributes _attributes;
@ -126,6 +127,7 @@ public abstract partial class BaseWeapon : Item, IWeapon, IFactionItem, ICraftab
[SerializableFieldDefault(24)]
private AosAttributes AttributesDefaultValue() => new(this);
[SerializedIgnoreDupe]
[SerializableField(25, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosWeaponAttributes _weaponAttributes;
@ -145,6 +147,7 @@ public abstract partial class BaseWeapon : Item, IWeapon, IFactionItem, ICraftab
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool ShouldSerializePlayerConstructed() => _playerConstructed;
[SerializedIgnoreDupe]
[SerializableField(27, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosSkillBonuses _skillBonuses;
@ -165,6 +168,7 @@ public abstract partial class BaseWeapon : Item, IWeapon, IFactionItem, ICraftab
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool ShouldSerializeSlayer2() => _slayer2 != SlayerName.None;
[SerializedIgnoreDupe]
[SerializableField(29, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosElementAttributes _aosElementDamages;
@ -932,6 +936,12 @@ public abstract partial class BaseWeapon : Item, IWeapon, IFactionItem, ICraftab
weap.AosElementDamages = new AosElementAttributes(newItem, AosElementDamages);
weap.SkillBonuses = new AosSkillBonuses(newItem, SkillBonuses);
weap.WeaponAttributes = new AosWeaponAttributes(newItem, WeaponAttributes);
// Set hue again because of resource
weap.Hue = Hue;
// Set HP/Max again because of durability
weap.HitPoints = HitPoints;
weap.MaxHitPoints = MaxHitPoints;
}
public int GetDurabilityBonus()

View file

@ -11,6 +11,7 @@ namespace Server.Items;
[SerializationGenerator(2, false)]
public partial class HouseTeleporter : Item, ISecurable
{
[SerializedIgnoreDupe]
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private SecureLevel _level;