Merge branch 'main' into webSupport

This commit is contained in:
Kamron Batman 2021-10-04 15:31:43 -07:00 committed by GitHub
commit e933198a85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1681 additions and 496 deletions

View file

@ -55,7 +55,8 @@ namespace Server
public static readonly bool IsDarwin = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
public static readonly bool IsFreeBSD = RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD);
public static readonly bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || IsFreeBSD;
public static readonly bool Unix = IsDarwin || IsFreeBSD || IsLinux;
public static readonly bool IsBSD = IsDarwin || IsFreeBSD;
public static readonly bool Unix = IsBSD || IsLinux;
private const string AssembliesConfiguration = "Data/assemblies.json";
@ -516,7 +517,6 @@ namespace Server
// Handle networking
TcpServer.Slice();
NetState.HandleAllReceives();
NetState.Slice();
// Execute captured post-await methods (like Timer.Pause)

View file

@ -23,6 +23,8 @@ namespace Server.Network
{
public interface ISocket
{
public IntPtr Handle { get; }
public EndPoint LocalEndPoint { get; }
public EndPoint RemoteEndPoint { get; }
@ -33,6 +35,8 @@ namespace Server.Network
public Task<int> ReceiveAsync(IList<ArraySegment<byte>> buffer, SocketFlags flags);
public int Receive(IList<ArraySegment<byte>> buffers, SocketFlags flags);
public void Shutdown(SocketShutdown how);
public void Close();

View file

@ -32,7 +32,7 @@ namespace Server.Network
foreach (var ns in TcpServer.Instances)
{
file.WriteLine($"{ns}, {ns._recvState}, {ns._sendState}, {ns._protocolState}, {ns._parserState}");
file.WriteLine($"{ns}, {ns._protocolState}, {ns._parserState}");
}
}
}

303
Projects/Server/Network/NetState/NetState.cs Normal file → Executable file
View file

