From a845f8a1c0fd48d3837539fd5f348c101d45a83f Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 3 Oct 2021 18:04:53 -0700 Subject: [PATCH 1/5] feat: Networking v3 using epoll/kqueue (#813) * Replaces networking internals with epoll/kqueue --- Projects/Server/Main.cs | 4 +- Projects/Server/Network/ISocket.cs | 4 + .../Server/Network/NetState/DumpNetStates.cs | 2 +- Projects/Server/Network/NetState/NetState.cs | 303 +++++++----------- Projects/Server/Network/NetworkSocket.cs | 10 + .../Server/Network/PollGroup/EPollGroup.cs | 206 ++++++++++++ .../Network/PollGroup/KQueuePollGroup.cs | 248 ++++++++++++++ .../Server/Network/PollGroup/PollGroup.cs | 39 +++ Projects/Server/Network/TcpServer.cs | 22 +- Projects/Server/Server.csproj | 9 +- Projects/Server/wepoll.dll | Bin 0 -> 21504 bytes Projects/UOContent/Compression/ZstdArchive.cs | 4 +- THIRD-PARTY-NOTICES | 31 ++ version.json | 2 +- 14 files changed, 686 insertions(+), 198 deletions(-) mode change 100644 => 100755 Projects/Server/Network/NetState/NetState.cs create mode 100755 Projects/Server/Network/PollGroup/EPollGroup.cs create mode 100644 Projects/Server/Network/PollGroup/KQueuePollGroup.cs create mode 100644 Projects/Server/Network/PollGroup/PollGroup.cs create mode 100644 Projects/Server/wepoll.dll diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index 1d56edbb6..49f48e669 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -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) diff --git a/Projects/Server/Network/ISocket.cs b/Projects/Server/Network/ISocket.cs index b1182c77a..75d3d8475 100644 --- a/Projects/Server/Network/ISocket.cs +++ b/Projects/Server/Network/ISocket.cs @@ -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 ReceiveAsync(IList> buffer, SocketFlags flags); + public int Receive(IList> buffers, SocketFlags flags); + public void Shutdown(SocketShutdown how); public void Close(); diff --git a/Projects/Server/Network/NetState/DumpNetStates.cs b/Projects/Server/Network/NetState/DumpNetStates.cs index ad0e3a821..6992254dc 100644 --- a/Projects/Server/Network/NetState/DumpNetStates.cs +++ b/Projects/Server/Network/NetState/DumpNetStates.cs @@ -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}"); } } } diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs old mode 100644 new mode 100755 index c60147f81..136b84320 --- a/Projects/Server/Network/NetState/NetState.cs +++ b/Projects/Server/Network/NetState/NetState.cs @@ -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 FlushPending = new(2048); + private static readonly Queue FlushedPartials = new(2048); private static readonly ConcurrentQueue 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(GC.AllocateUninitializedArray(RecvPipeSize)); SendPipe = new Pipe(GC.AllocateUninitializedArray(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 Trades { get; } - public bool Running - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _running == 1; - } - public bool Seeded { get; set; } public Pipe 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[] 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; diff --git a/Projects/Server/Network/NetworkSocket.cs b/Projects/Server/Network/NetworkSocket.cs index e4e4c3a8a..f762ba1cf 100644 --- a/Projects/Server/Network/NetworkSocket.cs +++ b/Projects/Server/Network/NetworkSocket.cs @@ -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 ReceiveAsync(IList> buffers, SocketFlags flags) => _connection.ReceiveAsync(buffers, flags); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Receive(IList> buffers, SocketFlags flags) => + _connection.Receive(buffers, flags); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Shutdown(SocketShutdown how) => _connection.Shutdown(how); diff --git a/Projects/Server/Network/PollGroup/EPollGroup.cs b/Projects/Server/Network/PollGroup/EPollGroup.cs new file mode 100755 index 000000000..bf615e4cc --- /dev/null +++ b/Projects/Server/Network/PollGroup/EPollGroup.cs @@ -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 . * + *************************************************************************/ + +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; + } + + } +} diff --git a/Projects/Server/Network/PollGroup/KQueuePollGroup.cs b/Projects/Server/Network/PollGroup/KQueuePollGroup.cs new file mode 100644 index 000000000..41466b968 --- /dev/null +++ b/Projects/Server/Network/PollGroup/KQueuePollGroup.cs @@ -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 . * + *************************************************************************/ + +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()); + 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; + } + } +} diff --git a/Projects/Server/Network/PollGroup/PollGroup.cs b/Projects/Server/Network/PollGroup/PollGroup.cs new file mode 100644 index 000000000..9089f871b --- /dev/null +++ b/Projects/Server/Network/PollGroup/PollGroup.cs @@ -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 . * + *************************************************************************/ + +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(); + } + } +} diff --git a/Projects/Server/Network/TcpServer.cs b/Projects/Server/Network/TcpServer.cs index 06d7bd744..428c40cf9 100644 --- a/Projects/Server/Network/TcpServer.cs +++ b/Projects/Server/Network/TcpServer.cs @@ -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 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(); } } diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index aa97275b0..a0d4042b7 100755 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -31,6 +31,12 @@ + + + Always + Always + + @@ -46,7 +52,4 @@ - - - diff --git a/Projects/Server/wepoll.dll b/Projects/Server/wepoll.dll new file mode 100644 index 0000000000000000000000000000000000000000..d3f207d746070b8b0b540ca110c5a6822f10accb GIT binary patch literal 21504 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 literal 0 HcmV?d00001 diff --git a/Projects/UOContent/Compression/ZstdArchive.cs b/Projects/UOContent/Compression/ZstdArchive.cs index 08246902c..72cc56e94 100755 --- a/Projects/UOContent/Compression/ZstdArchive.cs +++ b/Projects/UOContent/Compression/ZstdArchive.cs @@ -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}\"" } }; diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 76fdea608..68a241499 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -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 +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. diff --git a/version.json b/version.json index 629bc3bab..dd101024d 100644 --- a/version.json +++ b/version.json @@ -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" } From 8e4d5bb16949f81aa2e5acb71c2c549070c221cc Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 3 Oct 2021 19:19:26 -0700 Subject: [PATCH 2/5] fix: Fixes movement issue (#814) * Fixes wrong item list used for item detection --- Projects/Server/TileData.cs | 1 + Projects/UOContent/Engines/Pathing/Movement.cs | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Projects/Server/TileData.cs b/Projects/Server/TileData.cs index a566368c7..6e2b66aaf 100644 --- a/Projects/Server/TileData.cs +++ b/Projects/Server/TileData.cs @@ -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)] diff --git a/Projects/UOContent/Engines/Pathing/Movement.cs b/Projects/UOContent/Engines/Pathing/Movement.cs index b5b3665ad..8ce683ad9 100644 --- a/Projects/UOContent/Engines/Pathing/Movement.cs +++ b/Projects/UOContent/Engines/Pathing/Movement.cs @@ -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) @@ -619,10 +623,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 +651,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; } From 4893094bb4465b527cadb295695ae925dc25f886 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 3 Oct 2021 22:58:46 -0700 Subject: [PATCH 3/5] fix: Fixes pooled timers orphaning each other (#809) --- Projects/Server/Timer/Timer.DelayCall.cs | 61 +++++++++----------- Projects/Server/Timer/Timer.Pool.cs | 11 +++- Projects/Server/Timer/Timer.TimerWheel.cs | 7 ++- Projects/Server/Timer/Timer.cs | 6 +- Projects/Server/Timer/TimerExecutionToken.cs | 9 ++- Projects/Server/World/World.cs | 2 +- Projects/UOContent/World Saves/AutoSave.cs | 2 +- 7 files changed, 55 insertions(+), 43 deletions(-) diff --git a/Projects/Server/Timer/Timer.DelayCall.cs b/Projects/Server/Timer/Timer.DelayCall.cs index 81fb9d5b6..5d98180bb 100644 --- a/Projects/Server/Timer/Timer.DelayCall.cs +++ b/Projects/Server/Timer/Timer.DelayCall.cs @@ -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 diff --git a/Projects/Server/Timer/Timer.Pool.cs b/Projects/Server/Timer/Timer.Pool.cs index a7db9c273..cb92fc18f 100644 --- a/Projects/Server/Timer/Timer.Pool.cs +++ b/Projects/Server/Timer/Timer.Pool.cs @@ -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; diff --git a/Projects/Server/Timer/Timer.TimerWheel.cs b/Projects/Server/Timer/Timer.TimerWheel.cs index 5389eb557..f8e4e451c 100644 --- a/Projects/Server/Timer/Timer.TimerWheel.cs +++ b/Projects/Server/Timer/Timer.TimerWheel.cs @@ -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); diff --git a/Projects/Server/Timer/Timer.cs b/Projects/Server/Timer/Timer.cs index 5ed8ac3d9..786d1c86c 100644 --- a/Projects/Server/Timer/Timer.cs +++ b/Projects/Server/Timer/Timer.cs @@ -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() + { + } } } diff --git a/Projects/Server/Timer/TimerExecutionToken.cs b/Projects/Server/Timer/TimerExecutionToken.cs index 9535e726e..9e9992e3b 100644 --- a/Projects/Server/Timer/TimerExecutionToken.cs +++ b/Projects/Server/Timer/TimerExecutionToken.cs @@ -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; } diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index 53968cce2..142b554a2 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -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 Mobiles { get; private set; } diff --git a/Projects/UOContent/World Saves/AutoSave.cs b/Projects/UOContent/World Saves/AutoSave.cs index a97446d89..96051e6a1 100644 --- a/Projects/UOContent/World Saves/AutoSave.cs +++ b/Projects/UOContent/World Saves/AutoSave.cs @@ -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) { From 5725fba3733c1ee2f15cc1b202ca014d2b098d23 Mon Sep 17 00:00:00 2001 From: IAmDanielDinner <38978615+SpeedyDevil@users.noreply.github.com> Date: Mon, 4 Oct 2021 08:04:06 +0200 Subject: [PATCH 4/5] fix: Adds missing ML Abilities (#799) --- .../Items/Weapons/Abilities/ArmorPierce.cs | 13 +- .../Items/Weapons/Abilities/Bladeweave.cs | 106 +++++++++ .../Items/Weapons/Abilities/Block.cs | 13 +- .../Items/Weapons/Abilities/CrushingBlow.cs | 18 ++ .../Items/Weapons/Abilities/DefenseMastery.cs | 16 +- .../Items/Weapons/Abilities/Disarm.cs | 44 ++-- .../Items/Weapons/Abilities/Dismount.cs | 11 +- .../Items/Weapons/Abilities/DoubleShot.cs | 15 +- .../Items/Weapons/Abilities/DualWield.cs | 20 +- .../Items/Weapons/Abilities/Feint.cs | 19 +- .../Items/Weapons/Abilities/ForceArrow.cs | 164 ++++++++++++++ .../Items/Weapons/Abilities/ForceOfNature.cs | 111 ++++++++++ .../Weapons/Abilities/FrenziedWhirlwind.cs | 14 +- .../Weapons/Abilities/InfectiousStrike.cs | 2 +- .../Items/Weapons/Abilities/LightningArrow.cs | 58 +++++ .../Items/Weapons/Abilities/NerveStrike.cs | 13 +- .../Items/Weapons/Abilities/ParalyzingBlow.cs | 20 +- .../Items/Weapons/Abilities/PsychicAttack.cs | 93 ++++++++ .../Items/Weapons/Abilities/RidingSwipe.cs | 14 +- .../Items/Weapons/Abilities/SerpentArrow.cs | 46 ++++ .../Items/Weapons/Abilities/ShadowStrike.cs | 4 + .../Items/Weapons/Abilities/TalonStrike.cs | 12 +- .../Items/Weapons/Abilities/WeaponAbility.cs | 109 +++++++-- .../Weapons/Abilities/WhirlwindAttack.cs | 2 +- .../UOContent/Items/Weapons/BaseWeapon.cs | 26 ++- .../Items/Weapons/Staves/GnarledStaff.cs | 2 +- Projects/UOContent/Misc/BuffIcons.cs | 209 ++++++++++++++---- 27 files changed, 931 insertions(+), 243 deletions(-) create mode 100644 Projects/UOContent/Items/Weapons/Abilities/Bladeweave.cs create mode 100644 Projects/UOContent/Items/Weapons/Abilities/ForceArrow.cs create mode 100644 Projects/UOContent/Items/Weapons/Abilities/ForceOfNature.cs create mode 100644 Projects/UOContent/Items/Weapons/Abilities/LightningArrow.cs create mode 100644 Projects/UOContent/Items/Weapons/Abilities/PsychicAttack.cs create mode 100644 Projects/UOContent/Items/Weapons/Abilities/SerpentArrow.cs diff --git a/Projects/UOContent/Items/Weapons/Abilities/ArmorPierce.cs b/Projects/UOContent/Items/Weapons/Abilities/ArmorPierce.cs index 448de8b64..1d3cb2507 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/ArmorPierce.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/ArmorPierce.cs @@ -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) { diff --git a/Projects/UOContent/Items/Weapons/Abilities/Bladeweave.cs b/Projects/UOContent/Items/Weapons/Abilities/Bladeweave.cs new file mode 100644 index 000000000..48e69f012 --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Abilities/Bladeweave.cs @@ -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 _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); + } + } +} diff --git a/Projects/UOContent/Items/Weapons/Abilities/Block.cs b/Projects/UOContent/Items/Weapons/Abilities/Block.cs index 811d71dd6..8ace4d3bd 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/Block.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/Block.cs @@ -11,18 +11,7 @@ namespace Server.Items private static readonly Dictionary _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) { diff --git a/Projects/UOContent/Items/Weapons/Abilities/CrushingBlow.cs b/Projects/UOContent/Items/Weapons/Abilities/CrushingBlow.cs index a172b82ee..0dc5d9a60 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/CrushingBlow.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/CrushingBlow.cs @@ -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)) diff --git a/Projects/UOContent/Items/Weapons/Abilities/DefenseMastery.cs b/Projects/UOContent/Items/Weapons/Abilities/DefenseMastery.cs index 174951364..ee6ce5792 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/DefenseMastery.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/DefenseMastery.cs @@ -12,18 +12,7 @@ namespace Server.Items private static readonly Dictionary _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)) { diff --git a/Projects/UOContent/Items/Weapons/Abilities/Disarm.cs b/Projects/UOContent/Items/Weapons/Abilities/Disarm.cs index e8a6c9c1f..f3ede2c1d 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/Disarm.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/Disarm.cs @@ -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); } } diff --git a/Projects/UOContent/Items/Weapons/Abilities/Dismount.cs b/Projects/UOContent/Items/Weapons/Abilities/Dismount.cs index 8512bf0db..903803d19 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/Dismount.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/Dismount.cs @@ -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) diff --git a/Projects/UOContent/Items/Weapons/Abilities/DoubleShot.cs b/Projects/UOContent/Items/Weapons/Abilities/DoubleShot.cs index a97ad27f0..8f01dea14 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/DoubleShot.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/DoubleShot.cs @@ -1,3 +1,5 @@ +using System; + namespace Server.Items { /// @@ -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) { diff --git a/Projects/UOContent/Items/Weapons/Abilities/DualWield.cs b/Projects/UOContent/Items/Weapons/Abilities/DualWield.cs index 76d2597ff..c8ea1b1af 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/DualWield.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/DualWield.cs @@ -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; diff --git a/Projects/UOContent/Items/Weapons/Abilities/Feint.cs b/Projects/UOContent/Items/Weapons/Abilities/Feint.cs index 70f32fc43..20c738a77 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/Feint.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/Feint.cs @@ -11,18 +11,7 @@ namespace Server.Items public static Dictionary 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(); diff --git a/Projects/UOContent/Items/Weapons/Abilities/ForceArrow.cs b/Projects/UOContent/Items/Weapons/Abilities/ForceArrow.cs new file mode 100644 index 000000000..d8b5c9add --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Abilities/ForceArrow.cs @@ -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> _table = new Dictionary>(); + + 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() { 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; + } + } + } +} diff --git a/Projects/UOContent/Items/Weapons/Abilities/ForceOfNature.cs b/Projects/UOContent/Items/Weapons/Abilities/ForceOfNature.cs new file mode 100644 index 000000000..17d4ffc8a --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Abilities/ForceOfNature.cs @@ -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 _table = new Dictionary(); + + 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); + } + } + } + } +} diff --git a/Projects/UOContent/Items/Weapons/Abilities/FrenziedWhirlwind.cs b/Projects/UOContent/Items/Weapons/Abilities/FrenziedWhirlwind.cs index a523b7a1b..f6b2c6167 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/FrenziedWhirlwind.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/FrenziedWhirlwind.cs @@ -12,18 +12,8 @@ namespace Server.Items public override int BaseMana => 30; public static Dictionary 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) { diff --git a/Projects/UOContent/Items/Weapons/Abilities/InfectiousStrike.cs b/Projects/UOContent/Items/Weapons/Abilities/InfectiousStrike.cs index ce155ce09..05b0e9969 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/InfectiousStrike.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/InfectiousStrike.cs @@ -30,7 +30,7 @@ namespace Server.Items ClearCurrentAbility(attacker); - if (!(attacker.Weapon is BaseWeapon weapon)) + if (attacker.Weapon is not BaseWeapon weapon) { return; } diff --git a/Projects/UOContent/Items/Weapons/Abilities/LightningArrow.cs b/Projects/UOContent/Items/Weapons/Abilities/LightningArrow.cs new file mode 100644 index 000000000..750b0e4cf --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Abilities/LightningArrow.cs @@ -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 targets = new List(); + 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); + } + } + } +} diff --git a/Projects/UOContent/Items/Weapons/Abilities/NerveStrike.cs b/Projects/UOContent/Items/Weapons/Abilities/NerveStrike.cs index cead5302b..11eb81352 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/NerveStrike.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/NerveStrike.cs @@ -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) { diff --git a/Projects/UOContent/Items/Weapons/Abilities/ParalyzingBlow.cs b/Projects/UOContent/Items/Weapons/Abilities/ParalyzingBlow.cs index 9c8184ebd..43f985df1 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/ParalyzingBlow.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/ParalyzingBlow.cs @@ -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); diff --git a/Projects/UOContent/Items/Weapons/Abilities/PsychicAttack.cs b/Projects/UOContent/Items/Weapons/Abilities/PsychicAttack.cs new file mode 100644 index 000000000..c38d387dd --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Abilities/PsychicAttack.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; + +namespace Server.Items +{ + public class PsychicAttack : WeaponAbility + { + public static Dictionary 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); + } + } + } +} diff --git a/Projects/UOContent/Items/Weapons/Abilities/RidingSwipe.cs b/Projects/UOContent/Items/Weapons/Abilities/RidingSwipe.cs index bd880c6b9..cf2be4a65 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/RidingSwipe.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/RidingSwipe.cs @@ -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) { diff --git a/Projects/UOContent/Items/Weapons/Abilities/SerpentArrow.cs b/Projects/UOContent/Items/Weapons/Abilities/SerpentArrow.cs new file mode 100644 index 000000000..138dc25bf --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Abilities/SerpentArrow.cs @@ -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); + } + } +} diff --git a/Projects/UOContent/Items/Weapons/Abilities/ShadowStrike.cs b/Projects/UOContent/Items/Weapons/Abilities/ShadowStrike.cs index a43bf94f2..29d6e5eaa 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/ShadowStrike.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/ShadowStrike.cs @@ -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)) diff --git a/Projects/UOContent/Items/Weapons/Abilities/TalonStrike.cs b/Projects/UOContent/Items/Weapons/Abilities/TalonStrike.cs index 9741fdcb6..13963d11d 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/TalonStrike.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/TalonStrike.cs @@ -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) { diff --git a/Projects/UOContent/Items/Weapons/Abilities/WeaponAbility.cs b/Projects/UOContent/Items/Weapons/Abilities/WeaponAbility.cs index dff1d95c6..9ad7e996f 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/WeaponAbility.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/WeaponAbility.cs @@ -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; + + /// + /// Return primary skill for ability. Default 70/90 + /// + /// + /// 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; + } + + /// + /// Returns secondary skill for Ability. Default 50. + /// + /// + /// + public virtual double GetRequiredSecondarySkill(Mobile from) => 50.0; + + /// + /// Return secondary skillName. Default bushido/ninjitsu + /// + /// + /// + public virtual SkillName GetSecondarySkillName(Mobile from) => from.Skills[SkillName.Ninjitsu].Base > from.Skills[SkillName.Bushido].Base ? SkillName.Ninjitsu : SkillName.Bushido; + + /// + /// Returns tactics needed for Ability. Default 70/90. + /// + /// + /// + 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 } /* */ - 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() { diff --git a/Projects/UOContent/Items/Weapons/Abilities/WhirlwindAttack.cs b/Projects/UOContent/Items/Weapons/Abilities/WhirlwindAttack.cs index 316ae4c53..32496a2a7 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/WhirlwindAttack.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/WhirlwindAttack.cs @@ -28,7 +28,7 @@ namespace Server.Items return; } - if (!(attacker.Weapon is BaseWeapon weapon)) + if (attacker.Weapon is not BaseWeapon weapon) { return; } diff --git a/Projects/UOContent/Items/Weapons/BaseWeapon.cs b/Projects/UOContent/Items/Weapons/BaseWeapon.cs index aef60a032..b50b97f32 100644 --- a/Projects/UOContent/Items/Weapons/BaseWeapon.cs +++ b/Projects/UOContent/Items/Weapons/BaseWeapon.cs @@ -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) diff --git a/Projects/UOContent/Items/Weapons/Staves/GnarledStaff.cs b/Projects/UOContent/Items/Weapons/Staves/GnarledStaff.cs index aaa4d6e19..8ca5f06a6 100644 --- a/Projects/UOContent/Items/Weapons/Staves/GnarledStaff.cs +++ b/Projects/UOContent/Items/Weapons/Staves/GnarledStaff.cs @@ -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; diff --git a/Projects/UOContent/Misc/BuffIcons.cs b/Projects/UOContent/Misc/BuffIcons.cs index 7008ed1cd..0c9e48340 100644 --- a/Projects/UOContent/Misc/BuffIcons.cs +++ b/Projects/UOContent/Misc/BuffIcons.cs @@ -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, } } From da31153b2054e8e9e7a513c07954e4a24a04be26 Mon Sep 17 00:00:00 2001 From: IAmDanielDinner <38978615+SpeedyDevil@users.noreply.github.com> Date: Mon, 4 Oct 2021 22:40:45 +0200 Subject: [PATCH 5/5] fix: Movement issue (#816) --- Projects/UOContent/Engines/Pathing/Movement.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Projects/UOContent/Engines/Pathing/Movement.cs b/Projects/UOContent/Engines/Pathing/Movement.cs index 8ce683ad9..1b733bea3 100644 --- a/Projects/UOContent/Engines/Pathing/Movement.cs +++ b/Projects/UOContent/Engines/Pathing/Movement.cs @@ -475,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; }