Compare commits

...

32 commits

Author SHA1 Message Date
Crypto137
9ed41d43a8
Merge pull request #199 from AlexBond2/pvp
PvP Commands
2026-03-31 21:33:52 +03:00
AlexBond
8595b65421 Add TeamSizeOverride in GetHashCode 2026-03-31 21:30:27 +03:00
AlexBond
75c78bec4c Call match from MatchQueueStatus 2026-03-31 21:19:20 +03:00
AlexBond
fb56fe7e50 Fix 5v5 output 2026-03-31 18:57:59 +03:00
AlexBond
34b059fc96 Implement PvPCommands 2026-03-31 18:49:29 +03:00
Crypto137
088590eca2 Adjust DisableMissionXPBonuses setting behavior 2026-03-28 17:27:14 +03:00
Crypto137
b3e9fed249 Fix DifficultyTier property not being set on spawns and summons 2026-03-27 03:05:25 +03:00
Crypto137
4ee8419132 Remove percentile based login queue reconnect permissions 2026-03-25 18:57:14 +03:00
Crypto137
5c9b4b7aff Add missing semicolon 2026-03-25 18:46:36 +03:00
Crypto137
74e39fb00a Adjust resource migration filter 2026-03-25 18:42:06 +03:00
Crypto137
346965116f Implemented !server whitelist command 2026-03-25 17:53:26 +03:00
Crypto137
a3551c8ab8 Add EnsureCapacity() call to AchievementState.Serialize() 2026-03-24 20:45:02 +03:00
Crypto137
c38018316b Add reusable picker for IntervalPowersRandomTarget hotspots 2026-03-24 20:29:50 +03:00
Crypto137
5acbeb2602 Remove reconnect permissions from the reconnect login queue 2026-03-24 18:11:01 +03:00
Crypto137
32ec15fd1a Expand login queue reconnects to clients that successfully pass the queue 2026-03-24 17:54:41 +03:00
Crypto137
e0508fb29c
Merge pull request #196 from Crypto137/dev
Optimization pass and login queue improvements
2026-03-23 18:32:19 +03:00
Crypto137
cc241e6325 Cache SessionEncryptionChanged message in LoginQueueManager 2026-03-23 18:30:37 +03:00
Crypto137
b93e433227 Implement login queue reconnects 2026-03-23 18:26:21 +03:00
Crypto137
c29d8634ad Implement BypassLoginQueue account flag 2026-03-23 17:32:52 +03:00
Crypto137
66233d2fbe Overhaul LoginQueueManager 2026-03-23 17:23:38 +03:00
Crypto137
877adaf80d Remove SequenceEqual check when serializing player entity blobs 2026-03-23 15:21:22 +03:00
Crypto137
8740e60e35 Rename FastCodedOutputStream to RecyclableCodedOutputStream and remove ArrayPool 2026-03-23 15:09:35 +03:00
Crypto137
08a875b69f Implement InvasiveList iterator pooling 2026-03-23 14:23:11 +03:00
Crypto137
c62be10413 Fix index for EntityCollection.All 2026-03-23 13:50:27 +03:00
Crypto137
a934f07f3a Overhaul InvasiveList iteration 2026-03-23 13:39:19 +03:00
Crypto137
03839a2699 Optimize InvasiveListNode 2026-03-23 00:19:22 +03:00
Crypto137
be411b2684 Optimize array allocations when saving player entity blobs 2026-03-22 20:58:21 +03:00
Crypto137
fc83aa151b Increase initial size for Archive.SharedAutoBuffer 2026-03-22 20:46:57 +03:00
Crypto137
1c75103166 Optimize allocations when writing float fields in protobuf messages 2026-03-22 20:37:27 +03:00
Crypto137
7f3837583d Disable EventScheduler trigger time logging in Release builds 2026-03-21 23:43:12 +03:00
Crypto137
85cd903586 Remove null region logging in Player.DiscoverMapPosition() 2026-03-21 23:27:34 +03:00
Crypto137
641c35258f Change loot context for the !item give command to CashShop 2026-03-20 14:40:08 +03:00
40 changed files with 1001 additions and 218 deletions

