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/Collections/InvasiveList.cs b/src/MHServerEmu.Core/Collections/InvasiveList.cs index a2ab377a..7ff0a4a4 100644 --- a/src/MHServerEmu.Core/Collections/InvasiveList.cs +++ b/src/MHServerEmu.Core/Collections/InvasiveList.cs @@ -1,58 +1,59 @@ using System.Collections; +using System.Runtime.CompilerServices; namespace MHServerEmu.Core.Collections { public class InvasiveList { + private readonly Iterator[] _iterators; + + private readonly Stack _iteratorPool; + private Iterator _reusableIterator; + + 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]; - } + _iterators = new Iterator[maxIterators]; + + if (maxIterators > 1) + _iteratorPool = new(); - public InvasiveList(int maxIterators, int id) - { - _maxIterators = maxIterators; - _iterators = new Iterator[_maxIterators]; Id = id; } - public IEnumerable Iterate() + public IEnumerator GetEnumerator() { - var iterator = new Iterator(this); + Iterator iterator; - try + if (_iteratorPool != null) { - while (iterator.End() == false) - { - var element = iterator.Current; - iterator.MoveNext(); - yield return element; - } + if (_iteratorPool.TryPop(out iterator) == false) + iterator = new(this); } - finally + else { - UnregisterIterator(iterator); + _reusableIterator ??= new(this); + iterator = _reusableIterator; } + + iterator.Initialize(); + return iterator; } - public bool IsEmpty() => Head == null; - public void Remove(T element) { 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,20 +138,23 @@ namespace MHServerEmu.Core.Collections Count++; } - public virtual InvasiveListNode GetInvasiveListNode(T element, int listId) => null; + public virtual ref InvasiveListNode GetInvasiveListNode(T element, int listId) + { + return 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); } 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; } @@ -165,71 +169,75 @@ namespace MHServerEmu.Core.Collections _iterators[_numIterators - 1] = null; _numIterators--; + + // pool iterator instance for reuse + iterator.Reset(); + _iteratorPool?.Push(iterator); + return; } 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; + } + + public void Initialize() + { _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 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.Core/Network/GameServiceProtocol.cs b/src/MHServerEmu.Core/Network/GameServiceProtocol.cs index a115f568..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 @@ -830,6 +831,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.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..4957bab6 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 RecyclableCodedOutputStream cos = RecyclableCodedOutputStream.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/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); } } 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); + } +} diff --git a/src/MHServerEmu.Core/Serialization/RecyclableCodedOutputStream.cs b/src/MHServerEmu.Core/Serialization/RecyclableCodedOutputStream.cs new file mode 100644 index 00000000..bdbdea34 --- /dev/null +++ b/src/MHServerEmu.Core/Serialization/RecyclableCodedOutputStream.cs @@ -0,0 +1,459 @@ +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 RecyclableCodedOutputStream : ICodedOutputStreamEx, IDisposable + { + private static readonly ConcurrentBag Instances = new(); + + private readonly byte[] _primaryBuffer = new byte[CodedOutputStream.DefaultBufferSize]; + private readonly byte[] _floatBuffer = new byte[sizeof(float)]; + + private CodedOutputStream _cos; + + private RecyclableCodedOutputStream() { } + + private void Initialize(Stream stream) + { + _cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(stream, _primaryBuffer); + } + + public static RecyclableCodedOutputStream CreateInstance(Stream stream) + { + if (Instances.TryTake(out RecyclableCodedOutputStream cos) == false) + cos = new(); + + cos.Initialize(stream); + return cos; + } + + #region IDisposable + + public void Dispose() + { + if (_cos != null) + { + _cos.Flush(); + _cos = null; + } + + Instances.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.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.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; 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; 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..d16b5aa2 100644 --- a/src/MHServerEmu.Games/Entities/EntityManager.cs +++ b/src/MHServerEmu.Games/Entities/EntityManager.cs @@ -25,13 +25,13 @@ namespace MHServerEmu.Games.Entities { Simulated = 0, Locomotion = 1, - All = 3, + All = 2, // unused? } - public class EntityInvasiveCollection : InvasiveList + public sealed 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 @@ -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/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; diff --git a/src/MHServerEmu.Games/Entities/Physics/ForceSystem.cs b/src/MHServerEmu.Games/Entities/Physics/ForceSystem.cs index 306f71be..05a8418e 100644 --- a/src/MHServerEmu.Games/Entities/Physics/ForceSystem.cs +++ b/src/MHServerEmu.Games/Entities/Physics/ForceSystem.cs @@ -15,31 +15,24 @@ namespace MHServerEmu.Games.Entities.Physics } } - public class ForceSystemMemberList : InvasiveList + public sealed 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/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/Entities/Player.cs b/src/MHServerEmu.Games/Entities/Player.cs index cc6d3341..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; @@ -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); 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."); 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 69a5f1d1..17d2c516 100644 --- a/src/MHServerEmu.Games/Navi/NaviTriangle.cs +++ b/src/MHServerEmu.Games/Navi/NaviTriangle.cs @@ -11,21 +11,22 @@ 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 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(); } diff --git a/src/MHServerEmu.Games/Network/MigrationUtility.cs b/src/MHServerEmu.Games/Network/MigrationUtility.cs index e642fd89..2fcf3e1a 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/Network/PlayerConnection.cs b/src/MHServerEmu.Games/Network/PlayerConnection.cs index 35a5b9e1..4e275c36 100644 --- a/src/MHServerEmu.Games/Network/PlayerConnection.cs +++ b/src/MHServerEmu.Games/Network/PlayerConnection.cs @@ -259,9 +259,18 @@ 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(); + + // 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 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 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 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/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/Matchmaking/Match.cs b/src/MHServerEmu.PlayerManagement/Matchmaking/Match.cs index 99c1ec0c..fbe32b28 100644 --- a/src/MHServerEmu.PlayerManagement/Matchmaking/Match.cs +++ b/src/MHServerEmu.PlayerManagement/Matchmaking/Match.cs @@ -27,11 +27,13 @@ namespace MHServerEmu.PlayerManagement.Matchmaking QueueParams = queueParams; int[] teamLimits = Queue.Prototype.TeamLimits; + 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); } } 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..5cb36768 100644 --- a/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueueParams.cs +++ b/src/MHServerEmu.PlayerManagement/Matchmaking/RegionRequestQueueParams.cs @@ -10,22 +10,24 @@ 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() { - return HashCode.Combine(DifficultyTierRef, MetaStateRef, IsBypass); + return HashCode.Combine(DifficultyTierRef, MetaStateRef, IsBypass, TeamSizeOverride); } public override bool Equals(object obj) @@ -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/Network/PlayerManagerServiceMailbox.cs b/src/MHServerEmu.PlayerManagement/Network/PlayerManagerServiceMailbox.cs index ead02804..25754e66 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; @@ -425,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; } @@ -627,6 +632,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.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..24225974 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,24 +16,55 @@ 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 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 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; + + private readonly Dictionary _reconnectPermissions = new(); + private CooldownTimer _reconnectPermissionPurgeTimer = new(TimeSpan.FromMinutes(1)); + + 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() { TimeOutPendingClients(); + PurgeReconnectPermissions(); AcceptNewClients(); ProcessLoginQueue(); } @@ -72,13 +104,29 @@ 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) + { + _reconnectPermissions.Remove(kvp.Key); + Logger.Info($"Reconnect permission expired for account 0x{kvp.Key:X}"); + } + } + } + /// /// Accepts asynchronously added clients to the login queue. /// private void AcceptNewClients() { - int maxLoginQueueClients = _playerManager.Config.MaxLoginQueueClients; - _newClientQueue.Swap(); while (_newClientQueue.CurrentCount > 0) @@ -98,21 +146,29 @@ 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) + 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); - } + LinkedList queueToUse = _defaultQueue; + if (_reconnectPermissions.Remove(client.DbId)) + { + queueToUse = _reconnectQueue; + Logger.Info($"Consumed reconnect permission for client [{client}]"); + } + + queueNode.Value = client; + queueToUse.AddLast(queueNode); + } Logger.Info($"Accepted client [{client}] into the login queue"); } @@ -127,37 +183,40 @@ 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(); - ProcessQueuedClient(client, ref availableCapacity); + IFrontendClient client = _highPriorityQueue.Dequeue(); + ProcessQueuedClient(client, ref availableCapacity, false); } - // 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, false); + ProcessQueue(_defaultQueue, totalCapacity, ref availableCapacity, true); + + // 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) { - // 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 @@ -165,8 +224,27 @@ namespace MHServerEmu.PlayerManagement.Players return false; } - private bool ProcessQueuedClient(IFrontendClient client, ref int availableCapacity) + private void ProcessQueue(LinkedList queue, int totalCapacity, ref int availableCapacity, bool allowReconnect) { + while (queue.Count > 0 && (totalCapacity <= 0 || availableCapacity > 0)) + { + LinkedListNode queueNode = queue.First; + IFrontendClient client = queueNode.Value; + + RemoveQueueNode(queueNode); + ProcessQueuedClient(client, ref availableCapacity, allowReconnect); + } + } + + private bool ProcessQueuedClient(IFrontendClient client, ref int availableCapacity, bool allowReconnect) + { + // Allow all clients that pass the default queue to reconnect because of the client-side afk timer bug. + if (allowReconnect) + { + _reconnectPermissions[client.DbId] = Clock.UnixTime; + Logger.Info($"Added reconnect permission for client [{client}]"); + } + if (client.IsConnected == false) { Logger.Warn($"ProcessQueuedClient(): Client [{client}] disconnected while waiting in the login queue"); @@ -178,10 +256,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"); @@ -190,6 +266,47 @@ namespace MHServerEmu.PlayerManagement.Players return true; } + 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); + } + private void RemoveClientSession(IFrontendClient client) { ulong sessionId = client.Session.Id; diff --git a/src/MHServerEmu.PlayerManagement/Players/PlayerHandle.cs b/src/MHServerEmu.PlayerManagement/Players/PlayerHandle.cs index 60f4215c..be9dc490 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) { - _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/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; diff --git a/src/MHServerEmu/Commands/Implementations/PvPCommands.cs b/src/MHServerEmu/Commands/Implementations/PvPCommands.cs new file mode 100644 index 00000000..9ae91f98 --- /dev/null +++ b/src/MHServerEmu/Commands/Implementations/PvPCommands.cs @@ -0,0 +1,67 @@ +using Gazillion; +using MHServerEmu.Commands.Attributes; +using MHServerEmu.Core.Network; +using MHServerEmu.DatabaseAccess.Models; +using MHServerEmu.Games.GameData; +using MHServerEmu.Games.Network; + +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; + + int limit = (size == 5) ? -1 : size; + player.MatchQueueStatus.TryRegionRequestCommand(RegionRef, DifficultyRef, 0, command, limit); + 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(5, client); + } +} 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")] 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]