#W# Confict: Fixed BaseCreature git merge conflict.

This commit is contained in:
WarrentyExpired 2026-10-10 23:17:41 -04:00
commit ad38314ca7
112 changed files with 5397 additions and 616 deletions

View file

@ -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)
{

View file

@ -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()

View file

@ -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)
{

View file

@ -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)
{

View file

@ -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)
{

View file

@ -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)]

View file

@ -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;
}
}
}

View file

@ -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;
}

View file

@ -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>

View 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
);

View file

@ -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);
}
}

View file

@ -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" />