modernuo/Projects/UOContent/Accounting/Account.cs

1177 lines
31 KiB
C#

using System;
using System.Collections.Generic;
using System.Net;
using System.Runtime.CompilerServices;
using System.Xml;
using ModernUO.CodeGeneratedEvents;
using ModernUO.Serialization;
using Server.Accounting.Security;
using Server.Misc;
using Server.Mobiles;
using Server.Multis;
using Server.Network;
namespace Server.Accounting;
[SerializationGenerator(6)]
public partial class Account : IAccount, IComparable<Account>
{
public static readonly TimeSpan YoungDuration = TimeSpan.FromHours(40.0);
public static readonly TimeSpan InactiveDuration = TimeSpan.FromDays(180.0);
public static readonly TimeSpan EmptyInactiveDuration = TimeSpan.FromDays(30.0);
[InternString]
[SerializableField(0)]
private string _username;
[SerializableField(1)]
private PasswordProtectionAlgorithm _passwordAlgorithm;
[SerializableField(2)]
private string _password;
[SerializableField(3)]
private AccessLevel _accessLevel;
[SerializableField(4)]
private int _flags;
[SerializableField(5)]
private DateTime _lastLogin;
/// <summary>
/// This amount represents the current amount of Gold owned by the player.
/// The value does not include the value of Platinum and ranges from
/// 0 to 999,999,999 by default.
/// </summary>
[SerializableField(6, setter: "private")]
[SerializedCommandProperty(AccessLevel.Administrator)]
public int _totalGold;
/// <summary>
/// This amount represents the current amount of Platinum owned by the player.
/// The value does not include the value of Gold and ranges from
/// 0 to 2,147,483,647 by default.
/// One Platinum represents the value of CurrencyThreshold in Gold.
/// </summary>
[SerializableField(7, setter: "private")]
[SerializedCommandProperty(AccessLevel.Administrator)]
public int _totalPlat;
private Mobile[] _mobiles;
[SerializableProperty(9)]
public List<AccountComment> Comments
{
get => _comments ??= [];
private set
{
_comments = value;
this.MarkDirty();
}
}
[SerializableProperty(10)]
public List<AccountTag> Tags
{
get => _tags ??= [];
private set
{
_tags = value;
this.MarkDirty();
}
}
[SerializableField(11)]
private IPAddress[] _loginIPs;
/// <summary>
/// Gets the total game time of this account, also considering the game time of characters
/// that have been deleted.
/// </summary>
[SerializableProperty(12)]
public TimeSpan TotalGameTime
{
get
{
for (var i = 0; i < _mobiles.Length; i++)
{
if (_mobiles[i] is PlayerMobile m && m.NetState != null)
{
return _totalGameTime + (Core.Now - m.SessionStart);
}
}
return _totalGameTime;
}
private set
{
_totalGameTime = value;
this.MarkDirty();
}
}
[SerializableField(13)]
[SerializedCommandProperty(AccessLevel.Administrator)]
private string _email;
private Timer m_YoungTimer;
public Account(string username, string password) : this(Accounts.NewAccount)
{
_username = username;
SetPassword(password);
_accessLevel = AccessLevel.Player;
_lastLogin = Core.Now;
_totalGameTime = TimeSpan.Zero;
_mobiles = new Mobile[7];
_loginIPs = [];
Accounts.Add(this);
this.MarkDirty();
}
public Account(XmlElement node)
{
Serial = Accounts.NewAccount;
_username = Utility.GetText(node["username"], "empty");
Enum.TryParse(Utility.GetText(node["passwordAlgorithm"], null), true, out _passwordAlgorithm);
// Backward compatibility with RunUO/ServUO
if (_passwordAlgorithm == PasswordProtectionAlgorithm.None)
{
var upgraded =
UpgradePassword(
Utility.GetText(node["newSecureCryptPassword"], null),
PasswordProtectionAlgorithm.SHA2
) ||
UpgradePassword(Utility.GetText(node["newCryptPassword"], null), PasswordProtectionAlgorithm.SHA1) ||
UpgradePassword(Utility.GetText(node["cryptPassword"], null), PasswordProtectionAlgorithm.MD5);
// Automatically upgrade plain passwords to current algorithm.
if (!upgraded)
{
SetPassword(Utility.GetText(node["password"], null));
}
}
else
{
_password = Utility.GetText(node["password"], null);
}
Enum.TryParse(Utility.GetText(node["accessLevel"], "Player"), true, out _accessLevel);
_flags = Utility.GetXMLInt32(Utility.GetText(node["flags"], "0"), 0);
Created = Utility.GetXMLDateTime(Utility.GetText(node["created"], null), Core.Now);
_lastLogin = Utility.GetXMLDateTime(Utility.GetText(node["lastLogin"], null), Core.Now);
_totalGold = Utility.GetXMLInt32(Utility.GetText(node["totalGold"], "0"), 0);
_totalPlat = Utility.GetXMLInt32(Utility.GetText(node["totalPlat"], "0"), 0);
_mobiles = LoadMobiles(node);
_comments = LoadComments(node);
_tags = LoadTags(node);
_loginIPs = LoadAddressList(node);
for (var i = 0; i < _mobiles.Length; ++i)
{
if (_mobiles[i] != null)
{
_mobiles[i].Account = this;
}
}
var totalGameTime = Utility.GetXMLTimeSpan(Utility.GetText(node["totalGameTime"], null), TimeSpan.Zero);
if (totalGameTime == TimeSpan.Zero)
{
for (var i = 0; i < _mobiles.Length; i++)
{
if (_mobiles[i] is PlayerMobile m)
{
totalGameTime += m.GameTime;
}
}
}
_totalGameTime = totalGameTime;
if (Young)
{
CheckYoung();
}
Accounts.Add(this);
this.MarkDirty();
}
/// <summary>
/// Object detailing information about the hardware of the last person to log into this account
/// </summary>
public HardwareInfo HardwareInfo { get; set; }
/// <summary>
/// Gets or sets a flag indicating if this account is banned.
/// </summary>
public bool Banned
{
get
{
var isBanned = GetFlag(0);
if (!isBanned)
{
return false;
}
if (GetBanTags(out var banTime, out var banDuration))
{
if (banDuration != TimeSpan.MaxValue && Core.Now >= banTime + banDuration)
{
SetUnspecifiedBan(null); // clear
Banned = false;
return false;
}
}
return true;
}
set => SetFlag(0, value);
}
/// <summary>
/// Gets or sets a flag indicating if the characters created on this account will have the young status.
/// </summary>
public bool Young
{
get => !GetFlag(1);
set
{
SetFlag(1, !value);
m_YoungTimer?.Stop();
m_YoungTimer = null;
}
}
/// <summary>
/// An account is considered inactive based upon LastLogin and InactiveDuration. If the account is empty, it is based upon
/// EmptyInactiveDuration
/// </summary>
public bool Inactive
{
get
{
if (AccessLevel != AccessLevel.Player)
{
return false;
}
var inactiveLength = Core.Now - _lastLogin;
return inactiveLength > (Count == 0 ? EmptyInactiveDuration : InactiveDuration);
}
}
public TimeSpan AccountAge => Core.Now - Created;
[CommandProperty(AccessLevel.GameMaster, readOnly: true)]
public DateTime Created { get; set; } = Core.Now;
public Serial Serial { get; set; }
public byte SerializedThread { get; set; }
public int SerializedPosition { get; set; }
public int SerializedLength { get; set; }
[AfterDeserialization(false)]
private void AfterDeserialization()
{
if (_comments?.Count == 0)
{
_comments = null;
}
if (_tags?.Count == 0)
{
_tags = null;
}
for (var i = 0; i < _mobiles.Length; ++i)
{
if (_mobiles[i] != null)
{
_mobiles[i].Account = this;
}
}
if (_totalGameTime == TimeSpan.Zero)
{
for (var i = 0; i < _mobiles.Length; i++)
{
if (_mobiles[i] is PlayerMobile m)
{
_totalGameTime += m.GameTime;
}
}
}
if (Young)
{
CheckYoung();
}
}
/// <summary>
/// Deletes the account, all characters of the account, and all houses of those characters
/// </summary>
public void Delete()
{
for (var i = 0; i < Length; ++i)
{
var m = this[i];
if (m == null)
{
continue;
}
var list = BaseHouse.GetHouses(m);
for (var j = 0; j < list.Count; ++j)
{
list[j].Delete();
}
m.Delete();
m.Account = null;
_mobiles[i] = null;
}
if (_loginIPs.Length != 0 && AccountHandler.IPTable.ContainsKey(_loginIPs[0]))
{
--AccountHandler.IPTable[_loginIPs[0]];
}
Deleted = true;
Accounts.Remove(this);
}
public bool Deleted { get; private set; }
public void SetPassword(string plainPassword)
{
var phrase = _passwordAlgorithm is PasswordProtectionAlgorithm.SHA1 or PasswordProtectionAlgorithm.SHA2
? $"{_username}{plainPassword}"
: plainPassword;
Password = AccountSecurity.CurrentPasswordProtection.EncryptPassword(phrase);
PasswordAlgorithm = AccountSecurity.CurrentAlgorithm;
}
public bool CheckPassword(string plainPassword)
{
var phrase = _passwordAlgorithm is PasswordProtectionAlgorithm.SHA1 or PasswordProtectionAlgorithm.SHA2
? $"{_username}{plainPassword}"
: plainPassword;
var ok = AccountSecurity.GetPasswordProtection(_passwordAlgorithm).ValidatePassword(Password, phrase);
if (!ok)
{
return false;
}
// Upgrade the password protection in case we change the algorithm
if (_passwordAlgorithm != AccountSecurity.CurrentAlgorithm)
{
SetPassword(plainPassword);
}
return true;
}
/// <summary>
/// Gets the current number of characters on this account.
/// </summary>
public int Count
{
get
{
var count = 0;
for (var i = 0; i < Length; i++)
{
if (this[i] != null)
{
count++;
}
}
return count;
}
}
/// <summary>
/// Gets the maximum amount of characters allowed to be created on this account. Values other than 1, 5, 6, or 7 are not
/// supported by the client.
/// </summary>
public int Limit => Core.SA ? 7 : Core.AOS ? 6 : 5;
/// <summary>
/// Gets the maximum amount of characters that this account can hold.
/// </summary>
public int Length => _mobiles.Length;
/// <summary>
/// Gets or sets the character at a specified index for this account. Out of bound index values are handled; null returned
/// for get, ignored for set.
/// </summary>
public Mobile this[int index]
{
get
{
if (index >= 0 && index < _mobiles.Length)
{
var m = _mobiles[index];
if (m?.Deleted != true)
{
return m;
}
// This is the only place that clears a mobile for garbage collection
// outside of an entire account deletion.
m.Account = null;
_mobiles[index] = null;
this.MarkDirty();
}
return null;
}
set
{
if (index >= 0 && index < _mobiles.Length)
{
if (_mobiles[index] != null)
{
_mobiles[index].Account = null;
}
_mobiles[index] = value;
this.MarkDirty();
if (_mobiles[index] != null)
{
_mobiles[index].Account = this;
}
}
}
}
public int CompareTo(IAccount other) => string.CompareOrdinal(Username, other?.Username);
/// <summary>
/// Attempts to deposit the given amount of Gold into this account.
/// If the given amount is greater than the CurrencyThreshold,
/// Platinum will be deposited to offset the difference.
/// </summary>
/// <param name="amount">Amount to deposit.</param>
/// <returns>True if successful, false if amount given is less than or equal to zero.</returns>
public bool DepositGold(int amount)
{
if (amount <= 0)
{
return false;
}
var plat = Math.DivRem(amount, AccountGold.CurrencyThreshold, out var gold);
TotalPlat += plat;
TotalGold += gold;
return true;
}
/// <summary>
/// Attempts to deposit the given amount of Platinum into this account.
/// </summary>
/// <param name="amount">Amount to deposit.</param>
/// <returns>True if successful, false if amount given is less than or equal to zero.</returns>
public bool DepositPlat(int amount)
{
if (amount <= 0)
{
return false;
}
TotalPlat += amount;
return true;
}
/// <summary>
/// Attempts to withdraw the given amount of Gold from this account.
/// If the given amount is greater than the CurrencyThreshold,
/// Platinum will be withdrawn to offset the difference.
/// </summary>
/// <param name="amount">Amount to withdraw.</param>
/// <returns>True if successful, false if balance was too low.</returns>
public bool WithdrawGold(int amount)
{
if (amount <= 0)
{
return true;
}
if (amount > _totalGold)
{
return false;
}
TotalGold -= amount;
return true;
}
/// <summary>
/// Attempts to withdraw the given amount of Platinum from this account.
/// </summary>
/// <param name="amount">Amount to withdraw.</param>
/// <returns>True if successful, false if balance was too low.</returns>
public bool WithdrawPlat(int amount)
{
if (amount <= 0)
{
return true;
}
if (amount > _totalPlat)
{
return false;
}
TotalPlat -= amount;
return true;
}
/// <summary>
/// Returns total gold inclusive of platinum.
/// This is strictly for backwards compatibility
/// </summary>
/// <returns>Total gold, capped at Int32.MaxValue</returns>
public long GetTotalGold() => _totalGold + _totalPlat * AccountGold.CurrencyThreshold;
public int CompareTo(Account other) => string.CompareOrdinal(_username, other?._username);
/// <summary>
/// Gets the value of a specific flag in the Flags bitfield.
/// </summary>
/// <param name="index">The zero-based flag index.</param>
public bool GetFlag(int index) => (_flags & (1 << index)) != 0;
/// <summary>
/// Sets the value of a specific flag in the Flags bitfield.
/// </summary>
/// <param name="index">The zero-based flag index.</param>
/// <param name="value">The value to set.</param>
public void SetFlag(int index, bool value)
{
if (value)
{
Flags |= 1 << index;
}
else
{
Flags &= ~(1 << index);
}
}
/// <summary>
/// Adds a new tag to this account. This method does not check for duplicate names.
/// </summary>
/// <param name="name">New tag name.</param>
/// <param name="value">New tag value.</param>
public void AddTag(string name, string value)
{
Tags.Add(new AccountTag(name, value));
this.MarkDirty();
}
/// <summary>
/// Removes all tags with the specified name from this account.
/// </summary>
/// <param name="name">Tag name to remove.</param>
public void RemoveTag(string name)
{
if (_tags == null)
{
return;
}
for (var i = _tags.Count - 1; i >= 0; --i)
{
if (i >= _tags.Count)
{
continue;
}
var tag = _tags[i];
if (tag.Name == name)
{
_tags.RemoveAt(i);
this.MarkDirty();
}
}
}
/// <summary>
/// Modifies an existing tag or adds a new tag if no tag exists.
/// </summary>
/// <param name="name">Tag name.</param>
/// <param name="value">Tag value.</param>
public void SetTag(string name, string value)
{
for (var i = 0; i < Tags.Count; ++i)
{
var tag = _tags[i];
if (tag.Name == name)
{
tag.Value = value;
this.MarkDirty();
return;
}
}
AddTag(name, value);
}
/// <summary>
/// Gets the value of a tag -or- null if there are no tags with the specified name.
/// </summary>
/// <param name="name">Name of the desired tag value.</param>
public string GetTag(string name)
{
for (var i = 0; i < Tags.Count; ++i)
{
var tag = _tags[i];
if (tag.Name == name)
{
return tag.Value;
}
}
return null;
}
public void SetUnspecifiedBan(Mobile from)
{
SetBanTags(from, DateTime.MinValue, TimeSpan.Zero);
}
public void SetBanTags(Mobile from, DateTime banTime, TimeSpan banDuration)
{
if (from == null)
{
RemoveTag("BanDealer");
}
else
{
SetTag("BanDealer", from.ToString());
}
if (banTime == DateTime.MinValue)
{
RemoveTag("BanTime");
}
else
{
SetTag("BanTime", XmlConvert.ToString(banTime, XmlDateTimeSerializationMode.Utc));
}
if (banDuration == TimeSpan.Zero)
{
RemoveTag("BanDuration");
}
else
{
SetTag("BanDuration", banDuration.ToString());
}
}
public bool GetBanTags(out DateTime banTime, out TimeSpan banDuration)
{
var tagDuration = GetTag("BanDuration");
banTime = Utility.GetXMLDateTime(GetTag("BanTime"), DateTime.MinValue);
if (tagDuration == "Infinite")
{
banDuration = TimeSpan.MaxValue;
}
else if (tagDuration != null)
{
banDuration = Utility.ToTimeSpan(tagDuration);
}
else
{
banDuration = TimeSpan.Zero;
}
return banTime != DateTime.MinValue && banDuration != TimeSpan.Zero;
}
public static void Initialize()
{
EventSink.Connected += EventSink_Connected;
EventSink.Disconnected += EventSink_Disconnected;
}
private static void EventSink_Connected(Mobile m)
{
if (m.Account is not Account acc)
{
return;
}
if (acc.Young && acc.m_YoungTimer == null)
{
acc.m_YoungTimer = new YoungTimer(acc);
acc.m_YoungTimer.Start();
}
}
private static void EventSink_Disconnected(Mobile m)
{
if (m.Account is not Account acc)
{
return;
}
if (acc.m_YoungTimer != null)
{
acc.m_YoungTimer.Stop();
acc.m_YoungTimer = null;
}
if (m is not PlayerMobile pm)
{
return;
}
acc.TotalGameTime += Core.Now - pm.SessionStart;
}
[OnEvent(nameof(PlayerMobile.PlayerLoginEvent))]
public static void OnLogin(PlayerMobile pm)
{
if (pm.Account is not Account acc || !pm.Young || !acc.Young)
{
return;
}
var ts = YoungDuration - acc.TotalGameTime;
var hours = Math.Max((int)ts.TotalHours, 0);
if (hours == 1)
{
pm.SendAsciiMessage($"You will enjoy the benefits and relatively safe status of a young player for {hours} more hour.");
}
else
{
pm.SendAsciiMessage($"You will enjoy the benefits and relatively safe status of a young player for {hours} more hours.");
}
}
public void RemoveYoungStatus(int message)
{
Young = false;
for (var i = 0; i < _mobiles.Length; i++)
{
if (_mobiles[i] is PlayerMobile { Young: true } m)
{
m.Young = false;
if (m.NetState != null)
{
if (message > 0)
{
m.SendLocalizedMessage(message);
}
// You are no longer considered a young player of Ultima Online,
// and are no longer subject to the limitations and benefits of being in that caste.
m.SendLocalizedMessage(1019039);
}
}
}
}
public void CheckYoung()
{
if (TotalGameTime >= YoungDuration)
{
// You are old enough to be considered an adult, and have outgrown your status as a young player!
RemoveYoungStatus(1019038);
}
}
private bool UpgradePassword(string password, PasswordProtectionAlgorithm algorithm)
{
if (password == null || algorithm < _passwordAlgorithm)
{
return false;
}
PasswordAlgorithm = algorithm;
Password = password.ReplaceOrdinal("-", string.Empty);
return true;
}
/// <summary>
/// Deserializes a list of string values from an xml element. Null values are not added to the list.
/// </summary>
/// <param name="node">The XmlElement from which to deserialize.</param>
/// <returns>String list. Value will never be null.</returns>
private static string[] LoadAccessCheck(XmlElement node)
{
string[] stringList;
var accessCheck = node["accessCheck"];
if (accessCheck != null)
{
var list = new List<string>();
foreach (XmlElement ip in accessCheck.GetElementsByTagName("ip"))
{
var text = Utility.GetText(ip, null);
if (text != null)
{
list.Add(text);
}
}
stringList = list.ToArray();
}
else
{
stringList = [];
}
return stringList;
}
/// <summary>
/// Deserializes a list of IPAddress values from an xml element.
/// </summary>
/// <param name="node">The XmlElement from which to deserialize.</param>
/// <returns>Address list. Value will never be null.</returns>
private static IPAddress[] LoadAddressList(XmlElement node)
{
IPAddress[] list;
var addressList = node["addressList"];
if (addressList != null)
{
var count = Utility.GetXMLInt32(Utility.GetAttribute(addressList, "count", "0"), 0);
list = new IPAddress[count];
count = 0;
foreach (XmlElement ip in addressList.GetElementsByTagName("ip"))
{
if (count < list.Length)
{
if (IPAddress.TryParse(Utility.GetText(ip, null), out var address))
{
list[count] = Utility.Intern(address);
count++;
}
}
}
if (count != list.Length)
{
var old = list;
list = new IPAddress[count];
for (var i = 0; i < count && i < old.Length; ++i)
{
list[i] = old[i];
}
}
}
else
{
list = [];
}
return list;
}
/// <summary>
/// Deserializes a list of Mobile instances from an xml element.
/// </summary>
/// <param name="node">The XmlElement instance from which to deserialize.</param>
/// <returns>Mobile list. Value will never be null.</returns>
private static Mobile[] LoadMobiles(XmlElement node)
{
var list = new Mobile[7];
var chars = node["chars"];
// int length = Accounts.GetInt32( Accounts.GetAttribute( chars, "length", "6" ), 6 );
// list = new Mobile[length];
// Above is legacy, no longer used
if (chars != null)
{
foreach (XmlElement ele in chars.GetElementsByTagName("char"))
{
try
{
var index = Utility.GetXMLInt32(Utility.GetAttribute(ele, "index", "0"), 0);
var serial = (Serial)Utility.GetXMLUInt32(Utility.GetText(ele, "0"), 0);
if (index >= 0 && index < list.Length)
{
list[index] = World.FindMobile(serial);
}
}
catch
{
// ignored
}
}
}
return list;
}
/// <summary>
/// Deserializes a list of AccountComment instances from an xml element.
/// </summary>
/// <param name="node">The XmlElement from which to deserialize.</param>
/// <returns>Comment list. Value will never be null.</returns>
private static List<AccountComment> LoadComments(XmlElement node)
{
List<AccountComment> list = null;
var comments = node["comments"];
if (comments != null)
{
list = [];
foreach (XmlElement comment in comments.GetElementsByTagName("comment"))
{
try
{
list.Add(new AccountComment(comment));
}
catch
{
// ignored
}
}
}
return list;
}
/// <summary>
/// Deserializes a list of AccountTag instances from an xml element.
/// </summary>
/// <param name="node">The XmlElement from which to deserialize.</param>
/// <returns>Tag list. Value will never be null.</returns>
private static List<AccountTag> LoadTags(XmlElement node)
{
List<AccountTag> list = null;
var tags = node["tags"];
if (tags != null)
{
list = [];
foreach (XmlElement tag in tags.GetElementsByTagName("tag"))
{
try
{
list.Add(new AccountTag(tag));
}
catch
{
// ignored
}
}
}
return list;
}
/// <summary>
/// Checks if a specific NetState is allowed access to this account.
/// </summary>
/// <param name="ns">NetState instance to check.</param>
/// <returns>True if allowed, false if not.</returns>
public bool HasAccess(NetState ns) => ns != null && HasAccess(ns.Address);
public bool HasAccess(IPAddress ipAddress)
{
var level = AccountHandler.LockdownLevel;
if (level <= AccessLevel.Player || _accessLevel >= level)
{
return true;
}
for (var i = 0; i < Length; ++i)
{
var m = this[i];
if (m?.AccessLevel >= level)
{
return true;
}
}
return false;
}
/// <summary>
/// Records the IP address of 'ns' in its 'LoginIPs' list.
/// </summary>
/// <param name="ns">NetState instance to record.</param>
public void LogAccess(NetState ns)
{
if (ns != null)
{
LogAccess(ns.Address);
}
}
public void LogAccess(IPAddress ipAddress)
{
if (_loginIPs.Length == 0)
{
AccountHandler.IPTable.TryGetValue(ipAddress, out var result);
AccountHandler.IPTable[ipAddress] = result + 1;
}
var contains = false;
for (var i = 0; !contains && i < _loginIPs.Length; ++i)
{
contains = _loginIPs[i].Equals(ipAddress);
}
if (contains)
{
return;
}
var old = _loginIPs;
LoginIPs = new IPAddress[old.Length + 1];
for (var i = 0; i < old.Length; ++i)
{
LoginIPs[i] = old[i];
}
LoginIPs[old.Length] = ipAddress;
}
/// <summary>
/// Checks if a specific NetState is allowed access to this account. If true, the NetState IPAddress is added to the address
/// list.
/// </summary>
/// <param name="ns">NetState instance to check.</param>
/// <returns>True if allowed, false if not.</returns>
public bool CheckAccess(NetState ns) => ns != null && CheckAccess(ns.Address);
public bool CheckAccess(IPAddress ipAddress)
{
var hasAccess = HasAccess(ipAddress);
if (hasAccess)
{
LogAccess(ipAddress);
}
return hasAccess;
}
public override string ToString() => _username;
private class YoungTimer : Timer
{
private readonly Account m_Account;
public YoungTimer(Account account)
: base(TimeSpan.FromMinutes(1.0), TimeSpan.FromMinutes(1.0))
{
m_Account = account;
}
protected override void OnTick()
{
m_Account.CheckYoung();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Enumerator GetEnumerator() => new(_mobiles);
[SerializableProperty(8, useField: nameof(_mobiles))]
public Enumerator Mobiles
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => GetEnumerator();
}
public ref struct Enumerator
{
private readonly Mobile[] _mobiles;
private int _index;
private Mobile _current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Enumerator(Mobile[] mobs)
{
_mobiles = mobs;
_index = 0;
_current = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
Mobile[] localList = _mobiles;
while ((uint)_index < (uint)localList.Length)
{
_current = localList[_index++];
if (_current?.Deleted == false)
{
return true;
}
}
return false;
}
public Mobile Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _current;
}
}
}