View file

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

View file

@ -1,58 +1,59 @@
using System.Collections;
using System.Runtime.CompilerServices;
namespace MHServerEmu.Core.Collections
{
public class InvasiveList<T>
{
private readonly Iterator[] _iterators;
private readonly Stack<Iterator> _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<T> Iterate()
public IEnumerator<T> 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<T> GetInvasiveListNode(T element, int listId) => null;
public virtual ref InvasiveListNode<T> GetInvasiveListNode(T element, int listId)
{
return ref Unsafe.NullRef<InvasiveListNode<T>>();
}
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<T>
public sealed class Iterator : IEnumerator<T>
{
private InvasiveList<T> _list;
public bool SkipNext { get; set; }
private readonly InvasiveList<T> _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<T> 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<T>
public struct InvasiveListNode<T>
{
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<T>
{
private readonly InvasiveListNode<T>[] _nodes;
private int _numLists;
public InvasiveListNodeCollection(int numLists)
{
_numLists = numLists;
_nodes = new InvasiveListNode<T>[_numLists];
for (int i = 0; i < _numLists; i++)
_nodes[i] = new();
}
public InvasiveListNode<T> GetInvasiveListNode(int listIndex)
{
if (listIndex >= 0 && listIndex < _numLists)
return _nodes[listIndex];
else
return null;
}
}
}

View file

@ -461,7 +461,7 @@ namespace MHServerEmu.Core.Network
/// <summary>
/// [Game -> PlayerManager] Relays a match region request command from a client.
/// </summary>
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
}
}

View file

@ -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);
}
/// <summary>
/// Writes this <see cref="MessagePackageOut"/> to the provided <see cref="ICodedOutputStreamEx"/>.
/// </summary>
public void WriteTo(ICodedOutputStreamEx stream)
{
stream.WriteRawVarint32(Id);
stream.WriteRawVarint32((uint)Message.SerializedSize);
Message.WriteTo(stream);
}
}
}

View file

@ -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<List<MessagePackageOut>> MessageListPool = new(4096, static () => new(4096));
private static readonly ArrayPool<byte> BufferPool = ArrayPool<byte>.Create();
private readonly List<MessagePackageOut> _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;
}

View file

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

View file

@ -0,0 +1,18 @@
using Google.ProtocolBuffers;
namespace MHServerEmu.Core.Serialization
{
/// <summary>
/// Extended version of <see cref="ICodedOutputStream"/> that exposes additional low level writing functionality.
/// </summary>
public interface ICodedOutputStreamEx : ICodedOutputStream
{
void WriteRawVarint32(uint value);
void WriteRawVarint64(ulong value);
void WriteRawByte(byte value);
void WriteRawBytes(byte[] value);
}
}

View file

