diff --git a/Projects/Benchmarks/Benchmarks/Packets/BenchmarkPacketBroadcast.cs b/Projects/Benchmarks/Benchmarks/Packets/BenchmarkPacketBroadcast.cs index 937186aee..bd2390170 100644 --- a/Projects/Benchmarks/Benchmarks/Packets/BenchmarkPacketBroadcast.cs +++ b/Projects/Benchmarks/Benchmarks/Packets/BenchmarkPacketBroadcast.cs @@ -133,7 +133,7 @@ namespace Benchmarks buffer, Serial.MinusOne, -1, MessageType.Regular, 0x3B2, 3, "ENU", "System", text ); - buffer = buffer.Slice(0, length); + buffer = buffer.SliceToLength(length); foreach (var pipe in _pipes) { @@ -152,7 +152,7 @@ namespace Benchmarks buffer, Serial.MinusOne, -1, MessageType.Regular, 0x3B2, 3, "ENU", "System", text ); - buffer = buffer.Slice(0, length); + buffer = buffer.SliceToLength(length); var result = pipe.Writer.TryGetMemory(); result.CopyFrom(buffer); pipe.Writer.Advance((uint)buffer.Length); diff --git a/Projects/Server.Tests/Tests/Buffers/CircularBufferReaderTests.cs b/Projects/Server.Tests/Tests/Buffers/CircularBufferReaderTests.cs index 00abc253a..7eee1476d 100644 --- a/Projects/Server.Tests/Tests/Buffers/CircularBufferReaderTests.cs +++ b/Projects/Server.Tests/Tests/Buffers/CircularBufferReaderTests.cs @@ -65,7 +65,7 @@ namespace Server.Tests.Network ; encoding.GetBytes(chars, buffer.Slice(offset)); - var reader = new CircularBufferReader(buffer.Slice(0, firstSize), buffer.Slice(firstSize)); + var reader = new CircularBufferReader(buffer.SliceToLength(firstSize), buffer.Slice(firstSize)); reader.Seek(offset, SeekOrigin.Begin); var actual = reader.ReadString(encoding, isSafe, fixedLength); diff --git a/Projects/Server.Tests/Tests/Buffers/CircularBufferWriterTests.cs b/Projects/Server.Tests/Tests/Buffers/CircularBufferWriterTests.cs index f57f5ef24..7f9f95012 100644 --- a/Projects/Server.Tests/Tests/Buffers/CircularBufferWriterTests.cs +++ b/Projects/Server.Tests/Tests/Buffers/CircularBufferWriterTests.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.IO; +using Server.Network; using Xunit; namespace Server.Tests.Buffers @@ -54,7 +55,7 @@ namespace Server.Tests.Buffers var strLength = fixedLength > -1 ? Math.Min(value.Length, fixedLength) : value.Length; var chars = value.AsSpan(0, strLength); - var writer = new CircularBufferWriter(buffer.Slice(0, firstSize), buffer.Slice(firstSize)); + var writer = new CircularBufferWriter(buffer.SliceToLength(firstSize), buffer.Slice(firstSize)); writer.Seek(offset, SeekOrigin.Begin); writer.WriteString(chars, encoding); @@ -62,7 +63,7 @@ namespace Server.Tests.Buffers { Span testEmpty = stackalloc byte[offset]; testEmpty.Clear(); - AssertThat.Equal(buffer.Slice(0, offset), testEmpty); + AssertThat.Equal(buffer.SliceToLength(offset), testEmpty); } Span expectedStr = stackalloc byte[encoding.GetByteCount(chars)]; diff --git a/Projects/Server/Buffers/CircularBufferReader.cs b/Projects/Server/Buffers/CircularBufferReader.cs index a33258c0b..af822cfde 100644 --- a/Projects/Server/Buffers/CircularBufferReader.cs +++ b/Projects/Server/Buffers/CircularBufferReader.cs @@ -284,7 +284,7 @@ namespace Server.Network } else { - index = _second.Slice(0, remaining).IndexOfTerminator(sizeT); + index = _second.SliceToLength(remaining).IndexOfTerminator(sizeT); int secondLength = index < 0 ? remaining : index; int length = firstLength + secondLength; @@ -292,7 +292,7 @@ namespace Server.Network // Assume no strings should be too long for the stack Span bytes = stackalloc byte[length]; _first.Slice(Position).CopyTo(bytes); - _second.Slice(0, secondLength).CopyTo(bytes.Slice(firstLength)); + _second.SliceToLength(secondLength).CopyTo(bytes.Slice(firstLength)); Position += length + (index >= 0 ? sizeT : 0); return TextEncoding.GetString(bytes, encoding, safeString); @@ -309,7 +309,7 @@ namespace Server.Network if (index >= 0) { - span = span.Slice(0, index); + span = span.SliceToLength(index); } else { diff --git a/Projects/Server/Buffers/CircularBufferWriter.cs b/Projects/Server/Buffers/CircularBufferWriter.cs index 252261753..2b017732d 100644 --- a/Projects/Server/Buffers/CircularBufferWriter.cs +++ b/Projects/Server/Buffers/CircularBufferWriter.cs @@ -18,6 +18,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Text; using Server; +using Server.Network; using Server.Text; namespace System.Buffers @@ -363,7 +364,7 @@ namespace System.Buffers if (Position < _first.Length) { var sz = Math.Min(buffer.Length, _first.Length - Position); - buffer.Slice(0, sz).CopyTo(_first.Slice(Position)); + buffer.SliceToLength(sz).CopyTo(_first.Slice(Position)); if (sz < buffer.Length) { buffer.Slice(sz).CopyTo(_second); @@ -398,7 +399,7 @@ namespace System.Buffers if (Position < _first.Length) { count = Math.Min(_first.Length - Position, byteCount); - bytes.Slice(0, count).CopyTo(_first.Slice(Position)); + bytes.SliceToLength(count).CopyTo(_first.Slice(Position)); byteCount -= count; Position += count; } diff --git a/Projects/Server/Buffers/SpanWriter.cs b/Projects/Server/Buffers/SpanWriter.cs index 0f59dc0ff..67f69f1c7 100644 --- a/Projects/Server/Buffers/SpanWriter.cs +++ b/Projects/Server/Buffers/SpanWriter.cs @@ -21,6 +21,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using Server; +using Server.Network; using Server.Text; namespace System.Buffers @@ -50,7 +51,7 @@ namespace System.Buffers public int Capacity => _buffer.Length; - public ReadOnlySpan Span => _buffer.Slice(0, Position); + public ReadOnlySpan Span => _buffer.SliceToLength(Position); public Span RawBuffer => _buffer; @@ -78,7 +79,7 @@ namespace System.Buffers var newSize = Math.Max(BytesWritten + additionalCapacity, _buffer.Length * 2); byte[] poolArray = ArrayPool.Shared.Rent(newSize); - _buffer.Slice(0, BytesWritten).CopyTo(poolArray); + _buffer.SliceToLength(BytesWritten).CopyTo(poolArray); byte[]? toReturn = _arrayToReturnToPool; _buffer = _arrayToReturnToPool = poolArray; diff --git a/Projects/Server/Buffers/ValueStringBuilder.cs b/Projects/Server/Buffers/ValueStringBuilder.cs index 94bf77679..ecdf73072 100644 --- a/Projects/Server/Buffers/ValueStringBuilder.cs +++ b/Projects/Server/Buffers/ValueStringBuilder.cs @@ -5,6 +5,7 @@ using System; using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Server.Network; namespace Server.Buffers { @@ -63,7 +64,7 @@ namespace Server.Buffers public ref char this[int index] => ref _chars[index]; - public override string ToString() => _chars.Slice(0, Length).ToString(); + public override string ToString() => _chars.SliceToLength(Length).ToString(); /// Returns the underlying storage of the builder. public Span RawChars => _chars; @@ -79,16 +80,16 @@ namespace Server.Buffers EnsureCapacity(Length + 1); _chars[Length] = '\0'; } - return _chars.Slice(0, Length); + return _chars.SliceToLength(Length); } - public ReadOnlySpan AsSpan() => _chars.Slice(0, Length); + public ReadOnlySpan AsSpan() => _chars.SliceToLength(Length); public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, Length - start); public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length); public bool TryCopyTo(Span destination, out int charsWritten) { - if (_chars.Slice(0, Length).TryCopyTo(destination)) + if (_chars.SliceToLength(Length).TryCopyTo(destination)) { charsWritten = Length; return true; @@ -275,7 +276,7 @@ namespace Server.Buffers { char[] poolArray = ArrayPool.Shared.Rent(Math.Max(Length + additionalCapacityBeyondPos, _chars.Length * 2)); - _chars.Slice(0, Length).CopyTo(poolArray); + _chars.SliceToLength(Length).CopyTo(poolArray); char[]? toReturn = _arrayToReturnToPool; _chars = _arrayToReturnToPool = poolArray; @@ -349,7 +350,7 @@ namespace Server.Buffers } else if (startIndex + length == Length) { - _chars = _chars.Slice(0, startIndex); + _chars = _chars.SliceToLength(startIndex); } else { diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs index 54b328a51..d7791863f 100644 --- a/Projects/Server/Items/Item.cs +++ b/Projects/Server/Items/Item.cs @@ -1154,7 +1154,7 @@ namespace Server var length = OutgoingItemPackets.CreateWorldItemNew(hsWorldItem, this, true); if (length != hsWorldItem.Length) { - hsWorldItem = hsWorldItem.Slice(0, length); + hsWorldItem = hsWorldItem.SliceToLength(length); } SendInfoTo(state, hsWorldItem, opl); @@ -1164,7 +1164,7 @@ namespace Server var length = OutgoingItemPackets.CreateWorldItemNew(saWorldItem, this, false); if (length != saWorldItem.Length) { - saWorldItem = saWorldItem.Slice(0, length); + saWorldItem = saWorldItem.SliceToLength(length); } SendInfoTo(state, saWorldItem, opl); @@ -1174,7 +1174,7 @@ namespace Server var length = OutgoingItemPackets.CreateWorldItem(oldWorldItem, this); if (length != oldWorldItem.Length) { - oldWorldItem = oldWorldItem.Slice(0, length); + oldWorldItem = oldWorldItem.SliceToLength(length); } SendInfoTo(state, oldWorldItem, opl); @@ -3345,7 +3345,7 @@ namespace Server if (length != buffer.Length) { - buffer = buffer.Slice(0, length); // Adjust to the actual size + buffer = buffer.SliceToLength(length); // Adjust to the actual size } state.Send(buffer); @@ -3379,7 +3379,7 @@ namespace Server if (length != buffer.Length) { - buffer = buffer.Slice(0, length); // Adjust to the actual size + buffer = buffer.SliceToLength(length); // Adjust to the actual size } state.Send(buffer); diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index 66c12e643..57bc4b552 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -5796,7 +5796,7 @@ namespace Server if (length != regBuffer.Length) { - regBuffer = regBuffer.Slice(0, length); // Adjust to the actual size + regBuffer = regBuffer.SliceToLength(length); // Adjust to the actual size } heard.OnSpeech(regArgs); @@ -5810,7 +5810,7 @@ namespace Server if (length != mutBuffer.Length) { - mutBuffer = mutBuffer.Slice(0, length); // Adjust to the actual size + mutBuffer = mutBuffer.SliceToLength(length); // Adjust to the actual size } heard.OnSpeech(mutatedArgs); @@ -9162,7 +9162,7 @@ namespace Server if (length != buffer.Length) { - buffer = buffer.Slice(0, length); // Adjust to the actual size + buffer = buffer.SliceToLength(length); // Adjust to the actual size } state.Send(buffer); @@ -9193,7 +9193,7 @@ namespace Server if (length != buffer.Length) { - buffer = buffer.Slice(0, length); // Adjust to the actual size + buffer = buffer.SliceToLength(length); // Adjust to the actual size } state.Send(buffer); @@ -9227,7 +9227,7 @@ namespace Server if (length != buffer.Length) { - buffer = buffer.Slice(0, length); // Adjust to the actual size + buffer = buffer.SliceToLength(length); // Adjust to the actual size } state.Send(buffer); @@ -9275,7 +9275,7 @@ namespace Server if (length != buffer.Length) { - buffer = buffer.Slice(0, length); // Adjust to the actual size + buffer = buffer.SliceToLength(length); // Adjust to the actual size } state.Send(buffer); @@ -9306,7 +9306,7 @@ namespace Server if (length != buffer.Length) { - buffer = buffer.Slice(0, length); // Adjust to the actual size + buffer = buffer.SliceToLength(length); // Adjust to the actual size } state.Send(buffer); diff --git a/Projects/Server/Network/PacketUtilities.cs b/Projects/Server/Network/PacketUtilities.cs index 83e268728..2edbda4db 100644 --- a/Projects/Server/Network/PacketUtilities.cs +++ b/Projects/Server/Network/PacketUtilities.cs @@ -40,6 +40,12 @@ namespace Server.Network writer.Seek(0, SeekOrigin.End); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span SliceToLength(this Span span, int length) => span.Slice(0, length); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan SliceToLength(this ReadOnlySpan span, int length) => span.Slice(0, length); + // If LOCAL INIT is off, then stack/heap allocations have garbage data // Initializes the first byte (Packet ID) so it can be used as a flag. [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Projects/Server/Network/Packets/OutgoingItemPackets.cs b/Projects/Server/Network/Packets/OutgoingItemPackets.cs index eeeae9835..c438439aa 100644 --- a/Projects/Server/Network/Packets/OutgoingItemPackets.cs +++ b/Projects/Server/Network/Packets/OutgoingItemPackets.cs @@ -29,7 +29,7 @@ namespace Server.Network { return buffer.Length; } - + var itemID = item is BaseMulti ? item.ItemID | 0x4000 : item.ItemID & 0x3FFF; var hasAmount = item.Amount != 0; var amount = item.Amount; @@ -95,7 +95,7 @@ namespace Server.Network CreateWorldItemNew(buffer, item, ns.HighSeas) : CreateWorldItem(buffer, item); - ns.Send(buffer.Slice(0, length)); + ns.Send(buffer.SliceToLength(length)); } public static int CreateWorldItemNew(Span buffer, Item item, bool isHS) diff --git a/Projects/Server/Network/Packets/OutgoingMessagePackets.cs b/Projects/Server/Network/Packets/OutgoingMessagePackets.cs index 015d75d8f..a47e58810 100644 --- a/Projects/Server/Network/Packets/OutgoingMessagePackets.cs +++ b/Projects/Server/Network/Packets/OutgoingMessagePackets.cs @@ -46,7 +46,7 @@ namespace Server.Network buffer, serial, graphic, type, hue, font, number, name, args ); - ns.Send(buffer.Slice(0, length)); + ns.Send(buffer.SliceToLength(length)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -102,7 +102,7 @@ namespace Server.Network buffer, serial, graphic, type, hue, font, number, name, affixType, affix, args ); - ns.Send(buffer.Slice(0, length)); + ns.Send(buffer.SliceToLength(length)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -171,7 +171,7 @@ namespace Server.Network text ); - ns.Send(buffer.Slice(0, length)); + ns.Send(buffer.SliceToLength(length)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Projects/Server/Network/Packets/OutgoingMobilePackets.cs b/Projects/Server/Network/Packets/OutgoingMobilePackets.cs index fccbf42b0..4cddfe7d2 100644 --- a/Projects/Server/Network/Packets/OutgoingMobilePackets.cs +++ b/Projects/Server/Network/Packets/OutgoingMobilePackets.cs @@ -470,7 +470,7 @@ namespace Server.Network } var length = CreateMobileStatus(buffer, beholder, beheld, version, beheld.CanBeRenamedBy(beholder)); - ns.Send(buffer.Slice(0, length)); + ns.Send(buffer.SliceToLength(length)); } public static int CreateMobileStatus( diff --git a/Projects/Server/Serialization/BufferWriter.cs b/Projects/Server/Serialization/BufferWriter.cs index b6a8be01a..81f764c3b 100644 --- a/Projects/Server/Serialization/BufferWriter.cs +++ b/Projects/Server/Serialization/BufferWriter.cs @@ -20,6 +20,7 @@ using System.IO; using System.Net; using System.Runtime.CompilerServices; using System.Text; +using Server.Network; using Server.Text; namespace Server @@ -222,7 +223,7 @@ namespace Server Span stack = stackalloc byte[16]; value.TryWriteBytes(stack, out var bytesWritten); Write((byte)bytesWritten); - Write(stack.Slice(0, bytesWritten)); + Write(stack.SliceToLength(bytesWritten)); } public void Write(TimeSpan value) @@ -438,7 +439,7 @@ namespace Server charsLeft -= charCount; current += charCount; - Write(span.Slice(0, bytesWritten)); + Write(span.SliceToLength(bytesWritten)); } } } diff --git a/Projects/Server/Text/StringHelpers.cs b/Projects/Server/Text/StringHelpers.cs index 624100d5e..63a8d5a81 100644 --- a/Projects/Server/Text/StringHelpers.cs +++ b/Projects/Server/Text/StringHelpers.cs @@ -18,6 +18,7 @@ using System.Buffers; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Server.Network; namespace Server { @@ -57,7 +58,7 @@ namespace Server throw new OutOfMemoryException(nameof(buffer)); } - sliced.Slice(0, indexOf).CopyTo(buffer.Slice(size)); + sliced.SliceToLength(indexOf).CopyTo(buffer.Slice(size)); size += indexOf; if (indexOf == sliced.Length) @@ -96,7 +97,7 @@ namespace Server a.Remove(b, comparison, span, out var size); - var str = span.Slice(0, size).ToString(); + var str = span.SliceToLength(size).ToString(); if (chrs != null) { @@ -192,7 +193,7 @@ namespace Server if (newLineLength == perLine || newLineLength == span.Length) { - list.Add(span.Slice(0, newLineLength).ToString()); + list.Add(span.SliceToLength(newLineLength).ToString()); if (list.Count == maxLines || newLineLength == span.Length) { break; @@ -207,7 +208,7 @@ namespace Server } else if (lineLength > 0 && lineLength <= perLine) { - list.Add(span.Slice(0, lineLength - 1).ToString()); + list.Add(span.SliceToLength(lineLength - 1).ToString()); if (list.Count == maxLines) { break; diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index 9b6a135ce..cfda11e72 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -10,6 +10,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Xml; using Microsoft.Toolkit.HighPerformance.Extensions; +using Server.Network; using Server.Random; namespace Server @@ -386,7 +387,7 @@ namespace Server var sectionStart = 0; var hasCompressor = false; - var num = BinaryPrimitives.ReadUInt16BigEndian(ip.Slice(0, 2)); + var num = BinaryPrimitives.ReadUInt16BigEndian(ip.SliceToLength(2)); for (int i = 0; i < end; i++) { diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index 14a3209ec..1f4c7b76c 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -167,7 +167,7 @@ namespace Server if (length != buffer.Length) { - buffer = buffer.Slice(0, length); // Adjust to the actual size + buffer = buffer.SliceToLength(length); // Adjust to the actual size } ns.Send(buffer); diff --git a/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/BulletinBoardPacketTests.cs b/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/BulletinBoardPacketTests.cs new file mode 100644 index 000000000..6edeb8534 --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/BulletinBoardPacketTests.cs @@ -0,0 +1,68 @@ +using System; +using Server; +using Server.Items; +using Server.Network; +using Server.Tests; +using Server.Tests.Network; +using Xunit; + +namespace UOContent.Tests +{ + public class BulletinBoardPacketTests : IClassFixture + { + [Theory] + [InlineData("Test Name")] + [InlineData(null)] + [InlineData("🅵🅰🅽🅲🆈 🆃🅴🆇🆃")] + public void TestSendBBDisplayBoard(string boardName) + { + var bb = new TestBulletinBoard(0x234) { BoardName = boardName }; + + var expected = new BBDisplayBoard(bb).Compile(); + + using var ns = PacketTestUtilities.CreateTestNetState(); + ns.SendBBDisplayBoard(bb); + + var result = ns.SendPipe.Reader.TryRead(); + AssertThat.Equal(result.Buffer[0].AsSpan(0), expected); + } + + [Theory] + [InlineData("The Subject", false, "First Line", "Second Line", "Third Line")] + [InlineData("", false, "")] + [InlineData("🅵🅰🅽🅲🆈 🆃🅴🆇🆃", false, "First Line", "Second Line")] + [InlineData("The Subject", true, "First Line", "Second Line", "Third Line")] + [InlineData("", true, "")] + [InlineData("🅵🅰🅽🅲🆈 🆃🅴🆇🆃", true, "First Line", "Second Line")] + public void TestSendBBHeaderMessage(string subject, bool content, params string[] lines) + { + var poster = new Mobile(0x1024u) { Name = "Kamron" }; + poster.DefaultMobileInit(); + + var bb = new TestBulletinBoard(0x234); + bb.PostMessage(poster, null, subject, lines); + + var msg = bb.Items[0] as BulletinMessage; + + var expected = (content ? + (Packet)new BBMessageContent(bb, msg) : new BBMessageHeader(bb, msg)).Compile(); + + using var ns = PacketTestUtilities.CreateTestNetState(); + ns.SendBBMessage(bb, msg, content); + + var result = ns.SendPipe.Reader.TryRead(); + AssertThat.Equal(result.Buffer[0].AsSpan(0), expected); + } + } + + internal class TestBulletinBoard : BaseBulletinBoard + { + public TestBulletinBoard(int itemID) : base(itemID) + { + } + + public TestBulletinBoard(Serial serial) : base(serial) + { + } + } +} diff --git a/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/Packets.cs b/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/Packets.cs new file mode 100644 index 000000000..ae034a0eb --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/Packets.cs @@ -0,0 +1,149 @@ +using Server.Items; +using Server.Text; + +namespace Server.Network +{ + public class BBDisplayBoard : Packet + { + public BBDisplayBoard(BaseBulletinBoard board) : base(0x71) + { + EnsureCapacity(38); + + var buffer = (board.BoardName ?? "").GetBytesUtf8(); + + Stream.Write((byte)0x00); // PacketID + Stream.Write(board.Serial); // Bulletin board serial + + // Bulletin board name + if (buffer.Length >= 29) + { + Stream.Write(buffer, 0, 29); + Stream.Write((byte)0); + } + else + { + Stream.Write(buffer, 0, buffer.Length); + Stream.Fill(30 - buffer.Length); + } + } + } + + public class BBMessageHeader : Packet + { + public BBMessageHeader(BaseBulletinBoard board, BulletinMessage msg) : base(0x71) + { + var poster = SafeString(msg.PostedName); + var subject = SafeString(msg.Subject); + var time = SafeString(msg.GetTimeAsString()); + + EnsureCapacity(22 + poster.Length + subject.Length + time.Length); + + Stream.Write((byte)0x01); // PacketID + Stream.Write(board.Serial); // Bulletin board serial + Stream.Write(msg.Serial); // Message serial + + Stream.Write(msg.Thread?.Serial ?? Serial.Zero); // Thread serial--parent + + WriteString(poster); + WriteString(subject); + WriteString(time); + } + + public void WriteString(string v) + { + var buffer = v.GetBytesUtf8(); + var len = buffer.Length + 1; + + if (len > 255) + { + len = 255; + } + + Stream.Write((byte)len); + Stream.Write(buffer, 0, len - 1); + Stream.Write((byte)0); + } + + public string SafeString(string v) => v ?? string.Empty; + } + + public class BBMessageContent : Packet + { + public BBMessageContent(BaseBulletinBoard board, BulletinMessage msg) : base(0x71) + { + var poster = SafeString(msg.PostedName); + var subject = SafeString(msg.Subject); + var time = SafeString(msg.GetTimeAsString()); + + EnsureCapacity(22 + poster.Length + subject.Length + time.Length); + + Stream.Write((byte)0x02); // PacketID + Stream.Write(board.Serial); // Bulletin board serial + Stream.Write(msg.Serial); // Message serial + + WriteString(poster); + WriteString(subject); + WriteString(time); + + Stream.Write((short)msg.PostedBody); + Stream.Write((short)msg.PostedHue); + + var len = msg.PostedEquip.Length; + + if (len > 255) + { + len = 255; + } + + Stream.Write((byte)len); + + for (var i = 0; i < len; ++i) + { + var eq = msg.PostedEquip[i]; + + Stream.Write((short)eq.itemID); + Stream.Write((short)eq.hue); + } + + len = msg.Lines.Length; + + if (len > 255) + { + len = 255; + } + + Stream.Write((byte)len); + + for (var i = 0; i < len; ++i) + { + WriteString(msg.Lines[i], true); + } + } + + public void WriteString(string v, bool padding = false) + { + var buffer = v.GetBytesUtf8(); + var tail = padding ? 2 : 1; + var len = buffer.Length + tail; + + if (len > 255) + { + len = 255; + } + + Stream.Write((byte)len); + Stream.Write(buffer, 0, len - tail); + + if (padding) + { + Stream.Write((short)0); // padding compensates for a client bug + } + else + { + Stream.Write((byte)0); + } + } + + public string SafeString(string v) => v ?? string.Empty; + } +} diff --git a/Projects/UOContent/Accounting/Security/PBKDF2PasswordProtection.cs b/Projects/UOContent/Accounting/Security/PBKDF2PasswordProtection.cs index 3fd3d7685..a54e1f25a 100644 --- a/Projects/UOContent/Accounting/Security/PBKDF2PasswordProtection.cs +++ b/Projects/UOContent/Accounting/Security/PBKDF2PasswordProtection.cs @@ -16,6 +16,7 @@ using System; using System.Buffers.Binary; using System.Security.Cryptography; +using Server.Network; using Server.Text; namespace Server.Accounting.Security @@ -33,7 +34,7 @@ namespace Server.Accounting.Security { Span output = stackalloc byte[m_OutputSize]; var iterations = Utility.RandomMinMax(m_MinIterations, m_MaxIterations); - BinaryPrimitives.WriteUInt16LittleEndian(output.Slice(0, 2), (ushort)iterations); + BinaryPrimitives.WriteUInt16LittleEndian(output.SliceToLength(2), (ushort)iterations); var rfc2898 = new Rfc2898DeriveBytes(plainPassword, m_SaltSize, iterations, HashAlgorithmName.SHA256); rfc2898.Salt.CopyTo(output.Slice(2, m_SaltSize)); @@ -47,7 +48,7 @@ namespace Server.Accounting.Security Span encryptedBytes = stackalloc byte[m_OutputSize]; HexStringConverter.GetBytes(encryptedPassword, encryptedBytes); - var iterations = BinaryPrimitives.ReadUInt16LittleEndian(encryptedBytes.Slice(0, 2)); + var iterations = BinaryPrimitives.ReadUInt16LittleEndian(encryptedBytes.SliceToLength(2)); var salt = encryptedBytes.Slice(2, m_SaltSize); ReadOnlySpan hash = diff --git a/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleController.cs b/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleController.cs index 8860bb2ac..7141d68bc 100644 --- a/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleController.cs +++ b/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleController.cs @@ -524,7 +524,7 @@ namespace Server.Engines.Doom if (length != buffer.Length) { - buffer = buffer.Slice(0, length); // Adjust to the actual size + buffer = buffer.SliceToLength(length); // Adjust to the actual size } state.Send(buffer); diff --git a/Projects/UOContent/Engines/MLQuests/Mobiles/SirHelper.cs b/Projects/UOContent/Engines/MLQuests/Mobiles/SirHelper.cs index a6679c7ea..f846e95e8 100644 --- a/Projects/UOContent/Engines/MLQuests/Mobiles/SirHelper.cs +++ b/Projects/UOContent/Engines/MLQuests/Mobiles/SirHelper.cs @@ -110,7 +110,7 @@ namespace Server.Engines.MLQuests.Mobiles if (length != buffer.Length) { - buffer = buffer.Slice(0, length); + buffer = buffer.SliceToLength(length); } state.Send(buffer); diff --git a/Projects/UOContent/Engines/Party/Party.cs b/Projects/UOContent/Engines/Party/Party.cs index a0043be80..fae2d5bad 100644 --- a/Projects/UOContent/Engines/Party/Party.cs +++ b/Projects/UOContent/Engines/Party/Party.cs @@ -246,7 +246,7 @@ namespace Server.Engines.PartySystem ); // : joined the party. - buffer = buffer.Slice(0, length); + buffer = buffer.SliceToLength(length); SendToAll(buffer); SendToAllListeners(buffer); @@ -388,7 +388,7 @@ namespace Server.Engines.PartySystem Serial.MinusOne, -1, MessageType.Regular, hue, 3, number, "System", args ); - buffer = buffer.Slice(0, length); + buffer = buffer.SliceToLength(length); SendToAll(buffer); SendToAllListeners(buffer); @@ -438,7 +438,7 @@ namespace Server.Engines.PartySystem if (length != buffer.Length) { - buffer = buffer.Slice(0, length); // Adjust to the actual size + buffer = buffer.SliceToLength(length); // Adjust to the actual size } ns.Send(buffer); @@ -528,7 +528,7 @@ namespace Server.Engines.PartySystem if (length != buffer.Length) { - buffer = buffer.Slice(0, length); // Adjust to the actual size + buffer = buffer.SliceToLength(length); // Adjust to the actual size } m.NetState?.Send(buffer); diff --git a/Projects/UOContent/Items/Bulletin Boards/BulletinBoard.cs b/Projects/UOContent/Items/Bulletin Boards/BulletinBoard.cs new file mode 100644 index 000000000..e3744d805 --- /dev/null +++ b/Projects/UOContent/Items/Bulletin Boards/BulletinBoard.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections.Generic; +using Server.Network; + +namespace Server.Items +{ + [Flippable(0x1E5E, 0x1E5F)] + public class BulletinBoard : BaseBulletinBoard + { + [Constructible] + public BulletinBoard() : base(0x1E5E) + { + } + + public BulletinBoard(Serial serial) : base(serial) + { + } + + public override void Serialize(IGenericWriter writer) + { + base.Serialize(writer); + + writer.Write(0); // version + } + + public override void Deserialize(IGenericReader reader) + { + base.Deserialize(reader); + + var version = reader.ReadInt(); + } + } + + public abstract class BaseBulletinBoard : Item + { + // Threads will be removed six hours after the last post was made + private static readonly TimeSpan ThreadDeletionTime = TimeSpan.FromHours(6.0); + + // A player may only create a thread once every two minutes + private static readonly TimeSpan ThreadCreateTime = TimeSpan.FromMinutes(2.0); + + // A player may only reply once every thirty seconds + private static readonly TimeSpan ThreadReplyTime = TimeSpan.FromSeconds(30.0); + + public BaseBulletinBoard(int itemID) : base(itemID) + { + BoardName = "bulletin board"; + Movable = false; + } + + public BaseBulletinBoard(Serial serial) : base(serial) + { + } + + [CommandProperty(AccessLevel.GameMaster)] + public string BoardName { get; set; } + + public static bool CheckTime(DateTime time, TimeSpan range) => time + range < DateTime.UtcNow; + + public static string FormatTS(TimeSpan ts) + { + var totalSeconds = (int)ts.TotalSeconds; + var seconds = totalSeconds % 60; + var minutes = totalSeconds / 60; + + if (minutes != 0 && seconds != 0) + { + return $"{minutes} minute{(minutes == 1 ? "" : "s")} and {seconds} second{(seconds == 1 ? "" : "s")}"; + } + + if (minutes != 0) + { + return $"{minutes} minute{(minutes == 1 ? "" : "s")}"; + } + + return $"{seconds} second{(seconds == 1 ? "" : "s")}"; + } + + public virtual void Cleanup() + { + var items = Items; + + for (var i = items.Count - 1; i >= 0; --i) + { + if (i >= items.Count) + { + continue; + } + + if (!(items[i] is BulletinMessage msg)) + { + continue; + } + + if (msg.Thread == null && CheckTime(msg.LastPostTime, ThreadDeletionTime)) + { + msg.Delete(); + RecurseDelete(msg); // A root-level thread has expired + } + } + } + + private void RecurseDelete(BulletinMessage msg) + { + var found = new List(); + var items = Items; + + for (var i = items.Count - 1; i >= 0; --i) + { + if (i >= items.Count) + { + continue; + } + + if (!(items[i] is BulletinMessage check)) + { + continue; + } + + if (check.Thread == msg) + { + check.Delete(); + found.Add(check); + } + } + + for (var i = 0; i < found.Count; ++i) + { + RecurseDelete((BulletinMessage)found[i]); + } + } + + public virtual bool GetLastPostTime(Mobile poster, bool onlyCheckRoot, ref DateTime lastPostTime) + { + var items = Items; + var wasSet = false; + + for (var i = 0; i < items.Count; ++i) + { + if (!(items[i] is BulletinMessage msg) || msg.Poster != poster) + { + continue; + } + + if (onlyCheckRoot && msg.Thread != null) + { + continue; + } + + if (msg.Time > lastPostTime) + { + wasSet = true; + lastPostTime = msg.Time; + } + } + + return wasSet; + } + + public override void OnDoubleClick(Mobile from) + { + if (CheckRange(from)) + { + Cleanup(); + + var state = from.NetState; + + state.SendBBDisplayBoard(this); + state.SendContainerContent(from, this); + } + else + { + from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019045); // I can't reach that. + } + } + + public virtual bool CheckRange(Mobile from) => + from.AccessLevel >= AccessLevel.GameMaster || from.Map == Map && from.InRange(GetWorldLocation(), 2); + + public void PostMessage(Mobile from, BulletinMessage thread, string subject, string[] lines) + { + if (thread != null) + { + thread.LastPostTime = DateTime.UtcNow; + } + + AddItem(new BulletinMessage(from, thread, subject, lines)); + } + + public override void Serialize(IGenericWriter writer) + { + base.Serialize(writer); + + writer.Write(0); // version + + writer.Write(BoardName); + } + + public override void Deserialize(IGenericReader reader) + { + base.Deserialize(reader); + + var version = reader.ReadInt(); + + switch (version) + { + case 0: + { + BoardName = reader.ReadString(); + break; + } + } + } + + public static void Initialize() + { + IncomingPackets.Register(0x71, 0, true, BBClientRequest); + } + + public static void BBClientRequest(NetState state, CircularBufferReader reader) + { + var from = state.Mobile; + + int packetID = reader.ReadByte(); + + if (World.FindItem(reader.ReadUInt32()) is not BaseBulletinBoard board || !board.CheckRange(from)) + { + return; + } + + switch (packetID) + { + case 3: + BBRequestContent(from, board, reader); + break; + case 4: + BBRequestHeader(from, board, reader); + break; + case 5: + BBPostMessage(from, board, reader); + break; + case 6: + BBRemoveMessage(from, board, reader); + break; + } + } + + public static void BBRequestContent(Mobile from, BaseBulletinBoard board, CircularBufferReader reader) + { + if (World.FindItem(reader.ReadUInt32()) is not BulletinMessage msg || msg.Parent != board) + { + return; + } + + from.NetState.SendBBMessage(board, msg, true); + } + + public static void BBRequestHeader(Mobile from, BaseBulletinBoard board, CircularBufferReader reader) + { + if (World.FindItem(reader.ReadUInt32()) is not BulletinMessage msg || msg.Parent != board) + { + return; + } + + from.NetState.SendBBMessage(board, msg); + } + + public static void BBPostMessage(Mobile from, BaseBulletinBoard board, CircularBufferReader reader) + { + var thread = World.FindItem(reader.ReadUInt32()) as BulletinMessage; + + if (thread != null && thread.Parent != board) + { + thread = null; + } + + var breakout = 0; + + while (thread?.Thread != null && breakout++ < 10) + { + thread = thread.Thread; + } + + var lastPostTime = DateTime.MinValue; + + if (board.GetLastPostTime(from, thread == null, ref lastPostTime) && + !CheckTime(lastPostTime, thread == null ? ThreadCreateTime : ThreadReplyTime)) + { + if (thread == null) + { + from.SendMessage("You must wait {0} before creating a new thread.", FormatTS(ThreadCreateTime)); + } + else + { + from.SendMessage("You must wait {0} before replying to another thread.", FormatTS(ThreadReplyTime)); + } + + return; + } + + var subject = reader.ReadUTF8Safe(reader.ReadByte()); + + if (subject.Length == 0) + { + return; + } + + var lines = new string[reader.ReadByte()]; + + if (lines.Length == 0) + { + return; + } + + for (var i = 0; i < lines.Length; ++i) + { + lines[i] = reader.ReadUTF8Safe(reader.ReadByte()); + } + + board.PostMessage(from, thread, subject, lines); + } + + public static void BBRemoveMessage(Mobile from, BaseBulletinBoard board, CircularBufferReader reader) + { + if (World.FindItem(reader.ReadUInt32()) is not BulletinMessage msg || msg.Parent != board) + { + return; + } + + if (from.AccessLevel < AccessLevel.GameMaster && msg.Poster != from) + { + return; + } + + msg.Delete(); + } + } +} diff --git a/Projects/UOContent/Items/Bulletin Boards/BulletinBoardPackets.cs b/Projects/UOContent/Items/Bulletin Boards/BulletinBoardPackets.cs new file mode 100644 index 000000000..58f2f8ca5 --- /dev/null +++ b/Projects/UOContent/Items/Bulletin Boards/BulletinBoardPackets.cs @@ -0,0 +1,152 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: BulletinBoardPackets.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.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using Server.Items; +using Server.Text; + +namespace Server.Network +{ + public static class BulletinBoardPackets + { + public static void SendBBDisplayBoard(this NetState ns, BaseBulletinBoard board) + { + if (ns == null) + { + return; + } + + var writer = new SpanWriter(stackalloc byte[38]); + writer.Write((byte)0x71); // Packet ID + writer.Write((ushort)38); + writer.Write((byte)0x00); // Command + writer.Write(board.Serial); + + var text = board.BoardName ?? ""; + + var textChars = text.AsSpan(0, Math.Min(text.Length, 30)); + Span textBuffer = stackalloc byte[TextEncoding.UTF8.GetMaxByteCount(textChars.Length)]; // Max 30 * 3 (90 bytes) + + // We are ok with the string being cut-off mid character. The alternative is very slow. + var byteLength = Math.Min(29, TextEncoding.UTF8.GetBytes(textChars, textBuffer)); + writer.Write(textBuffer.SliceToLength(byteLength)); + writer.Clear(30 - byteLength); // terminator + + ns.Send(writer.Span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UpdateLengthCounters(this string text, ref int maxLength, ref int longestTextLine, bool pad = false) + { + var line = Math.Min(255, text.Length); + var byteCount = TextEncoding.UTF8.GetMaxByteCount(line) + (pad ? 3 : 2); // 1 + length + terminator + maxLength += byteCount; + longestTextLine = Math.Max(byteCount, longestTextLine); + } + + public static void SendBBMessage(this NetState ns, BaseBulletinBoard board, BulletinMessage msg, bool content = false) + { + if (ns == null) + { + return; + } + + var longestTextLine = 0; + var maxLength = 22; + var poster = msg.PostedName ?? ""; + poster.UpdateLengthCounters(ref maxLength, ref longestTextLine); + + var subject = msg.Subject ?? ""; + subject.UpdateLengthCounters(ref maxLength, ref longestTextLine); + + var time = msg.GetTimeAsString() ?? ""; + time.UpdateLengthCounters(ref maxLength, ref longestTextLine); + + var equipLength = Math.Min(255, msg.PostedEquip.Length); + var linesLength = Math.Min(255, msg.Lines.Length); + + if (content) + { + maxLength += 2 + equipLength * 4; // We have an extra 4 from the thread serial + for (int i = 0; i < linesLength; i++) + { + msg.Lines[i].UpdateLengthCounters(ref maxLength, ref longestTextLine, true); + } + } + + Span textBuffer = stackalloc byte[TextEncoding.UTF8.GetMaxByteCount(longestTextLine)]; + + var writer = maxLength > 81920 ? new SpanWriter(maxLength) : new SpanWriter(stackalloc byte[maxLength]); + writer.Write((byte)0x71); // Packet ID + writer.Seek(2, SeekOrigin.Current); + writer.Write((byte)(content ? 0x02 : 0x01)); // Command + writer.Write(board.Serial); + writer.Write(msg.Serial); + if (!content) + { + writer.Write(msg.Thread?.Serial ?? Serial.Zero); + } + + writer.WriteString(poster, textBuffer); + writer.WriteString(subject, textBuffer); + writer.WriteString(time, textBuffer); + + if (content) + { + writer.Write((short)msg.PostedBody); + writer.Write((short)msg.PostedHue); + + writer.Write((byte)equipLength); + for (var i = 0; i < equipLength; i++) + { + var eq = msg.PostedEquip[i]; + writer.Write((short)eq.itemID); + writer.Write((short)eq.hue); + } + + writer.Write((byte)linesLength); + for (var i = 0; i < linesLength; i++) + { + writer.WriteString(msg.Lines[i], textBuffer, true); + } + } + + writer.WritePacketLength(); + ns.Send(writer.Span); + + writer.Dispose(); + } + + private static void WriteString(this ref SpanWriter writer, string text, Span buffer, bool pad = false) + { + var tail = pad ? 2 : 1; + var length = Math.Min(pad ? 253 : 254, text.GetBytesUtf8(buffer)); + writer.Write((byte)(length + tail)); + writer.Write(buffer.SliceToLength(length)); + + if (pad) + { + writer.Write((ushort)0); // Compensating for an old client bug + } + else + { + writer.Write((byte)0); // Terminator + } + } + } +} diff --git a/Projects/UOContent/Items/Bulletin Boards/BulletinEquip.cs b/Projects/UOContent/Items/Bulletin Boards/BulletinEquip.cs new file mode 100644 index 000000000..13877cb2c --- /dev/null +++ b/Projects/UOContent/Items/Bulletin Boards/BulletinEquip.cs @@ -0,0 +1,14 @@ +namespace Server.Items +{ + public struct BulletinEquip + { + public int itemID; + public int hue; + + public BulletinEquip(int itemID, int hue) + { + this.itemID = itemID; + this.hue = hue; + } + } +} diff --git a/Projects/UOContent/Items/Bulletin Boards/BulletinMessage.cs b/Projects/UOContent/Items/Bulletin Boards/BulletinMessage.cs new file mode 100644 index 000000000..a37a07dd8 --- /dev/null +++ b/Projects/UOContent/Items/Bulletin Boards/BulletinMessage.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using Server.Targeting; + +namespace Server.Items +{ + public class BulletinMessage : Item + { + public BulletinMessage(Mobile poster, BulletinMessage thread, string subject, string[] lines) : base(0xEB0) + { + Movable = false; + + Poster = poster; + Subject = subject; + Time = DateTime.UtcNow; + LastPostTime = Time; + Thread = thread; + PostedName = Poster.Name; + PostedBody = Poster.Body; + PostedHue = Poster.Hue; + Lines = lines; + + var list = new List(); + + for (var i = 0; i < poster.Items.Count; ++i) + { + var item = poster.Items[i]; + + if (item.Layer >= Layer.OneHanded && item.Layer <= Layer.Mount) + { + list.Add(new BulletinEquip(item.ItemID, item.Hue)); + } + } + + PostedEquip = list.ToArray(); + } + + public BulletinMessage(Serial serial) : base(serial) + { + } + + public Mobile Poster { get; private set; } + + public BulletinMessage Thread { get; private set; } + + public string Subject { get; private set; } + + public DateTime Time { get; private set; } + + public DateTime LastPostTime { get; set; } + + public string PostedName { get; private set; } + + public int PostedBody { get; private set; } + + public int PostedHue { get; private set; } + + public BulletinEquip[] PostedEquip { get; private set; } + + public string[] Lines { get; private set; } + + // TODO: Memoize + public string GetTimeAsString() => Time.ToString("MMM dd, yyyy"); + + public override bool CheckTarget(Mobile from, Target targ, object targeted) => false; + + public override bool IsAccessibleTo(Mobile check) => false; + + public override void Serialize(IGenericWriter writer) + { + base.Serialize(writer); + + writer.Write(1); // version + + writer.Write(Poster); + writer.Write(Subject); + writer.Write(Time); + writer.Write(LastPostTime); + writer.Write(Thread != null); + writer.Write(Thread); + writer.Write(PostedName); + writer.Write(PostedBody); + writer.Write(PostedHue); + + writer.Write(PostedEquip.Length); + + for (var i = 0; i < PostedEquip.Length; ++i) + { + writer.Write(PostedEquip[i].itemID); + writer.Write(PostedEquip[i].hue); + } + + writer.Write(Lines.Length); + + for (var i = 0; i < Lines.Length; ++i) + { + writer.Write(Lines[i]); + } + } + + public override void Deserialize(IGenericReader reader) + { + base.Deserialize(reader); + + var version = reader.ReadInt(); + + switch (version) + { + case 1: + case 0: + { + Poster = reader.ReadEntity(); + Subject = reader.ReadString(); + Time = reader.ReadDateTime(); + LastPostTime = reader.ReadDateTime(); + var hasThread = reader.ReadBool(); + Thread = reader.ReadEntity(); + PostedName = reader.ReadString(); + PostedBody = reader.ReadInt(); + PostedHue = reader.ReadInt(); + + PostedEquip = new BulletinEquip[reader.ReadInt()]; + + for (var i = 0; i < PostedEquip.Length; ++i) + { + PostedEquip[i].itemID = reader.ReadInt(); + PostedEquip[i].hue = reader.ReadInt(); + } + + Lines = new string[reader.ReadInt()]; + + for (var i = 0; i < Lines.Length; ++i) + { + Lines[i] = reader.ReadString(); + } + + if (hasThread && Thread == null) + { + Delete(); + } + + if (version == 0) + { + ValidationQueue.Add(this); + } + + break; + } + } + } + + public void Validate() + { + if ((Parent as BulletinBoard)?.Items.Contains(this) == false) + { + Delete(); + } + } + } +} diff --git a/Projects/UOContent/Items/Misc/Blocker.cs b/Projects/UOContent/Items/Misc/Blocker.cs index 5acbb6cd9..1ee970946 100644 --- a/Projects/UOContent/Items/Misc/Blocker.cs +++ b/Projects/UOContent/Items/Misc/Blocker.cs @@ -47,7 +47,7 @@ namespace Server.Items BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(7, 2), GMItemId); } - ns.Send(buffer.Slice(0, length)); + ns.Send(buffer.SliceToLength(length)); } public override void Serialize(IGenericWriter writer) diff --git a/Projects/UOContent/Items/Misc/BulletinBoards.cs b/Projects/UOContent/Items/Misc/BulletinBoards.cs deleted file mode 100644 index cf91baba3..000000000 --- a/Projects/UOContent/Items/Misc/BulletinBoards.cs +++ /dev/null @@ -1,680 +0,0 @@ -using System; -using System.Collections.Generic; -using Server.Network; -using Server.Targeting; -using Server.Text; - -namespace Server.Items -{ - [Flippable(0x1E5E, 0x1E5F)] - public class BulletinBoard : BaseBulletinBoard - { - [Constructible] - public BulletinBoard() : base(0x1E5E) - { - } - - public BulletinBoard(Serial serial) : base(serial) - { - } - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.Write(0); // version - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - var version = reader.ReadInt(); - } - } - - public abstract class BaseBulletinBoard : Item - { - // Threads will be removed six hours after the last post was made - private static readonly TimeSpan ThreadDeletionTime = TimeSpan.FromHours(6.0); - - // A player may only create a thread once every two minutes - private static readonly TimeSpan ThreadCreateTime = TimeSpan.FromMinutes(2.0); - - // A player may only reply once every thirty seconds - private static readonly TimeSpan ThreadReplyTime = TimeSpan.FromSeconds(30.0); - - public BaseBulletinBoard(int itemID) : base(itemID) - { - BoardName = "bulletin board"; - Movable = false; - } - - public BaseBulletinBoard(Serial serial) : base(serial) - { - } - - [CommandProperty(AccessLevel.GameMaster)] - public string BoardName { get; set; } - - public static bool CheckTime(DateTime time, TimeSpan range) => time + range < DateTime.UtcNow; - - public static string FormatTS(TimeSpan ts) - { - var totalSeconds = (int)ts.TotalSeconds; - var seconds = totalSeconds % 60; - var minutes = totalSeconds / 60; - - if (minutes != 0 && seconds != 0) - { - return $"{minutes} minute{(minutes == 1 ? "" : "s")} and {seconds} second{(seconds == 1 ? "" : "s")}"; - } - - if (minutes != 0) - { - return $"{minutes} minute{(minutes == 1 ? "" : "s")}"; - } - - return $"{seconds} second{(seconds == 1 ? "" : "s")}"; - } - - public virtual void Cleanup() - { - var items = Items; - - for (var i = items.Count - 1; i >= 0; --i) - { - if (i >= items.Count) - { - continue; - } - - if (!(items[i] is BulletinMessage msg)) - { - continue; - } - - if (msg.Thread == null && CheckTime(msg.LastPostTime, ThreadDeletionTime)) - { - msg.Delete(); - RecurseDelete(msg); // A root-level thread has expired - } - } - } - - private void RecurseDelete(BulletinMessage msg) - { - var found = new List(); - var items = Items; - - for (var i = items.Count - 1; i >= 0; --i) - { - if (i >= items.Count) - { - continue; - } - - if (!(items[i] is BulletinMessage check)) - { - continue; - } - - if (check.Thread == msg) - { - check.Delete(); - found.Add(check); - } - } - - for (var i = 0; i < found.Count; ++i) - { - RecurseDelete((BulletinMessage)found[i]); - } - } - - public virtual bool GetLastPostTime(Mobile poster, bool onlyCheckRoot, ref DateTime lastPostTime) - { - var items = Items; - var wasSet = false; - - for (var i = 0; i < items.Count; ++i) - { - if (!(items[i] is BulletinMessage msg) || msg.Poster != poster) - { - continue; - } - - if (onlyCheckRoot && msg.Thread != null) - { - continue; - } - - if (msg.Time > lastPostTime) - { - wasSet = true; - lastPostTime = msg.Time; - } - } - - return wasSet; - } - - public override void OnDoubleClick(Mobile from) - { - if (CheckRange(from)) - { - Cleanup(); - - var state = from.NetState; - - state.Send(new BBDisplayBoard(this)); - state.SendContainerContent(from, this); - } - else - { - from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019045); // I can't reach that. - } - } - - public virtual bool CheckRange(Mobile from) - { - if (from.AccessLevel >= AccessLevel.GameMaster) - { - return true; - } - - return from.Map == Map && from.InRange(GetWorldLocation(), 2); - } - - public void PostMessage(Mobile from, BulletinMessage thread, string subject, string[] lines) - { - if (thread != null) - { - thread.LastPostTime = DateTime.UtcNow; - } - - AddItem(new BulletinMessage(from, thread, subject, lines)); - } - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.Write(0); // version - - writer.Write(BoardName); - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - var version = reader.ReadInt(); - - switch (version) - { - case 0: - { - BoardName = reader.ReadString(); - break; - } - } - } - - public static void Initialize() - { - IncomingPackets.Register(0x71, 0, true, BBClientRequest); - } - - public static void BBClientRequest(NetState state, CircularBufferReader reader) - { - var from = state.Mobile; - - int packetID = reader.ReadByte(); - - if (!(World.FindItem(reader.ReadUInt32()) is BaseBulletinBoard board) || !board.CheckRange(from)) - { - return; - } - - switch (packetID) - { - case 3: - BBRequestContent(from, board, reader); - break; - case 4: - BBRequestHeader(from, board, reader); - break; - case 5: - BBPostMessage(from, board, reader); - break; - case 6: - BBRemoveMessage(from, board, reader); - break; - } - } - - public static void BBRequestContent(Mobile from, BaseBulletinBoard board, CircularBufferReader reader) - { - if (!(World.FindItem(reader.ReadUInt32()) is BulletinMessage msg) || msg.Parent != board) - { - return; - } - - from.Send(new BBMessageContent(board, msg)); - } - - public static void BBRequestHeader(Mobile from, BaseBulletinBoard board, CircularBufferReader reader) - { - if (!(World.FindItem(reader.ReadUInt32()) is BulletinMessage msg) || msg.Parent != board) - { - return; - } - - from.Send(new BBMessageHeader(board, msg)); - } - - public static void BBPostMessage(Mobile from, BaseBulletinBoard board, CircularBufferReader reader) - { - var thread = World.FindItem(reader.ReadUInt32()) as BulletinMessage; - - if (thread != null && thread.Parent != board) - { - thread = null; - } - - var breakout = 0; - - while (thread?.Thread != null && breakout++ < 10) - { - thread = thread.Thread; - } - - var lastPostTime = DateTime.MinValue; - - if (board.GetLastPostTime(from, thread == null, ref lastPostTime)) - { - if (!CheckTime(lastPostTime, thread == null ? ThreadCreateTime : ThreadReplyTime)) - { - if (thread == null) - { - from.SendMessage("You must wait {0} before creating a new thread.", FormatTS(ThreadCreateTime)); - } - else - { - from.SendMessage("You must wait {0} before replying to another thread.", FormatTS(ThreadReplyTime)); - } - - return; - } - } - - var subject = reader.ReadUTF8Safe(reader.ReadByte()); - - if (subject.Length == 0) - { - return; - } - - var lines = new string[reader.ReadByte()]; - - if (lines.Length == 0) - { - return; - } - - for (var i = 0; i < lines.Length; ++i) - { - lines[i] = reader.ReadUTF8Safe(reader.ReadByte()); - } - - board.PostMessage(from, thread, subject, lines); - } - - public static void BBRemoveMessage(Mobile from, BaseBulletinBoard board, CircularBufferReader reader) - { - if (!(World.FindItem(reader.ReadUInt32()) is BulletinMessage msg) || msg.Parent != board) - { - return; - } - - if (from.AccessLevel < AccessLevel.GameMaster && msg.Poster != from) - { - return; - } - - msg.Delete(); - } - } - - public struct BulletinEquip - { - public int itemID; - public int hue; - - public BulletinEquip(int itemID, int hue) - { - this.itemID = itemID; - this.hue = hue; - } - } - - public class BulletinMessage : Item - { - public BulletinMessage(Mobile poster, BulletinMessage thread, string subject, string[] lines) : base(0xEB0) - { - Movable = false; - - Poster = poster; - Subject = subject; - Time = DateTime.UtcNow; - LastPostTime = Time; - Thread = thread; - PostedName = Poster.Name; - PostedBody = Poster.Body; - PostedHue = Poster.Hue; - Lines = lines; - - var list = new List(); - - for (var i = 0; i < poster.Items.Count; ++i) - { - var item = poster.Items[i]; - - if (item.Layer >= Layer.OneHanded && item.Layer <= Layer.Mount) - { - list.Add(new BulletinEquip(item.ItemID, item.Hue)); - } - } - - PostedEquip = list.ToArray(); - } - - public BulletinMessage(Serial serial) : base(serial) - { - } - - public Mobile Poster { get; private set; } - - public BulletinMessage Thread { get; private set; } - - public string Subject { get; private set; } - - public DateTime Time { get; private set; } - - public DateTime LastPostTime { get; set; } - - public string PostedName { get; private set; } - - public int PostedBody { get; private set; } - - public int PostedHue { get; private set; } - - public BulletinEquip[] PostedEquip { get; private set; } - - public string[] Lines { get; private set; } - - public string GetTimeAsString() => Time.ToString("MMM dd, yyyy"); - - public override bool CheckTarget(Mobile from, Target targ, object targeted) => false; - - public override bool IsAccessibleTo(Mobile check) => false; - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.Write(1); // version - - writer.Write(Poster); - writer.Write(Subject); - writer.Write(Time); - writer.Write(LastPostTime); - writer.Write(Thread != null); - writer.Write(Thread); - writer.Write(PostedName); - writer.Write(PostedBody); - writer.Write(PostedHue); - - writer.Write(PostedEquip.Length); - - for (var i = 0; i < PostedEquip.Length; ++i) - { - writer.Write(PostedEquip[i].itemID); - writer.Write(PostedEquip[i].hue); - } - - writer.Write(Lines.Length); - - for (var i = 0; i < Lines.Length; ++i) - { - writer.Write(Lines[i]); - } - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - var version = reader.ReadInt(); - - switch (version) - { - case 1: - case 0: - { - Poster = reader.ReadEntity(); - Subject = reader.ReadString(); - Time = reader.ReadDateTime(); - LastPostTime = reader.ReadDateTime(); - var hasThread = reader.ReadBool(); - Thread = reader.ReadEntity(); - PostedName = reader.ReadString(); - PostedBody = reader.ReadInt(); - PostedHue = reader.ReadInt(); - - PostedEquip = new BulletinEquip[reader.ReadInt()]; - - for (var i = 0; i < PostedEquip.Length; ++i) - { - PostedEquip[i].itemID = reader.ReadInt(); - PostedEquip[i].hue = reader.ReadInt(); - } - - Lines = new string[reader.ReadInt()]; - - for (var i = 0; i < Lines.Length; ++i) - { - Lines[i] = reader.ReadString(); - } - - if (hasThread && Thread == null) - { - Delete(); - } - - if (version == 0) - { - ValidationQueue.Add(this); - } - - break; - } - } - } - - public void Validate() - { - if ((Parent as BulletinBoard)?.Items.Contains(this) == false) - { - Delete(); - } - } - } - - public class BBDisplayBoard : Packet - { - public BBDisplayBoard(BaseBulletinBoard board) : base(0x71) - { - EnsureCapacity(38); - - var buffer = TextEncoding.UTF8.GetBytes(board.BoardName ?? ""); - - Stream.Write((byte)0x00); // PacketID - Stream.Write(board.Serial); // Bulletin board serial - - // Bulletin board name - if (buffer.Length >= 29) - { - Stream.Write(buffer, 0, 29); - Stream.Write((byte)0); - } - else - { - Stream.Write(buffer, 0, buffer.Length); - Stream.Fill(30 - buffer.Length); - } - } - } - - public class BBMessageHeader : Packet - { - public BBMessageHeader(BaseBulletinBoard board, BulletinMessage msg) : base(0x71) - { - var poster = SafeString(msg.PostedName); - var subject = SafeString(msg.Subject); - var time = SafeString(msg.GetTimeAsString()); - - EnsureCapacity(22 + poster.Length + subject.Length + time.Length); - - Stream.Write((byte)0x01); // PacketID - Stream.Write(board.Serial); // Bulletin board serial - Stream.Write(msg.Serial); // Message serial - - var thread = msg.Thread; - - if (thread == null) - { - Stream.Write(0); // Thread serial--root - } - else - { - Stream.Write(thread.Serial); // Thread serial--parent - } - - WriteString(poster); - WriteString(subject); - WriteString(time); - } - - public void WriteString(string v) - { - var buffer = TextEncoding.UTF8.GetBytes(v); - var len = buffer.Length + 1; - - if (len > 255) - { - len = 255; - } - - Stream.Write((byte)len); - Stream.Write(buffer, 0, len - 1); - Stream.Write((byte)0); - } - - public string SafeString(string v) => v ?? string.Empty; - } - - public class BBMessageContent : Packet - { - public BBMessageContent(BaseBulletinBoard board, BulletinMessage msg) : base(0x71) - { - var poster = SafeString(msg.PostedName); - var subject = SafeString(msg.Subject); - var time = SafeString(msg.GetTimeAsString()); - - EnsureCapacity(22 + poster.Length + subject.Length + time.Length); - - Stream.Write((byte)0x02); // PacketID - Stream.Write(board.Serial); // Bulletin board serial - Stream.Write(msg.Serial); // Message serial - - WriteString(poster); - WriteString(subject); - WriteString(time); - - Stream.Write((short)msg.PostedBody); - Stream.Write((short)msg.PostedHue); - - var len = msg.PostedEquip.Length; - - if (len > 255) - { - len = 255; - } - - Stream.Write((byte)len); - - for (var i = 0; i < len; ++i) - { - var eq = msg.PostedEquip[i]; - - Stream.Write((short)eq.itemID); - Stream.Write((short)eq.hue); - } - - len = msg.Lines.Length; - - if (len > 255) - { - len = 255; - } - - Stream.Write((byte)len); - - for (var i = 0; i < len; ++i) - { - WriteString(msg.Lines[i], true); - } - } - - public void WriteString(string v) - { - WriteString(v, false); - } - - public void WriteString(string v, bool padding) - { - var buffer = TextEncoding.UTF8.GetBytes(v); - var tail = padding ? 2 : 1; - var len = buffer.Length + tail; - - if (len > 255) - { - len = 255; - } - - Stream.Write((byte)len); - Stream.Write(buffer, 0, len - tail); - - if (padding) - { - Stream.Write((short)0); // padding compensates for a client bug - } - else - { - Stream.Write((byte)0); - } - } - - public string SafeString(string v) - { - if (v == null) - { - return string.Empty; - } - - return v; - } - } -} diff --git a/Projects/UOContent/Items/Misc/LOSBlocker.cs b/Projects/UOContent/Items/Misc/LOSBlocker.cs index 46b4126d4..96d99fbb9 100644 --- a/Projects/UOContent/Items/Misc/LOSBlocker.cs +++ b/Projects/UOContent/Items/Misc/LOSBlocker.cs @@ -53,7 +53,7 @@ namespace Server.Items BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(7, 2), GMItemId); } - ns.Send(buffer.Slice(0, length)); + ns.Send(buffer.SliceToLength(length)); } public override void Serialize(IGenericWriter writer) diff --git a/Projects/UOContent/Misc/Guild.cs b/Projects/UOContent/Misc/Guild.cs index 85d854621..7d1d2d2df 100644 --- a/Projects/UOContent/Misc/Guild.cs +++ b/Projects/UOContent/Misc/Guild.cs @@ -353,7 +353,7 @@ namespace Server.Guilds if (length != buffer.Length) { - buffer = buffer.Slice(0, length); // Adjust to the actual size + buffer = buffer.SliceToLength(length); // Adjust to the actual size } g.Members[j].NetState?.Send(buffer); @@ -1530,7 +1530,7 @@ namespace Server.Guilds if (length != buffer.Length) { - buffer = buffer.Slice(0, length); // Adjust to the actual size + buffer = buffer.SliceToLength(length); // Adjust to the actual size } Members[i].NetState?.Send(buffer); diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index 16c6a0300..89b5b1adc 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -2841,7 +2841,7 @@ namespace Server.Mobiles if (length != buffer.Length) { - buffer = buffer.Slice(0, length); // Adjust to the actual size + buffer = buffer.SliceToLength(length); // Adjust to the actual size } ns.Send(buffer);