@ -21,7 +21,7 @@ using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Runtime.InteropServices;
using Server.Accounting;
using Server.Diagnostics;
using Server.Gumps;
@ -43,12 +43,15 @@ namespace Server.Network
private const int RecvPipeSize = 1024 * 64;
private const int SendPipeSize = 1024 * 256;
private static int GumpCap = 512;
private static int HuePickerCap = 512;
private static int MenuCap = 512;
private static int PacketPerSecondThreshold = 3000;
private const int GumpCap = 512;
private const int HuePickerCap = 512;
private const int MenuCap = 512;
private const int PacketPerSecondThreshold = 3000;
private static NetState[] _polledStates = new NetState[2048];
private static readonly IPollGroup _pollGroup = PollGroup.Create();
private static readonly Queue<NetState> FlushPending = new(2048);
private static readonly Queue<NetState> FlushedPartials = new(2048);
private static readonly ConcurrentQueue<NetState> Disposed = new();
public static NetStateCreatedCallback CreatedCallback { get; set; }
@ -56,7 +59,7 @@ namespace Server.Network
private readonly string _toString;
private ClientVersion _version;
private long _nextActivityCheck;
private int _running;
private bool _running = true;
private volatile DecodePacket _packetDecoder;
private volatile EncodePacket _packetEncoder;
private bool _flushQueued;
@ -66,14 +69,12 @@ namespace Server.Network
internal int _authId;
internal int _seed;
internal ParserState _parserState = ParserState.Uninitialized;
internal ProtocolState _protocolState = ProtocolState.Uninitialized;
internal RecvState _recvState = RecvState.Uninitialized;
internal SendState _sendState = SendState.Uninitialized;
internal ParserState _parserState = ParserState.AwaitingNextPacket;
internal ProtocolState _protocolState = ProtocolState.AwaitingSeed;
internal GCHandle _handle;
internal enum ParserState
{
Uninitialized,
AwaitingNextPacket,
AwaitingPartialPacket,
ProcessingPacket,
@ -83,7 +84,6 @@ namespace Server.Network
internal enum ProtocolState
{
Uninitialized,
AwaitingSeed, // Based on the way the seed arrives, we know if this is a login server or a game server connection
LoginServer_AwaitingLogin,
@ -96,32 +96,6 @@ namespace Server.Network
Error
}
internal enum RecvState
{
Uninitialized,
AwaitingMemory,
AwaitingRecv,
DataReceived,
Exited,
}
internal enum SendState
{
Uninitialized,
AwaitingData,
Sending,
SendCompleted,
Exited,
}
public static void Configure()
{
GumpCap = ServerConfiguration.GetOrUpdateSetting("netstate.gumpCap", GumpCap);
HuePickerCap = ServerConfiguration.GetOrUpdateSetting("netstate.huePickerCap", HuePickerCap);
MenuCap = ServerConfiguration.GetOrUpdateSetting("netstate.menuCap", MenuCap);
PacketPerSecondThreshold = ServerConfiguration.GetOrUpdateSetting("netstate.packetsPerSecondThreshold", PacketPerSecondThreshold);
}
public static void Initialize()
{
Timer.DelayCall(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1.5), CheckAllAlive);
@ -138,6 +112,7 @@ namespace Server.Network
RecvPipe = new Pipe<byte>(GC.AllocateUninitializedArray<byte>(RecvPipeSize));
SendPipe = new Pipe<byte>(GC.AllocateUninitializedArray<byte>(SendPipeSize));
_nextActivityCheck = Core.TickCount + 30000;
ConnectedOn = Core.Now;
try
{
@ -151,7 +126,17 @@ namespace Server.Network
_toString = "(error)";
}
ConnectedOn = Core.Now;
_handle = GCHandle.Alloc(this);
try
{
_pollGroup.Add(this);
}
catch (Exception ex)
{
TraceException(ex);
Disconnect("Unable to add socket to poll group");
}
CreatedCallback?.Invoke(this);
}
@ -180,12 +165,6 @@ namespace Server.Network
public List<SecureTrade> Trades { get; }
public bool Running
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _running == 1;
}
public bool Seeded { get; set; }
public Pipe<byte> RecvPipe { get; }
@ -524,33 +503,21 @@ namespace Server.Network
}
}
internal void Start()
{
if (Interlocked.CompareExchange(ref _running, 1, 0) == 1 || Connection == null)
{
return;
}
_parserState = ParserState.AwaitingNextPacket;
_protocolState = ProtocolState.AwaitingSeed;
ThreadPool.UnsafeQueueUserWorkItem(RecvTask, null);
ThreadPool.UnsafeQueueUserWorkItem(SendTask, null);
}
public void HandleReceive()
{
if (!Running)
if (!_running)
{
return;
}
ReceiveData();
var reader = RecvPipe.Reader;
try
{
// Process as many packets as we can synchronously
while (Running && _parserState != ParserState.Error && _protocolState != ProtocolState.Error)
while (_running && _parserState != ParserState.Error && _protocolState != ProtocolState.Error)
{
var result = reader.TryRead();
var length = result.Length;
@ -574,12 +541,6 @@ namespace Server.Network
{
switch (_protocolState)
{
case ProtocolState.Uninitialized:
{
HandleError(packetId, packetLength);
return;
}
case ProtocolState.AwaitingSeed:
{
if (packetId == 0xEF)
@ -803,83 +764,56 @@ namespace Server.Network
return ParserState.AwaitingNextPacket;
}
private async void SendTask(object state)
private bool Flush()
{
_flushQueued = false;
if (Connection == null)
{
return true;
}
SendPipe.Writer.Flush();
var reader = SendPipe.Reader;
var result = reader.TryRead();
if (result.IsClosed || result.Length == 0)
{
return true;
}
var bytesWritten = 0;
try
{
while (Running)
{
_sendState = SendState.AwaitingData;
var result = await reader.Read();
if (result.IsClosed || !Running)
{
break;
}
if (result.Length <= 0)
{
continue;
}
_sendState = SendState.Sending;
var bytesWritten = await Connection.SendAsync(result.Buffer, SocketFlags.None);
_sendState = SendState.SendCompleted;
if (bytesWritten > 0)
{
_nextActivityCheck = Core.TickCount + 90000;
reader.Advance((uint)bytesWritten);
}
}
// Grab any remaining data and flush it
var data = reader.TryRead();
if (data.Length > 0)
{
_sendState = SendState.Sending;
Connection.Send(data.Buffer, SocketFlags.None);
reader.Advance((uint)data.Length);
}
bytesWritten = Connection.Send(result.Buffer, SocketFlags.None);
}
catch (SocketException ex)
{
// If the user closes the connection (or the recv side does)
// between the check for m_Running above and the call to SendAsync,
// we can still get a socket exception here. That's ok.
// Socket exceptions are generally ok, just spammy
#if DEBUG
Console.WriteLine(ex);
#endif
Disconnect(string.Empty);
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine(ex);
#endif
Disconnect($"Disconnected with error: {ex}");
TraceException(ex);
}
finally
{
try
{
Connection.Shutdown(SocketShutdown.Both);
Connection.Close();
}
catch (Exception ex)
{
TraceException(ex);
}
Disconnect("Exiting SendTask.");
_sendState = SendState.Exited;
if (bytesWritten > 0)
{
_nextActivityCheck = Core.TickCount + 90000;
reader.Advance((uint)bytesWritten);
}
return bytesWritten == result.Length;
}
private void DecodePacket(ArraySegment<byte>[] buffer, ref int length)
@ -888,47 +822,21 @@ namespace Server.Network
_packetDecoder?.Invoke(cBuffer, ref length);
}
private async void RecvTask(object state)
private void ReceiveData()
{
var socket = Connection;
var writer = RecvPipe.Writer;
var result = writer.TryGetMemory();
if (result.IsClosed || result.Length == 0)
{
return;
}
var bytesWritten = 0;
try
{
while (Running)
{
_recvState = RecvState.AwaitingMemory;
var result = await writer.GetMemory();
if (result.IsClosed || !Running)
{
break;
}
if (result.Length <= 0)
{
continue;
}
_recvState = RecvState.AwaitingRecv;
var bytesWritten = await socket.ReceiveAsync(result.Buffer, SocketFlags.None);
if (bytesWritten <= 0)
{
break;
}
_recvState = RecvState.DataReceived;
DecodePacket(result.Buffer, ref bytesWritten);
writer.Advance((uint)bytesWritten);
_nextActivityCheck = Core.TickCount + 90000;
// No need to flush
}
Disconnect(string.Empty);
bytesWritten = Connection.Receive(result.Buffer, SocketFlags.None);
}
catch (SocketException ex)
{
@ -945,31 +853,20 @@ namespace Server.Network
#if DEBUG
Console.WriteLine(ex);
#endif
Disconnect("RecvTask exited unexpectedly.");
Disconnect($"Disconnected with error: {ex}");
TraceException(ex);
}
finally
{
_recvState = RecvState.Exited;
}
}
public static void HandleAllReceives()
{
foreach (var ns in TcpServer.Instances)
if (bytesWritten <= 0)
{
ns.HandleReceive();
}
}
public void Flush()
{
if (Connection != null)
{
SendPipe.Writer.Flush();
Disconnect(string.Empty);
return;
}
_flushQueued = false;
DecodePacket(result.Buffer, ref bytesWritten);
writer.Advance((uint)bytesWritten);
_nextActivityCheck = Core.TickCount + 90000;
}
public static void FlushAll()
@ -982,16 +879,36 @@ namespace Server.Network
public static void Slice()
{
while (FlushPending.Count != 0)
int count = _pollGroup.Poll(ref _polledStates);
if (count > 0)
{
FlushPending.Dequeue()?.Flush();
for (int i = 0; i < count; i++)
{
_polledStates[i].HandleReceive();
_polledStates[i] = null;
}
}
while (FlushPending.TryDequeue(out var ns))
{
if (!ns.Flush())
{
// Incomplete data, so we need to requeue
FlushedPartials.Enqueue(ns);
}
}
var hasDisposes = !Disposed.IsEmpty;
while (Disposed.TryDequeue(out var ns))
{
TcpServer.Instances.Remove(ns);
ns.Dispose();
}
if (hasDisposes)
{
_pollGroup.Poll(ref _polledStates);
}
}
public void CheckAlive(long curTicks)
@ -1045,11 +962,13 @@ namespace Server.Network
public void Disconnect(string reason)
{
if (Interlocked.CompareExchange(ref _running, 0, 1) == 0)
if (!_running)
{
return;
}
_running = false;
try
{
if (_disconnectReason != string.Empty)
@ -1063,7 +982,6 @@ namespace Server.Network
}
_disconnectReason = reason;
Disposed.Enqueue(this);
}
@ -1095,6 +1013,11 @@ namespace Server.Network
{
TraceDisconnect(_disconnectReason, _toString);
if (_running)
{
throw new Exception("Disconnected a NetState that is still running.");
}
#if THREADGUARD
if (Thread.CurrentThread != Core.Thread)
{
@ -1106,8 +1029,18 @@ namespace Server.Network
}
#endif
RecvPipe.Writer.Close();
SendPipe.Writer.Close();
TcpServer.Instances.Remove(this);
try
{
_pollGroup.Remove(this);
}
catch (Exception ex)
{
TraceException(ex);
}
Connection.Close();
_handle.Free();
var m = Mobile;
Mobile = null;

View file

@ -32,6 +32,12 @@ namespace Server.Network
get => _connection;
}
public IntPtr Handle
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _connection.Handle;
}
public EndPoint LocalEndPoint
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -58,6 +64,10 @@ namespace Server.Network
public Task<int> ReceiveAsync(IList<ArraySegment<byte>> buffers, SocketFlags flags) =>
_connection.ReceiveAsync(buffers, flags);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Receive(IList<ArraySegment<byte>> buffers, SocketFlags flags) =>
_connection.Receive(buffers, flags);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Shutdown(SocketShutdown how) => _connection.Shutdown(how);

View file

@ -0,0 +1,206 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2021 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: EPollGroup.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Runtime.InteropServices;
namespace Server.Network
{
public sealed class EPollGroup : IPollGroup
{
[Flags]
private enum epoll_flags
{
NONE = 0,
CLOEXEC = 0x02000000,
NONBLOCK = 0x04000,
}
[Flags]
private enum epoll_events : uint
{
EPOLLIN = 0x001,
EPOLLPRI = 0x002,
EPOLLOUT = 0x004,
EPOLLRDNORM = 0x040,
EPOLLRDBAND = 0x080,
EPOLLWRNORM = 0x100,
EPOLLWRBAND = 0x200,
EPOLLMSG = 0x400,
EPOLLERR = 0x008,
EPOLLHUP = 0x010,
EPOLLRDHUP = 0x2000,
EPOLLONESHOT = 1 << 30,
EPOLLET = unchecked((uint)(1 << 31))
}
private enum epoll_op
{
EPOLL_CTL_ADD = 1,
EPOLL_CTL_DEL = 2,
EPOLL_CTL_MOD = 3,
}
[StructLayout(LayoutKind.Explicit)]
private struct epoll_data
{
[FieldOffset(0)]
public int fd;
[FieldOffset(0)]
public IntPtr ptr;
[FieldOffset(0)]
public uint u32;
[FieldOffset(0)]
public ulong u64;
}
[StructLayout(LayoutKind.Sequential)]
private struct epoll_event
{
public epoll_events events;
public epoll_data data;
}
private static class Windows
{
[DllImport("wepoll.dll", SetLastError = true)]
public static extern int epoll_create1(epoll_flags flags);
[DllImport("wepoll.dll", SetLastError = true)]
public static extern int epoll_close(int epfd);
[DllImport("wepoll.dll", SetLastError = true)]
public static extern int epoll_ctl(int epfd, epoll_op op, int fd, ref epoll_event ee);
[DllImport("wepoll.dll", SetLastError = true)]
public static extern int epoll_wait(int epfd, [In, Out] epoll_event[] ee, int maxevents, int timeout);
}
private static class Linux
{
[DllImport("libc", SetLastError = true)]
public static extern int epoll_create1(epoll_flags flags);
[DllImport("libc", SetLastError = true)]
public static extern int epoll_close(int epfd);
[DllImport("libc", SetLastError = true)]
public static extern int epoll_ctl(int epfd, epoll_op op, int fd, ref epoll_event ee);
[DllImport("libc", SetLastError = true)]
public static extern int epoll_wait(int epfd, [In, Out] epoll_event[] ee, int maxevents, int timeout);
}
private readonly int _epHndle;
public EPollGroup()
{
_epHndle = Core.IsWindows ? Windows.epoll_create1(epoll_flags.NONE) : Linux.epoll_create1(epoll_flags.NONE);
if (_epHndle == 0)
{
throw new Exception("Unable to initialize poll group");
}
}
public void Dispose()
{
if (Core.IsWindows)
{
Windows.epoll_close(_epHndle);
}
else
{
Linux.epoll_close(_epHndle);
}
}
public void Add(NetState state)
{
var ev = new epoll_event
{
events = epoll_events.EPOLLIN | epoll_events.EPOLLERR
};
ev.data.ptr = (IntPtr)state._handle;
var rc = Core.IsWindows ?
Windows.epoll_ctl(_epHndle, epoll_op.EPOLL_CTL_ADD, (int)state.Connection.Handle, ref ev) :
Linux.epoll_ctl(_epHndle, epoll_op.EPOLL_CTL_ADD, (int)state.Connection.Handle, ref ev);
if (rc != 0)
{
throw new Exception($"epoll_ctl failed with error code {Marshal.GetLastWin32Error()}");
}
}
public void Remove(NetState state)
{
var ev = new epoll_event
{
events = epoll_events.EPOLLIN | epoll_events.EPOLLERR,
};
ev.data.ptr = (IntPtr)state._handle;
var rc = Core.IsWindows ?
Windows.epoll_ctl(_epHndle, epoll_op.EPOLL_CTL_DEL, (int)state.Connection.Handle, ref ev) :
Linux.epoll_ctl(_epHndle, epoll_op.EPOLL_CTL_DEL, (int)state.Connection.Handle, ref ev);
if (rc != 0)
{
throw new Exception($"epoll_ctl failed with error code {Marshal.GetLastWin32Error()}");
}
}
private epoll_event[] _events = new epoll_event[2048];
public int Poll(ref NetState[] states)
{
if (states.Length > _events.Length)
{
var newLength = Math.Max(states.Length, _events.Length + (_events.Length >> 2));
_events = new epoll_event[newLength];
}
var rc = Core.IsWindows ?
Windows.epoll_wait(_epHndle, _events, states.Length, 0) :
Linux.epoll_wait(_epHndle, _events, states.Length, 0);
if (rc <= 0)
{
return rc;
}
int count = 0;
for (int i = 0; i < rc; i++)
{
if (((GCHandle)_events[i].data.ptr).Target is not NetState state)
{
continue;
}
states[count++] = state;
}
return count;
}
}
}

View file

@ -0,0 +1,248 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2021 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: KQueuePollGroup.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Server.Network
{
public class KQueuePollGroup : IPollGroup
{
[StructLayout(LayoutKind.Sequential)]
private struct kevent
{
public IntPtr ident;
public kqueue_filter filter;
public kqueue_flags flags;
public kqueue_fflags fflags;
public IntPtr data;
public IntPtr udata;
}
[Flags]
private enum kqueue_filter : short
{
READ = -1,
WRITE = -2,
AIO = -3,
VNODE = -4,
PROC = -5,
SIGNAL = -6,
TIMER = -7,
MACHPORT = -8,
FS = -9,
USER = -10,
UNUSED = -11,
VM = -12,
EXCEPT = -15
}
[Flags]
private enum kqueue_flags : ushort
{
ADD = 0x0001,
DELETE = 0x0002,
ENABLE = 0x0004,
DISABLE = 0x0008,
ONESHOT = 0x0010,
CLEAR = 0x0020,
RECEIPT = 0x0040,
DISPATCH = 0x0080,
UDATA_SPECIFIC = 0x0100,
DISPATCH2 = (DISPATCH | UDATA_SPECIFIC),
VANISHED = 0x0200,
SYSFLAGS = 0xF000,
FLAG0 = 0x1000,
FLAG1 = 0x2000,
EOF = 0x8000,
ERROR = 0x4000
}
[Flags]
private enum kqueue_fflags : uint
{
TRIGGER = 0x01000000,
FFNOP = 0x00000000,
FFAND = 0x40000000,
FFOR = 0x80000000,
FFCOPY = 0xc0000000,
FFCTRLMASK = 0xc0000000,
FFFLAGSMASK = 0x00ffffff,
LOWAT = 0x00000001,
DELETE = 0x00000001,
WRITE = 0x00000002,
EXTEND = 0x00000004,
ATTRIB = 0x00000008,
LINK = 0x00000010,
RENAME = 0x00000020,
REVOKE = 0x00000040,
NONE = 0x00000080,
}
[StructLayout(LayoutKind.Sequential)]
struct timespec
{
public long tv_sec;
public long tv_nsec;
public timespec(long sec, long nsec)
{
tv_sec = sec;
tv_nsec = nsec;
}
}
private static readonly kevent[] _singleEvent = new kevent[1];
private static readonly IntPtr _zeroTimeoutPtr;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private static readonly timespec _zeroTimeout;
static KQueuePollGroup()
{
_zeroTimeout = new timespec(0, 0);
_zeroTimeoutPtr = Marshal.AllocHGlobal(Marshal.SizeOf<timespec>());
Marshal.StructureToPtr(_zeroTimeout, _zeroTimeoutPtr, false);
}
private static class BSD
{
[DllImport ("libc", SetLastError = true)]
public static extern int close (int fd);
[DllImport("libc", SetLastError = true)]
public static extern int kqueue();
[DllImport("libc", SetLastError = true)]
public static extern int kevent(int kq, kevent[] changelist, int nchanges, [In, Out] kevent[] eventlist, int nevents, IntPtr timeout);
public static int kevent(
int kq,
IntPtr ident,
kqueue_filter filter,
kqueue_flags flags,
kqueue_fflags fflags = 0,
IntPtr data = default,
IntPtr udata = default
)
{
_singleEvent[0] = new kevent
{
ident = ident,
filter = filter,
flags = flags,
fflags = fflags,
data = data,
udata = udata
};
var rc = kevent(kq, _singleEvent, 1, null, 0, _zeroTimeoutPtr);
if (rc != 0)
{
throw new Exception($"kqueue failed to {flags} with error code {Marshal.GetLastWin32Error()}");
}
if (_singleEvent[0].flags.HasFlag(kqueue_flags.ERROR))
{
throw new IOException($"kqueue failed to {flags} with error {_singleEvent[0].data}");
}
return rc;
}
}
private readonly int _kqueueHndle;
public KQueuePollGroup()
{
_kqueueHndle = BSD.kqueue();
if (_kqueueHndle == 0)
{
throw new Exception("Unable to initialize poll group");
}
}
public void Dispose()
{
BSD.close(_kqueueHndle);
Marshal.FreeHGlobal(_zeroTimeoutPtr);
}
public void Add(NetState state)
{
var rc = BSD.kevent(
_kqueueHndle,
state.Connection.Handle,
kqueue_filter.READ | kqueue_filter.WRITE,
kqueue_flags.ADD | kqueue_flags.CLEAR,
udata: (IntPtr)state._handle
);
if (rc != 0)
{
throw new Exception($"kevent failed with error code {Marshal.GetLastWin32Error()}");
}
}
public void Remove(NetState state)
{
var rc = BSD.kevent(
_kqueueHndle,
state.Connection.Handle,
kqueue_filter.READ | kqueue_filter.WRITE,
kqueue_flags.DELETE,
udata: (IntPtr)state._handle
);
if (rc != 0)
{
throw new Exception($"kevent failed with error code {Marshal.GetLastWin32Error()}");
}
}
private kevent[] _events = new kevent[2048];
public int Poll(ref NetState[] states)
{
if (states.Length > _events.Length)
{
var newLength = Math.Max(states.Length, _events.Length + (_events.Length >> 2));
_events = new kevent[newLength];
}
var rc = BSD.kevent(_kqueueHndle, null, 0, _events, _events.Length, _zeroTimeoutPtr);
if (rc <= 0)
{
return rc;
}
int count = 0;
for (int i = 0; i < rc; i++)
{
if (((GCHandle)_events[i].udata).Target is not NetState state)
{
continue;
}
states[count++] = state;
}
return count;
}
}
}

View file

@ -0,0 +1,39 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2021 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: PollGroup.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
namespace Server.Network
{
public interface IPollGroup : IDisposable
{
void Add(NetState state);
void Remove(NetState state);
int Poll(ref NetState[] states);
}
public static class PollGroup
{
public static IPollGroup Create()
{
if (Core.IsBSD)
{
return new KQueuePollGroup();
}
return new EPollGroup();
}
}
}

View file

@ -84,6 +84,14 @@ namespace Server.Network
Listeners = listeners.ToArray();
}
public static void Shutdown()
{
foreach (var listener in Listeners)
{
listener.Server.Close();
}
}
public static IEnumerable<IPEndPoint> GetListeningAddresses(IPEndPoint ipep) =>
NetworkInterface.GetAllNetworkInterfaces().SelectMany(adapter =>
adapter.GetIPProperties().UnicastAddresses
@ -93,12 +101,19 @@ namespace Server.Network
public static TcpListener CreateListener(IPEndPoint ipep)
{
var listener = new TcpListener(ipep);
listener.Server.ExclusiveAddressUse = false;
var listener = new TcpListener(ipep)
{
Server =
{
LingerState = new LingerOption(false, 0),
ExclusiveAddressUse = true,
NoDelay = true
}
};
try
{
listener.Start(8);
listener.Start(32);
return listener;
}
catch (SocketException se)
@ -130,7 +145,6 @@ namespace Server.Network
{
Instances.Add(ns);
ns.LogInfo("Connected. [{0} Online]", Instances.Count);
ns.Start();
}
}

View file

@ -31,6 +31,12 @@
<Delete Files="..\..\Distribution\$(AssemblyName).runtimeconfig.dev.json" ContinueOnError="true" />
<Delete Files="..\..\Distribution\$(AssemblyName).runtimeconfig.json" ContinueOnError="true" />
</Target>
<ItemGroup Condition="'$(IsWindows)'=='true' OR '$(IsWindows)'==''">
<Content Include="wepoll.dll">
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.0.2" />
<PackageReference Include="Zlib.Bindings" Version="1.5.0" />
@ -46,7 +52,4 @@
<ItemGroup>
<AdditionalFiles Include="Migrations/*.v*.json" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Serialization\Attributes\SerializableFieldSaveValueDefaultAttribute.cs" />
</ItemGroup>
</Project>

View file

@ -150,6 +150,7 @@ namespace Server
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this[TileFlag.Impassable | TileFlag.Surface];
set => this[TileFlag.Impassable | TileFlag.Surface] = value;
}
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]

View file

@ -71,7 +71,7 @@ namespace Server
public static void StartTimer(TimeSpan delay, TimeSpan interval, int count, Action callback)
{
DelayCallTimer t = DelayCallTimer.GetTimer(delay, interval, count, callback);
t._selfReturn = true;
t._returnOnDetach = true;
t.Start();
#if DEBUG_TIMERS
@ -115,7 +115,7 @@ namespace Server
public sealed class DelayCallTimer : Timer, INotifyCompletion
{
internal bool _selfReturn;
internal bool _returnOnDetach;
#if DEBUG_TIMERS
internal bool _allowFinalization;
#endif
@ -143,17 +143,7 @@ namespace Server
_continuation?.Invoke();
}
public override void Stop()
{
base.Stop();
if (_selfReturn)
{
Return();
}
}
internal void Return()
internal override void OnDetach()
{
if (Running)
{
@ -161,39 +151,42 @@ namespace Server
return;
}
Version++; // Increment the version so if this is called from OnTick() and another timer is started, we don't have a problem
#if DEBUG_TIMERS
_stackTraces.Remove(GetHashCode());
#endif
if (_poolCount >= _poolCapacity)
if (_returnOnDetach)
{
#if DEBUG_TIMERS
logger.Warning($"DelayCallTimer pool reached maximum of {_poolCapacity} timers");
_allowFinalization = true;
#endif
return;
}
// Increment the version so if this is called from OnTick() and another timer is started, we don't have a problem
// Version++;
_continuation = null;
ReturnToPool(1, this, this);
#if DEBUG_TIMERS
_stackTraces.Remove(GetHashCode());
#endif
if (_poolCount >= _poolCapacity)
{
#if DEBUG_TIMERS
logger.Warning($"DelayCallTimer pool reached maximum of {_poolCapacity} timers");
_allowFinalization = true;
#endif
return;
}
_continuation = null;
_returnOnDetach = false;
ReturnToPool(1, this, this);
}
}
public static DelayCallTimer GetTimer(TimeSpan delay, TimeSpan interval, int count, Action callback)
{
if (_poolHead != null)
var timer = GetFromPool();
if (timer != null)
{
_poolCount--;
#if DEBUG_TIMERS
logger.Information($"Pool count: {_poolCount} / {_poolCapacity}");
logger.Information($"Getting from pool: ({_poolCount} / {_poolCapacity})");
#endif
var timer = GetFromPool();
timer.Init(delay, interval, count);
timer._continuation = callback;
timer._selfReturn = false;
timer._returnOnDetach = false;
#if DEBUG_TIMERS
timer._allowFinalization = false;
#endif

View file

@ -62,22 +62,29 @@ namespace Server
_poolHead = head;
_poolCount += amount;
#if DEBUG_TIMERS
logger.Information($"Pool count: {_poolCount} / {_poolCapacity}");
logger.Information($"Returning to pool. ({_poolCount} / {_poolCapacity})");
#endif
}
private static DelayCallTimer GetFromPool()
{
if (_poolHead == null)
{
return null;
}
var timer = _poolHead;
_poolHead = _poolHead._nextTimer as DelayCallTimer;
timer.Detach();
_poolCount--;
return timer;
}
internal static void RefillPool(int amount, out DelayCallTimer head, out DelayCallTimer tail)
{
#if DEBUG_TIMERS
logger.Information($"Filling pool with {amount} timers.");
logger.Information($"Filling pool with {amount} timers.");
#endif
head = null;

View file

@ -113,6 +113,11 @@ namespace Server
}
}
if (!timer.Running)
{
timer.OnDetach();
}
timer = next;
} while (timer != null);
}
@ -150,7 +155,7 @@ namespace Server
private static void AddTimer(Timer timer, long delay)
{
#if DEBUG_TIMERS
var originalDelay = delay
var originalDelay = delay;
#endif
delay = Math.Max(0, delay);

View file

@ -116,8 +116,8 @@ namespace Server
Running = false;
Version++;
var prof = GetProfile();
var prof = GetProfile();
if (prof != null)
{
prof.Stopped++;
@ -152,5 +152,9 @@ namespace Server
_nextTimer = null;
_prevTimer = null;
}
internal virtual void OnDetach()
{
}
}
}

View file

@ -50,9 +50,12 @@ namespace Server
public void Cancel()
{
_timer?.Stop();
_timer?.Return();
_timer = null;
if (_timer != null)
{
_timer._returnOnDetach = true;
_timer?.Stop();
_timer = null;
}
this = default;
}

View file

@ -132,7 +132,7 @@ namespace Server
public static WorldState WorldState { get; private set; }
public static bool Saving => WorldState == WorldState.Saving;
public static bool Running => WorldState == WorldState.Running;
public static bool Running => WorldState is not WorldState.Loading and not WorldState.Initial;
public static bool Loading => WorldState == WorldState.Loading;
public static Dictionary<Serial, Mobile> Mobiles { get; private set; }

BIN
Projects/Server/wepoll.dll Normal file

Binary file not shown.

View file

@ -23,7 +23,7 @@ namespace Server.Compression
StartInfo = new ProcessStartInfo
{
FileName = _pathToZstd,
Arguments = $"--no-progress -d \"{fileNamePath}\" -o \"${tempTarArchive}\""
Arguments = $"-q -d \"{fileNamePath}\" -o \"${tempTarArchive}\""
}
};
@ -70,7 +70,7 @@ namespace Server.Compression
StartInfo = new ProcessStartInfo
{
FileName = Path.Combine(_pathToZstd, "zstd.exe"),
Arguments = $"--no-progress -10 \"{tempTarArchive}\" -o \"{destinationArchiveFileName}\""
Arguments = $"-q -10 \"{tempTarArchive}\" -o \"{destinationArchiveFileName}\""
}
};

View file

@ -212,10 +212,14 @@ namespace Server.Movement
continue;
}
if (item.AtWorldPoint(xStart, yStart) || sectorStartIsForward && item.AtWorldPoint(xForward, yForward))
if (item.AtWorldPoint(xStart, yStart))
{
itemsStart.Add(item);
}
else if (sectorStartIsForward && item.AtWorldPoint(xForward, yForward))
{
itemsForward.Add(item);
}
}
if (checkMobs)
@ -471,14 +475,9 @@ namespace Server.Movement
return true;
}
if (!item.Movable)
{
continue;
}
var notWater = !itemData.Wet;
if (!itemData.Surface && itemData.Impassable && (!canSwim || notWater) || cantWalk && notWater)
if (item.Movable || !itemData.Surface && itemData.Impassable && (!canSwim || notWater) || cantWalk && notWater)
{
continue;
}
@ -619,10 +618,9 @@ namespace Server.Movement
{
var tile = staticTiles[i];
var id = TileData.ItemTable[tile.ID & TileData.MaxItemValue];
var calcTop = tile.Z + id.CalcHeight;
if (isSet && calcTop < zCenter || !id.Surface && !(m.CanSwim && id.Wet) || loc.Z < calcTop)
if (isSet && calcTop < zCenter || loc.Z < calcTop || !id.Surface && !(m.CanSwim && id.Wet))
{
continue;
}
@ -648,12 +646,10 @@ namespace Server.Movement
for (var i = 0; i < itemList.Count; ++i)
{
var item = itemList[i];
var id = item.ItemData;
var calcTop = item.Z + id.CalcHeight;
if (isSet && calcTop < zCenter || !id.Surface && !(m.CanSwim && id.Wet) || loc.Z < calcTop)
if (isSet && calcTop < zCenter || loc.Z < calcTop || !id.Surface && !(m.CanSwim && id.Wet))
{
continue;
}

View file

@ -10,18 +10,7 @@ namespace Server.Items
public override double DamageScalar => 1.5;
public override bool RequiresSE => true;
public override bool CheckSkills(Mobile from)
{
if (GetSkill(from, SkillName.Ninjitsu) < 50.0 && GetSkill(from, SkillName.Bushido) < 50.0)
{
// You need ~1_SKILL_REQUIREMENT~ Bushido or Ninjitsu skill to perform that attack!
from.SendLocalizedMessage(1063347, "50");
return false;
}
return base.CheckSkills(from);
}
public override bool RequiresSecondarySkill(Mobile from) => true;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{

View file

@ -0,0 +1,106 @@
using Server.Mobiles;
using System;
using System.Collections.Generic;
namespace Server.Items
{
public class Bladeweave : WeaponAbility
{
private class BladeWeaveRedirect
{
public readonly WeaponAbility NewAbility;
public readonly int ClilocEntry;
public BladeWeaveRedirect(WeaponAbility ability, int cliloc)
{
NewAbility = ability;
ClilocEntry = cliloc;
}
}
private static readonly Dictionary<Mobile, BladeWeaveRedirect> _newAttack = new();
public static bool BladeWeaving(Mobile attacker, out WeaponAbility a)
{
if (_newAttack.TryGetValue(attacker, out var bwr))
{
a = bwr.NewAbility;
return true;
}
a = null;
return false;
}
public override int BaseMana => 30;
public override bool OnBeforeSwing(Mobile attacker, Mobile defender)
{
if (!Validate(attacker) || !CheckMana(attacker, false))
{
return false;
}
// Bladeweave is only from 90 fighting and tactics skill, so you need only to check bushido/ninjitsu value over 50 to perform block and feint.
var requiredBushido = Math.Max(attacker.Skills.Bushido.Value, attacker.Skills.Ninjitsu.Value);
var ran = Utility.Random(requiredBushido >= 50 ? 9 : 7);
var newAttack = _newAttack[attacker] = GetBladeWeaveRedirect(ran);
return newAttack.NewAbility.OnBeforeSwing(attacker, defender);
}
private static BladeWeaveRedirect GetBladeWeaveRedirect(int number) =>
number switch
{
0 => new BladeWeaveRedirect(ArmorIgnore, 1028838),
1 => new BladeWeaveRedirect(BleedAttack, 1028839),
2 => new BladeWeaveRedirect(ConcussionBlow, 1028840),
3 => new BladeWeaveRedirect(CrushingBlow, 1028841),
4 => new BladeWeaveRedirect(DoubleStrike, 1028844),
5 => new BladeWeaveRedirect(MortalStrike, 1028846),
6 => new BladeWeaveRedirect(ParalyzingBlow, 1028848),
7 => new BladeWeaveRedirect(Block, 1028853),
_ => new BladeWeaveRedirect(Feint, 1028857) //8
};
public override bool OnBeforeDamage(Mobile attacker, Mobile defender) =>
_newAttack.TryGetValue(attacker, out var bwr)
? bwr.NewAbility.OnBeforeDamage(attacker, defender)
: base.OnBeforeDamage(attacker, defender);
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
if (CheckMana(attacker, false))
{
if (_newAttack.TryGetValue(attacker, out var bwr))
{
attacker.SendLocalizedMessage(1072841, $"#{bwr.ClilocEntry}");
bwr.NewAbility.OnHit(attacker, defender, damage);
}
else
{
base.OnHit(attacker, defender, damage);
}
_newAttack.Remove(attacker);
ClearCurrentAbility(attacker);
}
}
public override void OnMiss(Mobile attacker, Mobile defender)
{
if (_newAttack.TryGetValue(attacker, out var bwr))
{
bwr.NewAbility.OnMiss(attacker, defender);
}
else
{
base.OnMiss(attacker, defender);
}
_newAttack.Remove(attacker);
}
}
}

View file

@ -11,18 +11,7 @@ namespace Server.Items
private static readonly Dictionary<Mobile, InternalTimer> _table = new();
public override int BaseMana => 30;
public override bool CheckSkills(Mobile from)
{
if (GetSkill(from, SkillName.Ninjitsu) < 50.0 && GetSkill(from, SkillName.Bushido) < 50.0)
{
// You need ~1_SKILL_REQUIREMENT~ Bushido or Ninjitsu skill to perform that attack!
from.SendLocalizedMessage(1063347, "50");
return false;
}
return base.CheckSkills(from);
}
public override bool RequiresSecondarySkill(Mobile from) => true;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{

View file

@ -8,6 +8,24 @@ namespace Server.Items
public override int BaseMana => 25;
public override double DamageScalar => 1.5;
public virtual double GetRequiredTactics(Mobile from)
{
if (from.Weapon is BaseWeapon weapon)
{
if (weapon.PrimaryAbility == this)
{
return 30.0;
}
if (weapon.SecondaryAbility == this)
{
return 60.0;
}
}
return 200.0;
}
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
if (!Validate(attacker) || !CheckMana(attacker, true))

View file

@ -12,18 +12,7 @@ namespace Server.Items
private static readonly Dictionary<Mobile, DefenseMasteryInfo> _table = new();
public override int BaseMana => 30;
public override bool CheckSkills(Mobile from)
{
if (GetSkill(from, SkillName.Ninjitsu) < 50.0 && GetSkill(from, SkillName.Bushido) < 50.0)
{
// You need ~1_SKILL_REQUIREMENT~ Bushido or Ninjitsu skill to perform that attack!
from.SendLocalizedMessage(1063347, "50");
return false;
}
return base.CheckSkills(from);
}
public override bool RequiresSecondarySkill(Mobile from) => true;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
@ -40,8 +29,7 @@ namespace Server.Items
var modifier =
(int)(30.0 *
((Math.Max(attacker.Skills.Bushido.Value, attacker.Skills.Ninjitsu.Value) -
50.0) / 70.0));
((Math.Max(attacker.Skills.Bushido.Value, attacker.Skills.Ninjitsu.Value) - 50.0) / 70.0));
if (_table.TryGetValue(attacker, out var info))
{

View file

@ -12,33 +12,25 @@ namespace Server.Items
public override int BaseMana => 20;
// No longer active in pub21:
/*public override bool CheckSkills( Mobile from )
public override bool RequiresTactics(Mobile from) => from.Weapon is not BaseWeapon { Skill: SkillName.Wrestling };
// Disarm is special. Doesnt need tactics when wresling and tactics need is lower than fighting skill.
public virtual double GetRequiredTactics(Mobile from)
{
if (!base.CheckSkills( from ))
return false;
if (!(from.Weapon is Fists))
return true;
Skill skill = from.Skills.ArmsLore;
if (skill?.Base >= 80.0)
return true;
from.SendLocalizedMessage( 1061812 ); // You lack the required skill in armslore to perform that attack!
return false;
}*/
public override bool RequiresTactics(Mobile from)
{
if (!(from.Weapon is BaseWeapon weapon))
if (from.Weapon is BaseWeapon weapon)
{
return false;
if (weapon.PrimaryAbility == this)
{
return 30.0;
}
if (weapon.SecondaryAbility == this)
{
return 60.0;
}
}
return weapon.Skill != SkillName.Wrestling;
return 200.0;
}
public override void OnHit(Mobile attacker, Mobile defender, int damage)
@ -52,7 +44,7 @@ namespace Server.Items
var toDisarm = defender.FindItemOnLayer(Layer.OneHanded);
if (toDisarm?.Movable == false)
if (toDisarm?.Movable != false)
{
toDisarm = defender.FindItemOnLayer(Layer.TwoHanded);
}
@ -63,7 +55,7 @@ namespace Server.Items
{
attacker.SendLocalizedMessage(1004001); // You cannot disarm your opponent.
}
else if (!Core.ML && toDisarm == null || toDisarm is BaseShield || toDisarm is Spellbook)
else if (!Core.ML && toDisarm == null || toDisarm is BaseShield or Spellbook)
{
attacker.SendLocalizedMessage(1060849); // Your target is already unarmed!
}
@ -77,6 +69,8 @@ namespace Server.Items
pack.DropItem(toDisarm);
BuffInfo.AddBuff(defender, new BuffInfo(BuffIcon.NoRearm, 1075637, BlockEquipDuration, defender));
BaseWeapon.BlockEquip(defender, BlockEquipDuration);
}
}

View file

@ -44,8 +44,8 @@ namespace Server.Items
return;
}
if (attacker.Mounted && (!(attacker.Weapon is Lance) || !(defender.Weapon is Lance))
) // TODO: Should there be a message here?
// TODO: Should there be a message here?
if (attacker.Mounted && (attacker.Weapon is not Lance || defender.Weapon is not Lance))
{
return;
}
@ -101,12 +101,9 @@ namespace Server.Items
{
playerMobile.SetMountBlock(BlockMountType.DismountRecovery, RemountDelay, true);
}
else if (Core.ML && attacker is BaseCreature bc)
else if (Core.ML && attacker is BaseCreature { ControlMaster: PlayerMobile pm })
{
if (bc.ControlMaster is PlayerMobile pm)
{
pm.SetMountBlock(BlockMountType.DismountRecovery, RemountDelay, false);
}
pm.SetMountBlock(BlockMountType.DismountRecovery, RemountDelay, false);
}
if (!attacker.Mounted)

View file

@ -1,3 +1,5 @@
using System;
namespace Server.Items
{
/// <summary>
@ -7,17 +9,8 @@ namespace Server.Items
{
public override int BaseMana => 30;
public override bool CheckSkills(Mobile from)
{
if (GetSkill(from, SkillName.Ninjitsu) < 50.0 && GetSkill(from, SkillName.Bushido) < 50.0)
{
// You need ~1_SKILL_REQUIREMENT~ Bushido or Ninjitsu skill to perform that attack!
from.SendLocalizedMessage(1063347, "50");
return false;
}
return base.CheckSkills(from);
}
public override bool RequiresTactics(Mobile from) => false;
public override bool RequiresSecondarySkill(Mobile from) => true;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{

View file

@ -12,17 +12,8 @@ namespace Server.Items
public override int BaseMana => 30;
public override bool CheckSkills(Mobile from)
{
if (GetSkill(from, SkillName.Ninjitsu) < 50.0)
{
// You need ~1_SKILL_REQUIREMENT~ Ninjitsu skill to perform that attack!
from.SendLocalizedMessage(1063352, "50");
return false;
}
return base.CheckSkills(from);
}
public override bool RequiresSecondarySkill(Mobile from) => true;
public override SkillName GetSecondarySkillName(Mobile from) => SkillName.Ninjitsu;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
@ -44,8 +35,8 @@ namespace Server.Items
timer = new DualWieldTimer(
attacker,
(int)(20.0 + 3.0 * (attacker.Skills.Ninjitsu.Value - 50.0) / 7.0)
); // 20-50 % increase
(int)(20.0 + 3.0 * (attacker.Skills.Ninjitsu.Value - 50.0) / 7.0) // 20-50 % increase
);
timer.Start();
Registry.Add(attacker, timer);
@ -55,8 +46,7 @@ namespace Server.Items
{
private readonly Mobile m_Owner;
public DualWieldTimer(Mobile owner, int bonusSwingSpeed)
: base(TimeSpan.FromSeconds(6.0))
public DualWieldTimer(Mobile owner, int bonusSwingSpeed) : base(TimeSpan.FromSeconds(6.0))
{
m_Owner = owner;
BonusSwingSpeed = bonusSwingSpeed;

View file

@ -11,18 +11,7 @@ namespace Server.Items
public static Dictionary<Mobile, FeintTimer> Registry { get; } = new();
public override int BaseMana => 30;
public override bool CheckSkills(Mobile from)
{
if (GetSkill(from, SkillName.Ninjitsu) < 50.0 && GetSkill(from, SkillName.Bushido) < 50.0)
{
// You need ~1_SKILL_REQUIREMENT~ Bushido or Ninjitsu skill to perform that attack!
from.SendLocalizedMessage(1063347, "50");
return false;
}
return base.CheckSkills(from);
}
public override bool RequiresSecondarySkill(Mobile from) => true;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
@ -46,10 +35,8 @@ namespace Server.Items
timer = new FeintTimer(
defender,
(int)(20.0 + 3.0 * (Math.Max(
attacker.Skills.Ninjitsu.Value,
attacker.Skills.Bushido.Value
) - 50.0) / 7.0)
(int)(20.0 + 3.0 *
(Math.Max(attacker.Skills.Ninjitsu.Value, attacker.Skills.Bushido.Value) - 50.0) / 7.0)
); // 20-50 % decrease
timer.Start();

View file

@ -0,0 +1,164 @@
using Server.Spells;
using System;
using System.Collections.Generic;
namespace Server.Items
{
public class ForceArrow : WeaponAbility
{
public override int BaseMana => 20;
public override bool RequiresTactics(Mobile from) => false;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
if (!Validate(attacker) || !CheckMana(attacker, true))
{
return;
}
ClearCurrentAbility(attacker);
attacker.SendLocalizedMessage(1074381); // You fire an arrow of pure force.
defender.SendLocalizedMessage(1074382); // You are struck by a force arrow!
if (0.4 > Utility.RandomDouble())
{
defender.Combatant = null;
defender.Warmode = false;
}
ForceArrowInfo info = GetInfo(attacker, defender);
if (info == null)
{
BeginForceArrow(attacker, defender);
}
else if (info.Timer.Running)
{
info.Timer.IncreaseExpiration();
BuffInfo.RemoveBuff(defender, BuffIcon.ForceArrow);
BuffInfo.AddBuff(defender, new BuffInfo(BuffIcon.ForceArrow, 1151285, 1151286, info.DefenseChanceMalus.ToString()));
}
if (defender.Spell is Spell spell && spell.IsCasting)
{
spell.Disturb(DisturbType.Hurt, false, true);
}
}
private static readonly Dictionary<Mobile, List<ForceArrowInfo>> _table = new Dictionary<Mobile, List<ForceArrowInfo>>();
public static void BeginForceArrow(Mobile attacker, Mobile defender)
{
ForceArrowInfo info = new ForceArrowInfo(attacker, defender);
info.Timer = new ForceArrowTimer(info);
if (_table.TryGetValue(attacker, out var list))
{
list.Add(info);
}
else
{
_table.Add(attacker, new List<ForceArrowInfo>() { info });
}
BuffInfo.AddBuff(defender, new BuffInfo(BuffIcon.ForceArrow, 1151285, 1151286, info.DefenseChanceMalus.ToString()));
}
public static void EndForceArrow(ForceArrowInfo info)
{
if (info == null)
{
return;
}
Mobile attacker = info.Attacker;
if (_table.TryGetValue(attacker, out var list) && list.Remove(info) && list.Count == 0)
{
_table.Remove(attacker);
}
BuffInfo.RemoveBuff(info.Defender, BuffIcon.ForceArrow);
}
public static bool HasForceArrow(Mobile attacker, Mobile defender)
{
if (_table.TryGetValue(attacker, out var list))
{
foreach (ForceArrowInfo info in list)
{
if (info.Defender == defender)
{
return true;
}
}
}
return false;
}
public static ForceArrowInfo GetInfo(Mobile attacker, Mobile defender)
{
if (_table.TryGetValue(attacker,out var list))
{
foreach (ForceArrowInfo info in list)
{
if (info.Defender == defender)
{
return info;
}
}
}
return null;
}
public class ForceArrowInfo
{
public Mobile Attacker { get; }
public Mobile Defender { get; }
public ForceArrowTimer Timer { get; set; }
public int DefenseChanceMalus { get; set; }
public ForceArrowInfo(Mobile attacker, Mobile defender)
{
Attacker = attacker;
Defender = defender;
DefenseChanceMalus = 10;
}
}
public class ForceArrowTimer : Timer
{
private readonly ForceArrowInfo _info;
private DateTime _expires;
public ForceArrowTimer(ForceArrowInfo info)
: base(TimeSpan.FromSeconds(1.0), TimeSpan.FromSeconds(1))
{
_info = info;
_expires = Core.Now + TimeSpan.FromSeconds(10);
Start();
}
protected override void OnTick()
{
if (_expires < Core.Now)
{
Stop();
EndForceArrow(_info);
}
}
public void IncreaseExpiration()
{
_expires += TimeSpan.FromSeconds(2);
_info.DefenseChanceMalus += 5;
}
}
}
}

View file

@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
namespace Server.Items
{
public class ForceOfNature : WeaponAbility
{
public override int BaseMana => 35;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
if (!Validate(attacker) || !CheckMana(attacker, true))
{
return;
}
ClearCurrentAbility(attacker);
attacker.SendLocalizedMessage(1074374); // You attack your enemy with the force of nature!
defender.SendLocalizedMessage(1074375); // You are assaulted with great force!
defender.PlaySound(0x22F);
defender.FixedParticles(0x36CB, 1, 9, 9911, 67, 5, EffectLayer.Head);
defender.FixedParticles(0x374A, 1, 17, 9502, 1108, 4, (EffectLayer)255);
Remove(attacker);
ForceOfNatureTimer t = new ForceOfNatureTimer(attacker, defender);
t.Start();
_table[attacker] = t;
}
private static readonly Dictionary<Mobile, ForceOfNatureTimer> _table = new Dictionary<Mobile, ForceOfNatureTimer>();
public static void Remove(Mobile m)
{
if (_table.Remove(m, out var timer))
{
timer.Stop();
}
}
public static void OnHit(Mobile from, Mobile target)
{
if (_table.TryGetValue(from,out var t))
{
t.Hits++;
t.LastHit = Core.Now;
if (t.Hits % 12 == 0)
{
int duration = target.Skills[SkillName.MagicResist].Value >= 90.0 ? 1 : 2;
target.Paralyze(TimeSpan.FromSeconds(duration));
target.FixedEffect(0x376A, 9, 32);
target.PlaySound(0x204);
t.Hits = 0;
from.SendLocalizedMessage(1004013); // You successfully stun your opponent!
target.SendLocalizedMessage(1004014); // You have been stunned!
}
}
}
public static double GetDamageScalar(Mobile from, Mobile target)
{
if (_table.TryGetValue(from, out var t) && t.Target == target)
{
var bonus = Math.Min(100, Math.Max(50, from.Str - 50));
return (100.0 + bonus) / 100.0;
}
return 1.0;
}
private class ForceOfNatureTimer : Timer
{
public Mobile Target { get; }
public Mobile From { get; }
public int Hits { get; set; }
public DateTime LastHit { get; set; }
public ForceOfNatureTimer(Mobile from, Mobile target)
: base(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1))
{
Target = target;
From = from;
Hits = 1;
LastHit = Core.Now;
}
protected override void OnTick()
{
if (!From.Alive || !Target.Alive || Target.Map != From.Map || Target.GetDistanceToSqrt(From.Location) > 10 || LastHit + TimeSpan.FromSeconds(20) < Core.Now || Index > 36)
{
Remove(From);
return;
}
if (Index == 1)
{
int damage = Utility.RandomMinMax(15, 35);
AOS.Damage(From, From, damage, false, 0, 0, 0, 0, 0, 0, 100, false, false, false);
}
}
}
}
}

View file

@ -12,18 +12,8 @@ namespace Server.Items
public override int BaseMana => 30;
public static Dictionary<Mobile, FrenziedWirlwindTimer> Registry { get; } = new();
public override bool CheckSkills(Mobile from)
{
if (GetSkill(from, SkillName.Ninjitsu) < 50.0 && GetSkill(from, SkillName.Bushido) < 50.0)
{
// You need ~1_SKILL_REQUIREMENT~ Bushido or Ninjitsu skill to perform that attack!
from.SendLocalizedMessage(1063347, "50");
return false;
}
return base.CheckSkills(from);
}
public override bool RequiresTactics(Mobile from) => false;
public override bool RequiresSecondarySkill(Mobile from) => true;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{

View file

@ -30,7 +30,7 @@ namespace Server.Items
ClearCurrentAbility(attacker);
if (!(attacker.Weapon is BaseWeapon weapon))
if (attacker.Weapon is not BaseWeapon weapon)
{
return;
}

View file

@ -0,0 +1,58 @@
using Server.Spells;
using System;
using System.Collections.Generic;
namespace Server.Items
{
public class LightningArrow : WeaponAbility
{
public override int BaseMana => 20;
//TODO - add ConsumeAmmo on weaponabilities
// public override bool ConsumeAmmo => false;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
if (!Validate(attacker))
{
return;
}
ClearCurrentAbility(attacker);
Map map = attacker.Map;
if (map == null || attacker.Weapon is not BaseWeapon weapon || !CheckMana(attacker, true))
{
return;
}
List<Mobile> targets = new List<Mobile>();
IPooledEnumerable eable = defender.GetMobilesInRange(5);
foreach (Mobile m in eable)
{
if (m != defender && m != attacker && SpellHelper.ValidIndirectTarget(attacker, m) && m?.Deleted == false &&
m.Map == attacker.Map && m.Alive && attacker.CanSee(m) && attacker.CanBeHarmful(m) &&
attacker.InRange(m, weapon.MaxRange) && attacker.InLOS(m))
{
targets.Add(m);
}
}
eable.Free();
defender.BoltEffect(0);
var mobilesLeft = Math.Min(targets.Count, 2);
while (mobilesLeft-- > 0)
{
var index = Utility.Random(targets.Count);
var m = targets[index];
targets.RemoveAt(index);
m.BoltEffect(0);
AOS.Damage(m, attacker, Utility.RandomMinMax(29, 40), 0, 0, 0, 0, 100);
}
}
}
}

View file

@ -9,17 +9,8 @@ namespace Server.Items
{
public override int BaseMana => 30;
public override bool CheckSkills(Mobile from)
{
if (GetSkill(from, SkillName.Bushido) < 50.0)
{
// You need ~1_SKILL_REQUIREMENT~ Bushido skill to perform that attack!
from.SendLocalizedMessage(1070768, "50");
return false;
}
return base.CheckSkills(from);
}
public override bool RequiresSecondarySkill(Mobile from) => true;
public override SkillName GetSecondarySkillName(Mobile from) => SkillName.Bushido;
public override bool OnBeforeSwing(Mobile attacker, Mobile defender)
{

View file

@ -17,25 +17,7 @@ namespace Server.Items
public override int BaseMana => 30;
// No longer active in pub21:
/*public override bool CheckSkills( Mobile from )
{
if (!base.CheckSkills( from ))
return false;
if (!(from.Weapon is Fists))
return true;
Skill skill = from.Skills.Anatomy;
if (skill?.Base >= 80.0)
return true;
from.SendLocalizedMessage( 1061811 ); // You lack the required anatomy skill to perform that attack!
return false;
}*/
// When using Wrestling, tactics isnt needed.
public override bool RequiresTactics(Mobile from) =>
!(from.Weapon is BaseWeapon weapon && weapon.Skill == SkillName.Wrestling);

View file

@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
namespace Server.Items
{
public class PsychicAttack : WeaponAbility
{
public static Dictionary<Mobile, PsychicAttackTimer> Registry { get; } = new();
public override int BaseMana => 30;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
if (!Validate(attacker) || !CheckMana(attacker, true))
{
return;
}
ClearCurrentAbility(attacker);
attacker.SendLocalizedMessage(1074383); // Your shot sends forth a wave of psychic energy.
defender.SendLocalizedMessage(1074384); // Your mind is attacked by psychic force!
defender.FixedParticles(0x3789, 10, 25, 5032, EffectLayer.Head);
defender.PlaySound(0x1F8);
if (Registry.TryGetValue(defender, out var timer))
{
if (!timer.DoneIncrease)
{
timer.SpellDamageMalus *= 2;
timer.ManaCostMalus *= 2;
}
}
else
{
timer = new PsychicAttackTimer(defender);
timer.Start();
Registry.Add(defender, timer);
}
BuffInfo.RemoveBuff(defender, BuffIcon.PsychicAttack);
string args = $"{timer.SpellDamageMalus}\t{timer.ManaCostMalus}";
BuffInfo.AddBuff(defender, new BuffInfo(BuffIcon.PsychicAttack, 1151296, 1151297, args));
}
public static void RemoveEffects(Mobile defender)
{
if (defender == null)
{
return;
}
BuffInfo.RemoveBuff(defender, BuffIcon.PsychicAttack);
Registry.Remove(defender);
defender.SendLocalizedMessage(1150292); // You recover from the effects of the psychic attack.
}
public class PsychicAttackTimer : Timer
{
private readonly Mobile _defender;
private int _spellDamageMalus;
private int _manaCostMalus;
public int SpellDamageMalus
{
get => _spellDamageMalus;
set { _spellDamageMalus = value; DoneIncrease = true; }
}
public int ManaCostMalus
{
get => _manaCostMalus;
set { _manaCostMalus = value; DoneIncrease = true; }
}
public bool DoneIncrease { get; private set; }
public PsychicAttackTimer(Mobile defender) : base(TimeSpan.FromSeconds(10))
{
_defender = defender;
_spellDamageMalus = 15;
_manaCostMalus = 15;
DoneIncrease = false;
}
protected override void OnTick()
{
RemoveEffects(_defender);
}
}
}
}

View file

@ -13,18 +13,8 @@ namespace Server.Items
public override int BaseMana => 30;
public override bool RequiresSE => true;
public override bool CheckSkills(Mobile from)
{
if (GetSkill(from, SkillName.Bushido) < 50.0)
{
// You need ~1_SKILL_REQUIREMENT~ Bushido skill to perform that attack!
from.SendLocalizedMessage(1070768, "50");
return false;
}
return base.CheckSkills(from);
}
public override bool RequiresSecondarySkill(Mobile from) => true;
public override SkillName GetSecondarySkillName(Mobile from) => SkillName.Bushido;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{

View file

@ -0,0 +1,46 @@
namespace Server.Items
{
public class SerpentArrow : WeaponAbility
{
public override int BaseMana => 25;
// Serpent arrow is used only as secondary ability on "Elven composite longbow" so needed skill is always 60, but for sure we will use GetRequiredSecondarySkill
public override bool RequiresSecondarySkill(Mobile from) => true;
public override double GetRequiredSecondarySkill(Mobile from) => 60;
public override SkillName GetSecondarySkillName(Mobile from) => SkillName.Poisoning;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
if (!Validate(attacker) || !CheckMana(attacker, true))
{
return;
}
ClearCurrentAbility(attacker);
defender.SendLocalizedMessage(1112369); // You have been poisoned by a lethal arrow!
int level;
if (attacker.InRange(defender, 2))
{
level = attacker.Skills.Poisoning.Fixed / 2 switch
{
>= 1000 => 3,
> 850 => 2,
> 650 => 1,
_ => 0
};
}
else
{
level = 0;
}
defender.ApplyPoison(attacker, Poison.GetPoison(level));
defender.FixedParticles(0x374A, 10, 15, 5021, EffectLayer.Waist);
defender.PlaySound(0x474);
}
}
}

View file

@ -12,6 +12,10 @@ namespace Server.Items
public override bool RequiresTactics(Mobile from) => false;
public override bool RequiresSecondarySkill(Mobile from) => true;
public override double GetRequiredSecondarySkill(Mobile from) => 80;
public override SkillName GetSecondarySkillName(Mobile from) => SkillName.Stealth;
public override bool CheckSkills(Mobile from)
{
if (!base.CheckSkills(from))

View file

@ -13,17 +13,9 @@ namespace Server.Items
public override int BaseMana => 30;
public override double DamageScalar => 1.2;
public override bool CheckSkills(Mobile from)
{
if (GetSkill(from, SkillName.Ninjitsu) < 50.0)
{
// You need ~1_SKILL_REQUIREMENT~ Ninjitsu skill to perform that attack!
from.SendLocalizedMessage(1063352, "50");
return false;
}
public override bool RequiresSecondarySkill(Mobile from) => true;
public override SkillName GetSecondarySkillName(Mobile from) => SkillName.Ninjitsu;
return base.CheckSkills(from);
}
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{

View file

@ -38,12 +38,12 @@ namespace Server.Items
new DualWield(),
new DoubleShot(),
new ArmorPierce(),
null,
null,
null,
null,
null,
null,
new Bladeweave(),
new ForceArrow(),
new LightningArrow(),
new PsychicAttack(),
new SerpentArrow(),
new ForceOfNature(),
new Disrobe()
};
@ -109,16 +109,60 @@ namespace Server.Items
public virtual bool RequiresTactics(Mobile from) => true;
public virtual bool RequiresSecondarySkill(Mobile from) => false;
/// <summary>
/// Return primary skill for ability. Default 70/90
/// </summary>
/// <param name="from"></param>
/// <returns></returns>
public virtual double GetRequiredSkill(Mobile from)
{
if (from.Weapon is BaseWeapon weapon)
{
if (weapon.PrimaryAbility == this)
if (weapon.PrimaryAbility == this || weapon.PrimaryAbility == Bladeweave)
{
return 70.0;
}
if (weapon.SecondaryAbility == this)
if (weapon.SecondaryAbility == this || weapon.SecondaryAbility == Bladeweave)
{
return 90.0;
}
}
return 200.0;
}
/// <summary>
/// Returns secondary skill for Ability. Default 50.
/// </summary>
/// <param name="from"></param>
/// <returns></returns>
public virtual double GetRequiredSecondarySkill(Mobile from) => 50.0;
/// <summary>
/// Return secondary skillName. Default bushido/ninjitsu
/// </summary>
/// <param name="from"></param>
/// <returns></returns>
public virtual SkillName GetSecondarySkillName(Mobile from) => from.Skills[SkillName.Ninjitsu].Base > from.Skills[SkillName.Bushido].Base ? SkillName.Ninjitsu : SkillName.Bushido;
/// <summary>
/// Returns tactics needed for Ability. Default 70/90.
/// </summary>
/// <param name="from"></param>
/// <returns></returns>
public virtual double GetRequiredTactics(Mobile from)
{
if (from.Weapon is BaseWeapon weapon)
{
if (weapon.PrimaryAbility == this || weapon.PrimaryAbility == Bladeweave)
{
return 70.0;
}
if (weapon.SecondaryAbility == this || weapon.SecondaryAbility == Bladeweave)
{
return 90.0;
}
@ -184,13 +228,32 @@ namespace Server.Items
var reqSkill = GetRequiredSkill(from);
var reqTactics = Core.ML && RequiresTactics(from);
if (Core.ML && reqTactics && from.Skills.Tactics.Base < reqSkill)
if (reqTactics && from.Skills.Tactics.Base < GetRequiredTactics(from))
{
// You need ~1_SKILL_REQUIREMENT~ weapon and tactics skill to perform that attack
from.SendLocalizedMessage(1079308, reqSkill.ToString());
return false;
}
var reqSecondarySkill = GetRequiredSecondarySkill(from);
SkillName secondarySkill = GetSecondarySkillName(from);
if (RequiresSecondarySkill(from) && from.Skills[secondarySkill].Base < reqSecondarySkill)
{
int loc = GetSkillLocalization(secondarySkill);
if (loc == 1060184)
{
from.SendLocalizedMessage(loc);
}
else
{
from.SendLocalizedMessage(loc, reqSecondarySkill.ToString());
}
return false;
}
if (skill?.Base >= reqSkill)
{
return true;
@ -207,19 +270,22 @@ namespace Server.Items
}
/* </UBWS> */
if (reqTactics)
{
// You need ~1_SKILL_REQUIREMENT~ weapon and tactics skill to perform that attack
from.SendLocalizedMessage(1079308, reqSkill.ToString());
}
else
{
// You need ~1_SKILL_REQUIREMENT~ weapon skill to perform that attack
from.SendLocalizedMessage(1060182, reqSkill.ToString());
}
// You need ~1_SKILL_REQUIREMENT~ weapon skill to perform that attack
from.SendLocalizedMessage(1060182, reqSkill.ToString());
return false;
}
private int GetSkillLocalization(SkillName skill)
{
return skill switch
{
SkillName.Bushido => 1063347, // You need ~1_SKILL_REQUIREMENT~ Bushido or Ninjitsu skill to perform that attack!
SkillName.Ninjitsu => 1063347, // You need ~1_SKILL_REQUIREMENT~ Bushido or Ninjitsu skill to perform that attack!
SkillName.Poisoning => 1060184, // You lack the required poisoning to perform that attack
_ => 1157351 // You need ~1_SKILL_REQUIREMENT~ weapon and tactics skill to perform that attack
};
}
public virtual bool CheckSkills(Mobile from) => CheckWeaponSkill(from);
@ -420,10 +486,7 @@ namespace Server.Items
{
private readonly Mobile m_Mobile;
public WeaponAbilityTimer(Mobile from) : base(TimeSpan.FromSeconds(3.0))
{
m_Mobile = from;
}
public WeaponAbilityTimer(Mobile from) : base(TimeSpan.FromSeconds(3.0)) => m_Mobile = from;
protected override void OnTick()
{

View file

@ -28,7 +28,7 @@ namespace Server.Items
return;
}
if (!(attacker.Weapon is BaseWeapon weapon))
if (attacker.Weapon is not BaseWeapon weapon)
{
return;
}

View file

@ -1024,6 +1024,7 @@ namespace Server.Items
}
ImmolatingWeaponSpell.StopImmolating(this);
ForceOfNature.Remove(m);
m.CheckStatTimers();
@ -1157,6 +1158,13 @@ namespace Server.Items
bonus = AosAttributes.GetValue(defender, AosAttribute.DefendChance);
var info = ForceArrow.GetInfo(attacker, defender);
if (info != null && info.Defender == defender)
{
bonus -= info.DefenseChanceMalus;
}
if (DivineFurySpell.UnderEffect(defender))
{
bonus -= 20; // defender loses 20% bonus when they're under divine fury
@ -1662,6 +1670,8 @@ namespace Server.Items
percentageBonus += (int)(move.GetDamageScalar(attacker, defender) * 100) - 100;
}
percentageBonus += (int)(ForceOfNature.GetDamageScalar(attacker, defender) * 100) - 100;
percentageBonus += (int)(damageBonus * 100) - 100;
var cs = CheckSlayers(attacker, defender);
@ -1864,7 +1874,10 @@ namespace Server.Items
move = null;
}
var ignoreArmor = a is ArmorIgnore || move?.IgnoreArmor(attacker) == true;
var ignoreArmor = a is ArmorIgnore ||
move?.IgnoreArmor(attacker) == true ||
Bladeweave.BladeWeaving(attacker, out var bladeweavingAbi) &&
bladeweavingAbi is ArmorIgnore;
var damageGiven = AOS.Damage(
defender,
@ -1981,8 +1994,8 @@ namespace Server.Items
mobile.LocalOverheadMessage(
MessageType.Regular,
0x3B2,
1061121
); // Your equipment is severely damaged.
1061121 // Your equipment is severely damaged.
);
}
}
else
@ -2101,6 +2114,8 @@ namespace Server.Items
a?.OnHit(attacker, defender, damage);
move?.OnHit(attacker, defender, damage);
ForceOfNature.OnHit(attacker, defender);
if (defender is IHonorTarget it)
{
it.ReceivedHonorContext?.OnTargetHit(attacker);
@ -2142,6 +2157,11 @@ namespace Server.Items
// SDI bonus
damageBonus += AosAttributes.GetValue(attacker, AosAttribute.SpellDamage);
if(PsychicAttack.Registry.TryGetValue(attacker,out var timer))
{
damageBonus -= timer.SpellDamageMalus;
}
var context = TransformationSpellHelper.GetContext(attacker);
if (context?.Spell is ReaperFormSpell spell)

View file

@ -8,7 +8,7 @@ namespace Server.Items
public GnarledStaff() : base(0x13F8) => Weight = 3.0;
public override WeaponAbility PrimaryAbility => WeaponAbility.ConcussionBlow;
public override WeaponAbility SecondaryAbility => WeaponAbility.ParalyzingBlow;
public override WeaponAbility SecondaryAbility => WeaponAbility.ForceOfNature;
public override int AosStrengthReq => 20;
public override int AosMinDamage => 15;

View file

@ -228,57 +228,190 @@ namespace Server
{
DismountPrevention = 0x3E9,
NoRearm = 0x3EA,
// Currently, no 0x3EB or 0x3EC
NightSight = 0x3ED, // *
//Currently, no 0x3EB or 0x3EC
NightSight = 0x3ED, //*
DeathStrike,
EvilOmen,
UnknownStandingSwirl, // Which is healing throttle & Stamina throttle?
UnknownKneelingSword,
DivineFury, // *
EnemyOfOne, // *
HidingAndOrStealth, // *
ActiveMeditation, // *
BloodOathCaster, // *
BloodOathCurse, // *
CorpseSkin, // *
Mindrot, // *
PainSpike, // *
HonoredDebuff,
AchievePerfection,
DivineFury, //*
EnemyOfOne, //*
HidingAndOrStealth, //*
ActiveMeditation, //*
BloodOathCaster, //*
BloodOathCurse, //*
CorpseSkin, //*
Mindrot, //*
PainSpike, //*
Strangle,
GiftOfRenewal, // *
AttuneWeapon, // *
Thunderstorm, // *
EssenceOfWind, // *
EtherealVoyage, // *
GiftOfLife, // *
ArcaneEmpowerment, // *
GiftOfRenewal, //*
AttuneWeapon, //*
Thunderstorm, //*
EssenceOfWind, //*
EtherealVoyage, //*
GiftOfLife, //*
ArcaneEmpowerment, //*
MortalStrike,
ReactiveArmor, // *
Protection, // *
ReactiveArmor, //*
Protection, //*
ArchProtection,
MagicReflection, // *
Incognito, // *
MagicReflection, //*
Incognito, //*
Disguised,
AnimalForm,
Polymorph,
Invisibility, // *
Paralyze, // *
Invisibility, //*
Paralyze, //*
Poison,
Bleed,
Clumsy, // *
FeebleMind, // *
Weaken, // *
Curse, // *
Clumsy, //*
FeebleMind, //*
Weaken, //*
Curse, //*
MassCurse,
Agility, // *
Cunning, // *
Strength, // *
Bless, // *
Agility, //*
Cunning, //*
Strength, //*
Bless, //*
Sleep,
StoneForm,
SpellPlague,
SpellTrigger,
NetherBolt,
Fly
Berserk,
MassSleep,
Fly,
Inspire,
Invigorate,
Resilience,
Perseverance,
TribulationTarget,
DespairTarget,
FishPie = 0x426,
HitLowerAttack,
HitLowerDefense,
DualWield,
Block,
DefenseMastery,
DespairCaster,
Healing,
SpellFocusingBuff,
SpellFocusingDebuff,
RageFocusingDebuff,
RageFocusingBuff,
Warding,
TribulationCaster,
ForceArrow,
Disarm,
Surge,
Feint,
TalonStrike,
PsychicAttack,
ConsecrateWeapon,
GrapesOfWrath,
EnemyOfOneDebuff,
HorrificBeast,
LichForm,
VampiricEmbrace,
CurseWeapon,
ReaperForm,
ImmolatingWeapon,
Enchant,
HonorableExecution,
Confidence,
Evasion,
CounterAttack,
LightningStrike,
MomentumStrike,
OrangePetals,
RoseOfTrinsic,
PoisonImmunity,
Veterinary,
Perfection,
Honored,
ManaPhase,
FanDancerFanFire,
Rage,
Webbing,
MedusaStone,
TrueFear,
AuraOfNausea,
HowlOfCacophony,
GazeDespair,
HiryuPhysicalResistance,
RuneBeetleCorruption,
BloodwormAnemia,
RotwormBloodDisease,
SkillUseDelay,
FactionStatLoss,
HeatOfBattleStatus,
CriminalStatus,
ArmorPierce,
SplinteringEffect,
SwingSpeedDebuff,
WraithForm,
CityTradeDeal = 0x466,
HumilityDebuff = 0x467,
Spirituality,
Humility,
// Skill Masteries
Rampage,
Stagger, // Debuff
Toughness,
Thrust,
Pierce, // Debuff
PlayingTheOdds,
FocusedEye,
Onslaught, // Debuff
ElementalFury,
ElementalFuryDebuff, // Debuff
CalledShot,
Knockout,
SavingThrow,
Conduit,
EtherealBurst,
MysticWeapon,
ManaShield,
AnticipateHit,
Warcry,
Shadow,
WhiteTigerForm,
Bodyguard,
HeightenedSenses,
Tolerance,
DeathRay,
DeathRayDebuff,
Intuition,
EnchantedSummoning,
ShieldBash,
Whispering,
CombatTraining,
InjectedStrikeDebuff,
InjectedStrike,
UnknownTomato,
PlayingTheOddsDebuff,
DragonTurtleDebuff,
Boarding,
Potency,
ThrustDebuff,
FistsOfFury, // 1169
BarrabHemolymphConcentrate,
JukariBurnPoiltice,
KurakAmbushersEssence,
BarakoDraftOfMight,
UraliTranceTonic,
SakkhraProphylaxis, // 1175
Sparks,
Swarm,
BoneBreaker,
Unknown2,
SwarmImmune,
BoneBreakerImmune,
UnknownGoblin,
UnknownRedDrop,
UnknownStar,
FeintDebuff,
CaddelliteInfused,
PotionGloriousFortune,
MysticalPolymorphTotem,
UnknownDebuff,
}
}

View file

@ -77,7 +77,7 @@ namespace Server.Saves
return;
}
if (SavesEnabled && !AutoRestart.Restarting && !Core.Closing && World.Running)
if (SavesEnabled && !AutoRestart.Restarting && !Core.Closing && !World.Saving)
{
if (Warning > TimeSpan.Zero)
{

View file

@ -84,3 +84,34 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
License notice for wepoll
---------------------------
wepoll - epoll for Windows
https://github.com/piscisaureus/wepoll
Copyright 2012-2020, Bert Belder <bertbelder@gmail.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -1,4 +1,4 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
"version": "0.8.2"
"version": "0.8.3"
}