@ -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
{
/// <summary>
/// A more memory efficient version of <see cref="CodedOutputStream"/>.
/// </summary>
public sealed class RecyclableCodedOutputStream : ICodedOutputStreamEx, IDisposable
{
private static readonly ConcurrentBag<RecyclableCodedOutputStream> 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<bool> 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<ByteString> 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<double> 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<T>(int fieldNumber, string fieldName, IEnumerable<T> 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<uint> 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<ulong> 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<byte, float>(_floatBuffer)[0] = value;
_cos.WriteRawBytes(_floatBuffer, 0, 4);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFloatArray(int fieldNumber, string fieldName, IEnumerable<float> 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<T>(int fieldNumber, string fieldName, IEnumerable<T> 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<int> 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<long> 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<T>(int fieldNumber, string fieldName, IEnumerable<T> 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<bool> list)
{
_cos.WritePackedBoolArray(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedDoubleArray(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<double> list)
{
_cos.WritePackedDoubleArray(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedEnumArray<T>(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<T> 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<uint> list)
{
_cos.WritePackedFixed32Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedFixed64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<ulong> list)
{
_cos.WritePackedFixed64Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedFloatArray(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<float> list)
{
_cos.WritePackedFloatArray(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedInt32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<int> list)
{
_cos.WritePackedInt32Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedInt64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<long> list)
{
_cos.WritePackedInt64Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedSFixed32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<int> list)
{
_cos.WritePackedSFixed32Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedSFixed64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<long> list)
{
_cos.WritePackedSFixed64Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedSInt32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<int> list)
{
_cos.WritePackedSInt32Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedSInt64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<long> list)
{
_cos.WritePackedSInt64Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedUInt32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<uint> list)
{
_cos.WritePackedUInt32Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedUInt64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<ulong> 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<int> 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<long> 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<int> 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<long> 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<string> 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<uint> 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<ulong> 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
}
}

View file

@ -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,
}
/// <summary>

View file

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

View file

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

View file

@ -106,7 +106,7 @@ namespace MHServerEmu.Games.Entities
private static readonly Logger Logger = LogManager.CreateLogger();
private readonly InvasiveListNodeCollection<Entity> _entityListNodes = new(3);
private InlineArray3<InvasiveListNode<Entity>> _entityListNodes;
private readonly EventGroup _pendingEvents = new();
@ -1307,9 +1307,15 @@ namespace MHServerEmu.Games.Entities
};
}
public InvasiveListNode<Entity> GetInvasiveListNode(int listId)
public ref InvasiveListNode<Entity> 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<InvasiveListNode<Entity>>();
}
#endregion

View file

@ -25,13 +25,13 @@ namespace MHServerEmu.Games.Entities
{
Simulated = 0,
Locomotion = 1,
All = 3,
All = 2, // unused?
}
public class EntityInvasiveCollection : InvasiveList<Entity>
public sealed class EntityInvasiveCollection : InvasiveList<Entity>
{
public EntityInvasiveCollection(EntityCollection collectionType, int maxIterators = 8) : base(maxIterators, (int)collectionType) { }
public override InvasiveListNode<Entity> GetInvasiveListNode(Entity element, int listId) => element.GetInvasiveListNode(listId);
public override ref InvasiveListNode<Entity> 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)

View file

@ -49,6 +49,8 @@ namespace MHServerEmu.Games.Entities
private int _activePowerTargetCount;
private bool _killSelf;
private Picker<ulong> _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<ulong> picker = new(Game.Random);
Picker<ulong> picker = _targetPicker;
var hasLOS = TriBool.Undefined;
ulong prevTargetId = InvalidId;

View file

@ -15,31 +15,24 @@ namespace MHServerEmu.Games.Entities.Physics
}
}
public class ForceSystemMemberList : InvasiveList<ForceSystemMember>
public sealed class ForceSystemMemberList : InvasiveList<ForceSystemMember>
{
public ForceSystemMemberList(int maxIterators = 1) : base(maxIterators) { }
public override InvasiveListNode<ForceSystemMember> GetInvasiveListNode(ForceSystemMember element, int listId) => element.InvasiveListNode;
public override ref InvasiveListNode<ForceSystemMember> GetInvasiveListNode(ForceSystemMember element, int listId) => ref element.InvasiveListNode;
}
public class ForceSystemMember
{
private InvasiveListNode<ForceSystemMember> _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<ForceSystemMember> InvasiveListNode { get; private set; }
public ref InvasiveListNode<ForceSystemMember> 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() { }
}
}

View file

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

View file

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

View file

@ -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.");

View file

@ -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<NaviEdge>.Instance.Get(out List<NaviEdge> 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<NaviPoint> 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);

View file

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

View file

@ -11,21 +11,22 @@ namespace MHServerEmu.Games.Navi
Markup = 1 << 1,
}
public class TriangleList : InvasiveList<NaviTriangle>
public sealed class TriangleList : InvasiveList<NaviTriangle>
{
public TriangleList(int maxIterators = 1) : base(maxIterators) { }
public override InvasiveListNode<NaviTriangle> GetInvasiveListNode(NaviTriangle element, int listId) => element.InvasiveListNode;
public override ref InvasiveListNode<NaviTriangle> GetInvasiveListNode(NaviTriangle element, int listId) => ref element.InvasiveListNode;
}
public class NaviTriangle
{
private InlineArray3<NaviEdge> _edges;
private InvasiveListNode<NaviTriangle> _invasiveListNode;
public ref InlineArray3<NaviEdge> Edges { get => ref _edges; }
public byte EdgeSideFlags { get; private set; }
public NaviTriangleFlags Flags { get; private set; }
public PathFlags PathingFlags { get; set; }
public InvasiveListNode<NaviTriangle> InvasiveListNode { get; private set; }
public ref InvasiveListNode<NaviTriangle> 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();
}

View file

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

View file

@ -259,9 +259,18 @@ namespace MHServerEmu.Games.Network
using (Archive archive = new(ArchiveSerializeType.Database))
{
DBPlayer dbPlayer = _dbAccount.Player;
Span<byte> oldArchiveData = dbPlayer.ArchiveData ?? Span<byte>.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<byte> 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

View file

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

View file

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

View file

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

View file

@ -214,7 +214,7 @@ namespace MHServerEmu.Games.Regions.MatchQueues
/// Handles a <see cref="RegionRequestQueueCommandVar"/> request from a client.
/// </summary>
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;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -49,6 +49,8 @@ namespace MHServerEmu.PlayerManagement
/// </summary>
public PlayerManagerService()
{
Config = ConfigManager.Instance.GetConfig<PlayerManagerConfig>();
_serviceMailbox = new(this);
SessionManager = new(this);
@ -62,8 +64,6 @@ namespace MHServerEmu.PlayerManagement
RegionRequestQueueManager = new(this);
EventScheduler = new();
Config = ConfigManager.Instance.GetConfig<PlayerManagerConfig>();
}
#region IGameService Implementation

View file

@ -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<IFrontendClient> _newClientQueue = new();
private readonly Queue<IFrontendClient> _loginQueue = new();
private readonly Queue<IFrontendClient> _highPriorityLoginQueue = new();
private readonly LinkedList<IFrontendClient> _defaultQueue = new();
private readonly LinkedList<IFrontendClient> _reconnectQueue = new();
private readonly Queue<IFrontendClient> _highPriorityQueue = new();
private readonly Stack<LinkedListNode<IFrontendClient>> _queueNodes;
// Pending clients are clients that have successfully passed the login queue
private readonly Dictionary<IFrontendClient, TimeSpan> _pendingClients = new();
private readonly PlayerManagerService _playerManager;
private readonly LoginQueueStatus.Builder _statusBuilder = LoginQueueStatus.CreateBuilder();
private readonly Dictionary<IFrontendClient, TimeSpan> _statusUpdateTimes;
private readonly Dictionary<ulong, TimeSpan> _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<IFrontendClient> 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}");
}
}
}
/// <summary>
/// Accepts asynchronously added clients to the login queue.
/// </summary>
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<IFrontendClient> 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<IFrontendClient> 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<IFrontendClient> queue, int totalCapacity, ref int availableCapacity, bool allowReconnect)
{
while (queue.Count > 0 && (totalCapacity <= 0 || availableCapacity > 0))
{
LinkedListNode<IFrontendClient> 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<IFrontendClient> queue, ref ulong nextPlaceInLine, TimeSpan now)
{
LinkedListNode<IFrontendClient> 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<IFrontendClient> 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<IFrontendClient> node)
{
_statusUpdateTimes.Remove(node.Value);
node.Remove();
_queueNodes.Push(node);
}
private void RemoveClientSession(IFrontendClient client)
{
ulong sessionId = client.Session.Id;

View file

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

View file

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

View file

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

View file

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

View file

@ -87,7 +87,7 @@ UseWhitelist=false
ShowNewsOnLogin=false
NewsUrl=http://localhost/news/index.html
ServerCapacity=0
MaxLoginQueueClients=10000
MaxLoginQueueClients=8192
EnableTownPlayerLimit=false
[SQLiteDBManager]