From 641c35258f4a8fa34d706752f9fc1e244f68c6f2 Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Fri, 20 Mar 2026 14:40:08 +0300 Subject: [PATCH 01/30] Change loot context for the !item give command to CashShop --- src/MHServerEmu/Commands/Implementations/ItemCommands.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MHServerEmu/Commands/Implementations/ItemCommands.cs b/src/MHServerEmu/Commands/Implementations/ItemCommands.cs index 13943267..3a2470f6 100644 --- a/src/MHServerEmu/Commands/Implementations/ItemCommands.cs +++ b/src/MHServerEmu/Commands/Implementations/ItemCommands.cs @@ -71,7 +71,7 @@ namespace MHServerEmu.Commands.Implementations LootManager lootGenerator = playerConnection.Game.LootManager; for (int i = 0; i < count; i++) - lootGenerator.GiveItem(itemProtoRef, LootContext.Drop, player); + lootGenerator.GiveItem(itemProtoRef, LootContext.CashShop, player); Logger.Debug($"GiveItem(): {itemProtoRef.GetName()}[{count}] to {player}"); return string.Empty; From 85cd90358623abe4949087e05550d276b9c0733e Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Sat, 21 Mar 2026 23:27:34 +0300 Subject: [PATCH 02/30] Remove null region logging in Player.DiscoverMapPosition() --- src/MHServerEmu.Games/Entities/Player.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/MHServerEmu.Games/Entities/Player.cs b/src/MHServerEmu.Games/Entities/Player.cs index cc6d3341..f31a5f1a 100644 --- a/src/MHServerEmu.Games/Entities/Player.cs +++ b/src/MHServerEmu.Games/Entities/Player.cs @@ -3029,11 +3029,12 @@ namespace MHServerEmu.Games.Entities public bool DiscoverMapPosition(Vector3 position, bool syncWithParty = true) { - var region = CurrentAvatar?.Region; - if (region == null) return Logger.WarnReturn(false, "UpdateMapDiscovery(): region == null"); + Region region = CurrentAvatar?.Region; + if (region == null) + return false; MapDiscoveryData mapDiscoveryData = GetMapDiscoveryDataForEntity(CurrentAvatar); - if (mapDiscoveryData == null) return Logger.WarnReturn(false, "UpdateDiscoveryMap(): mapDiscoveryData == null"); + if (mapDiscoveryData == null) return Logger.WarnReturn(false, "DiscoverMapPosition(): mapDiscoveryData == null"); bool reveal = mapDiscoveryData.RevealPosition(this, position); From 7f3837583d4b47c7b1ae718430f876f30483000f Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Sat, 21 Mar 2026 23:43:12 +0300 Subject: [PATCH 03/30] Disable EventScheduler trigger time logging in Release builds --- src/MHServerEmu.Games/Events/EventScheduler.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/MHServerEmu.Games/Events/EventScheduler.cs b/src/MHServerEmu.Games/Events/EventScheduler.cs index a668c7fe..f5a66975 100644 --- a/src/MHServerEmu.Games/Events/EventScheduler.cs +++ b/src/MHServerEmu.Games/Events/EventScheduler.cs @@ -15,7 +15,10 @@ namespace MHServerEmu.Games.Events private static readonly Logger Logger = LogManager.CreateLogger(); +#if DEBUG private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); +#endif + private readonly ScheduledEventPool _eventPool = new(); private readonly TimeSpan _quantumSize; @@ -195,12 +198,16 @@ namespace MHServerEmu.Games.Events @event.EventGroupNode.Remove(); @event.InvalidatePointers(); +#if DEBUG TimeSpan referenceTime = _stopwatch.Elapsed; @event.OnTriggered(); TimeSpan triggerTime = _stopwatch.Elapsed - referenceTime; if (triggerTime >= _quantumSize) Logger.Warn($"{@event.GetType().Name} took {(_stopwatch.Elapsed - referenceTime).TotalMilliseconds} ms"); +#else + @event.OnTriggered(); +#endif if (++numEvents > MaxEventsPerUpdate) throw new Exception($"Infinite loop detected in EventScheduler."); From 1c75103166b110c3173ab2eb3e4d2d8295347a90 Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Sun, 22 Mar 2026 20:37:27 +0300 Subject: [PATCH 04/30] Optimize allocations when writing float fields in protobuf messages --- .../Network/MessagePackageOut.cs | 11 + src/MHServerEmu.Core/Network/MuxPacket.cs | 13 +- .../Serialization/FastCodedOutputStream.cs | 471 ++++++++++++++++++ .../Serialization/ICodedOutputStreamEx.cs | 18 + 4 files changed, 503 insertions(+), 10 deletions(-) create mode 100644 src/MHServerEmu.Core/Serialization/FastCodedOutputStream.cs create mode 100644 src/MHServerEmu.Core/Serialization/ICodedOutputStreamEx.cs diff --git a/src/MHServerEmu.Core/Network/MessagePackageOut.cs b/src/MHServerEmu.Core/Network/MessagePackageOut.cs index 03854f1c..af6ef0e2 100644 --- a/src/MHServerEmu.Core/Network/MessagePackageOut.cs +++ b/src/MHServerEmu.Core/Network/MessagePackageOut.cs @@ -1,4 +1,5 @@ using Google.ProtocolBuffers; +using MHServerEmu.Core.Serialization; namespace MHServerEmu.Core.Network { @@ -43,5 +44,15 @@ namespace MHServerEmu.Core.Network stream.WriteRawVarint32((uint)Message.SerializedSize); Message.WriteTo(stream); } + + /// + /// Writes this to the provided . + /// + public void WriteTo(ICodedOutputStreamEx stream) + { + stream.WriteRawVarint32(Id); + stream.WriteRawVarint32((uint)Message.SerializedSize); + Message.WriteTo(stream); + } } } diff --git a/src/MHServerEmu.Core/Network/MuxPacket.cs b/src/MHServerEmu.Core/Network/MuxPacket.cs index 993bd537..b9d66750 100644 --- a/src/MHServerEmu.Core/Network/MuxPacket.cs +++ b/src/MHServerEmu.Core/Network/MuxPacket.cs @@ -1,10 +1,9 @@ -using System.Buffers; -using System.Collections; +using System.Collections; using Google.ProtocolBuffers; -using MHServerEmu.Core.Helpers; using MHServerEmu.Core.Logging; using MHServerEmu.Core.Memory; using MHServerEmu.Core.Network.Tcp; +using MHServerEmu.Core.Serialization; namespace MHServerEmu.Core.Network { @@ -17,7 +16,6 @@ namespace MHServerEmu.Core.Network // Packets apparently go as high as 2800+ messages based on logs, so we presize pooled lists to 4096 to fit that and extra. private static readonly ConcurrentPool> MessageListPool = new(4096, static () => new(4096)); - private static readonly ArrayPool BufferPool = ArrayPool.Create(); private readonly List _outboundMessageList = null; @@ -137,15 +135,10 @@ namespace MHServerEmu.Core.Network if (_outboundMessageList.Count == 0) return Logger.WarnReturn(false, "SerializeData(): Data packet contains no messages"); - // Use pooled buffers for coded output streams with reflection hackery, see ProtobufHelper for more info. - byte[] buffer = BufferPool.Rent(4096); + using FastCodedOutputStream cos = FastCodedOutputStream.CreateInstance(stream); - CodedOutputStream cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(stream, buffer); foreach (MessagePackageOut messagePackage in _outboundMessageList) messagePackage.WriteTo(cos); - cos.Flush(); - - BufferPool.Return(buffer); return true; } diff --git a/src/MHServerEmu.Core/Serialization/FastCodedOutputStream.cs b/src/MHServerEmu.Core/Serialization/FastCodedOutputStream.cs new file mode 100644 index 00000000..37cfd3d9 --- /dev/null +++ b/src/MHServerEmu.Core/Serialization/FastCodedOutputStream.cs @@ -0,0 +1,471 @@ +using System.Buffers; +using System.Collections; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Google.ProtocolBuffers; +using Google.ProtocolBuffers.Descriptors; +using MHServerEmu.Core.Helpers; + +namespace MHServerEmu.Core.Serialization +{ + /// + /// A more memory efficient version of . + /// + public sealed class FastCodedOutputStream : ICodedOutputStreamEx, IDisposable + { + private const int DefaultBufferSize = 4096; + + private static readonly ConcurrentBag InstancePool = new(); + private static readonly ArrayPool BufferPool = ArrayPool.Create(); + + private readonly byte[] _floatBuffer = new byte[sizeof(float)]; + + private byte[] _buffer; + private CodedOutputStream _cos; + + private FastCodedOutputStream() { } + + private void Initialize(Stream stream, int bufferSize) + { + // Use pooled buffers for coded output streams with reflection hackery, see ProtobufHelper for more info. + _buffer = BufferPool.Rent(bufferSize); + _cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(stream, _buffer); + } + + public static FastCodedOutputStream CreateInstance(Stream stream, int bufferSize = DefaultBufferSize) + { + if (InstancePool.TryTake(out FastCodedOutputStream cos) == false) + cos = new(); + + cos.Initialize(stream, bufferSize); + return cos; + } + + #region IDisposable + + public void Dispose() + { + if (_cos != null) + { + _cos.Flush(); + _cos = null; + } + + if (_buffer != null) + { + BufferPool.Return(_buffer); + _buffer = null; + } + + InstancePool.Add(this); + } + + #endregion + + #region ICodedOutputStream + + // For most of this we just pass everything to the default implementation. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Flush() + { + _cos.Flush(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteArray(FieldType fieldType, int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteArray(fieldType, fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBool(int fieldNumber, string fieldName, bool value) + { + _cos.WriteBool(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBoolArray(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteBoolArray(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBytes(int fieldNumber, string fieldName, ByteString value) + { + _cos.WriteBytes(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBytesArray(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteBytesArray(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteDouble(int fieldNumber, string fieldName, double value) + { + _cos.WriteDouble(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteDoubleArray(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteDoubleArray(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteEnum(int fieldNumber, string fieldName, int value, object rawValue) + { + _cos.WriteEnum(fieldNumber, fieldName, value, rawValue); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteEnumArray(int fieldNumber, string fieldName, IEnumerable list) where T : struct, IComparable, IFormattable + { + _cos.WriteEnumArray(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteField(FieldType fieldType, int fieldNumber, string fieldName, object value) + { + _cos.WriteField(fieldType, fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteFixed32(int fieldNumber, string fieldName, uint value) + { + _cos.WriteFixed32(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteFixed32Array(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteFixed32Array(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteFixed64(int fieldNumber, string fieldName, ulong value) + { + _cos.WriteFixed64(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteFixed64Array(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteFixed64Array(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteFloat(int fieldNumber, string fieldName, float value) + { + //_cos.WriteFloat(fieldNumber, fieldName, value); + + _cos.WriteTag(fieldNumber, WireFormat.WireType.Fixed32); + + MemoryMarshal.Cast(_floatBuffer)[0] = value; + _cos.WriteRawBytes(_floatBuffer, 0, 4); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteFloatArray(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteFloatArray(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteGroup(int fieldNumber, string fieldName, IMessageLite value) + { + _cos.WriteGroup(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteGroupArray(int fieldNumber, string fieldName, IEnumerable list) where T : IMessageLite + { + _cos.WriteGroupArray(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteInt32(int fieldNumber, string fieldName, int value) + { + _cos.WriteInt32(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteInt32Array(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteInt32Array(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteInt64(int fieldNumber, string fieldName, long value) + { + _cos.WriteInt64(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteInt64Array(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteInt64Array(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteMessage(int fieldNumber, string fieldName, IMessageLite value) + { + _cos.WriteMessage(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteMessageArray(int fieldNumber, string fieldName, IEnumerable list) where T : IMessageLite + { + _cos.WriteMessageArray(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteMessageEnd() + { + _cos.Flush(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteMessageSetExtension(int fieldNumber, string fieldName, IMessageLite value) + { + _cos.WriteMessageSetExtension(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteMessageSetExtension(int fieldNumber, string fieldName, ByteString value) + { + _cos.WriteMessageSetExtension(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteMessageStart() + { + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedArray(FieldType fieldType, int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WritePackedArray(fieldType, fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedBoolArray(int fieldNumber, string fieldName, int calculatedSize, IEnumerable list) + { + _cos.WritePackedBoolArray(fieldNumber, fieldName, calculatedSize, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedDoubleArray(int fieldNumber, string fieldName, int calculatedSize, IEnumerable list) + { + _cos.WritePackedDoubleArray(fieldNumber, fieldName, calculatedSize, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedEnumArray(int fieldNumber, string fieldName, int calculatedSize, IEnumerable list) where T : struct, IComparable, IFormattable + { + _cos.WritePackedEnumArray(fieldNumber, fieldName, calculatedSize, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedFixed32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable list) + { + _cos.WritePackedFixed32Array(fieldNumber, fieldName, calculatedSize, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedFixed64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable list) + { + _cos.WritePackedFixed64Array(fieldNumber, fieldName, calculatedSize, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedFloatArray(int fieldNumber, string fieldName, int calculatedSize, IEnumerable list) + { + _cos.WritePackedFloatArray(fieldNumber, fieldName, calculatedSize, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedInt32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable list) + { + _cos.WritePackedInt32Array(fieldNumber, fieldName, calculatedSize, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedInt64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable list) + { + _cos.WritePackedInt64Array(fieldNumber, fieldName, calculatedSize, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedSFixed32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable list) + { + _cos.WritePackedSFixed32Array(fieldNumber, fieldName, calculatedSize, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedSFixed64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable list) + { + _cos.WritePackedSFixed64Array(fieldNumber, fieldName, calculatedSize, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedSInt32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable list) + { + _cos.WritePackedSInt32Array(fieldNumber, fieldName, calculatedSize, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedSInt64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable list) + { + _cos.WritePackedSInt64Array(fieldNumber, fieldName, calculatedSize, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedUInt32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable list) + { + _cos.WritePackedUInt32Array(fieldNumber, fieldName, calculatedSize, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePackedUInt64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable list) + { + _cos.WritePackedUInt64Array(fieldNumber, fieldName, calculatedSize, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteSFixed32(int fieldNumber, string fieldName, int value) + { + _cos.WriteSFixed32(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteSFixed32Array(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteSFixed32Array(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteSFixed64(int fieldNumber, string fieldName, long value) + { + _cos.WriteSFixed64(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteSFixed64Array(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteSFixed64Array(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteSInt32(int fieldNumber, string fieldName, int value) + { + _cos.WriteSInt32(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteSInt32Array(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteSInt32Array(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteSInt64(int fieldNumber, string fieldName, long value) + { + _cos.WriteSInt64(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteSInt64Array(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteSInt64Array(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteString(int fieldNumber, string fieldName, string value) + { + _cos.WriteString(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteStringArray(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteStringArray(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteUInt32(int fieldNumber, string fieldName, uint value) + { + _cos.WriteUInt32(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteUInt32Array(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteUInt32Array(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteUInt64(int fieldNumber, string fieldName, ulong value) + { + _cos.WriteUInt64(fieldNumber, fieldName, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteUInt64Array(int fieldNumber, string fieldName, IEnumerable list) + { + _cos.WriteUInt64Array(fieldNumber, fieldName, list); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteUnknownBytes(int fieldNumber, ByteString value) + { + _cos.WriteUnknownBytes(fieldNumber, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteUnknownField(int fieldNumber, WireFormat.WireType wireType, ulong value) + { + _cos.WriteUnknownField(fieldNumber, wireType, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Obsolete] + public void WriteUnknownGroup(int fieldNumber, IMessageLite value) + { + _cos.WriteUnknownGroup(fieldNumber, value); + } + + #endregion + + #region ICodedOutputStreamEx + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteRawVarint32(uint value) + { + _cos.WriteRawVarint32(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteRawVarint64(ulong value) + { + _cos.WriteRawVarint64(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteRawByte(byte value) + { + _cos.WriteRawByte(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteRawBytes(byte[] value) + { + _cos.WriteRawBytes(value); + } + + #endregion + } +} diff --git a/src/MHServerEmu.Core/Serialization/ICodedOutputStreamEx.cs b/src/MHServerEmu.Core/Serialization/ICodedOutputStreamEx.cs new file mode 100644 index 00000000..0bd1577a --- /dev/null +++ b/src/MHServerEmu.Core/Serialization/ICodedOutputStreamEx.cs @@ -0,0 +1,18 @@ +using Google.ProtocolBuffers; + +namespace MHServerEmu.Core.Serialization +{ + /// + /// Extended version of that exposes additional low level writing functionality. + /// + public interface ICodedOutputStreamEx : ICodedOutputStream + { + void WriteRawVarint32(uint value); + + void WriteRawVarint64(ulong value); + + void WriteRawByte(byte value); + + void WriteRawBytes(byte[] value); + } +} From fc83aa151b6f9dc959bae3f3afa1dd87145de185 Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Sun, 22 Mar 2026 20:46:57 +0300 Subject: [PATCH 05/30] Increase initial size for Archive.SharedAutoBuffer --- src/MHServerEmu.Core/Serialization/Archive.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MHServerEmu.Core/Serialization/Archive.cs b/src/MHServerEmu.Core/Serialization/Archive.cs index 2048e917..3e71cddb 100644 --- a/src/MHServerEmu.Core/Serialization/Archive.cs +++ b/src/MHServerEmu.Core/Serialization/Archive.cs @@ -173,7 +173,7 @@ namespace MHServerEmu.Core.Serialization { ReadBuffer = new byte[4096]; WriteBuffer = new byte[32]; // We flush after every value, so we can use very small buffer sizes for output (default is 4096). - SharedAutoBuffer = new(1024); + SharedAutoBuffer = new(65536); } } From be411b2684284c8cc0b0245b89c302215a482d7b Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Sun, 22 Mar 2026 20:58:21 +0300 Subject: [PATCH 06/30] Optimize array allocations when saving player entity blobs --- src/MHServerEmu.Games/Network/PlayerConnection.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/MHServerEmu.Games/Network/PlayerConnection.cs b/src/MHServerEmu.Games/Network/PlayerConnection.cs index 35a5b9e1..68ca67e2 100644 --- a/src/MHServerEmu.Games/Network/PlayerConnection.cs +++ b/src/MHServerEmu.Games/Network/PlayerConnection.cs @@ -259,9 +259,20 @@ namespace MHServerEmu.Games.Network using (Archive archive = new(ArchiveSerializeType.Database)) { + DBPlayer dbPlayer = _dbAccount.Player; + Span oldArchiveData = dbPlayer.ArchiveData ?? Span.Empty; + // NOTE: Use Transfer() and NOT Player.Serialize() to make sure we pack the size of the player Serializer.Transfer(archive, Player); - _dbAccount.Player.ArchiveData = archive.AccessAutoBuffer().ToArray(); + Span newArchiveData = archive.AsSpan(); + + if (newArchiveData.SequenceEqual(oldArchiveData) == false) + { + if (newArchiveData.Length == oldArchiveData.Length) + newArchiveData.CopyTo(oldArchiveData); + else + dbPlayer.ArchiveData = newArchiveData.ToArray(); + } } // Save last town as a separate database field to be able to access it without deserializing the player entity From 03839a2699c0e5634d9fcc4ba633d229682906b8 Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Mon, 23 Mar 2026 00:19:22 +0300 Subject: [PATCH 07/30] Optimize InvasiveListNode --- .../Collections/InvasiveList.cs | 67 +++++++------------ src/MHServerEmu.Games/Entities/Entity.cs | 12 +++- .../Entities/EntityManager.cs | 2 +- .../Entities/Physics/ForceSystem.cs | 17 ++--- src/MHServerEmu.Games/Navi/NaviTriangle.cs | 6 +- 5 files changed, 41 insertions(+), 63 deletions(-) diff --git a/src/MHServerEmu.Core/Collections/InvasiveList.cs b/src/MHServerEmu.Core/Collections/InvasiveList.cs index a2ab377a..3daa6004 100644 --- a/src/MHServerEmu.Core/Collections/InvasiveList.cs +++ b/src/MHServerEmu.Core/Collections/InvasiveList.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Runtime.CompilerServices; namespace MHServerEmu.Core.Collections { @@ -51,8 +52,8 @@ namespace MHServerEmu.Core.Collections { if (element == null || Contains(element) == false) return; - var node = GetInvasiveListNode(element, Id); - if (node == null) return; + ref var node = ref GetInvasiveListNode(element, Id); + if (Unsafe.IsNullRef(ref node)) return; for (int i = 0; i < _numIterators; i++) { @@ -68,16 +69,16 @@ namespace MHServerEmu.Core.Collections if (node.Next != null) { T nextElement = node.Next; - var nextNode = GetInvasiveListNode(nextElement, Id); - if (nextNode != null) + ref var nextNode = ref GetInvasiveListNode(nextElement, Id); + if (Unsafe.IsNullRef(ref nextNode) == false) nextNode.Prev = node.Prev; } if (node.Prev != null) { T prevElement = node.Prev; - var prevNode = GetInvasiveListNode(prevElement, Id); - if (prevNode != null) + ref var prevNode = ref GetInvasiveListNode(prevElement, Id); + if (Unsafe.IsNullRef(ref prevNode) == false) prevNode.Next = node.Next; } @@ -93,11 +94,11 @@ namespace MHServerEmu.Core.Collections if (oldElement == null || Contains(oldElement) == false) return; if (element == null || Contains(element)) return; - var node = GetInvasiveListNode(element, Id); - if (node == null) return; + ref var node = ref GetInvasiveListNode(element, Id); + if (Unsafe.IsNullRef(ref node)) return; - var oldNode = GetInvasiveListNode(oldElement, Id); - if (oldNode == null) return; + ref var oldNode = ref GetInvasiveListNode(oldElement, Id); + if (Unsafe.IsNullRef(ref oldNode)) return; var oldPrev = oldNode.Prev; oldNode.Prev = element; @@ -106,8 +107,8 @@ namespace MHServerEmu.Core.Collections if (oldPrev != null) { - var oldPrevNode = GetInvasiveListNode(oldPrev, Id); - if (oldPrevNode == null) return; + ref var oldPrevNode = ref GetInvasiveListNode(oldPrev, Id); + if (Unsafe.IsNullRef(ref oldPrevNode)) return; oldPrevNode.Next = element; } else @@ -120,14 +121,14 @@ namespace MHServerEmu.Core.Collections { if (element == null || Contains(element)) return; - var node = GetInvasiveListNode(element, Id); - if (node == null) return; + ref var node = ref GetInvasiveListNode(element, Id); + if (Unsafe.IsNullRef(ref node)) return; node.Prev = Tail; if (Tail != null) { - var tailNode = GetInvasiveListNode(Tail, Id); - if (tailNode == null) return; + ref var tailNode = ref GetInvasiveListNode(Tail, Id); + if (Unsafe.IsNullRef(ref tailNode)) return; tailNode.Next = element; } else @@ -137,13 +138,13 @@ namespace MHServerEmu.Core.Collections Count++; } - public virtual InvasiveListNode GetInvasiveListNode(T element, int listId) => null; + public virtual ref InvasiveListNode GetInvasiveListNode(T element, int listId) => ref Unsafe.NullRef>(); public bool Contains(T element) { if (element == null) return false; - var node = GetInvasiveListNode(element, Id); - if (node == null) return false; + ref var node = ref GetInvasiveListNode(element, Id); + if (Unsafe.IsNullRef(ref node)) return false; return node.Next != null || node.Prev != null || element.Equals(Head); } @@ -203,33 +204,11 @@ namespace MHServerEmu.Core.Collections } - public class InvasiveListNode + public struct InvasiveListNode { - public T Next { get; set; } - public T Prev { get; set; } + public T Next; + public T Prev; public void Clear() => Next = Prev = default; } - - public class InvasiveListNodeCollection - { - private readonly InvasiveListNode[] _nodes; - private int _numLists; - - public InvasiveListNodeCollection(int numLists) - { - _numLists = numLists; - _nodes = new InvasiveListNode[_numLists]; - for (int i = 0; i < _numLists; i++) - _nodes[i] = new(); - } - - public InvasiveListNode GetInvasiveListNode(int listIndex) - { - if (listIndex >= 0 && listIndex < _numLists) - return _nodes[listIndex]; - else - return null; - } - } } diff --git a/src/MHServerEmu.Games/Entities/Entity.cs b/src/MHServerEmu.Games/Entities/Entity.cs index 4b5a3972..c92a5fc3 100644 --- a/src/MHServerEmu.Games/Entities/Entity.cs +++ b/src/MHServerEmu.Games/Entities/Entity.cs @@ -106,7 +106,7 @@ namespace MHServerEmu.Games.Entities private static readonly Logger Logger = LogManager.CreateLogger(); - private readonly InvasiveListNodeCollection _entityListNodes = new(3); + private InlineArray3> _entityListNodes; private readonly EventGroup _pendingEvents = new(); @@ -1307,9 +1307,15 @@ namespace MHServerEmu.Games.Entities }; } - public InvasiveListNode GetInvasiveListNode(int listId) + public ref InvasiveListNode GetInvasiveListNode(int listId) { - return _entityListNodes.GetInvasiveListNode(listId); + // InvasiveListNodeCollection is inlined into this function because of CS8170. + const int NumLists = 3; + + if (listId >= 0 && listId < NumLists) + return ref _entityListNodes[listId]; + else + return ref Unsafe.NullRef>(); } #endregion diff --git a/src/MHServerEmu.Games/Entities/EntityManager.cs b/src/MHServerEmu.Games/Entities/EntityManager.cs index ab710143..919dd8d3 100644 --- a/src/MHServerEmu.Games/Entities/EntityManager.cs +++ b/src/MHServerEmu.Games/Entities/EntityManager.cs @@ -31,7 +31,7 @@ namespace MHServerEmu.Games.Entities public class EntityInvasiveCollection : InvasiveList { public EntityInvasiveCollection(EntityCollection collectionType, int maxIterators = 8) : base(maxIterators, (int)collectionType) { } - public override InvasiveListNode GetInvasiveListNode(Entity element, int listId) => element.GetInvasiveListNode(listId); + public override ref InvasiveListNode GetInvasiveListNode(Entity element, int listId) => ref element.GetInvasiveListNode(listId); } public readonly struct DestroyEntityEvent(Entity entity) : IGameEventData diff --git a/src/MHServerEmu.Games/Entities/Physics/ForceSystem.cs b/src/MHServerEmu.Games/Entities/Physics/ForceSystem.cs index 306f71be..c08656e4 100644 --- a/src/MHServerEmu.Games/Entities/Physics/ForceSystem.cs +++ b/src/MHServerEmu.Games/Entities/Physics/ForceSystem.cs @@ -18,28 +18,21 @@ namespace MHServerEmu.Games.Entities.Physics public class ForceSystemMemberList : InvasiveList { public ForceSystemMemberList(int maxIterators = 1) : base(maxIterators) { } - public override InvasiveListNode GetInvasiveListNode(ForceSystemMember element, int listId) => element.InvasiveListNode; + public override ref InvasiveListNode GetInvasiveListNode(ForceSystemMember element, int listId) => ref element.InvasiveListNode; } public class ForceSystemMember { + private InvasiveListNode _invasiveListNode; + public ulong EntityId { get; set; } public Vector3 Position { get; set; } public Vector3 Direction { get; set; } public float Time { get; set; } public float Speed { get; set; } public float Acceleration { get; set; } - public InvasiveListNode InvasiveListNode { get; private set; } + public ref InvasiveListNode InvasiveListNode { get => ref _invasiveListNode; } - public ForceSystemMember() - { - EntityId = 0; - Position = Vector3.Zero; - Direction = Vector3.Zero; - Time = 0.0f; - Speed = 0.0f; - Acceleration = 0.0f; - InvasiveListNode = new(); - } + public ForceSystemMember() { } } } diff --git a/src/MHServerEmu.Games/Navi/NaviTriangle.cs b/src/MHServerEmu.Games/Navi/NaviTriangle.cs index 69a5f1d1..fc571940 100644 --- a/src/MHServerEmu.Games/Navi/NaviTriangle.cs +++ b/src/MHServerEmu.Games/Navi/NaviTriangle.cs @@ -14,18 +14,19 @@ namespace MHServerEmu.Games.Navi public class TriangleList : InvasiveList { public TriangleList(int maxIterators = 1) : base(maxIterators) { } - public override InvasiveListNode GetInvasiveListNode(NaviTriangle element, int listId) => element.InvasiveListNode; + public override ref InvasiveListNode GetInvasiveListNode(NaviTriangle element, int listId) => ref element.InvasiveListNode; } public class NaviTriangle { private InlineArray3 _edges; + private InvasiveListNode _invasiveListNode; public ref InlineArray3 Edges { get => ref _edges; } public byte EdgeSideFlags { get; private set; } public NaviTriangleFlags Flags { get; private set; } public PathFlags PathingFlags { get; set; } - public InvasiveListNode InvasiveListNode { get; private set; } + public ref InvasiveListNode InvasiveListNode { get => ref _invasiveListNode; } public ContentFlagCounts ContentFlagCounts; // ContentFlagCounts needs to be a field for Clear() calls @@ -34,7 +35,6 @@ namespace MHServerEmu.Games.Navi Edges[0] = e0; Edges[1] = e1; Edges[2] = e2; - InvasiveListNode = new(); UpdateEdgeSideFlags(); Attach(); } From a934f07f3a3003829a6e075d218d0d77b7d4f8f1 Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Mon, 23 Mar 2026 13:39:19 +0300 Subject: [PATCH 08/30] Overhaul InvasiveList iteration --- .../Collections/InvasiveList.cs | 99 +++++++++---------- .../Entities/EntityManager.cs | 6 +- .../Entities/Physics/ForceSystem.cs | 2 +- .../Entities/Physics/PhysicsManager.cs | 4 +- src/MHServerEmu.Games/Navi/NaviCdt.cs | 16 +-- src/MHServerEmu.Games/Navi/NaviMesh.cs | 4 +- src/MHServerEmu.Games/Navi/NaviTriangle.cs | 2 +- 7 files changed, 66 insertions(+), 67 deletions(-) diff --git a/src/MHServerEmu.Core/Collections/InvasiveList.cs b/src/MHServerEmu.Core/Collections/InvasiveList.cs index 3daa6004..0ab1ec1f 100644 --- a/src/MHServerEmu.Core/Collections/InvasiveList.cs +++ b/src/MHServerEmu.Core/Collections/InvasiveList.cs @@ -5,49 +5,28 @@ namespace MHServerEmu.Core.Collections { public class InvasiveList { + private readonly Iterator[] _iterators; + + private int _numIterators; + public int Id { get; private set; } - public T Head { get; set; } + public T Head { get; private set; } public T Tail { get; private set; } public int Count { get; private set; } - private Iterator[] _iterators; - private int _numIterators; - private int _maxIterators; + public bool IsEmpty { get => Head == null; } - public InvasiveList(int maxIterators) + public InvasiveList(int maxIterators, int id = 0) { - _maxIterators = maxIterators; - _iterators = new Iterator[_maxIterators]; - } - - public InvasiveList(int maxIterators, int id) - { - _maxIterators = maxIterators; - _iterators = new Iterator[_maxIterators]; + _iterators = new Iterator[maxIterators]; Id = id; } - public IEnumerable Iterate() + public IEnumerator GetEnumerator() { - var iterator = new Iterator(this); - - try - { - while (iterator.End() == false) - { - var element = iterator.Current; - iterator.MoveNext(); - yield return element; - } - } - finally - { - UnregisterIterator(iterator); - } + return new Iterator(this); } - public bool IsEmpty() => Head == null; - public void Remove(T element) { if (element == null || Contains(element) == false) return; @@ -138,7 +117,10 @@ namespace MHServerEmu.Core.Collections Count++; } - public virtual ref InvasiveListNode GetInvasiveListNode(T element, int listId) => ref Unsafe.NullRef>(); + public virtual ref InvasiveListNode GetInvasiveListNode(T element, int listId) + { + return ref Unsafe.NullRef>(); + } public bool Contains(T element) { @@ -150,8 +132,8 @@ namespace MHServerEmu.Core.Collections private void RegisterIterator(Iterator iterator) { - if (_numIterators >= _maxIterators) - throw new InvalidOperationException($"Too many iterators '{_maxIterators}' for invasive list"); + if (_numIterators >= _iterators.Length) + throw new InvalidOperationException($"Too many iterators '{_iterators.Length}' for invasive list"); _iterators[_numIterators++] = iterator; } @@ -172,36 +154,53 @@ namespace MHServerEmu.Core.Collections throw new InvalidOperationException("Iterator not found in iterator collection of invasive list!"); } - public class Iterator : IEnumerator + public sealed class Iterator : IEnumerator { - private InvasiveList _list; - public bool SkipNext { get; set; } + private readonly InvasiveList _list; + + private bool _start = true; + + public T Current { get; private set; } + object IEnumerator.Current { get => Current; } + + public bool SkipNext { get; set; } = false; public Iterator(InvasiveList invasiveList) { _list = invasiveList; - Current = _list.Head; - SkipNext = false; _list.RegisterIterator(this); } - public T Current { get; private set; } - object IEnumerator.Current => Current; - public void Dispose() { } - public void Reset() { } + public void Dispose() + { + _list.UnregisterIterator(this); + } + + public void Reset() + { + _start = true; + Current = default; + SkipNext = false; + } public bool MoveNext() { - if (SkipNext) SkipNext = false; - else if (Current != null) - Current = _list.GetInvasiveListNode(Current, _list.Id).Next; + if (_start) + { + Current = _list.Head; + _start = false; + } + else + { + if (SkipNext) + SkipNext = false; + else if (Current != null) + Current = _list.GetInvasiveListNode(Current, _list.Id).Next; + } - return true; + return Current != null; } - - public bool End() => Current == null; } - } public struct InvasiveListNode diff --git a/src/MHServerEmu.Games/Entities/EntityManager.cs b/src/MHServerEmu.Games/Entities/EntityManager.cs index 919dd8d3..c8096951 100644 --- a/src/MHServerEmu.Games/Entities/EntityManager.cs +++ b/src/MHServerEmu.Games/Entities/EntityManager.cs @@ -28,7 +28,7 @@ namespace MHServerEmu.Games.Entities All = 3, } - public class EntityInvasiveCollection : InvasiveList + public sealed class EntityInvasiveCollection : InvasiveList { public EntityInvasiveCollection(EntityCollection collectionType, int maxIterators = 8) : base(maxIterators, (int)collectionType) { } public override ref InvasiveListNode GetInvasiveListNode(Entity element, int listId) => ref element.GetInvasiveListNode(listId); @@ -449,7 +449,7 @@ namespace MHServerEmu.Games.Entities public void LocomoteEntities() { - foreach (var entity in LocomotionEntities.Iterate()) + foreach (var entity in LocomotionEntities) if (entity is WorldEntity worldEntity) worldEntity?.Locomotor.Locomote(); } @@ -586,7 +586,7 @@ namespace MHServerEmu.Games.Entities IsAIEnabled = enable; if (enable) - foreach (var entity in SimulatedEntities.Iterate()) + foreach (var entity in SimulatedEntities) if (entity is Agent agent) agent.AIController?.SetIsEnabled(true); foreach (var entity in _entityDict.Values) diff --git a/src/MHServerEmu.Games/Entities/Physics/ForceSystem.cs b/src/MHServerEmu.Games/Entities/Physics/ForceSystem.cs index c08656e4..05a8418e 100644 --- a/src/MHServerEmu.Games/Entities/Physics/ForceSystem.cs +++ b/src/MHServerEmu.Games/Entities/Physics/ForceSystem.cs @@ -15,7 +15,7 @@ namespace MHServerEmu.Games.Entities.Physics } } - public class ForceSystemMemberList : InvasiveList + public sealed class ForceSystemMemberList : InvasiveList { public ForceSystemMemberList(int maxIterators = 1) : base(maxIterators) { } public override ref InvasiveListNode GetInvasiveListNode(ForceSystemMember element, int listId) => ref element.InvasiveListNode; diff --git a/src/MHServerEmu.Games/Entities/Physics/PhysicsManager.cs b/src/MHServerEmu.Games/Entities/Physics/PhysicsManager.cs index 39c39ded..209782ce 100644 --- a/src/MHServerEmu.Games/Entities/Physics/PhysicsManager.cs +++ b/src/MHServerEmu.Games/Entities/Physics/PhysicsManager.cs @@ -196,7 +196,7 @@ namespace MHServerEmu.Games.Entities.Physics EntityManager entityManager = _game.EntityManager; - foreach (var member in forceSystem.Members.Iterate()) + foreach (var member in forceSystem.Members) { if (member == null) continue; bool active = false; @@ -684,7 +684,7 @@ namespace MHServerEmu.Games.Entities.Physics float distanceSq = Vector3.DistanceSquared(epicenter, member.Position); var pendingMembers = pendingForce.Members; - foreach (var pendingMember in pendingMembers.Iterate()) + foreach (var pendingMember in pendingMembers) if (distanceSq > Vector3.DistanceSquared(epicenter, pendingMember.Position)) { pendingForce.Members.InsertBefore(member, pendingMember); diff --git a/src/MHServerEmu.Games/Navi/NaviCdt.cs b/src/MHServerEmu.Games/Navi/NaviCdt.cs index e810ddc0..8ffdc20a 100644 --- a/src/MHServerEmu.Games/Navi/NaviCdt.cs +++ b/src/MHServerEmu.Games/Navi/NaviCdt.cs @@ -55,7 +55,7 @@ namespace MHServerEmu.Games.Navi public void Release() { - foreach (var triangle in TriangleList.Iterate()) + foreach (var triangle in TriangleList) RemoveTriangle(triangle); _lastTriangle = null; @@ -126,7 +126,7 @@ namespace MHServerEmu.Games.Navi using var collinearEdgesHandle = ListPool.Instance.Get(out List collinearEdges); using NaviSerialCheck naviSerialCheck = new(this); - foreach (var triangle in TriangleList.Iterate()) + foreach (var triangle in TriangleList) foreach (var edge in triangle.Edges) { if (edge.Triangles[0] == null || edge.Triangles[1] == null) continue; @@ -925,7 +925,7 @@ namespace MHServerEmu.Games.Navi { StringBuilder hashes = new(); int id = 0; - foreach (var triangle in TriangleList.Iterate()) + foreach (var triangle in TriangleList) hashes.AppendLine($"[{id++}] {triangle.ToHashString()}"); FileHelper.SaveTextFileToRoot(fileName, hashes.ToString()); } @@ -934,7 +934,7 @@ namespace MHServerEmu.Games.Navi { StringBuilder hashes = new(); int id = 0; - foreach (var triangle in TriangleList.Iterate()) + foreach (var triangle in TriangleList) hashes.AppendLine($"[{id++}] {triangle.ToHashString2()}"); FileHelper.SaveTextFileToRoot(fileName, hashes.ToString()); } @@ -943,12 +943,12 @@ namespace MHServerEmu.Games.Navi { NaviSvgHelper svg = new(this); Stack influences = new(); - foreach (var triangle in TriangleList.Iterate()) + foreach (var triangle in TriangleList) foreach (var edge in triangle.Edges) foreach (var point in edge.Points) if (point.InfluenceRadius > 0 && influences.Contains(point) == false) influences.Push(point); - foreach (var triangle in TriangleList.Iterate()) + foreach (var triangle in TriangleList) svg.AddTriangle(triangle); foreach (var point in influences) svg.AddCircle(point.Pos, point.InfluenceRadius); @@ -962,7 +962,7 @@ namespace MHServerEmu.Games.Navi // Vertices int newId = 1; - foreach (var triangle in TriangleList.Iterate()) + foreach (var triangle in TriangleList) if (triangle.PathingFlags.HasFlag(filterFlags)) foreach (var edge in triangle.Edges) foreach (var point in edge.Points) @@ -975,7 +975,7 @@ namespace MHServerEmu.Games.Navi } // Faces - foreach (var triangle in TriangleList.Iterate()) + foreach (var triangle in TriangleList) if (triangle.PathingFlags.HasFlag(filterFlags)) { var p0 = triangle.PointCW(0); diff --git a/src/MHServerEmu.Games/Navi/NaviMesh.cs b/src/MHServerEmu.Games/Navi/NaviMesh.cs index 47f4390d..248f4518 100644 --- a/src/MHServerEmu.Games/Navi/NaviMesh.cs +++ b/src/MHServerEmu.Games/Navi/NaviMesh.cs @@ -345,7 +345,7 @@ namespace MHServerEmu.Games.Navi private void ReverseMarkupMesh() { - foreach (var triangle in TriangleList.Iterate()) + foreach (var triangle in TriangleList) for (int edgeIndex = 0; edgeIndex < 3; edgeIndex++) { NaviEdge edge = triangle.Edges[edgeIndex]; @@ -367,7 +367,7 @@ namespace MHServerEmu.Games.Navi private void ClearMarkup() { - foreach (var triangle in TriangleList.Iterate()) + foreach (var triangle in TriangleList) { triangle.ClearFlag(NaviTriangleFlags.Markup); triangle.PathingFlags = PathFlags.None; diff --git a/src/MHServerEmu.Games/Navi/NaviTriangle.cs b/src/MHServerEmu.Games/Navi/NaviTriangle.cs index fc571940..17d2c516 100644 --- a/src/MHServerEmu.Games/Navi/NaviTriangle.cs +++ b/src/MHServerEmu.Games/Navi/NaviTriangle.cs @@ -11,7 +11,7 @@ namespace MHServerEmu.Games.Navi Markup = 1 << 1, } - public class TriangleList : InvasiveList + public sealed class TriangleList : InvasiveList { public TriangleList(int maxIterators = 1) : base(maxIterators) { } public override ref InvasiveListNode GetInvasiveListNode(NaviTriangle element, int listId) => ref element.InvasiveListNode; From c62be1041335683df6b0e9b33a48d6e0963e6a3a Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Mon, 23 Mar 2026 13:50:27 +0300 Subject: [PATCH 09/30] Fix index for EntityCollection.All --- src/MHServerEmu.Games/Entities/EntityManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MHServerEmu.Games/Entities/EntityManager.cs b/src/MHServerEmu.Games/Entities/EntityManager.cs index c8096951..d16b5aa2 100644 --- a/src/MHServerEmu.Games/Entities/EntityManager.cs +++ b/src/MHServerEmu.Games/Entities/EntityManager.cs @@ -25,7 +25,7 @@ namespace MHServerEmu.Games.Entities { Simulated = 0, Locomotion = 1, - All = 3, + All = 2, // unused? } public sealed class EntityInvasiveCollection : InvasiveList From 08a875b69f6661887db5e635e035f25f98c65d53 Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Mon, 23 Mar 2026 14:23:11 +0300 Subject: [PATCH 10/30] Implement InvasiveList iterator pooling --- .../Collections/InvasiveList.cs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/MHServerEmu.Core/Collections/InvasiveList.cs b/src/MHServerEmu.Core/Collections/InvasiveList.cs index 0ab1ec1f..7ff0a4a4 100644 --- a/src/MHServerEmu.Core/Collections/InvasiveList.cs +++ b/src/MHServerEmu.Core/Collections/InvasiveList.cs @@ -7,6 +7,9 @@ namespace MHServerEmu.Core.Collections { private readonly Iterator[] _iterators; + private readonly Stack _iteratorPool; + private Iterator _reusableIterator; + private int _numIterators; public int Id { get; private set; } @@ -19,12 +22,30 @@ namespace MHServerEmu.Core.Collections public InvasiveList(int maxIterators, int id = 0) { _iterators = new Iterator[maxIterators]; + + if (maxIterators > 1) + _iteratorPool = new(); + Id = id; } public IEnumerator GetEnumerator() { - return new Iterator(this); + Iterator iterator; + + if (_iteratorPool != null) + { + if (_iteratorPool.TryPop(out iterator) == false) + iterator = new(this); + } + else + { + _reusableIterator ??= new(this); + iterator = _reusableIterator; + } + + iterator.Initialize(); + return iterator; } public void Remove(T element) @@ -148,6 +169,11 @@ namespace MHServerEmu.Core.Collections _iterators[_numIterators - 1] = null; _numIterators--; + + // pool iterator instance for reuse + iterator.Reset(); + _iteratorPool?.Push(iterator); + return; } @@ -168,6 +194,10 @@ namespace MHServerEmu.Core.Collections public Iterator(InvasiveList invasiveList) { _list = invasiveList; + } + + public void Initialize() + { _list.RegisterIterator(this); } From 8740e60e353682356260630997cdd9b64e188177 Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Mon, 23 Mar 2026 15:09:35 +0300 Subject: [PATCH 11/30] Rename FastCodedOutputStream to RecyclableCodedOutputStream and remove ArrayPool --- src/MHServerEmu.Core/Network/MuxPacket.cs | 2 +- ...ream.cs => RecyclableCodedOutputStream.cs} | 34 ++++++------------- 2 files changed, 12 insertions(+), 24 deletions(-) rename src/MHServerEmu.Core/Serialization/{FastCodedOutputStream.cs => RecyclableCodedOutputStream.cs} (93%) diff --git a/src/MHServerEmu.Core/Network/MuxPacket.cs b/src/MHServerEmu.Core/Network/MuxPacket.cs index b9d66750..4957bab6 100644 --- a/src/MHServerEmu.Core/Network/MuxPacket.cs +++ b/src/MHServerEmu.Core/Network/MuxPacket.cs @@ -135,7 +135,7 @@ namespace MHServerEmu.Core.Network if (_outboundMessageList.Count == 0) return Logger.WarnReturn(false, "SerializeData(): Data packet contains no messages"); - using FastCodedOutputStream cos = FastCodedOutputStream.CreateInstance(stream); + using RecyclableCodedOutputStream cos = RecyclableCodedOutputStream.CreateInstance(stream); foreach (MessagePackageOut messagePackage in _outboundMessageList) messagePackage.WriteTo(cos); diff --git a/src/MHServerEmu.Core/Serialization/FastCodedOutputStream.cs b/src/MHServerEmu.Core/Serialization/RecyclableCodedOutputStream.cs similarity index 93% rename from src/MHServerEmu.Core/Serialization/FastCodedOutputStream.cs rename to src/MHServerEmu.Core/Serialization/RecyclableCodedOutputStream.cs index 37cfd3d9..bdbdea34 100644 --- a/src/MHServerEmu.Core/Serialization/FastCodedOutputStream.cs +++ b/src/MHServerEmu.Core/Serialization/RecyclableCodedOutputStream.cs @@ -1,5 +1,4 @@ -using System.Buffers; -using System.Collections; +using System.Collections; using System.Collections.Concurrent; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -12,33 +11,28 @@ namespace MHServerEmu.Core.Serialization /// /// A more memory efficient version of . /// - public sealed class FastCodedOutputStream : ICodedOutputStreamEx, IDisposable + public sealed class RecyclableCodedOutputStream : ICodedOutputStreamEx, IDisposable { - private const int DefaultBufferSize = 4096; - - private static readonly ConcurrentBag InstancePool = new(); - private static readonly ArrayPool BufferPool = ArrayPool.Create(); + private static readonly ConcurrentBag Instances = new(); + private readonly byte[] _primaryBuffer = new byte[CodedOutputStream.DefaultBufferSize]; private readonly byte[] _floatBuffer = new byte[sizeof(float)]; - private byte[] _buffer; private CodedOutputStream _cos; - private FastCodedOutputStream() { } + private RecyclableCodedOutputStream() { } - private void Initialize(Stream stream, int bufferSize) + private void Initialize(Stream stream) { - // Use pooled buffers for coded output streams with reflection hackery, see ProtobufHelper for more info. - _buffer = BufferPool.Rent(bufferSize); - _cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(stream, _buffer); + _cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(stream, _primaryBuffer); } - public static FastCodedOutputStream CreateInstance(Stream stream, int bufferSize = DefaultBufferSize) + public static RecyclableCodedOutputStream CreateInstance(Stream stream) { - if (InstancePool.TryTake(out FastCodedOutputStream cos) == false) + if (Instances.TryTake(out RecyclableCodedOutputStream cos) == false) cos = new(); - cos.Initialize(stream, bufferSize); + cos.Initialize(stream); return cos; } @@ -52,13 +46,7 @@ namespace MHServerEmu.Core.Serialization _cos = null; } - if (_buffer != null) - { - BufferPool.Return(_buffer); - _buffer = null; - } - - InstancePool.Add(this); + Instances.Add(this); } #endregion From 877adaf80dd4e2f587cfdc70b27abae0e0e7e20b Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Mon, 23 Mar 2026 15:21:22 +0300 Subject: [PATCH 12/30] Remove SequenceEqual check when serializing player entity blobs --- src/MHServerEmu.Games/Network/PlayerConnection.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/MHServerEmu.Games/Network/PlayerConnection.cs b/src/MHServerEmu.Games/Network/PlayerConnection.cs index 68ca67e2..4e275c36 100644 --- a/src/MHServerEmu.Games/Network/PlayerConnection.cs +++ b/src/MHServerEmu.Games/Network/PlayerConnection.cs @@ -266,13 +266,11 @@ namespace MHServerEmu.Games.Network Serializer.Transfer(archive, Player); Span newArchiveData = archive.AsSpan(); - if (newArchiveData.SequenceEqual(oldArchiveData) == false) - { - if (newArchiveData.Length == oldArchiveData.Length) - newArchiveData.CopyTo(oldArchiveData); - else - dbPlayer.ArchiveData = newArchiveData.ToArray(); - } + // No point in doing a SequenceEqual check here, it's always different in practice. + if (newArchiveData.Length == oldArchiveData.Length) + newArchiveData.CopyTo(oldArchiveData); + else + dbPlayer.ArchiveData = newArchiveData.ToArray(); } // Save last town as a separate database field to be able to access it without deserializing the player entity From 66233d2fbef2099b7538a5e4d6b4873965d9ca43 Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Mon, 23 Mar 2026 17:23:38 +0300 Subject: [PATCH 13/30] Overhaul LoginQueueManager --- .../PlayerManagerConfig.cs | 2 +- .../PlayerManagerService.cs | 4 +- .../Players/LoginQueueManager.cs | 129 ++++++++++++++---- src/MHServerEmu/Config.ini | 2 +- 4 files changed, 105 insertions(+), 32 deletions(-) diff --git a/src/MHServerEmu.PlayerManagement/PlayerManagerConfig.cs b/src/MHServerEmu.PlayerManagement/PlayerManagerConfig.cs index edb2aa5a..5fc140f4 100644 --- a/src/MHServerEmu.PlayerManagement/PlayerManagerConfig.cs +++ b/src/MHServerEmu.PlayerManagement/PlayerManagerConfig.cs @@ -14,7 +14,7 @@ namespace MHServerEmu.PlayerManagement public bool ShowNewsOnLogin { get; private set; } = false; public string NewsUrl { get; private set; } = "http://localhost/"; public int ServerCapacity { get; private set; } = 0; - public int MaxLoginQueueClients { get; private set; } = 10000; + public int MaxLoginQueueClients { get; private set; } = 8192; public bool EnableTownPlayerLimit { get; private set; } = false; } } diff --git a/src/MHServerEmu.PlayerManagement/PlayerManagerService.cs b/src/MHServerEmu.PlayerManagement/PlayerManagerService.cs index 269927ef..dca9ec71 100644 --- a/src/MHServerEmu.PlayerManagement/PlayerManagerService.cs +++ b/src/MHServerEmu.PlayerManagement/PlayerManagerService.cs @@ -49,6 +49,8 @@ namespace MHServerEmu.PlayerManagement /// public PlayerManagerService() { + Config = ConfigManager.Instance.GetConfig(); + _serviceMailbox = new(this); SessionManager = new(this); @@ -62,8 +64,6 @@ namespace MHServerEmu.PlayerManagement RegionRequestQueueManager = new(this); EventScheduler = new(); - - Config = ConfigManager.Instance.GetConfig(); } #region IGameService Implementation diff --git a/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs b/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs index e29f815e..c616ab36 100644 --- a/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs +++ b/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs @@ -1,6 +1,7 @@ using Gazillion; using Google.ProtocolBuffers; using MHServerEmu.Core.Collections; +using MHServerEmu.Core.Extensions; using MHServerEmu.Core.Logging; using MHServerEmu.Core.Network; using MHServerEmu.Core.System.Time; @@ -15,19 +16,40 @@ namespace MHServerEmu.PlayerManagement.Players private static readonly Logger Logger = LogManager.CreateLogger(); private static readonly TimeSpan PendingClientTimeout = TimeSpan.FromSeconds(15); + private static readonly TimeSpan StatusUpdateInterval = TimeSpan.FromSeconds(10); private readonly DoubleBufferQueue _newClientQueue = new(); - private readonly Queue _loginQueue = new(); - private readonly Queue _highPriorityLoginQueue = new(); + + private readonly LinkedList _defaultQueue = new(); + private readonly LinkedList _reconnectQueue = new(); + private readonly Queue _highPriorityQueue = new(); + + private readonly Stack> _queueNodes; // Pending clients are clients that have successfully passed the login queue private readonly Dictionary _pendingClients = new(); private readonly PlayerManagerService _playerManager; + private readonly LoginQueueStatus.Builder _statusBuilder = LoginQueueStatus.CreateBuilder(); + private readonly Dictionary _statusUpdateTimes; + + public int PlayersInLine { get => _defaultQueue.Count + _reconnectQueue.Count; } + public LoginQueueManager(PlayerManagerService playerManager) { _playerManager = playerManager; + + int maxQueueClients = playerManager.Config.MaxLoginQueueClients; + + _queueNodes = new(maxQueueClients); + for (int i = 0; i < maxQueueClients; i++) + { + LinkedListNode node = new(null); + _queueNodes.Push(node); + } + + _statusUpdateTimes = new(maxQueueClients); } public void Update() @@ -77,8 +99,6 @@ namespace MHServerEmu.PlayerManagement.Players /// private void AcceptNewClients() { - int maxLoginQueueClients = _playerManager.Config.MaxLoginQueueClients; - _newClientQueue.Swap(); while (_newClientQueue.CurrentCount > 0) @@ -98,22 +118,25 @@ namespace MHServerEmu.PlayerManagement.Players // High priority queue always ignores server capacity if (IsClientHighPriority(client)) { - _highPriorityLoginQueue.Enqueue(client); + _highPriorityQueue.Enqueue(client); } else { - if (_loginQueue.Count >= maxLoginQueueClients) + // TODO: check reconnect + LinkedList queueToUse = _defaultQueue; + + if (_queueNodes.TryPop(out LinkedListNode queueNode) == false) { - Logger.Warn($"AcceptNewClients(): Unable to accept client [{client}], the queue already has {maxLoginQueueClients} clients, which is the maximum number allowed by the current server configuration"); + Logger.Warn($"AcceptNewClients(): Unable to accept client [{client}], the queue already has {PlayersInLine} clients, which is the maximum number allowed by the current server configuration"); client.Disconnect(); RemoveClientSession(client); continue; } - _loginQueue.Enqueue(client); + queueNode.Value = client; + queueToUse.AddLast(queueNode); } - Logger.Info($"Accepted client [{client}] into the login queue"); } } @@ -127,31 +150,28 @@ namespace MHServerEmu.PlayerManagement.Players int availableCapacity = totalCapacity - _playerManager.ClientManager.PlayerCount - _pendingClients.Count; // Let clients from the high priority queue in first ignoring capacity - while (_highPriorityLoginQueue.Count > 0) + while (_highPriorityQueue.Count > 0) { - IFrontendClient client = _highPriorityLoginQueue.Dequeue(); + IFrontendClient client = _highPriorityQueue.Dequeue(); ProcessQueuedClient(client, ref availableCapacity); } - // Let clients from the normal login queue, check available capacity if enabled - while (_loginQueue.Count > 0 && (totalCapacity <= 0 || availableCapacity > 0)) + // Let clients from the reconnect and default queues, check available capacity if enabled + ProcessQueue(_reconnectQueue, totalCapacity, ref availableCapacity); + ProcessQueue(_defaultQueue, totalCapacity, ref availableCapacity); + + // Update status of remaining players + int playersInLine = PlayersInLine; + if (playersInLine > 0) { - IFrontendClient client = _loginQueue.Dequeue(); - ProcessQueuedClient(client, ref availableCapacity); + _statusBuilder.SetNumberOfPlayersInLine((ulong)playersInLine); + + TimeSpan now = Clock.UnixTime; + ulong nextPlaceInLine = 1; + + UpdateQueueStatus(_reconnectQueue, ref nextPlaceInLine, now); + UpdateQueueStatus(_defaultQueue, ref nextPlaceInLine, now); } - - // Send status updates to remaining players - int playersInLine = _loginQueue.Count; - if (playersInLine == 0) - return; - - LoginQueueStatus.Builder statusBuilder = LoginQueueStatus.CreateBuilder() - .SetNumberOfPlayersInLine((ulong)playersInLine); - - ulong placeInLine = 1; - - foreach (IFrontendClient client in _loginQueue) - client.SendMessage(MuxChannel, statusBuilder.SetPlaceInLine(placeInLine++).Build()); } private static bool IsClientHighPriority(IFrontendClient client) @@ -165,6 +185,18 @@ namespace MHServerEmu.PlayerManagement.Players return false; } + private void ProcessQueue(LinkedList queue, int totalCapacity, ref int availableCapacity) + { + while (queue.Count > 0 && (totalCapacity <= 0 || availableCapacity > 0)) + { + LinkedListNode queueNode = queue.First; + IFrontendClient client = queueNode.Value; + + RemoveQueueNode(queueNode); + ProcessQueuedClient(client, ref availableCapacity); + } + } + private bool ProcessQueuedClient(IFrontendClient client, ref int availableCapacity) { if (client.IsConnected == false) @@ -195,5 +227,46 @@ namespace MHServerEmu.PlayerManagement.Players ulong sessionId = client.Session.Id; _playerManager.SessionManager.RemoveActiveSession(sessionId); } + + private void UpdateQueueStatus(LinkedList queue, ref ulong nextPlaceInLine, TimeSpan now) + { + LinkedListNode current = queue.First; + while (current != null) + { + IFrontendClient client = current.Value; + + if (client.IsConnected == false) + { + Logger.Warn($"UpdateQueueStatus(): Client [{client}] disconnected while waiting in the login queue"); + + LinkedListNode prev = current; + current = current.Next; + RemoveQueueNode(prev); + + RemoveClientSession(client); + + continue; + } + + ulong placeInLine = nextPlaceInLine++; + + ref TimeSpan lastUpdateTime = ref _statusUpdateTimes.GetValueRefOrAddDefault(client); + if ((now - lastUpdateTime) >= StatusUpdateInterval) + { + lastUpdateTime = now; + LoginQueueStatus status = _statusBuilder.SetPlaceInLine(placeInLine).Build(); + client.SendMessage(MuxChannel, status); + } + + current = current.Next; + } + } + + private void RemoveQueueNode(LinkedListNode node) + { + _statusUpdateTimes.Remove(node.Value); + node.Remove(); + _queueNodes.Push(node); + } } } diff --git a/src/MHServerEmu/Config.ini b/src/MHServerEmu/Config.ini index 14c07402..47affec0 100644 --- a/src/MHServerEmu/Config.ini +++ b/src/MHServerEmu/Config.ini @@ -87,7 +87,7 @@ UseWhitelist=false ShowNewsOnLogin=false NewsUrl=http://localhost/news/index.html ServerCapacity=0 -MaxLoginQueueClients=10000 +MaxLoginQueueClients=8192 EnableTownPlayerLimit=false [SQLiteDBManager] From c29d8634ad4b8b7621244be9cf27e5c6abe929f8 Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Mon, 23 Mar 2026 17:32:52 +0300 Subject: [PATCH 14/30] Implement BypassLoginQueue account flag --- src/MHServerEmu.DatabaseAccess/Models/DBAccount.cs | 1 + .../Players/LoginQueueManager.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/MHServerEmu.DatabaseAccess/Models/DBAccount.cs b/src/MHServerEmu.DatabaseAccess/Models/DBAccount.cs index f34e017d..6c297995 100644 --- a/src/MHServerEmu.DatabaseAccess/Models/DBAccount.cs +++ b/src/MHServerEmu.DatabaseAccess/Models/DBAccount.cs @@ -22,6 +22,7 @@ namespace MHServerEmu.DatabaseAccess.Models IsPasswordExpired = 1 << 2, // 1 << 3 was previously used in 0.x for the Linux compatibility mode, it should not be set in any 1.x+ databases. IsWhitelisted = 1 << 4, + BypassLoginQueue = 1 << 5, } /// diff --git a/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs b/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs index c616ab36..f8733a39 100644 --- a/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs +++ b/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs @@ -176,8 +176,14 @@ namespace MHServerEmu.PlayerManagement.Players private static bool IsClientHighPriority(IFrontendClient client) { - // Users with elevated privileges (moderators / admins) have high priority - if (((IDBAccountOwner)client).Account.UserLevel > AccountUserLevel.User) + DBAccount account = ((IDBAccountOwner)client).Account; + + // Users with elevated privileges (moderators / admins) have high priority by default. + if (account.UserLevel > AccountUserLevel.User) + return true; + + // Accounts can be manually flagged to have high priority. + if (account.Flags.HasFlag(AccountFlags.BypassLoginQueue)) return true; // Add more cases as needed From b93e433227a63b32cd9c25d682a962003fdef90b Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Mon, 23 Mar 2026 18:26:21 +0300 Subject: [PATCH 15/30] Implement login queue reconnects --- .../Players/LoginQueueManager.cs | 62 ++++++++++++++++--- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs b/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs index f8733a39..278cada4 100644 --- a/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs +++ b/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs @@ -13,10 +13,12 @@ namespace MHServerEmu.PlayerManagement.Players public class LoginQueueManager { private const ushort MuxChannel = 1; + private const float ReconnectPermissionPercentile = 0.1f; private static readonly Logger Logger = LogManager.CreateLogger(); private static readonly TimeSpan PendingClientTimeout = TimeSpan.FromSeconds(15); private static readonly TimeSpan StatusUpdateInterval = TimeSpan.FromSeconds(10); + private static readonly TimeSpan ReconnectPermissionDuration = TimeSpan.FromMinutes(10); private readonly DoubleBufferQueue _newClientQueue = new(); @@ -34,6 +36,9 @@ namespace MHServerEmu.PlayerManagement.Players private readonly LoginQueueStatus.Builder _statusBuilder = LoginQueueStatus.CreateBuilder(); private readonly Dictionary _statusUpdateTimes; + private readonly Dictionary _reconnectPermissions = new(); + private CooldownTimer _reconnectPermissionPurgeTimer = new(TimeSpan.FromMinutes(1)); + public int PlayersInLine { get => _defaultQueue.Count + _reconnectQueue.Count; } public LoginQueueManager(PlayerManagerService playerManager) @@ -55,6 +60,7 @@ namespace MHServerEmu.PlayerManagement.Players public void Update() { TimeOutPendingClients(); + PurgeReconnectPermissions(); AcceptNewClients(); ProcessLoginQueue(); } @@ -94,6 +100,24 @@ namespace MHServerEmu.PlayerManagement.Players } } + private void PurgeReconnectPermissions() + { + if (_reconnectPermissionPurgeTimer.Check() == false) + return; + + TimeSpan now = Clock.UnixTime; + + foreach (var kvp in _reconnectPermissions) + { + TimeSpan duration = now - kvp.Value; + if (duration >= ReconnectPermissionDuration) + { + Logger.Info($"Purged reconnect permission for account 0x{kvp.Key:X}"); + _reconnectPermissions.Remove(kvp.Key); + } + } + } + /// /// Accepts asynchronously added clients to the login queue. /// @@ -122,9 +146,6 @@ namespace MHServerEmu.PlayerManagement.Players } else { - // TODO: check reconnect - LinkedList queueToUse = _defaultQueue; - if (_queueNodes.TryPop(out LinkedListNode queueNode) == false) { Logger.Warn($"AcceptNewClients(): Unable to accept client [{client}], the queue already has {PlayersInLine} clients, which is the maximum number allowed by the current server configuration"); @@ -133,6 +154,14 @@ namespace MHServerEmu.PlayerManagement.Players continue; } + LinkedList queueToUse = _defaultQueue; + + if (_reconnectPermissions.Remove(client.DbId)) + { + queueToUse = _reconnectQueue; + Logger.Info($"Consumed reconnect permission for client [{client}]"); + } + queueNode.Value = client; queueToUse.AddLast(queueNode); } @@ -208,6 +237,7 @@ namespace MHServerEmu.PlayerManagement.Players if (client.IsConnected == false) { Logger.Warn($"ProcessQueuedClient(): Client [{client}] disconnected while waiting in the login queue"); + TryAddReconnectPermission(client, 1, Clock.UnixTime); RemoveClientSession(client); return false; } @@ -228,12 +258,6 @@ namespace MHServerEmu.PlayerManagement.Players return true; } - private void RemoveClientSession(IFrontendClient client) - { - ulong sessionId = client.Session.Id; - _playerManager.SessionManager.RemoveActiveSession(sessionId); - } - private void UpdateQueueStatus(LinkedList queue, ref ulong nextPlaceInLine, TimeSpan now) { LinkedListNode current = queue.First; @@ -249,6 +273,7 @@ namespace MHServerEmu.PlayerManagement.Players current = current.Next; RemoveQueueNode(prev); + TryAddReconnectPermission(client, nextPlaceInLine, now); RemoveClientSession(client); continue; @@ -274,5 +299,24 @@ namespace MHServerEmu.PlayerManagement.Players node.Remove(); _queueNodes.Push(node); } + + private void RemoveClientSession(IFrontendClient client) + { + ulong sessionId = client.Session.Id; + _playerManager.SessionManager.RemoveActiveSession(sessionId); + } + + private void TryAddReconnectPermission(IFrontendClient client, ulong placeInLine, TimeSpan now) + { + if (PlayersInLine == 0) + return; + + float percentile = (float)placeInLine / PlayersInLine; + if (placeInLine == 1 || percentile <= ReconnectPermissionPercentile) + { + _reconnectPermissions[client.DbId] = now; + Logger.Info($"Added reconnect permission for client [{client}] (placeInLine={placeInLine})"); + } + } } } From cc241e6325dc55a8dbc8d545d4045cef87795cdb Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Mon, 23 Mar 2026 18:30:37 +0300 Subject: [PATCH 16/30] Cache SessionEncryptionChanged message in LoginQueueManager --- .../Players/LoginQueueManager.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs b/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs index 278cada4..edfe0988 100644 --- a/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs +++ b/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs @@ -20,6 +20,11 @@ namespace MHServerEmu.PlayerManagement.Players private static readonly TimeSpan StatusUpdateInterval = TimeSpan.FromSeconds(10); private static readonly TimeSpan ReconnectPermissionDuration = TimeSpan.FromMinutes(10); + private static readonly SessionEncryptionChanged SessionEncryptionChangedMessage = SessionEncryptionChanged.CreateBuilder() + .SetRandomNumberIndex(0) + .SetEncryptedRandomNumber(ByteString.Empty) + .Build(); + private readonly DoubleBufferQueue _newClientQueue = new(); private readonly LinkedList _defaultQueue = new(); @@ -246,10 +251,8 @@ namespace MHServerEmu.PlayerManagement.Players // However, if a malicious user modifies their client, it may try to skip ahead, so we need to verify this. _pendingClients.Add(client, Clock.UnixTime); - client.SendMessage(MuxChannel, SessionEncryptionChanged.CreateBuilder() - .SetRandomNumberIndex(0) - .SetEncryptedRandomNumber(ByteString.Empty) - .Build()); + // Gazillion never finished implementing encryption, so the SessionEncryptionChanged message is just a dummy we can cache and reuse. + client.SendMessage(MuxChannel, SessionEncryptionChangedMessage); Logger.Info($"Client [{client}] passed the login queue"); From 32ec15fd1a818895ce2acafb3ae49c367e797fb9 Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Tue, 24 Mar 2026 17:54:41 +0300 Subject: [PATCH 17/30] Expand login queue reconnects to clients that successfully pass the queue --- .../Players/LoginQueueManager.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs b/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs index edfe0988..2245fa8f 100644 --- a/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs +++ b/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs @@ -187,7 +187,7 @@ namespace MHServerEmu.PlayerManagement.Players while (_highPriorityQueue.Count > 0) { IFrontendClient client = _highPriorityQueue.Dequeue(); - ProcessQueuedClient(client, ref availableCapacity); + ProcessQueuedClient(client, ref availableCapacity, false); } // Let clients from the reconnect and default queues, check available capacity if enabled @@ -233,16 +233,19 @@ namespace MHServerEmu.PlayerManagement.Players IFrontendClient client = queueNode.Value; RemoveQueueNode(queueNode); - ProcessQueuedClient(client, ref availableCapacity); + ProcessQueuedClient(client, ref availableCapacity, true); } } - private bool ProcessQueuedClient(IFrontendClient client, ref int availableCapacity) + private bool ProcessQueuedClient(IFrontendClient client, ref int availableCapacity, bool allowReconnect) { + // Allow all clients that pass the queue without bypass to reconnect because of the client-side afk timer bug. + if (allowReconnect) + TryAddReconnectPermission(client, 1, Clock.UnixTime); + if (client.IsConnected == false) { Logger.Warn($"ProcessQueuedClient(): Client [{client}] disconnected while waiting in the login queue"); - TryAddReconnectPermission(client, 1, Clock.UnixTime); RemoveClientSession(client); return false; } From 5acbeb2602e54364468f83b93acbc0e000151951 Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Tue, 24 Mar 2026 18:11:01 +0300 Subject: [PATCH 18/30] Remove reconnect permissions from the reconnect login queue --- .../Players/LoginQueueManager.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs b/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs index 2245fa8f..9b7fbdda 100644 --- a/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs +++ b/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs @@ -191,8 +191,8 @@ namespace MHServerEmu.PlayerManagement.Players } // Let clients from the reconnect and default queues, check available capacity if enabled - ProcessQueue(_reconnectQueue, totalCapacity, ref availableCapacity); - ProcessQueue(_defaultQueue, totalCapacity, ref availableCapacity); + ProcessQueue(_reconnectQueue, totalCapacity, ref availableCapacity, false); + ProcessQueue(_defaultQueue, totalCapacity, ref availableCapacity, true); // Update status of remaining players int playersInLine = PlayersInLine; @@ -225,7 +225,7 @@ namespace MHServerEmu.PlayerManagement.Players return false; } - private void ProcessQueue(LinkedList queue, int totalCapacity, ref int availableCapacity) + private void ProcessQueue(LinkedList queue, int totalCapacity, ref int availableCapacity, bool allowReconnect) { while (queue.Count > 0 && (totalCapacity <= 0 || availableCapacity > 0)) { @@ -233,13 +233,13 @@ namespace MHServerEmu.PlayerManagement.Players IFrontendClient client = queueNode.Value; RemoveQueueNode(queueNode); - ProcessQueuedClient(client, ref availableCapacity, true); + ProcessQueuedClient(client, ref availableCapacity, allowReconnect); } } private bool ProcessQueuedClient(IFrontendClient client, ref int availableCapacity, bool allowReconnect) { - // Allow all clients that pass the queue without bypass to reconnect because of the client-side afk timer bug. + // Allow all clients that pass the default queue to reconnect because of the client-side afk timer bug. if (allowReconnect) TryAddReconnectPermission(client, 1, Clock.UnixTime); From c38018316b05ac994b2ad5bd804775484464f06f Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Tue, 24 Mar 2026 20:29:50 +0300 Subject: [PATCH 19/30] Add reusable picker for IntervalPowersRandomTarget hotspots --- src/MHServerEmu.Games/Entities/Hotspot.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/MHServerEmu.Games/Entities/Hotspot.cs b/src/MHServerEmu.Games/Entities/Hotspot.cs index a26180c5..d0a20dea 100644 --- a/src/MHServerEmu.Games/Entities/Hotspot.cs +++ b/src/MHServerEmu.Games/Entities/Hotspot.cs @@ -49,6 +49,8 @@ namespace MHServerEmu.Games.Entities private int _activePowerTargetCount; private bool _killSelf; + private Picker _targetPicker; // Reusable picker for AppliesIntervalPowers hotspots, remove this if we implement picker pooling. + public Hotspot(Game game) : base(game) { SetFlag(EntityFlags.IsHotspot, true); @@ -67,6 +69,9 @@ namespace MHServerEmu.Games.Entities if (hotspotProto.DirectApplyToMissilesData?.EvalPropertiesToApply != null || hotspotProto.Negatable) SetFlag(EntityFlags.IsCollidableHotspot, true); + if (hotspotProto.IntervalPowersRandomTarget) + _targetPicker = new(Game.Random); + return true; } @@ -796,7 +801,7 @@ namespace MHServerEmu.Games.Entities if (hotspotProto.IntervalPowersRandomTarget) { - Picker picker = new(Game.Random); + Picker picker = _targetPicker; var hasLOS = TriBool.Undefined; ulong prevTargetId = InvalidId; From a3551c8ab895ae0f048cd3fbe0173307b2725b7f Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Tue, 24 Mar 2026 20:45:02 +0300 Subject: [PATCH 20/30] Add EnsureCapacity() call to AchievementState.Serialize() --- src/MHServerEmu.Games/Achievements/AchievementState.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/MHServerEmu.Games/Achievements/AchievementState.cs b/src/MHServerEmu.Games/Achievements/AchievementState.cs index cbe31145..b3eee961 100644 --- a/src/MHServerEmu.Games/Achievements/AchievementState.cs +++ b/src/MHServerEmu.Games/Achievements/AchievementState.cs @@ -68,6 +68,8 @@ namespace MHServerEmu.Games.Achievements AchievementProgressMap.Clear(); _scoreCached = false; + AchievementProgressMap.EnsureCapacity((int)achievementCount); + for (uint i = 0; i < achievementCount; i++) { uint achievementId = 0; From 346965116f82d964bb80884b89669924a3a86b9e Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Wed, 25 Mar 2026 17:53:26 +0300 Subject: [PATCH 21/30] Implemented !server whitelist command --- docs/ServerEmu/ServerCommands.md | 23 ++++++++++--------- .../Network/GameServiceProtocol.cs | 6 +++++ .../Auth/SessionManager.cs | 14 ++++++++++- .../Network/PlayerManagerServiceMailbox.cs | 11 +++++++++ .../Implementations/ServerCommands.cs | 23 +++++++++++++++++++ 5 files changed, 65 insertions(+), 12 deletions(-) diff --git a/docs/ServerEmu/ServerCommands.md b/docs/ServerEmu/ServerCommands.md index 8334ad37..1aa826d2 100644 --- a/docs/ServerEmu/ServerCommands.md +++ b/docs/ServerEmu/ServerCommands.md @@ -1,6 +1,6 @@ # Server Commands -This list was automatically generated on `2026.03.09 15:06:03 UTC` using server version `1.0.0`. +This list was automatically generated on `2026.03.25 14:52:06 UTC` using server version `1.0.0`. To see an up to date list of all commands, type !commands in the server console or the in-game chat. When invoking a command from in-game your account has to meet the user level requirement for the command. @@ -211,16 +211,17 @@ Region management commands. ## Server Server management commands. -| Command | Description | User Level | Invoker Type | -| --------------------------------- | ----------------------------------------- | ---------- | ------------- | -| !server broadcast | Broadcasts a notification to all players. | Admin | Any | -| !server reloadaddg | Reloads the Add G page. | Admin | ServerConsole | -| !server reloadcatalog | Reloads MTX store catalog. | Admin | ServerConsole | -| !server reloaddashboard | Reloads the web dashboard. | Admin | ServerConsole | -| !server reloadlivetuning | Reloads live tuning settings. | Admin | ServerConsole | -| !server reloadplayernameblacklist | Reloads the player name blacklist. | Admin | ServerConsole | -| !server shutdown | Shuts the server down. | Admin | Any | -| !server status | Prints server status. | Any | Any | +| Command | Description | User Level | Invoker Type | +| --------------------------------- | ------------------------------------------------- | ---------- | ------------- | +| !server broadcast | Broadcasts a notification to all players. | Admin | Any | +| !server reloadaddg | Reloads the Add G page. | Admin | ServerConsole | +| !server reloadcatalog | Reloads MTX store catalog. | Admin | ServerConsole | +| !server reloaddashboard | Reloads the web dashboard. | Admin | ServerConsole | +| !server reloadlivetuning | Reloads live tuning settings. | Admin | ServerConsole | +| !server reloadplayernameblacklist | Reloads the player name blacklist. | Admin | ServerConsole | +| !server shutdown | Shuts the server down. | Admin | Any | +| !server status | Prints server status. | Any | Any | +| !server whitelist | Enables or disables account whitelist for logins. | Admin | ServerConsole | ## Store Commands for interacting with the in-game store. diff --git a/src/MHServerEmu.Core/Network/GameServiceProtocol.cs b/src/MHServerEmu.Core/Network/GameServiceProtocol.cs index a115f568..bf127589 100644 --- a/src/MHServerEmu.Core/Network/GameServiceProtocol.cs +++ b/src/MHServerEmu.Core/Network/GameServiceProtocol.cs @@ -830,6 +830,12 @@ namespace MHServerEmu.Core.Network public readonly int ResultCode = resultCode; } + public readonly struct SetWhitelistEnabled(bool enable) + : IGameServiceMessage + { + public readonly bool Enable = enable; + } + #endregion } } diff --git a/src/MHServerEmu.PlayerManagement/Auth/SessionManager.cs b/src/MHServerEmu.PlayerManagement/Auth/SessionManager.cs index afe7915e..bf6998e0 100644 --- a/src/MHServerEmu.PlayerManagement/Auth/SessionManager.cs +++ b/src/MHServerEmu.PlayerManagement/Auth/SessionManager.cs @@ -33,6 +33,8 @@ namespace MHServerEmu.PlayerManagement.Auth private CooldownTimer _updateTimer = new(TimeSpan.FromMilliseconds(1000)); + public bool WhitelistEnabled { get; private set; } + public int PendingSessionCount { get => _pendingSessionDict.Count; } public int ActiveSessionCount { get => _activeSessionDict.Count; } @@ -42,6 +44,16 @@ namespace MHServerEmu.PlayerManagement.Auth public SessionManager(PlayerManagerService playerManager) { _playerManager = playerManager; + WhitelistEnabled = playerManager.Config.UseWhitelist; + } + + public void SetWhitelistEnabled(bool enable) + { + if (WhitelistEnabled == enable) + return; + + WhitelistEnabled = enable; + Logger.Info($"Whitelist {(enable ? "enabled" : "disabled")}"); } public void Update() @@ -92,7 +104,7 @@ namespace MHServerEmu.PlayerManagement.Auth } // Verify credentials - AuthStatusCode statusCode = AccountManager.TryGetAccountByLoginDataPB(loginDataPB, _playerManager.Config.UseWhitelist, out DBAccount account); + AuthStatusCode statusCode = AccountManager.TryGetAccountByLoginDataPB(loginDataPB, WhitelistEnabled, out DBAccount account); if (statusCode != AuthStatusCode.Success) return statusCode; diff --git a/src/MHServerEmu.PlayerManagement/Network/PlayerManagerServiceMailbox.cs b/src/MHServerEmu.PlayerManagement/Network/PlayerManagerServiceMailbox.cs index ead02804..7abfbaed 100644 --- a/src/MHServerEmu.PlayerManagement/Network/PlayerManagerServiceMailbox.cs +++ b/src/MHServerEmu.PlayerManagement/Network/PlayerManagerServiceMailbox.cs @@ -137,6 +137,10 @@ namespace MHServerEmu.PlayerManagement.Network OnAccountOperationRequest(accountOperationRequest); break; + case ServiceMessage.SetWhitelistEnabled setWhitelistEnabled: + OnSetWhitelistEnabled(setWhitelistEnabled); + break; + default: Logger.Warn($"ReceiveServiceMessage(): Unhandled service message type {message.GetType().Name}"); break; @@ -627,6 +631,13 @@ namespace MHServerEmu.PlayerManagement.Network return true; } + private bool OnSetWhitelistEnabled(in ServiceMessage.SetWhitelistEnabled setWhitelistEnabled) + { + _playerManager.SessionManager.SetWhitelistEnabled(setWhitelistEnabled.Enable); + + return true; + } + #endregion } } diff --git a/src/MHServerEmu/Commands/Implementations/ServerCommands.cs b/src/MHServerEmu/Commands/Implementations/ServerCommands.cs index dfe14e0d..6603f315 100644 --- a/src/MHServerEmu/Commands/Implementations/ServerCommands.cs +++ b/src/MHServerEmu/Commands/Implementations/ServerCommands.cs @@ -105,6 +105,29 @@ namespace MHServerEmu.Commands.Implementations return string.Empty; } + [Command("whitelist")] + [CommandDescription("Enables or disables account whitelist for logins.")] + [CommandParamCount(1)] + [CommandUserLevel(AccountUserLevel.Admin)] + [CommandInvokerType(CommandInvokerType.ServerConsole)] + public string Whitelist(string[] @params, NetClient client) + { + bool enable; + + if (bool.TryParse(@params[0], out enable) == false) + { + if (int.TryParse(@params[0], out int value) == false) + return "Invalid parameter. Please use boolean or integer values."; + + enable = value != 0; + } + + ServiceMessage.SetWhitelistEnabled message = new(enable); + ServerManager.Instance.SendMessageToService(GameServiceType.PlayerManager, message); + + return $"Sent a request to {(enable ? "enable" : "disable")} whitelist."; + } + [Command("shutdown")] [CommandDescription("Shuts the server down.")] [CommandUsage("server shutdown")] From 74e39fb00ae53d84937928c1e46031f3c8bb970d Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Wed, 25 Mar 2026 18:42:06 +0300 Subject: [PATCH 22/30] Adjust resource migration filter --- .../Network/MigrationUtility.cs | 22 ++++++++++++++++++- .../Properties/PropertyCollection.cs | 6 ++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/MHServerEmu.Games/Network/MigrationUtility.cs b/src/MHServerEmu.Games/Network/MigrationUtility.cs index e642fd89..6bbfb56b 100644 --- a/src/MHServerEmu.Games/Network/MigrationUtility.cs +++ b/src/MHServerEmu.Games/Network/MigrationUtility.cs @@ -1,7 +1,9 @@ using Gazillion; +using MHServerEmu.Core.Memory; using MHServerEmu.Core.Serialization; using MHServerEmu.DatabaseAccess.Models; using MHServerEmu.Games.Entities; +using MHServerEmu.Games.Entities.Avatars; using MHServerEmu.Games.GameData; using MHServerEmu.Games.GameData.Prototypes; using MHServerEmu.Games.Properties; @@ -54,7 +56,25 @@ namespace MHServerEmu.Games.Network { List<(ulong, ulong)> propertyList = migrationData.GetOrCreatePropertyList(entity.DatabaseUniqueId); propertyList.Clear(); - entity.Properties.GetPropertiesForMigration(propertyList); + + // HACK: Ugly property hack, remove this when we figure out an efficient way to migrate runtime-only conditions. + PropertyEnum propertyToIgnore = PropertyEnum.Invalid; + + if (entity is Avatar) + { + switch ((AvatarPrototypeId)entity.PrototypeDataRef) + { + case AvatarPrototypeId.AntMan: + propertyToIgnore = PropertyEnum.SecondaryResource + break; + + case AvatarPrototypeId.HumanTorch: + propertyToIgnore = PropertyEnum.Endurance; + break; + } + } + + entity.Properties.GetPropertiesForMigration(propertyList, propertyToIgnore); } private static void RestoreProperties(MigrationData migrationData, Entity entity) diff --git a/src/MHServerEmu.Games/Properties/PropertyCollection.cs b/src/MHServerEmu.Games/Properties/PropertyCollection.cs index 7cb4205f..f2d077d4 100644 --- a/src/MHServerEmu.Games/Properties/PropertyCollection.cs +++ b/src/MHServerEmu.Games/Properties/PropertyCollection.cs @@ -844,7 +844,7 @@ namespace MHServerEmu.Games.Properties return success; } - public void GetPropertiesForMigration(List<(ulong, ulong)> propertyList) + public void GetPropertiesForMigration(List<(ulong, ulong)> propertyList, PropertyEnum propertyToIgnore = PropertyEnum.Invalid) { PropertyEnum prevProperty = PropertyEnum.Invalid; PropertyInfoPrototype propInfoProto = null; @@ -860,9 +860,9 @@ namespace MHServerEmu.Games.Properties prevProperty = propertyEnum; } - // HACK: Do not migrate mana because we don't migrate conditions, which can cause some heroes to get stuck in bad state (e.g. Human Torch). + // HACK: Do not migrate some properties because we don't migrate conditions, which can cause some heroes to get stuck in bad state (e.g. Human Torch, Ant-Man). // This can be removed if we ever start migrating runtime-only conditions. - if (propertyEnum == PropertyEnum.Endurance) + if (propertyEnum == propertyToIgnore) continue; // Migrate properties that are not saved to the database, but are supposed to be replicated for transfer From 5c9b4b7aff6a70e6ae2df8631e803f64cb57e15c Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Wed, 25 Mar 2026 18:46:36 +0300 Subject: [PATCH 23/30] Add missing semicolon --- src/MHServerEmu.Games/Network/MigrationUtility.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MHServerEmu.Games/Network/MigrationUtility.cs b/src/MHServerEmu.Games/Network/MigrationUtility.cs index 6bbfb56b..2fcf3e1a 100644 --- a/src/MHServerEmu.Games/Network/MigrationUtility.cs +++ b/src/MHServerEmu.Games/Network/MigrationUtility.cs @@ -65,7 +65,7 @@ namespace MHServerEmu.Games.Network switch ((AvatarPrototypeId)entity.PrototypeDataRef) { case AvatarPrototypeId.AntMan: - propertyToIgnore = PropertyEnum.SecondaryResource + propertyToIgnore = PropertyEnum.SecondaryResource; break; case AvatarPrototypeId.HumanTorch: From 4ee84191324cbfbcf8755404f5c593db4dbcff99 Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Wed, 25 Mar 2026 18:57:14 +0300 Subject: [PATCH 24/30] Remove percentile based login queue reconnect permissions --- .../Players/LoginQueueManager.cs | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs b/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs index 9b7fbdda..24225974 100644 --- a/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs +++ b/src/MHServerEmu.PlayerManagement/Players/LoginQueueManager.cs @@ -13,7 +13,6 @@ namespace MHServerEmu.PlayerManagement.Players public class LoginQueueManager { private const ushort MuxChannel = 1; - private const float ReconnectPermissionPercentile = 0.1f; private static readonly Logger Logger = LogManager.CreateLogger(); private static readonly TimeSpan PendingClientTimeout = TimeSpan.FromSeconds(15); @@ -117,8 +116,8 @@ namespace MHServerEmu.PlayerManagement.Players TimeSpan duration = now - kvp.Value; if (duration >= ReconnectPermissionDuration) { - Logger.Info($"Purged reconnect permission for account 0x{kvp.Key:X}"); _reconnectPermissions.Remove(kvp.Key); + Logger.Info($"Reconnect permission expired for account 0x{kvp.Key:X}"); } } } @@ -241,7 +240,10 @@ namespace MHServerEmu.PlayerManagement.Players { // Allow all clients that pass the default queue to reconnect because of the client-side afk timer bug. if (allowReconnect) - TryAddReconnectPermission(client, 1, Clock.UnixTime); + { + _reconnectPermissions[client.DbId] = Clock.UnixTime; + Logger.Info($"Added reconnect permission for client [{client}]"); + } if (client.IsConnected == false) { @@ -279,7 +281,6 @@ namespace MHServerEmu.PlayerManagement.Players current = current.Next; RemoveQueueNode(prev); - TryAddReconnectPermission(client, nextPlaceInLine, now); RemoveClientSession(client); continue; @@ -311,18 +312,5 @@ namespace MHServerEmu.PlayerManagement.Players ulong sessionId = client.Session.Id; _playerManager.SessionManager.RemoveActiveSession(sessionId); } - - private void TryAddReconnectPermission(IFrontendClient client, ulong placeInLine, TimeSpan now) - { - if (PlayersInLine == 0) - return; - - float percentile = (float)placeInLine / PlayersInLine; - if (placeInLine == 1 || percentile <= ReconnectPermissionPercentile) - { - _reconnectPermissions[client.DbId] = now; - Logger.Info($"Added reconnect permission for client [{client}] (placeInLine={placeInLine})"); - } - } } } From b3e9fed24904804c6dc45144b8b008f829781921 Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Fri, 27 Mar 2026 03:05:25 +0300 Subject: [PATCH 25/30] Fix DifficultyTier property not being set on spawns and summons --- src/MHServerEmu.Games/Populations/SpawnSpec.cs | 1 + src/MHServerEmu.Games/Powers/PowerPayload.cs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/MHServerEmu.Games/Populations/SpawnSpec.cs b/src/MHServerEmu.Games/Populations/SpawnSpec.cs index 88255fe0..dc6c611d 100644 --- a/src/MHServerEmu.Games/Populations/SpawnSpec.cs +++ b/src/MHServerEmu.Games/Populations/SpawnSpec.cs @@ -110,6 +110,7 @@ namespace MHServerEmu.Games.Populations int level = area.GetCharacterLevel(entityProto); settingsProperties[PropertyEnum.CharacterLevel] = level; settingsProperties[PropertyEnum.CombatLevel] = level; + settingsProperties[PropertyEnum.DifficultyTier] = region.DifficultyTierRef; if (Group != null) { settingsProperties[PropertyEnum.SpawnGroupId] = Group.Id; diff --git a/src/MHServerEmu.Games/Powers/PowerPayload.cs b/src/MHServerEmu.Games/Powers/PowerPayload.cs index b79a1e0a..f7f43536 100644 --- a/src/MHServerEmu.Games/Powers/PowerPayload.cs +++ b/src/MHServerEmu.Games/Powers/PowerPayload.cs @@ -216,6 +216,10 @@ namespace MHServerEmu.Games.Powers if (PowerPrototype is not MovementPowerPrototype movementPowerProto || movementPowerProto.ConstantMoveTime == false) Properties.CopyProperty(power.Properties, PropertyEnum.MovementSpeedOverride); + // Difficulty tier for summons (e.g. Axis raid sentinels) + if (PowerPrototype is SummonPowerPrototype) + Properties.CopyProperty(powerOwner.Properties, PropertyEnum.DifficultyTier); + // Snapshot properties from triggering power results // TODO: Do we need full power results here? We should be able to get away with just the properties From 088590eca202c391d43086ce03422a696ac15e88 Mon Sep 17 00:00:00 2001 From: Crypto137 Date: Sat, 28 Mar 2026 17:27:14 +0300 Subject: [PATCH 26/30] Adjust DisableMissionXPBonuses setting behavior --- src/MHServerEmu.Games/Entities/Avatars/Avatar.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/MHServerEmu.Games/Entities/Avatars/Avatar.cs b/src/MHServerEmu.Games/Entities/Avatars/Avatar.cs index 9cdb157e..3afc88cb 100644 --- a/src/MHServerEmu.Games/Entities/Avatars/Avatar.cs +++ b/src/MHServerEmu.Games/Entities/Avatars/Avatar.cs @@ -4716,9 +4716,6 @@ namespace MHServerEmu.Games.Entities.Avatars if (IsInWorld == false) return 0f; - if (Game.CustomGameOptions.DisableMissionXPBonuses) - return 1f; - TuningPrototype tuningProto = tuningTable.Prototype; if (tuningProto == null) return Logger.WarnReturn(0f, "GetMissionXPMultiplier(): tuningProto == null"); @@ -4731,7 +4728,10 @@ namespace MHServerEmu.Games.Entities.Avatars float multiplier = pctXPFromLevelDeltaCurve.GetAt(level - CharacterLevel); multiplier *= tuningProto.PctXPMultiplier; multiplier *= playerXPByDifficultyIndex.GetAt(tuningTable.DifficultyIndex); - multiplier *= GetAvatarXPMultiplier(); + + if (Game.CustomGameOptions.DisableMissionXPBonuses == false) + multiplier *= GetAvatarXPMultiplier(); + multiplier *= GetPartyXPMultiplier(tuningProto); multiplier *= GetLiveTuningXPMultiplier(); return multiplier; From 34b059fc96988331cd34602a7daf6280155de06a Mon Sep 17 00:00:00 2001 From: AlexBond Date: Tue, 31 Mar 2026 18:49:29 +0300 Subject: [PATCH 27/30] Implement PvPCommands --- .../Matchmaking/Match.cs | 13 +++- .../Matchmaking/RegionRequestQueue.cs | 2 +- .../RegionRequestQueueCommandHandler.cs | 9 ++- .../Matchmaking/RegionRequestQueueParams.cs | 9 ++- .../PlayerManagerService.cs | 5 ++ .../Players/PlayerHandle.cs | 4 +- .../Commands/Implementations/PvPCommands.cs | 73 +++++++++++++++++++ 7 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 src/MHServerEmu/Commands/Implementations/PvPCommands.cs diff --git a/src/MHServerEmu.PlayerManagement/Matchmaking/Match.cs b/src/MHServerEmu.PlayerManagement/Matchmaking/Match.cs index 99c1ec0c..c8841e72 100644 --- a/src/MHServerEmu.PlayerManagement/Matchmaking/Match.cs +++ b/src/MHServerEmu.PlayerManagement/Matchmaking/Match.cs @@ -27,7 +27,13 @@ namespace MHServerEmu.PlayerManagement.Matchmaking QueueParams = queueParams; int[] teamLimits = Queue.Prototype.TeamLimits; - if (teamLimits.HasValue()) + + if (queueParams.TeamSizeOverride > 0) + { + _teams.Add(new MatchTeam(0, queueParams.TeamSizeOverride)); + _teams.Add(new MatchTeam(1, queueParams.TeamSizeOverride)); + } + else if (teamLimits.HasValue()) { for (int i = 0; i < teamLimits.Length; i++) { @@ -97,6 +103,11 @@ namespace MHServerEmu.PlayerManagement.Matchmaking return true; } + public bool IsCompatibleWith(in RegionRequestQueueParams queueParams) + { + return QueueParams.TeamSizeOverride == queueParams.TeamSizeOverride; + } + public bool IsReady() { if (IsFull() == false && IsBypass == false && Queue.Prototype.QueueDoNotWaitToFull == false) diff --git a/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueue.cs b/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueue.cs index 35b458e5..58729d24 100644 --- a/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueue.cs +++ b/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueue.cs @@ -45,7 +45,7 @@ namespace MHServerEmu.PlayerManagement.Matchmaking foreach (Match match in _matches) { - if (match.IsLookingForMore()) + if (match.IsLookingForMore() && match.IsCompatibleWith(queueParams)) lfmMatches.Add(match); } diff --git a/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueueCommandHandler.cs b/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueueCommandHandler.cs index b622c041..232d53c4 100644 --- a/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueueCommandHandler.cs +++ b/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueueCommandHandler.cs @@ -22,7 +22,7 @@ namespace MHServerEmu.PlayerManagement.Matchmaking } public void HandleCommand(PrototypeId regionRef, PrototypeId difficultyTierRef, PrototypeId metaStateRef, - RegionRequestQueueCommandVar command, ulong regionRequestGroupId, ulong targetPlayerDbId) + RegionRequestQueueCommandVar command, ulong regionRequestGroupId, ulong targetPlayerDbId, int teamSizeOverride = -1) { Logger.Trace($"HandleCommand(): command=[{command}], region=[{regionRef.GetNameFormatted()}], player=[{_player}]"); @@ -31,7 +31,7 @@ namespace MHServerEmu.PlayerManagement.Matchmaking case RegionRequestQueueCommandVar.eRRQC_AddToQueueSolo: case RegionRequestQueueCommandVar.eRRQC_AddToQueueParty: case RegionRequestQueueCommandVar.eRRQC_AddToQueueBypass: - OnAddToQueue(regionRef, difficultyTierRef, metaStateRef, command); + OnAddToQueue(regionRef, difficultyTierRef, metaStateRef, command, teamSizeOverride); break; case RegionRequestQueueCommandVar.eRRQC_RemoveFromQueue: @@ -53,7 +53,8 @@ namespace MHServerEmu.PlayerManagement.Matchmaking } } - private bool OnAddToQueue(PrototypeId regionRef, PrototypeId difficultyTierRef, PrototypeId metaStateRef, RegionRequestQueueCommandVar command) + private bool OnAddToQueue(PrototypeId regionRef, PrototypeId difficultyTierRef, PrototypeId metaStateRef, + RegionRequestQueueCommandVar command, int teamSizeOverride = -1) { RegionRequestQueue queue = PlayerManagerService.Instance.RegionRequestQueueManager.GetRegionRequestQueue(regionRef); if (queue == null) @@ -74,7 +75,7 @@ namespace MHServerEmu.PlayerManagement.Matchmaking // Create region request group MasterParty party = command == RegionRequestQueueCommandVar.eRRQC_AddToQueueParty ? _player.CurrentParty : null; - RegionRequestQueueParams queueParams = new(difficultyTierRef, metaStateRef, command == RegionRequestQueueCommandVar.eRRQC_AddToQueueBypass); + RegionRequestQueueParams queueParams = new(difficultyTierRef, metaStateRef, command == RegionRequestQueueCommandVar.eRRQC_AddToQueueBypass, teamSizeOverride); RegionRequestGroup group = RegionRequestGroup.Create(queue, queueParams, _player, party); if (group == null) diff --git a/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueueParams.cs b/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueueParams.cs index 306cae9d..90f8fc74 100644 --- a/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueueParams.cs +++ b/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueueParams.cs @@ -10,17 +10,19 @@ namespace MHServerEmu.PlayerManagement.Matchmaking public readonly PrototypeId DifficultyTierRef; public readonly PrototypeId MetaStateRef; public readonly bool IsBypass; + public readonly int TeamSizeOverride; - public RegionRequestQueueParams(PrototypeId difficultyTierRef, PrototypeId metaStateRef, bool isBypass) + public RegionRequestQueueParams(PrototypeId difficultyTierRef, PrototypeId metaStateRef, bool isBypass, int teamSizeOverride) { DifficultyTierRef = difficultyTierRef; MetaStateRef = metaStateRef; IsBypass = isBypass; + TeamSizeOverride = teamSizeOverride; } public override string ToString() { - return $"difficulty={DifficultyTierRef.GetNameFormatted()}, metaState={MetaStateRef.GetNameFormatted()}, isBypass={IsBypass}"; + return $"difficulty={DifficultyTierRef.GetNameFormatted()}, metaState={MetaStateRef.GetNameFormatted()}, isBypass={IsBypass}, teamSizeOverride={TeamSizeOverride}"; } public override int GetHashCode() @@ -40,7 +42,8 @@ namespace MHServerEmu.PlayerManagement.Matchmaking { return DifficultyTierRef == other.DifficultyTierRef && MetaStateRef == other.MetaStateRef && - IsBypass == other.IsBypass; + IsBypass == other.IsBypass && + TeamSizeOverride == other.TeamSizeOverride; } public static bool operator ==(RegionRequestQueueParams left, RegionRequestQueueParams right) diff --git a/src/MHServerEmu.PlayerManagement/PlayerManagerService.cs b/src/MHServerEmu.PlayerManagement/PlayerManagerService.cs index dca9ec71..3b576e61 100644 --- a/src/MHServerEmu.PlayerManagement/PlayerManagerService.cs +++ b/src/MHServerEmu.PlayerManagement/PlayerManagerService.cs @@ -147,6 +147,11 @@ namespace MHServerEmu.PlayerManagement statusDict["PlayerManagerPendingSessions"] = SessionManager.PendingSessionCount; } + public PlayerHandle GetPlayer(ulong playerDbId) + { + return ClientManager.GetPlayer(playerDbId); + } + private void OnRouteMessageBuffer(in ServiceMessage.RouteMessageBuffer routeMessageBuffer) { IFrontendClient client = routeMessageBuffer.Client; diff --git a/src/MHServerEmu.PlayerManagement/Players/PlayerHandle.cs b/src/MHServerEmu.PlayerManagement/Players/PlayerHandle.cs index 60f4215c..71b3b2fa 100644 --- a/src/MHServerEmu.PlayerManagement/Players/PlayerHandle.cs +++ b/src/MHServerEmu.PlayerManagement/Players/PlayerHandle.cs @@ -814,9 +814,9 @@ namespace MHServerEmu.PlayerManagement.Players } public void ReceiveRegionRequestQueueCommand(PrototypeId regionRef, PrototypeId difficultyTierRef, PrototypeId metaStateRef, - RegionRequestQueueCommandVar command, ulong regionRequestGroupId, ulong targetPlayerDbId) + RegionRequestQueueCommandVar command, ulong regionRequestGroupId, ulong targetPlayerDbId, int teamSizeOverride = -1) { - _regionRequestQueueCommandHandler.HandleCommand(regionRef, difficultyTierRef, metaStateRef, command, regionRequestGroupId, targetPlayerDbId); + _regionRequestQueueCommandHandler.HandleCommand(regionRef, difficultyTierRef, metaStateRef, command, regionRequestGroupId, targetPlayerDbId, teamSizeOverride); } public void AddToChatRoom(ChatRoomTypes roomType, ulong roomId) diff --git a/src/MHServerEmu/Commands/Implementations/PvPCommands.cs b/src/MHServerEmu/Commands/Implementations/PvPCommands.cs new file mode 100644 index 00000000..b3966644 --- /dev/null +++ b/src/MHServerEmu/Commands/Implementations/PvPCommands.cs @@ -0,0 +1,73 @@ +using Gazillion; +using MHServerEmu.Commands.Attributes; +using MHServerEmu.Core.Network; +using MHServerEmu.DatabaseAccess.Models; +using MHServerEmu.Games.GameData; +using MHServerEmu.Games.Network; +using MHServerEmu.PlayerManagement; +using MHServerEmu.PlayerManagement.Players; + +namespace MHServerEmu.Commands.Implementations +{ + [CommandGroup("pvp")] + [CommandGroupDescription("Commands related to PvP matchmaking.")] + [CommandGroupUserLevel(AccountUserLevel.User)] + public class PvPCommands : CommandGroup + { + private static readonly PrototypeId RegionRef = + GameDatabase.GetPrototypeRefByName("Metagame/DefenderPvP/Regions/PvPDefenderTier5Region.prototype"); + + private static readonly PrototypeId DifficultyRef = + GameDatabase.GetPrototypeRefByName("Difficulty/Tiers/Tier1Normal.prototype"); + + private string JoinQueue(int size, NetClient client) + { + PlayerConnection playerConnection = (PlayerConnection)client; + var player = playerConnection.Player; + if (player == null) return "Player not found."; + + bool isInParty = player.Party != null && player.Party.NumMembers > 1; + var command = isInParty + ? RegionRequestQueueCommandVar.eRRQC_AddToQueueParty + : RegionRequestQueueCommandVar.eRRQC_AddToQueueSolo; + + var playerManager = ServerManager.Instance.GetGameService(GameServiceType.PlayerManager) as PlayerManagerService; + if (playerManager == null) return "Failed to connect to the player manager."; + + PlayerHandle destPlayer = playerManager.GetPlayer(playerConnection.PlayerDbId); + destPlayer.ReceiveRegionRequestQueueCommand(RegionRef, DifficultyRef, PrototypeId.Invalid, command, 0, 0, size); + + return $"Queued for {size}v{size} PvP! ({(isInParty ? "Party" : "Solo")})"; + } + + [Command("1v1")] + [CommandDescription("Join 1v1 PvP queue.")] + [CommandUsage("pvp 1v1")] + [CommandInvokerType(CommandInvokerType.Client)] + public string Queue1v1(string[] @params, NetClient client) => JoinQueue(1, client); + + [Command("2v2")] + [CommandDescription("Join 2v2 PvP queue.")] + [CommandUsage("pvp 2v2")] + [CommandInvokerType(CommandInvokerType.Client)] + public string Queue2v2(string[] @params, NetClient client) => JoinQueue(2, client); + + [Command("3v3")] + [CommandDescription("Join 3v3 PvP queue.")] + [CommandUsage("pvp 3v3")] + [CommandInvokerType(CommandInvokerType.Client)] + public string Queue3v3(string[] @params, NetClient client) => JoinQueue(3, client); + + [Command("4v4")] + [CommandDescription("Join 4v4 PvP queue.")] + [CommandUsage("pvp 4v4")] + [CommandInvokerType(CommandInvokerType.Client)] + public string Queue4v4(string[] @params, NetClient client) => JoinQueue(4, client); + + [Command("5v5")] + [CommandDescription("Join 5v5 PvP queue.")] + [CommandUsage("pvp 5v5")] + [CommandInvokerType(CommandInvokerType.Client)] + public string Queue5v5(string[] @params, NetClient client) => JoinQueue(-1, client); + } +} From fb56fe7e50bd7451eb85dab7721e1b17fccc2272 Mon Sep 17 00:00:00 2001 From: AlexBond Date: Tue, 31 Mar 2026 18:57:59 +0300 Subject: [PATCH 28/30] Fix 5v5 output --- src/MHServerEmu/Commands/Implementations/PvPCommands.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/MHServerEmu/Commands/Implementations/PvPCommands.cs b/src/MHServerEmu/Commands/Implementations/PvPCommands.cs index b3966644..5384ea7c 100644 --- a/src/MHServerEmu/Commands/Implementations/PvPCommands.cs +++ b/src/MHServerEmu/Commands/Implementations/PvPCommands.cs @@ -31,11 +31,13 @@ namespace MHServerEmu.Commands.Implementations ? RegionRequestQueueCommandVar.eRRQC_AddToQueueParty : RegionRequestQueueCommandVar.eRRQC_AddToQueueSolo; + int limit = (size == 5) ? -1 : size; + var playerManager = ServerManager.Instance.GetGameService(GameServiceType.PlayerManager) as PlayerManagerService; if (playerManager == null) return "Failed to connect to the player manager."; PlayerHandle destPlayer = playerManager.GetPlayer(playerConnection.PlayerDbId); - destPlayer.ReceiveRegionRequestQueueCommand(RegionRef, DifficultyRef, PrototypeId.Invalid, command, 0, 0, size); + destPlayer.ReceiveRegionRequestQueueCommand(RegionRef, DifficultyRef, PrototypeId.Invalid, command, 0, 0, limit); return $"Queued for {size}v{size} PvP! ({(isInParty ? "Party" : "Solo")})"; } @@ -68,6 +70,6 @@ namespace MHServerEmu.Commands.Implementations [CommandDescription("Join 5v5 PvP queue.")] [CommandUsage("pvp 5v5")] [CommandInvokerType(CommandInvokerType.Client)] - public string Queue5v5(string[] @params, NetClient client) => JoinQueue(-1, client); + public string Queue5v5(string[] @params, NetClient client) => JoinQueue(5, client); } } From 75c78bec4c7dfa8bcd87025275d38bdc5004bc8a Mon Sep 17 00:00:00 2001 From: AlexBond Date: Tue, 31 Mar 2026 21:19:20 +0300 Subject: [PATCH 29/30] Call match from MatchQueueStatus --- .../Network/GameServiceProtocol.cs | 3 ++- src/MHServerEmu.Games/Entities/Player.cs | 4 ++-- .../Regions/MatchQueues/MatchQueueStatus.cs | 4 ++-- .../Matchmaking/Match.cs | 15 +++------------ .../Matchmaking/RegionRequestQueue.cs | 2 +- .../Network/PlayerManagerServiceMailbox.cs | 3 ++- .../PlayerManagerService.cs | 5 ----- .../Players/PlayerHandle.cs | 2 +- .../Commands/Implementations/PvPCommands.cs | 10 +--------- 9 files changed, 14 insertions(+), 34 deletions(-) diff --git a/src/MHServerEmu.Core/Network/GameServiceProtocol.cs b/src/MHServerEmu.Core/Network/GameServiceProtocol.cs index bf127589..ce727dd2 100644 --- a/src/MHServerEmu.Core/Network/GameServiceProtocol.cs +++ b/src/MHServerEmu.Core/Network/GameServiceProtocol.cs @@ -461,7 +461,7 @@ namespace MHServerEmu.Core.Network /// /// [Game -> PlayerManager] Relays a match region request command from a client. /// - public readonly struct MatchRegionRequestQueueCommand(ulong playerDbId, ulong regionProtoId, ulong difficultyTierProtoId, ulong metaStateProtoId, RegionRequestQueueCommandVar command, ulong regionRequestGroupId, ulong targetPlayerDbId) + public readonly struct MatchRegionRequestQueueCommand(ulong playerDbId, ulong regionProtoId, ulong difficultyTierProtoId, ulong metaStateProtoId, RegionRequestQueueCommandVar command, ulong regionRequestGroupId, ulong targetPlayerDbId, int teamSizeOverride) : IGameServiceMessage { public readonly ulong PlayerDbId = playerDbId; @@ -471,6 +471,7 @@ namespace MHServerEmu.Core.Network public readonly RegionRequestQueueCommandVar Command = command; public readonly ulong RegionRequestGroupId = regionRequestGroupId; public readonly ulong TargetPlayerDbId = targetPlayerDbId; + public readonly int TeamSizeOverride = teamSizeOverride; } // MatchQueueUpdate is based on PlayerMgrToGameServer.proto from 1.53 diff --git a/src/MHServerEmu.Games/Entities/Player.cs b/src/MHServerEmu.Games/Entities/Player.cs index f31a5f1a..70e794c5 100644 --- a/src/MHServerEmu.Games/Entities/Player.cs +++ b/src/MHServerEmu.Games/Entities/Player.cs @@ -2872,7 +2872,7 @@ namespace MHServerEmu.Games.Entities } public bool SendRegionRequestQueueCommandToPlayerManager(PrototypeId regionRef, PrototypeId difficultyTierRef, - RegionRequestQueueCommandVar command, ulong groupId = 0, ulong targetPlayerDbId = 0) + RegionRequestQueueCommandVar command, ulong groupId = 0, ulong targetPlayerDbId = 0, int teamSizeOverride = -1) { ulong playerDbId = DatabaseUniqueId; ulong regionProtoId = (ulong)regionRef; @@ -2904,7 +2904,7 @@ namespace MHServerEmu.Games.Entities break; } - ServiceMessage.MatchRegionRequestQueueCommand message = new(playerDbId, regionProtoId, difficultyTierProtoId, metaStateProtoId, command, groupId, targetPlayerDbId); + ServiceMessage.MatchRegionRequestQueueCommand message = new(playerDbId, regionProtoId, difficultyTierProtoId, metaStateProtoId, command, groupId, targetPlayerDbId, teamSizeOverride); ServerManager.Instance.SendMessageToService(GameServiceType.PlayerManager, message); return true; diff --git a/src/MHServerEmu.Games/Regions/MatchQueues/MatchQueueStatus.cs b/src/MHServerEmu.Games/Regions/MatchQueues/MatchQueueStatus.cs index 57d830d5..16caecad 100644 --- a/src/MHServerEmu.Games/Regions/MatchQueues/MatchQueueStatus.cs +++ b/src/MHServerEmu.Games/Regions/MatchQueues/MatchQueueStatus.cs @@ -214,7 +214,7 @@ namespace MHServerEmu.Games.Regions.MatchQueues /// Handles a request from a client. /// public bool TryRegionRequestCommand(PrototypeId regionRef, PrototypeId difficultyTierRef, - ulong groupId, RegionRequestQueueCommandVar command) + ulong groupId, RegionRequestQueueCommandVar command, int teamSizeOverride = -1) { if (regionRef == PrototypeId.Invalid) return Logger.WarnReturn(false, "TryRegionRequestCommand(): regionRef == PrototypeId.Invalid"); @@ -272,7 +272,7 @@ namespace MHServerEmu.Games.Regions.MatchQueues if (command == RegionRequestQueueCommandVar.eRRQC_AddToQueueBypass && regionProto.AllowsQueueBypass == false) return false; - _owner.SendRegionRequestQueueCommandToPlayerManager(regionRef, difficultyTierRef, command, groupId); + _owner.SendRegionRequestQueueCommandToPlayerManager(regionRef, difficultyTierRef, command, groupId, 0, teamSizeOverride); return true; } diff --git a/src/MHServerEmu.PlayerManagement/Matchmaking/Match.cs b/src/MHServerEmu.PlayerManagement/Matchmaking/Match.cs index c8841e72..fbe32b28 100644 --- a/src/MHServerEmu.PlayerManagement/Matchmaking/Match.cs +++ b/src/MHServerEmu.PlayerManagement/Matchmaking/Match.cs @@ -28,16 +28,12 @@ namespace MHServerEmu.PlayerManagement.Matchmaking int[] teamLimits = Queue.Prototype.TeamLimits; - if (queueParams.TeamSizeOverride > 0) - { - _teams.Add(new MatchTeam(0, queueParams.TeamSizeOverride)); - _teams.Add(new MatchTeam(1, queueParams.TeamSizeOverride)); - } - else if (teamLimits.HasValue()) + if (teamLimits.HasValue()) { for (int i = 0; i < teamLimits.Length; i++) { - MatchTeam team = new(i, teamLimits[i]); + int teamLimit = (queueParams.TeamSizeOverride > 0) ? queueParams.TeamSizeOverride : teamLimits[i]; + MatchTeam team = new(i, teamLimit); _teams.Add(team); } } @@ -103,11 +99,6 @@ namespace MHServerEmu.PlayerManagement.Matchmaking return true; } - public bool IsCompatibleWith(in RegionRequestQueueParams queueParams) - { - return QueueParams.TeamSizeOverride == queueParams.TeamSizeOverride; - } - public bool IsReady() { if (IsFull() == false && IsBypass == false && Queue.Prototype.QueueDoNotWaitToFull == false) diff --git a/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueue.cs b/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueue.cs index 58729d24..35b458e5 100644 --- a/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueue.cs +++ b/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueue.cs @@ -45,7 +45,7 @@ namespace MHServerEmu.PlayerManagement.Matchmaking foreach (Match match in _matches) { - if (match.IsLookingForMore() && match.IsCompatibleWith(queueParams)) + if (match.IsLookingForMore()) lfmMatches.Add(match); } diff --git a/src/MHServerEmu.PlayerManagement/Network/PlayerManagerServiceMailbox.cs b/src/MHServerEmu.PlayerManagement/Network/PlayerManagerServiceMailbox.cs index 7abfbaed..25754e66 100644 --- a/src/MHServerEmu.PlayerManagement/Network/PlayerManagerServiceMailbox.cs +++ b/src/MHServerEmu.PlayerManagement/Network/PlayerManagerServiceMailbox.cs @@ -429,9 +429,10 @@ namespace MHServerEmu.PlayerManagement.Network RegionRequestQueueCommandVar command = matchRegionRequestQueueCommand.Command; ulong regionRequestGroupId = matchRegionRequestQueueCommand.RegionRequestGroupId; ulong targetPlayerDbId = matchRegionRequestQueueCommand.TargetPlayerDbId; + int teamSizeOverride = matchRegionRequestQueueCommand.TeamSizeOverride; PlayerHandle player = _playerManager.ClientManager.GetPlayer(playerDbId); - player?.ReceiveRegionRequestQueueCommand(regionRef, difficultyTierRef, metaStateRef, command, regionRequestGroupId, targetPlayerDbId); + player?.ReceiveRegionRequestQueueCommand(regionRef, difficultyTierRef, metaStateRef, command, regionRequestGroupId, targetPlayerDbId, teamSizeOverride); return true; } diff --git a/src/MHServerEmu.PlayerManagement/PlayerManagerService.cs b/src/MHServerEmu.PlayerManagement/PlayerManagerService.cs index 3b576e61..dca9ec71 100644 --- a/src/MHServerEmu.PlayerManagement/PlayerManagerService.cs +++ b/src/MHServerEmu.PlayerManagement/PlayerManagerService.cs @@ -147,11 +147,6 @@ namespace MHServerEmu.PlayerManagement statusDict["PlayerManagerPendingSessions"] = SessionManager.PendingSessionCount; } - public PlayerHandle GetPlayer(ulong playerDbId) - { - return ClientManager.GetPlayer(playerDbId); - } - private void OnRouteMessageBuffer(in ServiceMessage.RouteMessageBuffer routeMessageBuffer) { IFrontendClient client = routeMessageBuffer.Client; diff --git a/src/MHServerEmu.PlayerManagement/Players/PlayerHandle.cs b/src/MHServerEmu.PlayerManagement/Players/PlayerHandle.cs index 71b3b2fa..be9dc490 100644 --- a/src/MHServerEmu.PlayerManagement/Players/PlayerHandle.cs +++ b/src/MHServerEmu.PlayerManagement/Players/PlayerHandle.cs @@ -814,7 +814,7 @@ namespace MHServerEmu.PlayerManagement.Players } public void ReceiveRegionRequestQueueCommand(PrototypeId regionRef, PrototypeId difficultyTierRef, PrototypeId metaStateRef, - RegionRequestQueueCommandVar command, ulong regionRequestGroupId, ulong targetPlayerDbId, int teamSizeOverride = -1) + RegionRequestQueueCommandVar command, ulong regionRequestGroupId, ulong targetPlayerDbId, int teamSizeOverride) { _regionRequestQueueCommandHandler.HandleCommand(regionRef, difficultyTierRef, metaStateRef, command, regionRequestGroupId, targetPlayerDbId, teamSizeOverride); } diff --git a/src/MHServerEmu/Commands/Implementations/PvPCommands.cs b/src/MHServerEmu/Commands/Implementations/PvPCommands.cs index 5384ea7c..9ae91f98 100644 --- a/src/MHServerEmu/Commands/Implementations/PvPCommands.cs +++ b/src/MHServerEmu/Commands/Implementations/PvPCommands.cs @@ -4,8 +4,6 @@ using MHServerEmu.Core.Network; using MHServerEmu.DatabaseAccess.Models; using MHServerEmu.Games.GameData; using MHServerEmu.Games.Network; -using MHServerEmu.PlayerManagement; -using MHServerEmu.PlayerManagement.Players; namespace MHServerEmu.Commands.Implementations { @@ -32,13 +30,7 @@ namespace MHServerEmu.Commands.Implementations : RegionRequestQueueCommandVar.eRRQC_AddToQueueSolo; int limit = (size == 5) ? -1 : size; - - var playerManager = ServerManager.Instance.GetGameService(GameServiceType.PlayerManager) as PlayerManagerService; - if (playerManager == null) return "Failed to connect to the player manager."; - - PlayerHandle destPlayer = playerManager.GetPlayer(playerConnection.PlayerDbId); - destPlayer.ReceiveRegionRequestQueueCommand(RegionRef, DifficultyRef, PrototypeId.Invalid, command, 0, 0, limit); - + player.MatchQueueStatus.TryRegionRequestCommand(RegionRef, DifficultyRef, 0, command, limit); return $"Queued for {size}v{size} PvP! ({(isInParty ? "Party" : "Solo")})"; } From 8595b65421610a2068192a852c3dfaa4d6714412 Mon Sep 17 00:00:00 2001 From: AlexBond Date: Tue, 31 Mar 2026 21:30:27 +0300 Subject: [PATCH 30/30] Add TeamSizeOverride in GetHashCode --- .../Matchmaking/RegionRequestQueueParams.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueueParams.cs b/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueueParams.cs index 90f8fc74..5cb36768 100644 --- a/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueueParams.cs +++ b/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueueParams.cs @@ -27,7 +27,7 @@ namespace MHServerEmu.PlayerManagement.Matchmaking public override int GetHashCode() { - return HashCode.Combine(DifficultyTierRef, MetaStateRef, IsBypass); + return HashCode.Combine(DifficultyTierRef, MetaStateRef, IsBypass, TeamSizeOverride); } public override bool Equals(object obj)