#W# Confict: Fixed BaseCreature git merge conflict.
This commit is contained in:
commit
ad38314ca7
112 changed files with 5397 additions and 616 deletions
|
|
@ -390,6 +390,21 @@ public partial class Container : Item
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct children are left out of world saves; the container itself is still saved and
|
||||
/// loads empty.
|
||||
/// </summary>
|
||||
public virtual bool SkipsChildSerialization => false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether <paramref name="child"/> was sent to every client in range rather than only to the root
|
||||
/// mobile, the secure-trade partner and openers. Removal of a public child is broadcast; removal of any
|
||||
/// other child reaches only the clients that were sent it. Overrides may only widen this beyond
|
||||
/// <see cref="IsPublicContainer"/>, never narrow it: a public container's children always broadcast
|
||||
/// their removal regardless of what an override returns here.
|
||||
/// </summary>
|
||||
public virtual bool IsChildPublic(Item child) => IsPublicContainer;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void OnItemAdded(Item item)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -922,7 +922,10 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
}
|
||||
|
||||
var info = LookupCompactInfo();
|
||||
var items = LookupItems();
|
||||
// Skipping containers never write their child serials: those children are not saved as
|
||||
// top-level records (see SkipSerialization), so a written serial can be handed to an
|
||||
// unrelated new item during the next load's synchronous deserialize loop.
|
||||
var items = this is Container { SkipsChildSerialization: true } ? EmptyItems : LookupItems();
|
||||
|
||||
if (m_Direction != Direction.North)
|
||||
{
|
||||
|
|
@ -1185,6 +1188,11 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
// are final yet.
|
||||
LastMoved = Core.Now;
|
||||
|
||||
// A parented item's removal already reached every client that was sent it (root, trade
|
||||
// parties, openers) via RemoveItem below; sending another remove at oldLocation would
|
||||
// broadcast it to bystanders who never knew about it.
|
||||
var wasContained = Parent != null;
|
||||
|
||||
if (Parent is Mobile mobile)
|
||||
{
|
||||
mobile.RemoveItem(this);
|
||||
|
|
@ -1202,7 +1210,7 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
{
|
||||
m_Map.OnLeave(this);
|
||||
|
||||
if (oldLocation.m_X != 0)
|
||||
if (!wasContained && oldLocation.m_X != 0)
|
||||
{
|
||||
SendRemovePacket(oldLocation);
|
||||
}
|
||||
|
|
@ -1278,7 +1286,7 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
}
|
||||
else if (m_Map != null)
|
||||
{
|
||||
if (oldLocation.m_X != 0)
|
||||
if (!wasContained && oldLocation.m_X != 0)
|
||||
{
|
||||
var removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
|
||||
|
||||
|
|
@ -1344,7 +1352,12 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
m_Map.OnLeave(this);
|
||||
}
|
||||
|
||||
SendRemovePacket();
|
||||
if (m_Parent is not Container)
|
||||
{
|
||||
// A contained item only changes map along with its parent or root; their own
|
||||
// removal already clears this subtree for every client that knew about it.
|
||||
SendRemovePacket();
|
||||
}
|
||||
}
|
||||
|
||||
var items = LookupItems();
|
||||
|
|
@ -1479,6 +1492,43 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
return;
|
||||
}
|
||||
|
||||
// A properties-only invalidation (no Update) on a private child must stay as scoped as every
|
||||
// other private-child send, otherwise a bystander who was never sent the item learns its OPL hash.
|
||||
if (!update && ObjectPropertyList.Enabled && (flags & ItemDelta.Properties) != 0 &&
|
||||
TryGetPrivateParent(out var cont))
|
||||
{
|
||||
GetPrivateChildRecipients(cont, out var root, out var tradeFrom, out var tradeTo);
|
||||
|
||||
SendPrivateOplTo(root, worldLoc);
|
||||
|
||||
if (tradeFrom != root)
|
||||
{
|
||||
SendPrivateOplTo(tradeFrom, worldLoc);
|
||||
}
|
||||
|
||||
if (tradeTo != root && tradeTo != tradeFrom)
|
||||
{
|
||||
SendPrivateOplTo(tradeTo, worldLoc);
|
||||
}
|
||||
|
||||
var privateOpeners = cont.Openers;
|
||||
|
||||
if (privateOpeners != null)
|
||||
{
|
||||
for (var i = 0; i < privateOpeners.Count; ++i)
|
||||
{
|
||||
var mob = privateOpeners[i];
|
||||
|
||||
if (mob != root && mob != tradeFrom && mob != tradeTo)
|
||||
{
|
||||
SendPrivateOplTo(mob, worldLoc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// A second item sharing an equipment layer is omitted by SendMobileIncoming (0x78);
|
||||
// sending it on its own via EquipUpdate/OPL would leave the client with two items on
|
||||
// one slot. Skip the per-client sends entirely for the dupe.
|
||||
|
|
@ -1581,7 +1631,7 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
ClearProperties();
|
||||
}
|
||||
|
||||
public virtual bool SkipSerialization => false;
|
||||
public virtual bool SkipSerialization => m_Parent is Container { SkipsChildSerialization: true };
|
||||
|
||||
[IgnoreDupe]
|
||||
public ISpawner Spawner
|
||||
|
|
@ -2926,6 +2976,13 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
m_Parent = null;
|
||||
}
|
||||
|
||||
// A skipping container never wrote its children's serials (see Serialize), so this
|
||||
// item's frozen parent reference is stale/unreachable: treat it as a missing parent.
|
||||
if (m_Parent is Container { SkipsChildSerialization: true })
|
||||
{
|
||||
m_Parent = null;
|
||||
}
|
||||
|
||||
if (m_Parent == null && (parent.IsMobile || parent.IsItem))
|
||||
{
|
||||
Timer.DelayCall(Delete);
|
||||
|
|
@ -3089,6 +3146,13 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
m_Parent = null;
|
||||
}
|
||||
|
||||
// A skipping container never wrote its children's serials (see Serialize), so this
|
||||
// item's frozen parent reference is stale/unreachable: treat it as a missing parent.
|
||||
if (m_Parent is Container { SkipsChildSerialization: true })
|
||||
{
|
||||
m_Parent = null;
|
||||
}
|
||||
|
||||
if (m_Parent == null && (parent.IsMobile || parent.IsItem))
|
||||
{
|
||||
Timer.DelayCall(Delete);
|
||||
|
|
@ -3552,8 +3616,19 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
{
|
||||
var items = LookupItems();
|
||||
|
||||
if (items.Remove(item))
|
||||
// Bulk removal (Delete, purges) walks children back to front; finding the tail in O(1) keeps
|
||||
// deleting a large container linear instead of quadratic.
|
||||
var index = items.Count - 1;
|
||||
|
||||
if (index < 0 || !ReferenceEquals(items[index], item))
|
||||
{
|
||||
index = items.IndexOf(item);
|
||||
}
|
||||
|
||||
if (index >= 0)
|
||||
{
|
||||
items.RemoveAt(index);
|
||||
|
||||
if (this is not Container)
|
||||
{
|
||||
AcquireCompactInfo().Version++;
|
||||
|
|
@ -4018,6 +4093,43 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
|
||||
var removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
|
||||
|
||||
if (TryGetPrivateParent(out var cont))
|
||||
{
|
||||
// Private children are only ever sent to these recipients (see ProcessDelta); nobody else knows them.
|
||||
OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
|
||||
|
||||
GetPrivateChildRecipients(cont, out var root, out var tradeFrom, out var tradeTo);
|
||||
|
||||
SendRemoveTo(root, worldLoc, removeEntity);
|
||||
|
||||
if (tradeFrom != root)
|
||||
{
|
||||
SendRemoveTo(tradeFrom, worldLoc, removeEntity);
|
||||
}
|
||||
|
||||
if (tradeTo != root && tradeTo != tradeFrom)
|
||||
{
|
||||
SendRemoveTo(tradeTo, worldLoc, removeEntity);
|
||||
}
|
||||
|
||||
var openers = cont.Openers;
|
||||
|
||||
if (openers != null)
|
||||
{
|
||||
for (var i = 0; i < openers.Count; ++i)
|
||||
{
|
||||
var mob = openers[i];
|
||||
|
||||
if (mob != root && mob != tradeFrom && mob != tradeTo)
|
||||
{
|
||||
SendRemoveTo(mob, worldLoc, removeEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var state in m_Map.GetClientsInRange(worldLoc, GetMaxUpdateRange()))
|
||||
{
|
||||
var m = state.Mobile;
|
||||
|
|
@ -4030,6 +4142,78 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
}
|
||||
}
|
||||
|
||||
private void SendRemoveTo(Mobile m, Point3D worldLoc, ReadOnlySpan<byte> removeEntity)
|
||||
{
|
||||
if (m?.NetState != null && m.Map == m_Map && m.InRange(worldLoc, GetUpdateRange(m)))
|
||||
{
|
||||
m.NetState.Send(removeEntity);
|
||||
}
|
||||
}
|
||||
|
||||
// Same recipient gate as SendRemoveTo, plus CanSee: this is an informational OPL push, not
|
||||
// cleanup of a reference the client may already hold regardless of visibility.
|
||||
private void SendPrivateOplTo(Mobile m, Point3D worldLoc)
|
||||
{
|
||||
var ns = m?.NetState;
|
||||
|
||||
if (ns != null && m.CanSee(this) && m.Map == m_Map && m.InRange(worldLoc, GetUpdateRange(m)))
|
||||
{
|
||||
SendOPLPacketTo(ns);
|
||||
}
|
||||
}
|
||||
|
||||
// Shared with IsSentTo so the private-child guard and recipient set (root/trade/openers) each live
|
||||
// in one place.
|
||||
private bool TryGetPrivateParent(out Container cont)
|
||||
{
|
||||
cont = m_Parent as Container;
|
||||
return cont != null && !cont.IsPublicContainer && !cont.IsChildPublic(this);
|
||||
}
|
||||
|
||||
private void GetPrivateChildRecipients(Container cont, out Mobile root, out Mobile tradeFrom, out Mobile tradeTo)
|
||||
{
|
||||
root = cont.RootParent as Mobile;
|
||||
|
||||
var trade = GetSecureTradeCont()?.Trade;
|
||||
// Trade.From/To are unassigned while SecureTrade's own constructor is still adding the
|
||||
// VirtualCheck to each side's container, so both must stay null-conditional.
|
||||
tradeFrom = trade?.From?.Mobile;
|
||||
tradeTo = trade?.To?.Mobile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether <paramref name="m"/>'s client is expected to hold this item; gates resends keyed by a
|
||||
/// client-supplied serial.
|
||||
/// </summary>
|
||||
public bool IsSentTo(Mobile m)
|
||||
{
|
||||
if (m == null || Deleted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryGetPrivateParent(out var cont))
|
||||
{
|
||||
GetPrivateChildRecipients(cont, out var root, out var tradeFrom, out var tradeTo);
|
||||
|
||||
if (m == root)
|
||||
{
|
||||
// Mirrors ProcessDelta's own root gate: CanSee and in range.
|
||||
return m.CanSee(this) && m.InRange(GetWorldLocation(), GetUpdateRange(m));
|
||||
}
|
||||
|
||||
var isTradeOrOpener = m == tradeFrom || m == tradeTo || cont.Openers?.Contains(m) == true;
|
||||
|
||||
// Trade parties and openers are re-validated against ProcessDelta's own gate (CanSee,
|
||||
// current map, update range), so an invisible item or a stale Openers entry that hasn't
|
||||
// been pruned yet never resyncs to them.
|
||||
return isTradeOrOpener && m.CanSee(this) && m.Map == m_Map &&
|
||||
m.InRange(GetWorldLocation(), GetUpdateRange(m));
|
||||
}
|
||||
|
||||
return m.CanSee(this) && m.InRange(GetWorldLocation(), GetUpdateRange(m));
|
||||
}
|
||||
|
||||
public virtual int GetDropSound() => -1;
|
||||
|
||||
public Point3D GetWorldLocation()
|
||||
|
|
|
|||
|
|
@ -85,6 +85,9 @@ public partial class Map
|
|||
private Rectangle2D _bounds;
|
||||
|
||||
private int _sectorStartX;
|
||||
private int _sectorStartY;
|
||||
private int _sectorEndX;
|
||||
private int _sectorEndY;
|
||||
private int _maxRing;
|
||||
|
||||
private int _ring; // -1 = uninitialized, then 0.._maxRing
|
||||
|
|
@ -115,11 +118,11 @@ public partial class Map
|
|||
var centerSectorX = center.m_X / SectorSize;
|
||||
var centerSectorY = center.m_Y / SectorSize;
|
||||
|
||||
map.CalculateSectors(_bounds, out _sectorStartX, out var sectorStartY, out var sectorEndX, out var sectorEndY);
|
||||
map.CalculateSectors(_bounds, out _sectorStartX, out _sectorStartY, out _sectorEndX, out _sectorEndY);
|
||||
|
||||
// Calculate max ring based on bounds
|
||||
var dx = Math.Max(centerSectorX - _sectorStartX, sectorEndX - centerSectorX);
|
||||
var dy = Math.Max(centerSectorY - sectorStartY, sectorEndY - centerSectorY);
|
||||
var dx = Math.Max(centerSectorX - _sectorStartX, _sectorEndX - centerSectorX);
|
||||
var dy = Math.Max(centerSectorY - _sectorStartY, _sectorEndY - centerSectorY);
|
||||
_maxRing = Math.Max(dx, dy);
|
||||
}
|
||||
|
||||
|
|
@ -202,7 +205,7 @@ public partial class Map
|
|||
_ringIndex = 0;
|
||||
sx = _center.m_X / SectorSize;
|
||||
sy = _center.m_Y / SectorSize;
|
||||
return sx >= _sectorStartX;
|
||||
return IsSectorInBounds(sx, sy);
|
||||
}
|
||||
|
||||
sx = sy = 0;
|
||||
|
|
@ -225,7 +228,7 @@ public partial class Map
|
|||
_ringIndex = nextIndex;
|
||||
CalculatePositionFromIndex(nextIndex, out sx, out sy);
|
||||
|
||||
if (sx >= _sectorStartX)
|
||||
if (IsSectorInBounds(sx, sy))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -233,6 +236,11 @@ public partial class Map
|
|||
}
|
||||
|
||||
|
||||
// Ring cells outside the clamped sector range would resolve to the shared invalid sector, once per cell
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool IsSectorInBounds(int sx, int sy) =>
|
||||
sx >= _sectorStartX && sx <= _sectorEndX && sy >= _sectorStartY && sy <= _sectorEndY;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void CalculatePositionFromIndex(int index, out int x, out int y)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -113,6 +113,9 @@ public partial class Map
|
|||
private Rectangle2D _bounds;
|
||||
|
||||
private int _sectorStartX;
|
||||
private int _sectorStartY;
|
||||
private int _sectorEndX;
|
||||
private int _sectorEndY;
|
||||
private int _maxRing;
|
||||
|
||||
private int _ring; // -1 = uninitialized, then 0.._maxRing
|
||||
|
|
@ -143,11 +146,11 @@ public partial class Map
|
|||
var centerSectorX = center.m_X / SectorSize;
|
||||
var centerSectorY = center.m_Y / SectorSize;
|
||||
|
||||
map.CalculateSectors(_bounds, out _sectorStartX, out var sectorStartY, out var sectorEndX, out var sectorEndY);
|
||||
map.CalculateSectors(_bounds, out _sectorStartX, out _sectorStartY, out _sectorEndX, out _sectorEndY);
|
||||
|
||||
// Calculate max ring based on bounds
|
||||
var dx = Math.Max(centerSectorX - _sectorStartX, sectorEndX - centerSectorX);
|
||||
var dy = Math.Max(centerSectorY - sectorStartY, sectorEndY - centerSectorY);
|
||||
var dx = Math.Max(centerSectorX - _sectorStartX, _sectorEndX - centerSectorX);
|
||||
var dy = Math.Max(centerSectorY - _sectorStartY, _sectorEndY - centerSectorY);
|
||||
_maxRing = Math.Max(dx, dy);
|
||||
}
|
||||
|
||||
|
|
@ -228,7 +231,7 @@ public partial class Map
|
|||
_ringIndex = 0;
|
||||
sx = _center.m_X / SectorSize;
|
||||
sy = _center.m_Y / SectorSize;
|
||||
return sx >= _sectorStartX;
|
||||
return IsSectorInBounds(sx, sy);
|
||||
}
|
||||
|
||||
sx = sy = 0;
|
||||
|
|
@ -251,7 +254,7 @@ public partial class Map
|
|||
_ringIndex = nextIndex;
|
||||
CalculatePositionFromIndex(nextIndex, out sx, out sy);
|
||||
|
||||
if (sx >= _sectorStartX)
|
||||
if (IsSectorInBounds(sx, sy))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -259,6 +262,11 @@ public partial class Map
|
|||
}
|
||||
|
||||
|
||||
// Ring cells outside the clamped sector range would resolve to the shared invalid sector, once per cell
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool IsSectorInBounds(int sx, int sy) =>
|
||||
sx >= _sectorStartX && sx <= _sectorEndX && sy >= _sectorStartY && sy <= _sectorEndY;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void CalculatePositionFromIndex(int index, out int x, out int y)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -108,6 +108,9 @@ public partial class Map
|
|||
private Rectangle2D _bounds;
|
||||
|
||||
private int _sectorStartX;
|
||||
private int _sectorStartY;
|
||||
private int _sectorEndX;
|
||||
private int _sectorEndY;
|
||||
private int _maxRing;
|
||||
|
||||
private int _ring; // -1 = uninitialized, then 0.._maxRing
|
||||
|
|
@ -138,11 +141,11 @@ public partial class Map
|
|||
var centerSectorX = center.m_X / SectorSize;
|
||||
var centerSectorY = center.m_Y / SectorSize;
|
||||
|
||||
map.CalculateSectors(_bounds, out _sectorStartX, out var sectorStartY, out var sectorEndX, out var sectorEndY);
|
||||
map.CalculateSectors(_bounds, out _sectorStartX, out _sectorStartY, out _sectorEndX, out _sectorEndY);
|
||||
|
||||
// Calculate max ring based on bounds
|
||||
var dx = Math.Max(centerSectorX - _sectorStartX, sectorEndX - centerSectorX);
|
||||
var dy = Math.Max(centerSectorY - sectorStartY, sectorEndY - centerSectorY);
|
||||
var dx = Math.Max(centerSectorX - _sectorStartX, _sectorEndX - centerSectorX);
|
||||
var dy = Math.Max(centerSectorY - _sectorStartY, _sectorEndY - centerSectorY);
|
||||
_maxRing = Math.Max(dx, dy);
|
||||
}
|
||||
|
||||
|
|
@ -223,7 +226,7 @@ public partial class Map
|
|||
_ringIndex = 0;
|
||||
sx = _center.m_X / SectorSize;
|
||||
sy = _center.m_Y / SectorSize;
|
||||
return sx >= _sectorStartX;
|
||||
return IsSectorInBounds(sx, sy);
|
||||
}
|
||||
|
||||
sx = sy = 0;
|
||||
|
|
@ -246,7 +249,7 @@ public partial class Map
|
|||
_ringIndex = nextIndex;
|
||||
CalculatePositionFromIndex(nextIndex, out sx, out sy);
|
||||
|
||||
if (sx >= _sectorStartX)
|
||||
if (IsSectorInBounds(sx, sy))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -254,6 +257,11 @@ public partial class Map
|
|||
}
|
||||
|
||||
|
||||
// Ring cells outside the clamped sector range would resolve to the shared invalid sector, once per cell
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool IsSectorInBounds(int sx, int sy) =>
|
||||
sx >= _sectorStartX && sx <= _sectorEndX && sy >= _sectorStartY && sy <= _sectorEndY;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void CalculatePositionFromIndex(int index, out int x, out int y)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -85,8 +85,6 @@ public partial class Map
|
|||
public MultiBoundsEnumerable<T> GetMultisInBounds<T>(Rectangle2D bounds, bool makeBoundsInclusive = false) where T : BaseMulti =>
|
||||
new(this, bounds, makeBoundsInclusive);
|
||||
|
||||
private static readonly HashSet<Serial> _sharedDupes = [];
|
||||
|
||||
public ref struct MultiSectorEnumerable<T>(Map map, Point2D loc) where T : BaseMulti
|
||||
{
|
||||
public static MultiSectorEnumerable<T> Empty
|
||||
|
|
@ -193,13 +191,11 @@ public partial class Map
|
|||
private int _currentVersion;
|
||||
private Sector _currentSector;
|
||||
private T _current;
|
||||
|
||||
private HashSet<Serial> _unanchoredSeen;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public MultiBoundsEnumerator(Map map, Rectangle2D bounds, bool makeBoundsInclusive)
|
||||
{
|
||||
_sharedDupes.Clear();
|
||||
|
||||
_map = map;
|
||||
_bounds = bounds;
|
||||
|
||||
|
|
@ -255,14 +251,10 @@ public partial class Map
|
|||
while (++_currentIndex < _currentList.Length)
|
||||
{
|
||||
var item = _currentList[_currentIndex];
|
||||
if (item is T { Deleted: false } o && bounds.Contains(o.Location))
|
||||
if (item is T { Deleted: false } o && bounds.Contains(o.Location) && ShouldYieldFromCurrentSector(map, o))
|
||||
{
|
||||
// Multis can span multiple sectors, so we need to deduplicate
|
||||
if (_sharedDupes.Add(o.Serial))
|
||||
{
|
||||
_current = o;
|
||||
return true;
|
||||
}
|
||||
_current = o;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -290,6 +282,27 @@ public partial class Map
|
|||
}
|
||||
}
|
||||
|
||||
// A multi is listed in every sector it spans, but its location is in exactly one sector, which the bounds
|
||||
// filter guarantees is visited. Yield it from that sector only. A multi missing from its location's sector
|
||||
// (components changed after it was placed) falls back to a per-enumerator set, so nested queries stay independent.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool ShouldYieldFromCurrentSector(Map map, T multi)
|
||||
{
|
||||
var home = map.GetSector(map.Bound(multi.Location));
|
||||
|
||||
if (home == _currentSector)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (home.Multis.Contains(multi))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (_unanchoredSeen ??= []).Add(multi.Serial);
|
||||
}
|
||||
|
||||
public T Current
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
|
|||
|
|
@ -99,7 +99,12 @@ public partial class Map
|
|||
{
|
||||
_tiles = multi.Components.Tiles[offsetX][offsetY];
|
||||
_index = -1;
|
||||
return SetTile();
|
||||
|
||||
// A multi can have no tile at this cell; later multis in the sector still may
|
||||
if (SetTile())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5209,7 +5209,8 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
{
|
||||
state.SendLiftReject(reject);
|
||||
|
||||
if (item.Deleted)
|
||||
// A rejection must not resynchronize state this client was never sent (client-supplied serial).
|
||||
if (item.Deleted || !item.IsSentTo(this))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,10 +56,7 @@ public partial class NetState
|
|||
internal static long _availableMemoryBytes;
|
||||
|
||||
private static Timer.DelayCallTimer _maintenanceTimer;
|
||||
private static long _lastTierCapacityBytes;
|
||||
private static int _lastTierInUse;
|
||||
private static int _lastTierRetainFloor;
|
||||
private static long _lastBaseCapacityBytes;
|
||||
private static SendBufferSweep _lastSweep;
|
||||
|
||||
private static readonly Queue<NetState> _disposed = [];
|
||||
private static readonly TimeSpan ConnectingSocketIdleLimit = TimeSpan.FromMilliseconds(5000); // 5 seconds
|
||||
|
|
@ -238,14 +235,23 @@ public partial class NetState
|
|||
_capRefusals = 0;
|
||||
|
||||
var stats = _socketManager.Maintain();
|
||||
var changed = stats.TierCapacityBytes != _lastTierCapacityBytes ||
|
||||
stats.TierInUse != _lastTierInUse ||
|
||||
stats.TierRetainFloor != _lastTierRetainFloor ||
|
||||
stats.BaseCapacityBytes != _lastBaseCapacityBytes;
|
||||
_lastTierCapacityBytes = stats.TierCapacityBytes;
|
||||
_lastTierInUse = stats.TierInUse;
|
||||
_lastTierRetainFloor = stats.TierRetainFloor;
|
||||
_lastBaseCapacityBytes = stats.BaseCapacityBytes;
|
||||
var changed = stats.TierCapacityBytes != _lastSweep.TierCapacityBytes ||
|
||||
stats.TierInUse != _lastSweep.TierInUse ||
|
||||
stats.TierRetainFloor != _lastSweep.TierRetainFloor ||
|
||||
stats.BaseCapacityBytes != _lastSweep.BaseCapacityBytes;
|
||||
_lastSweep = new SendBufferSweep(
|
||||
true,
|
||||
Core.TickCount,
|
||||
stats.TierCapacityBytes,
|
||||
stats.TierInUse,
|
||||
stats.TierRetainFloor,
|
||||
stats.BuffersReleased,
|
||||
stats.BaseCapacityBytes,
|
||||
stats.BaseBuffersReleased,
|
||||
stats.GrowthRefusals,
|
||||
capRefusals,
|
||||
ceilingRefusals
|
||||
);
|
||||
|
||||
// Quiet unless something moved
|
||||
if (changed || stats.BuffersReleased > 0 || stats.BaseBuffersReleased > 0 || stats.GrowthRefusals > 0 ||
|
||||
|
|
@ -266,6 +272,48 @@ public partial class NetState
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read-only diagnostics snapshot; never rotates the maintenance window. Cold path: allocates.
|
||||
/// </summary>
|
||||
public static NetworkStats GetNetworkStats()
|
||||
{
|
||||
var manager = _socketManager;
|
||||
var tiers = manager == null ? [] : new SendBufferTierUsage[manager.SendBufferTierCount];
|
||||
for (var i = 0; i < tiers.Length; i++)
|
||||
{
|
||||
var tier = manager.GetSendBufferTierStats(i);
|
||||
tiers[i] = new SendBufferTierUsage(tier.BufferSize, tier.Capacity, tier.InUse, tier.RetainFloor);
|
||||
}
|
||||
|
||||
var authenticated = 0;
|
||||
foreach (var ns in _instances)
|
||||
{
|
||||
if (ns.Account != null)
|
||||
{
|
||||
authenticated++;
|
||||
}
|
||||
}
|
||||
|
||||
return new NetworkStats(
|
||||
manager?.ConnectedCount ?? 0,
|
||||
manager?.MaxSockets ?? MaxConnections,
|
||||
authenticated,
|
||||
RecvBufferSize,
|
||||
SendBufferSize,
|
||||
MaxSendBufferSize,
|
||||
InitialRecvBufferSize,
|
||||
InitialSendBufferSize,
|
||||
_sendBufferGrowthBudget,
|
||||
_memoryCeilingPercent,
|
||||
_availableMemoryBytes,
|
||||
tiers,
|
||||
_lastSweep,
|
||||
_throttled.Count + _throttledPending.Count,
|
||||
_flushPending.Count,
|
||||
_pendingDisconnects.Count
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slabs each base pool is divided into, after the transport applies its minimum slab size.
|
||||
/// </summary>
|
||||
|
|
|
|||
91
Projects/Server/Network/NetworkStats.cs
Normal file
91
Projects/Server/Network/NetworkStats.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: NetworkStats.cs *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
namespace Server.Network;
|
||||
|
||||
/// <summary>One send buffer growth tier's pool usage, in buffers of <paramref name="BufferSize"/> bytes.</summary>
|
||||
/// <param name="BufferSize">Physical size of every buffer in this tier.</param>
|
||||
/// <param name="Capacity">Buffers the tier's allocated slabs hold.</param>
|
||||
/// <param name="InUse">Buffers currently handed out.</param>
|
||||
/// <param name="RetainFloor">Peak usage the retention window is still holding capacity for.</param>
|
||||
public readonly record struct SendBufferTierUsage(int BufferSize, int Capacity, int InUse, int RetainFloor);
|
||||
|
||||
/// <summary>
|
||||
/// What the last once-a-minute send buffer maintenance sweep saw. The refusal counts cover the
|
||||
/// minute before that sweep only.
|
||||
/// </summary>
|
||||
/// <param name="Ran">False until the first sweep; every other field is then meaningless.</param>
|
||||
/// <param name="Tick"><see cref="Core.TickCount"/> when the sweep ran; compare by subtraction only.</param>
|
||||
/// <param name="TierCapacityBytes">Slab capacity allocated across the growth tiers.</param>
|
||||
/// <param name="TierInUse">Growth-tier buffers handed out, summed across tiers.</param>
|
||||
/// <param name="TierRetainFloor">Retention floors summed across tiers, in buffers.</param>
|
||||
/// <param name="TierBuffersReleased">Growth-tier buffers the sweep returned to the OS.</param>
|
||||
/// <param name="BaseCapacityBytes">Slab capacity allocated across the base and pre-auth pools.</param>
|
||||
/// <param name="BaseBuffersReleased">Base and pre-auth buffers the sweep returned to the OS.</param>
|
||||
/// <param name="BudgetRefusals">Growth refused because the tiers were at the growth budget.</param>
|
||||
/// <param name="CapRefusals">Growth refused because the buffer was already at the maximum size.</param>
|
||||
/// <param name="CeilingRefusals">Growth refused because the process was above the memory ceiling.</param>
|
||||
public readonly record struct SendBufferSweep(
|
||||
bool Ran,
|
||||
long Tick,
|
||||
long TierCapacityBytes,
|
||||
int TierInUse,
|
||||
int TierRetainFloor,
|
||||
int TierBuffersReleased,
|
||||
long BaseCapacityBytes,
|
||||
int BaseBuffersReleased,
|
||||
int BudgetRefusals,
|
||||
int CapRefusals,
|
||||
int CeilingRefusals
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Read-only snapshot of the network layer for diagnostics. Connection counts and tier usage are
|
||||
/// live; everything under <see cref="LastSweep"/> is up to a minute old. Cold path: allocates.
|
||||
/// </summary>
|
||||
/// <param name="Connected">Sockets the transport currently holds, authenticated or not.</param>
|
||||
/// <param name="MaxConnections">Socket slots the transport was built with.</param>
|
||||
/// <param name="Authenticated">Connections past login, and so holding full-size buffers.</param>
|
||||
/// <param name="RecvBufferSize">Base receive buffer size.</param>
|
||||
/// <param name="SendBufferSize">Base send buffer size.</param>
|
||||
/// <param name="MaxSendBufferSize">Largest size a send buffer can grow to.</param>
|
||||
/// <param name="InitialRecvBufferSize">Pre-auth receive buffer size; 0 when pre-auth buffers are off.</param>
|
||||
/// <param name="InitialSendBufferSize">Pre-auth send buffer size; 0 when pre-auth buffers are off.</param>
|
||||
/// <param name="SendBufferGrowthBudget">Bytes the growth tiers may hold in total.</param>
|
||||
/// <param name="MemoryCeilingPercent">Growth stops once the process exceeds this share of available memory.</param>
|
||||
/// <param name="AvailableMemoryBytes">Memory available to the process as of the last sweep.</param>
|
||||
/// <param name="Tiers">Live usage of each growth tier, smallest first.</param>
|
||||
/// <param name="LastSweep">What the last maintenance sweep saw.</param>
|
||||
/// <param name="Throttled">Connections whose parser is paused on a packet throttle.</param>
|
||||
/// <param name="FlushPending">Connections with unsent bytes queued for the next flush.</param>
|
||||
/// <param name="PendingDisconnects">Connections closing after their final flush.</param>
|
||||
public readonly record struct NetworkStats(
|
||||
int Connected,
|
||||
int MaxConnections,
|
||||
int Authenticated,
|
||||
int RecvBufferSize,
|
||||
int SendBufferSize,
|
||||
int MaxSendBufferSize,
|
||||
int InitialRecvBufferSize,
|
||||
int InitialSendBufferSize,
|
||||
long SendBufferGrowthBudget,
|
||||
int MemoryCeilingPercent,
|
||||
long AvailableMemoryBytes,
|
||||
SendBufferTierUsage[] Tiers,
|
||||
SendBufferSweep LastSweep,
|
||||
int Throttled,
|
||||
int FlushPending,
|
||||
int PendingDisconnects
|
||||
);
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using Server.Buffers;
|
||||
using Server.Logging;
|
||||
|
||||
namespace Server.Network;
|
||||
|
|
@ -166,6 +167,10 @@ public static class OutgoingContainerPackets
|
|||
ns.Send(writer.Span);
|
||||
}
|
||||
|
||||
// 0x3C's length and item-count fields are both 16-bit, so a container's contents can never
|
||||
// legally fill more than this many bytes regardless of how many items it actually holds.
|
||||
private const int MaxContainerContentBytes = ushort.MaxValue;
|
||||
|
||||
public static void SendContainerContent(this NetState ns, Mobile beholder, Item beheld)
|
||||
{
|
||||
if (ns.CannotSendPackets())
|
||||
|
|
@ -175,43 +180,63 @@ public static class OutgoingContainerPackets
|
|||
|
||||
var items = beheld.Items;
|
||||
var count = items.Count;
|
||||
var entrySize = ns.ContainerGridLines ? 20 : 19;
|
||||
|
||||
var writer = new SpanWriter(stackalloc byte[5 + items.Count * (ns.ContainerGridLines ? 20 : 19)]);
|
||||
writer.Write((byte)0x3C); // Packet ID
|
||||
writer.Seek(4, SeekOrigin.Current); // Length & written count
|
||||
var maxEntries = (MaxContainerContentBytes - 5) / entrySize;
|
||||
var entriesToSend = Math.Min(count, maxEntries);
|
||||
var length = 5 + entriesToSend * entrySize;
|
||||
|
||||
var written = 0;
|
||||
// A container holding thousands of items would blow the stack; rent from the pool past a
|
||||
// small, safe threshold instead.
|
||||
byte[] rented = null;
|
||||
var buffer = length <= 1024 ? stackalloc byte[length] : rented = STArrayPool<byte>.Shared.Rent(length);
|
||||
|
||||
for (var i = 0; i < count; ++i)
|
||||
try
|
||||
{
|
||||
var child = items[i];
|
||||
var writer = new SpanWriter(buffer[..length]);
|
||||
writer.Write((byte)0x3C); // Packet ID
|
||||
writer.Seek(4, SeekOrigin.Current); // Length & written count
|
||||
|
||||
if (!child.Deleted && beholder.CanSee(child))
|
||||
var written = 0;
|
||||
|
||||
for (var i = 0; i < count && written < entriesToSend; ++i)
|
||||
{
|
||||
var loc = child.Location;
|
||||
var child = items[i];
|
||||
|
||||
writer.Write(child.Serial);
|
||||
writer.Write((ushort)child.ItemID);
|
||||
writer.Write((byte)0); // signed, itemID offset
|
||||
writer.Write((ushort)Math.Min(child.Amount, ushort.MaxValue));
|
||||
writer.Write((short)loc.X);
|
||||
writer.Write((short)loc.Y);
|
||||
if (ns.ContainerGridLines)
|
||||
if (!child.Deleted && beholder.CanSee(child))
|
||||
{
|
||||
writer.Write((byte)0); // Grid Location?
|
||||
}
|
||||
writer.Write(beheld.Serial);
|
||||
writer.Write((ushort)(child.QuestItem ? Item.QuestItemHue : child.Hue));
|
||||
var loc = child.Location;
|
||||
|
||||
++written;
|
||||
writer.Write(child.Serial);
|
||||
writer.Write((ushort)child.ItemID);
|
||||
writer.Write((byte)0); // signed, itemID offset
|
||||
writer.Write((ushort)Math.Min(child.Amount, ushort.MaxValue));
|
||||
writer.Write((short)loc.X);
|
||||
writer.Write((short)loc.Y);
|
||||
if (ns.ContainerGridLines)
|
||||
{
|
||||
writer.Write((byte)0); // Grid Location?
|
||||
}
|
||||
writer.Write(beheld.Serial);
|
||||
writer.Write((ushort)(child.QuestItem ? Item.QuestItemHue : child.Hue));
|
||||
|
||||
++written;
|
||||
}
|
||||
}
|
||||
|
||||
writer.Seek(1, SeekOrigin.Begin);
|
||||
writer.Write((ushort)writer.BytesWritten);
|
||||
writer.Write((ushort)written);
|
||||
writer.Seek(0, SeekOrigin.End);
|
||||
|
||||
ns.Send(writer.Span);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rented != null)
|
||||
{
|
||||
STArrayPool<byte>.Shared.Return(rented);
|
||||
}
|
||||
}
|
||||
|
||||
writer.Seek(1, SeekOrigin.Begin);
|
||||
writer.Write((ushort)writer.BytesWritten);
|
||||
writer.Write((ushort)written);
|
||||
writer.Seek(0, SeekOrigin.End);
|
||||
|
||||
ns.Send(writer.Span);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@
|
|||
<PackageReference Include="LibDeflate.Bindings" Version="1.0.4" />
|
||||
<PackageReference Include="System.IO.Hashing" Version="10.0.12" />
|
||||
|
||||
<PackageReference Include="ModernUO.Serialization.Annotations" Version="4.1.0" />
|
||||
<PackageReference Include="ModernUO.Serialization.Generator" Version="4.1.0" PrivateAssets="all" />
|
||||
<PackageReference Include="ModernUO.Serialization.Annotations" Version="4.2.0" />
|
||||
<PackageReference Include="ModernUO.Serialization.Generator" Version="4.2.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Migrations/*.v*.json" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue