Merge branch 'main' into webSupport
This commit is contained in:
commit
e933198a85
50 changed files with 1681 additions and 496 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
303
Projects/Server/Network/NetState/NetState.cs
Normal file → Executable 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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
206
Projects/Server/Network/PollGroup/EPollGroup.cs
Executable file
206
Projects/Server/Network/PollGroup/EPollGroup.cs
Executable 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
248
Projects/Server/Network/PollGroup/KQueuePollGroup.cs
Normal file
248
Projects/Server/Network/PollGroup/KQueuePollGroup.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
Projects/Server/Network/PollGroup/PollGroup.cs
Normal file
39
Projects/Server/Network/PollGroup/PollGroup.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
BIN
Projects/Server/wepoll.dll
Normal file
Binary file not shown.
|
|
@ -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}\""
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
106
Projects/UOContent/Items/Weapons/Abilities/Bladeweave.cs
Normal file
106
Projects/UOContent/Items/Weapons/Abilities/Bladeweave.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
164
Projects/UOContent/Items/Weapons/Abilities/ForceArrow.cs
Normal file
164
Projects/UOContent/Items/Weapons/Abilities/ForceArrow.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
111
Projects/UOContent/Items/Weapons/Abilities/ForceOfNature.cs
Normal file
111
Projects/UOContent/Items/Weapons/Abilities/ForceOfNature.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ namespace Server.Items
|
|||
|
||||
ClearCurrentAbility(attacker);
|
||||
|
||||
if (!(attacker.Weapon is BaseWeapon weapon))
|
||||
if (attacker.Weapon is not BaseWeapon weapon)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
58
Projects/UOContent/Items/Weapons/Abilities/LightningArrow.cs
Normal file
58
Projects/UOContent/Items/Weapons/Abilities/LightningArrow.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
93
Projects/UOContent/Items/Weapons/Abilities/PsychicAttack.cs
Normal file
93
Projects/UOContent/Items/Weapons/Abilities/PsychicAttack.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
46
Projects/UOContent/Items/Weapons/Abilities/SerpentArrow.cs
Normal file
46
Projects/UOContent/Items/Weapons/Abilities/SerpentArrow.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ namespace Server.Items
|
|||
return;
|
||||
}
|
||||
|
||||
if (!(attacker.Weapon is BaseWeapon weapon))
|
||||
if (attacker.Weapon is not BaseWeapon weapon)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue